catchup-library-web 1.20.32 → 1.20.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catchup-library-web",
3
- "version": "1.20.32",
3
+ "version": "1.20.34",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,5 +1,5 @@
1
1
  import { InlineMath } from "react-katex";
2
- import { useState, useEffect } from "react";
2
+ import { useState, useEffect, useRef } from "react";
3
3
  import BaseImage from "../../images/BaseImage";
4
4
  import { shuffleArray } from "../../../utilization/AppUtilization";
5
5
  import ShowMaterialMediaByContentType from "./ShowMaterialMediaByContentType";
@@ -20,37 +20,48 @@ const FillInTheBlanksActivityMaterialContent = ({
20
20
  isPreview,
21
21
  showCorrectAnswer,
22
22
  }: IFillInTheBlanksActivityMaterialProps) => {
23
- const [shuffleOptionList, setShuffleOptionList] = useState([]);
24
- const [selectedOption, setSelectedOption] = useState(null);
25
- const [draggedOption, setDraggedOption] = useState(null);
26
- const [dropTargetIndex, setDropTargetIndex] = useState(null);
23
+ const [shuffleOptionList, setShuffleOptionList] = useState<any[]>([]);
24
+ const [selectedOption, setSelectedOption] = useState<any>(null);
25
+ const [draggedOption, setDraggedOption] = useState<any>(null);
26
+ const [dropTargetIndex, setDropTargetIndex] = useState<string | null>(null);
27
+ const [draggedElement, setDraggedElement] = useState<HTMLElement | null>(
28
+ null
29
+ );
30
+ const dragElementRef = useRef<HTMLDivElement>(null);
31
+ const [touchPosition, setTouchPosition] = useState<{ x: number; y: number }>({
32
+ x: 0,
33
+ y: 0,
34
+ });
27
35
 
28
36
  useEffect(() => {
29
37
  setShuffleOptionList(shuffleArray(optionList));
30
- }, []);
38
+ }, [optionList]);
31
39
 
32
40
  useEffect(() => {
33
41
  if (!showCorrectAnswer) return;
34
42
  const foundAnswer = answer.data.find(
35
43
  (answerData: any) => answerData.type === "FILL_IN_THE_BLANKS"
36
44
  );
37
- if (foundAnswer.answerMap.length === 0) return;
45
+ if (!foundAnswer || foundAnswer.answerMap.length === 0) return;
38
46
  if (Object.keys(materialMap).length === 0) return;
39
47
  foundAnswer.answerMap = Object.keys(materialMap).map(
40
48
  (materialMapKey) => JSON.parse(materialMap[materialMapKey])[0]
41
49
  );
42
50
 
43
51
  onChange(answer, 0, JSON.parse(materialMap[0])[0]);
44
- }, [showCorrectAnswer]);
52
+ }, [showCorrectAnswer, answer, materialMap, onChange]);
45
53
 
46
- const retrieveAnswerMap = () => {
54
+ const retrieveAnswerMap = (): Record<string, any> => {
47
55
  const foundIndex = answer.data.findIndex(
48
56
  (answerData: any) => answerData.type === "FILL_IN_THE_BLANKS"
49
57
  );
50
58
  return answer.data[foundIndex].answerMap;
51
59
  };
52
60
 
53
- const checkAnswerState = (correctAnswerList: any, learnerAnswer: string) => {
61
+ const checkAnswerState = (
62
+ correctAnswerList: any[],
63
+ learnerAnswer: string
64
+ ): string | null => {
54
65
  if (!isPreview) return null;
55
66
  const foundIndex = correctAnswerList.findIndex(
56
67
  (correctAnswer: string) => correctAnswer === learnerAnswer
@@ -61,42 +72,85 @@ const FillInTheBlanksActivityMaterialContent = ({
61
72
  return "INCORRECT";
62
73
  };
63
74
 
64
- const checkAnswerProvided = (answerMap: any, option: string) => {
75
+ const checkAnswerProvided = (
76
+ answerMap: Record<string, any>,
77
+ option: string
78
+ ): boolean => {
65
79
  return (
66
80
  Object.keys(answerMap).findIndex((key) => answerMap[key] === option) !==
67
81
  -1
68
82
  );
69
83
  };
70
84
 
71
- const handleSelectOption = (option: any) => {
72
- setSelectedOption(option);
85
+ // Mouse drag handlers
86
+ const handleMouseDown = (e: React.MouseEvent, option: any): void => {
87
+ e.preventDefault();
88
+ setDraggedOption(option);
89
+ setSelectedOption(null);
90
+ };
91
+
92
+ const handleMouseUp = (): void => {
93
+ if (dropTargetIndex !== null && draggedOption !== null) {
94
+ onChange(answer, dropTargetIndex, draggedOption);
95
+ }
96
+ setDraggedOption(null);
97
+ setDropTargetIndex(null);
73
98
  };
74
99
 
75
- const handleDragStart = (option: any) => {
100
+ // Touch drag handlers
101
+ const handleTouchStart = (
102
+ e: React.TouchEvent,
103
+ option: any,
104
+ element: HTMLElement
105
+ ): void => {
106
+ const touch = e.touches[0];
76
107
  setDraggedOption(option);
108
+ setDraggedElement(element);
109
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
110
+ setSelectedOption(null);
77
111
  };
78
112
 
79
- const handleDragEnd = () => {
113
+ const handleTouchMove = (e: React.TouchEvent): void => {
114
+ if (!draggedOption) return;
115
+
116
+ const touch = e.touches[0];
117
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
118
+
119
+ // Find the element under the touch point
120
+ const elementUnder = document.elementFromPoint(
121
+ touch.clientX,
122
+ touch.clientY
123
+ );
124
+ const dropZone = elementUnder?.closest("[data-drop-zone]");
125
+
126
+ if (dropZone) {
127
+ const dropIndex = dropZone.getAttribute("data-drop-zone");
128
+ setDropTargetIndex(dropIndex);
129
+ } else {
130
+ setDropTargetIndex(null);
131
+ }
132
+ };
133
+
134
+ const handleTouchEnd = (): void => {
80
135
  if (dropTargetIndex !== null && draggedOption !== null) {
81
136
  onChange(answer, dropTargetIndex, draggedOption);
82
137
  }
83
138
  setDraggedOption(null);
84
139
  setDropTargetIndex(null);
140
+ setDraggedElement(null);
85
141
  };
86
142
 
87
- const handleDropZoneEnter = (index: any) => {
88
- setDropTargetIndex(index);
143
+ // Click/tap to select (for easier mobile interaction)
144
+ const handleSelectOption = (option: any): void => {
145
+ setSelectedOption(option);
146
+ setDraggedOption(null);
89
147
  };
90
148
 
91
- const handleDropZoneDrop = (index: any) => {
149
+ const handleDropZoneClick = (index: string): void => {
92
150
  if (selectedOption !== null) {
93
151
  onChange(answer, index, selectedOption);
94
152
  setSelectedOption(null);
95
- } else if (draggedOption !== null) {
96
- onChange(answer, index, draggedOption);
97
- setDraggedOption(null);
98
153
  }
99
- setDropTargetIndex(null);
100
154
  };
101
155
 
102
156
  const answerMap = retrieveAnswerMap();
@@ -112,6 +166,38 @@ const FillInTheBlanksActivityMaterialContent = ({
112
166
  <DividerLine />
113
167
  </div>
114
168
 
169
+ {/* Floating drag preview for touch */}
170
+ {draggedOption && touchPosition.x > 0 && (
171
+ <div
172
+ className="fixed pointer-events-none z-50 opacity-80"
173
+ style={{
174
+ left: `${touchPosition.x}px`,
175
+ top: `${touchPosition.y}px`,
176
+ transform: "translate(-50%, -50%)",
177
+ }}
178
+ >
179
+ {contentMap.type === "TEXT" ? (
180
+ <div className="border-catchup-blue border-2 px-2 rounded-catchup-xlarge bg-white shadow-lg">
181
+ <p className="italic whitespace-pre-wrap">
182
+ <InputWithSpecialExpression
183
+ value={draggedOption}
184
+ showSpecialOnly={false}
185
+ />
186
+ </p>
187
+ </div>
188
+ ) : (
189
+ <div className="border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge bg-white shadow-lg">
190
+ <ShowMaterialMediaByContentType
191
+ key={uniqueValue}
192
+ contentType={contentMap.type}
193
+ src={draggedOption}
194
+ canFullScreen={false}
195
+ />
196
+ </div>
197
+ )}
198
+ </div>
199
+ )}
200
+
115
201
  <div className="w-full flex flex-row flex-wrap gap-x-2 gap-y-2 my-2">
116
202
  {shuffleOptionList.map((option, index) =>
117
203
  checkAnswerProvided(answerMap, option) ? (
@@ -126,18 +212,23 @@ const FillInTheBlanksActivityMaterialContent = ({
126
212
  ) : (
127
213
  <div
128
214
  key={index}
129
- draggable
130
- onDragStart={() => handleDragStart(option)}
131
- onDragEnd={handleDragEnd}
215
+ ref={draggedOption === option ? dragElementRef : null}
132
216
  className={`${
133
- draggedOption === option ? "opacity-40" : "opacity-100"
134
- } transition-opacity duration-200`}
217
+ draggedOption === option
218
+ ? "opacity-40"
219
+ : selectedOption === option
220
+ ? "ring-2 ring-blue-500"
221
+ : "opacity-100"
222
+ } transition-all duration-200`}
223
+ onMouseDown={(e) => handleMouseDown(e, option)}
224
+ onTouchStart={(e) => handleTouchStart(e, option, e.currentTarget)}
225
+ onTouchMove={handleTouchMove}
226
+ onTouchEnd={handleTouchEnd}
135
227
  >
136
228
  {contentMap.type === "TEXT" ? (
137
229
  <div
138
- className="border-catchup-blue border-2 px-2 rounded-catchup-xlarge cursor-pointer select-none touch-none"
230
+ className="border-catchup-blue border-2 px-2 rounded-catchup-xlarge cursor-pointer select-none"
139
231
  onClick={() => handleSelectOption(option)}
140
- onTouchEnd={() => handleSelectOption(option)}
141
232
  >
142
233
  <p className="italic whitespace-pre-wrap">
143
234
  <InputWithSpecialExpression
@@ -148,9 +239,8 @@ const FillInTheBlanksActivityMaterialContent = ({
148
239
  </div>
149
240
  ) : (
150
241
  <div
151
- className="border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge cursor-pointer select-none touch-none"
242
+ className="border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge cursor-pointer select-none"
152
243
  onClick={() => handleSelectOption(option)}
153
- onTouchEnd={() => handleSelectOption(option)}
154
244
  >
155
245
  <ShowMaterialMediaByContentType
156
246
  key={`${uniqueValue}-${index}`}
@@ -164,7 +254,7 @@ const FillInTheBlanksActivityMaterialContent = ({
164
254
  )
165
255
  )}
166
256
  </div>
167
- <div className="w-full flex flex-row flex-wrap">
257
+ <div className="w-full flex flex-row flex-wrap" onMouseUp={handleMouseUp}>
168
258
  {Object.keys(answerMap).map((materialKey, index) => {
169
259
  const learnerAnswerState = checkAnswerState(
170
260
  JSON.parse(materialMap[materialKey]),
@@ -174,25 +264,17 @@ const FillInTheBlanksActivityMaterialContent = ({
174
264
  <div key={index} className="w-full md:w-1/2">
175
265
  <div className="mx-2">
176
266
  <div
177
- onDragOver={(e) => {
178
- e.preventDefault();
179
- handleDropZoneEnter(materialKey);
180
- }}
181
- onDragLeave={() => setDropTargetIndex(null)}
182
- onDrop={(e) => {
183
- e.preventDefault();
184
- handleDropZoneDrop(materialKey);
185
- }}
186
- onClick={() => {
187
- if (selectedOption !== null) {
188
- handleDropZoneDrop(materialKey);
189
- }
190
- }}
267
+ data-drop-zone={materialKey}
268
+ onMouseEnter={() =>
269
+ draggedOption && setDropTargetIndex(materialKey)
270
+ }
271
+ onMouseLeave={() => setDropTargetIndex(null)}
272
+ onClick={() => handleDropZoneClick(materialKey)}
191
273
  className={`${
192
274
  dropTargetIndex === materialKey
193
- ? "ring-2 ring-blue-400"
275
+ ? "ring-2 ring-blue-400 bg-blue-50"
194
276
  : ""
195
- }`}
277
+ } transition-all duration-200 rounded-lg`}
196
278
  >
197
279
  <div className="w-full flex flex-row my-2 gap-x-2">
198
280
  <div className="my-auto">
@@ -206,11 +288,12 @@ const FillInTheBlanksActivityMaterialContent = ({
206
288
  <div
207
289
  className={`w-full min-h-[44px] border rounded-lg ${
208
290
  answerMap[materialKey]
209
- ? "border-catchup-blue-400 px-2"
291
+ ? "border-catchup-blue-400 px-2 cursor-pointer"
210
292
  : "bg-catchup-gray-50 border-catchup-gray-200 border-dashed py-2 px-4"
211
293
  }`}
212
- onClick={() => {
294
+ onClick={(e) => {
213
295
  if (answerMap[materialKey]) {
296
+ e.stopPropagation();
214
297
  onChange(answer, materialKey, "");
215
298
  }
216
299
  }}
@@ -220,9 +303,7 @@ const FillInTheBlanksActivityMaterialContent = ({
220
303
  value={answerMap[materialKey]}
221
304
  showSpecialOnly={false}
222
305
  />
223
- ) : (
224
- <p className="text-gray-400 italic"></p>
225
- )}
306
+ ) : null}
226
307
  </div>
227
308
  </div>
228
309
 
@@ -263,7 +344,8 @@ const FillInTheBlanksActivityMaterialContent = ({
263
344
  ) : (
264
345
  <div
265
346
  className="flex-1 cursor-pointer"
266
- onClick={() => {
347
+ onClick={(e) => {
348
+ e.stopPropagation();
267
349
  onChange(answer, materialKey, "");
268
350
  }}
269
351
  >