catchup-library-web 1.20.35 → 1.20.36

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.35",
3
+ "version": "1.20.36",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,12 +1,9 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- import { useDrop } from "react-dnd";
3
2
  import ShowMaterialMediaByContentType from "./ShowMaterialMediaByContentType";
4
3
  import { InlineMath } from "react-katex";
5
4
  import { constructInputWithSpecialExpressionList } from "../../../utilization/CatchtivityUtilization";
6
5
  import DividerLine from "../../dividers/DividerLine";
7
6
  import { IGroupingActivityMaterialProps } from "../../../properties/ActivityProperties";
8
- import DraggableItem from "../../dnds/DraggableItem";
9
- import DroppableItem from "../../dnds/DroppableItem";
10
7
 
11
8
  const GroupingActivityMaterialContent = ({
12
9
  uniqueValue,
@@ -18,17 +15,18 @@ const GroupingActivityMaterialContent = ({
18
15
  isPreview,
19
16
  showCorrectAnswer,
20
17
  }: IGroupingActivityMaterialProps) => {
21
- const [selectedValue, setSelectedValue] = useState(null);
22
- const [selectedTargetKey, setSelectedTargetKey] = useState(null);
18
+ const [selectedValue, setSelectedValue] = useState<string | null>(null);
19
+ const [draggedValue, setDraggedValue] = useState<string | null>(null);
20
+ const [dropTargetKey, setDropTargetKey] = useState<string | null>(null);
21
+ const [draggedElement, setDraggedElement] = useState<HTMLElement | null>(
22
+ null
23
+ );
23
24
  const [isShuffled, setIsShuffled] = useState(false);
24
- const [shuffledMaterialList, setShuffledMaterialList] = useState([]);
25
- const [{ isOver, canDrop }, drop] = useDrop({
26
- accept: "GROUPING",
27
- drop: () => {},
28
- collect: (monitor: any) => ({
29
- isOver: monitor.isOver(),
30
- canDrop: monitor.canDrop(),
31
- }),
25
+ const [shuffledMaterialList, setShuffledMaterialList] = useState<any[]>([]);
26
+ const dragElementRef = useRef<HTMLDivElement>(null);
27
+ const [touchPosition, setTouchPosition] = useState<{ x: number; y: number }>({
28
+ x: 0,
29
+ y: 0,
32
30
  });
33
31
  const ref = useRef<HTMLDivElement>(null);
34
32
 
@@ -52,14 +50,14 @@ const GroupingActivityMaterialContent = ({
52
50
  }
53
51
  });
54
52
  setShuffledMaterialList(shuffleArray(materialList));
55
- }, []);
53
+ }, [materialMap, isShuffled]);
56
54
 
57
55
  useEffect(() => {
58
56
  if (!showCorrectAnswer) return;
59
57
  answer.data.find(
60
58
  (answerData: any) => answerData.type === "GROUPING"
61
59
  ).answerMap = materialMap;
62
- }, [showCorrectAnswer]);
60
+ }, [showCorrectAnswer, answer, materialMap]);
63
61
 
64
62
  const retrieveAnswerMap = () => {
65
63
  const foundIndex = answer.data.findIndex(
@@ -96,12 +94,79 @@ const GroupingActivityMaterialContent = ({
96
94
  return "INCORRECT";
97
95
  };
98
96
 
99
- const handleGroupingActivityItemOnChange = (
100
- selectedTargetKey: string,
101
- selectedValue: string
102
- ) => {
103
- if (checkCanAnswerQuestion()) {
104
- onChange(answer, selectedTargetKey, selectedValue, null);
97
+ // Mouse drag handlers
98
+ const handleMouseDown = (
99
+ e: React.MouseEvent,
100
+ materialValue: string
101
+ ): void => {
102
+ if (!checkCanAnswerQuestion()) return;
103
+ e.preventDefault();
104
+ setDraggedValue(materialValue);
105
+ setSelectedValue(null);
106
+ };
107
+
108
+ const handleMouseUp = (): void => {
109
+ if (dropTargetKey !== null && draggedValue !== null) {
110
+ onChange(answer, dropTargetKey, draggedValue, null);
111
+ }
112
+ setDraggedValue(null);
113
+ setDropTargetKey(null);
114
+ };
115
+
116
+ // Touch drag handlers
117
+ const handleTouchStart = (
118
+ e: React.TouchEvent,
119
+ materialValue: string,
120
+ element: HTMLElement
121
+ ): void => {
122
+ if (!checkCanAnswerQuestion()) return;
123
+ const touch = e.touches[0];
124
+ setDraggedValue(materialValue);
125
+ setDraggedElement(element);
126
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
127
+ setSelectedValue(null);
128
+ };
129
+
130
+ const handleTouchMove = (e: React.TouchEvent): void => {
131
+ if (!draggedValue) return;
132
+
133
+ const touch = e.touches[0];
134
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
135
+
136
+ // Find the element under the touch point
137
+ const elementUnder = document.elementFromPoint(
138
+ touch.clientX,
139
+ touch.clientY
140
+ );
141
+ const dropZone = elementUnder?.closest("[data-grouping-drop-zone]");
142
+
143
+ if (dropZone) {
144
+ const dropKey = dropZone.getAttribute("data-grouping-drop-zone");
145
+ setDropTargetKey(dropKey);
146
+ } else {
147
+ setDropTargetKey(null);
148
+ }
149
+ };
150
+
151
+ const handleTouchEnd = (): void => {
152
+ if (dropTargetKey !== null && draggedValue !== null) {
153
+ onChange(answer, dropTargetKey, draggedValue, null);
154
+ }
155
+ setDraggedValue(null);
156
+ setDropTargetKey(null);
157
+ setDraggedElement(null);
158
+ };
159
+
160
+ // Click/tap to select (for easier mobile interaction)
161
+ const handleSelectItem = (materialValue: string): void => {
162
+ if (!checkCanAnswerQuestion()) return;
163
+ setSelectedValue(materialValue);
164
+ setDraggedValue(null);
165
+ };
166
+
167
+ const handleDropZoneClick = (answerMapKey: string): void => {
168
+ if (selectedValue !== null) {
169
+ onChange(answer, answerMapKey, selectedValue, null);
105
170
  setSelectedValue(null);
106
171
  }
107
172
  };
@@ -110,90 +175,122 @@ const GroupingActivityMaterialContent = ({
110
175
  const filteredMaterialList = retrieveFilteredMaterialList(answerMap);
111
176
 
112
177
  return (
113
- <>
178
+ <div onMouseUp={handleMouseUp}>
179
+ {/* Floating drag preview for touch */}
180
+ {draggedValue && touchPosition.x > 0 && (
181
+ <div
182
+ className="fixed pointer-events-none z-50 opacity-80"
183
+ style={{
184
+ left: `${touchPosition.x}px`,
185
+ top: `${touchPosition.y}px`,
186
+ transform: "translate(-50%, -50%)",
187
+ }}
188
+ >
189
+ {contentMap.type === "TEXT" ? (
190
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
191
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px]">
192
+ <p className="text-xl text-center whitespace-pre-wrap">
193
+ {constructInputWithSpecialExpressionList(draggedValue).map(
194
+ (inputPart, index) => (
195
+ <span
196
+ key={index}
197
+ className={`${inputPart.isBold ? "font-bold" : ""} ${
198
+ inputPart.isUnderline ? "underline" : ""
199
+ }`}
200
+ >
201
+ {inputPart.isEquation ? (
202
+ <span className="text-2xl">
203
+ <InlineMath math={inputPart.value} />
204
+ </span>
205
+ ) : (
206
+ inputPart.value
207
+ )}
208
+ </span>
209
+ )
210
+ )}
211
+ </p>
212
+ </div>
213
+ </div>
214
+ ) : (
215
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
216
+ <ShowMaterialMediaByContentType
217
+ key={`${uniqueValue}-drag`}
218
+ contentType={contentMap.type}
219
+ src={draggedValue}
220
+ canFullScreen={false}
221
+ />
222
+ </div>
223
+ )}
224
+ </div>
225
+ )}
226
+
114
227
  {filteredMaterialList.length > 0 ? (
115
228
  <>
116
229
  <div className="flex-1 flex flex-row gap-x-4 overflow-x-auto py-2">
117
230
  {filteredMaterialList.map((materialValue, index) => {
118
231
  return (
119
- <DraggableItem
232
+ <div
120
233
  key={index}
121
- item={{ index: materialValue }}
122
- type={"GROUPING"}
123
- component={
124
- <div
125
- className={`${
126
- selectedValue === materialValue
127
- ? "border-catchup-blue"
128
- : "border-catchup-lighter-gray"
129
- } ${
130
- contentMap.type === "TEXT"
131
- ? "h-catchup-activity-text-box-item"
132
- : "h-catchup-activity-media-box-item"
133
- } flex flex-col items-center justify-center border-2 rounded-catchup-xlarge cursor-pointer transition-all duration-300`}
134
- onMouseDown={() => {
135
- if (checkCanAnswerQuestion()) {
136
- setSelectedValue(materialValue);
137
- }
138
- }}
139
- onTouchEnd={() => {
140
- if (checkCanAnswerQuestion()) {
141
- setSelectedValue(materialValue);
142
- }
143
- }}
144
- onMouseUp={() => {
145
- if (checkCanAnswerQuestion()) {
146
- setSelectedValue(null);
147
- }
148
- }}
149
- onTouchStart={() => {
150
- if (checkCanAnswerQuestion()) {
151
- setSelectedValue(null);
152
- }
153
- }}
154
- >
155
- {contentMap.type === "TEXT" ? (
156
- <div className="flex flex-col items-center justify-center m-2 min-w-[200px] overflow-y-auto">
157
- <p className="text-xl text-center whitespace-pre-wrap">
158
- {constructInputWithSpecialExpressionList(
159
- materialValue
160
- ).map((inputPart, index) => (
161
- <span
162
- key={index}
163
- className={`${
164
- inputPart.isBold ? "font-bold" : ""
165
- } ${inputPart.isUnderline ? "underline" : ""}`}
166
- >
167
- {inputPart.isEquation ? (
168
- <span className="text-2xl">
169
- <InlineMath math={inputPart.value} />
170
- </span>
171
- ) : (
172
- inputPart.value
173
- )}
174
- </span>
175
- ))}
176
- </p>
177
- </div>
178
- ) : (
179
- <ShowMaterialMediaByContentType
180
- key={`${uniqueValue}-${index}`}
181
- contentType={contentMap.type}
182
- src={materialValue}
183
- canFullScreen={true}
184
- />
185
- )}
186
- </div>
234
+ ref={draggedValue === materialValue ? dragElementRef : null}
235
+ className={`${
236
+ draggedValue === materialValue
237
+ ? "opacity-40"
238
+ : selectedValue === materialValue
239
+ ? "ring-2 ring-blue-500"
240
+ : "opacity-100"
241
+ } transition-all duration-200`}
242
+ onMouseDown={(e) => handleMouseDown(e, materialValue)}
243
+ onTouchStart={(e) =>
244
+ handleTouchStart(e, materialValue, e.currentTarget)
187
245
  }
188
- moveCardHandler={() => {
189
- if (!selectedTargetKey) return;
190
- if (!selectedValue) return;
191
- handleGroupingActivityItemOnChange(
192
- selectedTargetKey,
193
- selectedValue
194
- );
195
- }}
196
- />
246
+ onTouchMove={handleTouchMove}
247
+ onTouchEnd={handleTouchEnd}
248
+ >
249
+ <div
250
+ className={`${
251
+ selectedValue === materialValue
252
+ ? "border-catchup-blue"
253
+ : "border-catchup-lighter-gray"
254
+ } ${
255
+ contentMap.type === "TEXT"
256
+ ? "h-catchup-activity-text-box-item"
257
+ : "h-catchup-activity-media-box-item"
258
+ } flex flex-col items-center justify-center border-2 rounded-catchup-xlarge cursor-pointer transition-all duration-300`}
259
+ onClick={() => handleSelectItem(materialValue)}
260
+ >
261
+ {contentMap.type === "TEXT" ? (
262
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px] overflow-y-auto">
263
+ <p className="text-xl text-center whitespace-pre-wrap">
264
+ {constructInputWithSpecialExpressionList(
265
+ materialValue
266
+ ).map((inputPart, index) => (
267
+ <span
268
+ key={index}
269
+ className={`${
270
+ inputPart.isBold ? "font-bold" : ""
271
+ } ${inputPart.isUnderline ? "underline" : ""}`}
272
+ >
273
+ {inputPart.isEquation ? (
274
+ <span className="text-2xl">
275
+ <InlineMath math={inputPart.value} />
276
+ </span>
277
+ ) : (
278
+ inputPart.value
279
+ )}
280
+ </span>
281
+ ))}
282
+ </p>
283
+ </div>
284
+ ) : (
285
+ <ShowMaterialMediaByContentType
286
+ key={`${uniqueValue}-${index}`}
287
+ contentType={contentMap.type}
288
+ src={materialValue}
289
+ canFullScreen={true}
290
+ />
291
+ )}
292
+ </div>
293
+ </div>
197
294
  );
198
295
  })}
199
296
  </div>
@@ -239,123 +336,114 @@ const GroupingActivityMaterialContent = ({
239
336
  <div className="flex-1 min-w-0" ref={ref}>
240
337
  <div className="h-full py-3">
241
338
  <div
339
+ data-grouping-drop-zone={answerMapKey}
340
+ onMouseEnter={() =>
341
+ draggedValue && setDropTargetKey(answerMapKey)
342
+ }
343
+ onMouseLeave={() => setDropTargetKey(null)}
344
+ onClick={() => handleDropZoneClick(answerMapKey)}
242
345
  className={`${
243
- canDrop
244
- ? selectedTargetKey === answerMapKey
245
- ? "bg-catchup-light-blue"
246
- : "bg-catchup-light-blue opacity-40"
346
+ dropTargetKey === answerMapKey
347
+ ? "bg-catchup-light-blue ring-2 ring-blue-400"
247
348
  : ""
248
- } flex-1 border-catchup-blue rounded-catchup-xlarge border-2 h-full p-1`}
349
+ } flex-1 border-catchup-blue rounded-catchup-xlarge border-2 h-full p-1 transition-all duration-200`}
249
350
  >
250
- <DroppableItem
251
- key={index}
252
- item={{ index: answerMapKey }}
253
- type={"GROUPING"}
254
- target={selectedTargetKey}
255
- setTarget={setSelectedTargetKey}
256
- dropRef={drop}
257
- component={
258
- <div className="h-full w-full overflow-x-auto">
259
- <div className="flex flex-row items-center gap-2 w-max h-full">
260
- {answerMap[answerMapKey].map(
261
- (
262
- answerMapValue: string,
263
- answerMapIndex: number
264
- ) => {
265
- const learnerAnswerState = checkAnswerState(
266
- materialMap[answerMapKey],
267
- answerMapValue
268
- );
269
- return (
270
- <div key={answerMapIndex} className="p-1">
271
- <div
272
- className={`${
273
- contentMap.type === "TEXT"
274
- ? "h-catchup-activity-text-box-item"
275
- : "h-catchup-activity-media-box-item"
276
- }`}
277
- >
278
- <div
279
- className={`${
280
- learnerAnswerState === "EMPTY"
281
- ? "border-catchup-lighter-gray"
282
- : learnerAnswerState === "CORRECT"
283
- ? "border-catchup-green"
284
- : learnerAnswerState === "INCORRECT"
285
- ? "border-catchup-red"
286
- : "border-catchup-blue"
287
- } border-2 rounded-catchup-xlarge h-full flex flex-col items-center justify-center transition-all duration-300 cursor-pointer`}
288
- onClick={(e) => {
289
- e.preventDefault();
290
- if (checkCanAnswerQuestion()) {
291
- onChange(
292
- answer,
293
- answerMapKey,
294
- null,
295
- answerMapIndex
296
- );
297
- setSelectedValue(null);
298
- }
299
- }}
300
- >
301
- {contentMap.type === "TEXT" ? (
302
- <div className="flex flex-col items-center justify-center transition-all duration-300 min-w-[200px] overflow-y-auto">
303
- <div className="m-2">
304
- <p className="text-xl text-center whitespace-pre-wrap">
305
- {constructInputWithSpecialExpressionList(
306
- answerMapValue
307
- ).map((inputPart, index) => (
308
- <span
309
- key={index}
310
- className={`${
311
- inputPart.isBold
312
- ? "font-bold"
313
- : ""
314
- } ${
315
- inputPart.isUnderline
316
- ? "underline"
317
- : ""
318
- }`}
319
- >
320
- {inputPart.isEquation ? (
321
- <span className="text-2xl">
322
- <InlineMath
323
- math={inputPart.value}
324
- />
325
- </span>
326
- ) : (
327
- inputPart.value
328
- )}
351
+ <div className="h-full w-full overflow-x-auto">
352
+ <div className="flex flex-row items-center gap-2 w-max h-full">
353
+ {answerMap[answerMapKey].map(
354
+ (answerMapValue: string, answerMapIndex: number) => {
355
+ const learnerAnswerState = checkAnswerState(
356
+ materialMap[answerMapKey],
357
+ answerMapValue
358
+ );
359
+ return (
360
+ <div key={answerMapIndex} className="p-1">
361
+ <div
362
+ className={`${
363
+ contentMap.type === "TEXT"
364
+ ? "h-catchup-activity-text-box-item"
365
+ : "h-catchup-activity-media-box-item"
366
+ }`}
367
+ >
368
+ <div
369
+ className={`${
370
+ learnerAnswerState === "EMPTY"
371
+ ? "border-catchup-lighter-gray"
372
+ : learnerAnswerState === "CORRECT"
373
+ ? "border-catchup-green"
374
+ : learnerAnswerState === "INCORRECT"
375
+ ? "border-catchup-red"
376
+ : "border-catchup-blue"
377
+ } border-2 rounded-catchup-xlarge h-full flex flex-col items-center justify-center transition-all duration-300 cursor-pointer`}
378
+ onClick={(e) => {
379
+ e.stopPropagation();
380
+ if (checkCanAnswerQuestion()) {
381
+ onChange(
382
+ answer,
383
+ answerMapKey,
384
+ null,
385
+ answerMapIndex
386
+ );
387
+ setSelectedValue(null);
388
+ }
389
+ }}
390
+ >
391
+ {contentMap.type === "TEXT" ? (
392
+ <div className="flex flex-col items-center justify-center transition-all duration-300 min-w-[200px] overflow-y-auto">
393
+ <div className="m-2">
394
+ <p className="text-xl text-center whitespace-pre-wrap">
395
+ {constructInputWithSpecialExpressionList(
396
+ answerMapValue
397
+ ).map((inputPart, index) => (
398
+ <span
399
+ key={index}
400
+ className={`${
401
+ inputPart.isBold
402
+ ? "font-bold"
403
+ : ""
404
+ } ${
405
+ inputPart.isUnderline
406
+ ? "underline"
407
+ : ""
408
+ }`}
409
+ >
410
+ {inputPart.isEquation ? (
411
+ <span className="text-2xl">
412
+ <InlineMath
413
+ math={inputPart.value}
414
+ />
329
415
  </span>
330
- ))}
331
- </p>
332
- </div>
333
- </div>
334
- ) : (
335
- <ShowMaterialMediaByContentType
336
- key={`${uniqueValue}-${answerMapIndex}`}
337
- contentType={contentMap.type}
338
- src={answerMapValue}
339
- canFullScreen={false}
340
- />
341
- )}
416
+ ) : (
417
+ inputPart.value
418
+ )}
419
+ </span>
420
+ ))}
421
+ </p>
422
+ </div>
342
423
  </div>
343
- </div>
424
+ ) : (
425
+ <ShowMaterialMediaByContentType
426
+ key={`${uniqueValue}-${answerMapIndex}`}
427
+ contentType={contentMap.type}
428
+ src={answerMapValue}
429
+ canFullScreen={false}
430
+ />
431
+ )}
344
432
  </div>
345
- );
346
- }
347
- )}
348
- </div>
349
- </div>
350
- }
351
- />
433
+ </div>
434
+ </div>
435
+ );
436
+ }
437
+ )}
438
+ </div>
439
+ </div>
352
440
  </div>
353
441
  </div>
354
442
  </div>
355
443
  </div>
356
444
  ))}
357
445
  </div>
358
- </>
446
+ </div>
359
447
  );
360
448
  };
361
449