catchup-library-web 1.20.35 → 1.21.0

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.
@@ -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,22 @@ 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 [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({
28
+ x: 0,
29
+ y: 0,
30
+ });
31
+ const [touchPosition, setTouchPosition] = useState<{ x: number; y: number }>({
32
+ x: 0,
33
+ y: 0,
32
34
  });
33
35
  const ref = useRef<HTMLDivElement>(null);
34
36
 
@@ -52,14 +54,14 @@ const GroupingActivityMaterialContent = ({
52
54
  }
53
55
  });
54
56
  setShuffledMaterialList(shuffleArray(materialList));
55
- }, []);
57
+ }, [materialMap, isShuffled]);
56
58
 
57
59
  useEffect(() => {
58
60
  if (!showCorrectAnswer) return;
59
61
  answer.data.find(
60
62
  (answerData: any) => answerData.type === "GROUPING"
61
63
  ).answerMap = materialMap;
62
- }, [showCorrectAnswer]);
64
+ }, [showCorrectAnswer, answer, materialMap]);
63
65
 
64
66
  const retrieveAnswerMap = () => {
65
67
  const foundIndex = answer.data.findIndex(
@@ -96,12 +98,99 @@ const GroupingActivityMaterialContent = ({
96
98
  return "INCORRECT";
97
99
  };
98
100
 
99
- const handleGroupingActivityItemOnChange = (
100
- selectedTargetKey: string,
101
- selectedValue: string
102
- ) => {
103
- if (checkCanAnswerQuestion()) {
104
- onChange(answer, selectedTargetKey, selectedValue, null);
101
+ // Mouse drag handlers
102
+ const handleMouseDown = (
103
+ e: React.MouseEvent,
104
+ materialValue: string
105
+ ): void => {
106
+ if (!checkCanAnswerQuestion()) return;
107
+ e.preventDefault();
108
+ setDraggedValue(materialValue);
109
+ setSelectedValue(null);
110
+ setMousePosition({ x: e.clientX, y: e.clientY });
111
+ };
112
+
113
+ const handleMouseMove = (e: React.MouseEvent): void => {
114
+ if (!draggedValue) return;
115
+
116
+ setMousePosition({ x: e.clientX, y: e.clientY });
117
+
118
+ // Find the element under the mouse point
119
+ const elementUnder = document.elementFromPoint(e.clientX, e.clientY);
120
+ const dropZone = elementUnder?.closest("[data-grouping-drop-zone]");
121
+
122
+ if (dropZone) {
123
+ const dropKey = dropZone.getAttribute("data-grouping-drop-zone");
124
+ setDropTargetKey(dropKey);
125
+ } else {
126
+ setDropTargetKey(null);
127
+ }
128
+ };
129
+
130
+ const handleMouseUp = (): void => {
131
+ if (dropTargetKey !== null && draggedValue !== null) {
132
+ onChange(answer, dropTargetKey, draggedValue, null);
133
+ }
134
+ setDraggedValue(null);
135
+ setDropTargetKey(null);
136
+ setMousePosition({ x: 0, y: 0 });
137
+ };
138
+
139
+ // Touch drag handlers
140
+ const handleTouchStart = (
141
+ e: React.TouchEvent,
142
+ materialValue: string,
143
+ element: HTMLElement
144
+ ): void => {
145
+ if (!checkCanAnswerQuestion()) return;
146
+ const touch = e.touches[0];
147
+ setDraggedValue(materialValue);
148
+ setDraggedElement(element);
149
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
150
+ setSelectedValue(null);
151
+ };
152
+
153
+ const handleTouchMove = (e: React.TouchEvent): void => {
154
+ if (!draggedValue) return;
155
+
156
+ const touch = e.touches[0];
157
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
158
+
159
+ // Find the element under the touch point
160
+ const elementUnder = document.elementFromPoint(
161
+ touch.clientX,
162
+ touch.clientY
163
+ );
164
+ const dropZone = elementUnder?.closest("[data-grouping-drop-zone]");
165
+
166
+ if (dropZone) {
167
+ const dropKey = dropZone.getAttribute("data-grouping-drop-zone");
168
+ setDropTargetKey(dropKey);
169
+ } else {
170
+ setDropTargetKey(null);
171
+ }
172
+ };
173
+
174
+ const handleTouchEnd = (): void => {
175
+ if (dropTargetKey !== null && draggedValue !== null) {
176
+ onChange(answer, dropTargetKey, draggedValue, null);
177
+ }
178
+ setDraggedValue(null);
179
+ setDropTargetKey(null);
180
+ setDraggedElement(null);
181
+ setTouchPosition({ x: 0, y: 0 });
182
+ };
183
+
184
+ // Click/tap to select (for easier mobile interaction)
185
+ const handleSelectItem = (materialValue: string): void => {
186
+ if (!checkCanAnswerQuestion()) return;
187
+ setSelectedValue(materialValue);
188
+ setDraggedValue(null);
189
+ };
190
+
191
+ const handleDropZoneClick = (answerMapKey: string): void => {
192
+ if (selectedValue !== null) {
193
+ onChange(answer, answerMapKey, selectedValue, null);
105
194
  setSelectedValue(null);
106
195
  }
107
196
  };
@@ -110,90 +199,170 @@ const GroupingActivityMaterialContent = ({
110
199
  const filteredMaterialList = retrieveFilteredMaterialList(answerMap);
111
200
 
112
201
  return (
113
- <>
202
+ <div onMouseMove={handleMouseMove} onMouseUp={handleMouseUp}>
203
+ {/* Floating drag preview for mouse */}
204
+ {draggedValue && mousePosition.x > 0 && (
205
+ <div
206
+ className="fixed pointer-events-none z-50 opacity-80"
207
+ style={{
208
+ left: `${mousePosition.x}px`,
209
+ top: `${mousePosition.y}px`,
210
+ transform: "translate(-50%, -50%)",
211
+ }}
212
+ >
213
+ {contentMap.type === "TEXT" ? (
214
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
215
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px]">
216
+ <p className="text-xl text-center whitespace-pre-wrap">
217
+ {constructInputWithSpecialExpressionList(draggedValue).map(
218
+ (inputPart, index) => (
219
+ <span
220
+ key={index}
221
+ className={`${inputPart.isBold ? "font-bold" : ""} ${
222
+ inputPart.isUnderline ? "underline" : ""
223
+ }`}
224
+ >
225
+ {inputPart.isEquation ? (
226
+ <span className="text-2xl">
227
+ <InlineMath math={inputPart.value} />
228
+ </span>
229
+ ) : (
230
+ inputPart.value
231
+ )}
232
+ </span>
233
+ )
234
+ )}
235
+ </p>
236
+ </div>
237
+ </div>
238
+ ) : (
239
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
240
+ <ShowMaterialMediaByContentType
241
+ key={`${uniqueValue}-drag-mouse`}
242
+ contentType={contentMap.type}
243
+ src={draggedValue}
244
+ canFullScreen={false}
245
+ />
246
+ </div>
247
+ )}
248
+ </div>
249
+ )}
250
+
251
+ {/* Floating drag preview for touch */}
252
+ {draggedValue && touchPosition.x > 0 && (
253
+ <div
254
+ className="fixed pointer-events-none z-50 opacity-80"
255
+ style={{
256
+ left: `${touchPosition.x}px`,
257
+ top: `${touchPosition.y}px`,
258
+ transform: "translate(-50%, -50%)",
259
+ }}
260
+ >
261
+ {contentMap.type === "TEXT" ? (
262
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
263
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px]">
264
+ <p className="text-xl text-center whitespace-pre-wrap">
265
+ {constructInputWithSpecialExpressionList(draggedValue).map(
266
+ (inputPart, index) => (
267
+ <span
268
+ key={index}
269
+ className={`${inputPart.isBold ? "font-bold" : ""} ${
270
+ inputPart.isUnderline ? "underline" : ""
271
+ }`}
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
+ )}
283
+ </p>
284
+ </div>
285
+ </div>
286
+ ) : (
287
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
288
+ <ShowMaterialMediaByContentType
289
+ key={`${uniqueValue}-drag-touch`}
290
+ contentType={contentMap.type}
291
+ src={draggedValue}
292
+ canFullScreen={false}
293
+ />
294
+ </div>
295
+ )}
296
+ </div>
297
+ )}
298
+
114
299
  {filteredMaterialList.length > 0 ? (
115
300
  <>
116
301
  <div className="flex-1 flex flex-row gap-x-4 overflow-x-auto py-2">
117
302
  {filteredMaterialList.map((materialValue, index) => {
118
303
  return (
119
- <DraggableItem
304
+ <div
120
305
  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>
306
+ ref={draggedValue === materialValue ? dragElementRef : null}
307
+ className={`${
308
+ draggedValue === materialValue
309
+ ? "opacity-40"
310
+ : selectedValue === materialValue
311
+ ? "ring-2 ring-blue-500"
312
+ : "opacity-100"
313
+ } transition-all duration-200`}
314
+ onMouseDown={(e) => handleMouseDown(e, materialValue)}
315
+ onTouchStart={(e) =>
316
+ handleTouchStart(e, materialValue, e.currentTarget)
187
317
  }
188
- moveCardHandler={() => {
189
- if (!selectedTargetKey) return;
190
- if (!selectedValue) return;
191
- handleGroupingActivityItemOnChange(
192
- selectedTargetKey,
193
- selectedValue
194
- );
195
- }}
196
- />
318
+ onTouchMove={handleTouchMove}
319
+ onTouchEnd={handleTouchEnd}
320
+ >
321
+ <div
322
+ className={`${
323
+ selectedValue === materialValue
324
+ ? "border-catchup-blue"
325
+ : "border-catchup-lighter-gray"
326
+ } ${
327
+ contentMap.type === "TEXT"
328
+ ? "h-catchup-activity-text-box-item"
329
+ : "h-catchup-activity-media-box-item"
330
+ } flex flex-col items-center justify-center border-2 rounded-catchup-xlarge cursor-pointer transition-all duration-300`}
331
+ onClick={() => handleSelectItem(materialValue)}
332
+ >
333
+ {contentMap.type === "TEXT" ? (
334
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px] overflow-y-auto">
335
+ <p className="text-xl text-center whitespace-pre-wrap">
336
+ {constructInputWithSpecialExpressionList(
337
+ materialValue
338
+ ).map((inputPart, index) => (
339
+ <span
340
+ key={index}
341
+ className={`${
342
+ inputPart.isBold ? "font-bold" : ""
343
+ } ${inputPart.isUnderline ? "underline" : ""}`}
344
+ >
345
+ {inputPart.isEquation ? (
346
+ <span className="text-2xl">
347
+ <InlineMath math={inputPart.value} />
348
+ </span>
349
+ ) : (
350
+ inputPart.value
351
+ )}
352
+ </span>
353
+ ))}
354
+ </p>
355
+ </div>
356
+ ) : (
357
+ <ShowMaterialMediaByContentType
358
+ key={`${uniqueValue}-${index}`}
359
+ contentType={contentMap.type}
360
+ src={materialValue}
361
+ canFullScreen={true}
362
+ />
363
+ )}
364
+ </div>
365
+ </div>
197
366
  );
198
367
  })}
199
368
  </div>
@@ -239,123 +408,114 @@ const GroupingActivityMaterialContent = ({
239
408
  <div className="flex-1 min-w-0" ref={ref}>
240
409
  <div className="h-full py-3">
241
410
  <div
411
+ data-grouping-drop-zone={answerMapKey}
412
+ onMouseEnter={() =>
413
+ draggedValue && setDropTargetKey(answerMapKey)
414
+ }
415
+ onMouseLeave={() => setDropTargetKey(null)}
416
+ onClick={() => handleDropZoneClick(answerMapKey)}
242
417
  className={`${
243
- canDrop
244
- ? selectedTargetKey === answerMapKey
245
- ? "bg-catchup-light-blue"
246
- : "bg-catchup-light-blue opacity-40"
418
+ dropTargetKey === answerMapKey
419
+ ? "bg-catchup-light-blue ring-2 ring-blue-400"
247
420
  : ""
248
- } flex-1 border-catchup-blue rounded-catchup-xlarge border-2 h-full p-1`}
421
+ } flex-1 border-catchup-blue rounded-catchup-xlarge border-2 h-full p-1 transition-all duration-200`}
249
422
  >
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
- )}
423
+ <div className="h-full w-full overflow-x-auto">
424
+ <div className="flex flex-row items-center gap-2 w-max h-full">
425
+ {answerMap[answerMapKey].map(
426
+ (answerMapValue: string, answerMapIndex: number) => {
427
+ const learnerAnswerState = checkAnswerState(
428
+ materialMap[answerMapKey],
429
+ answerMapValue
430
+ );
431
+ return (
432
+ <div key={answerMapIndex} className="p-1">
433
+ <div
434
+ className={`${
435
+ contentMap.type === "TEXT"
436
+ ? "h-catchup-activity-text-box-item"
437
+ : "h-catchup-activity-media-box-item"
438
+ }`}
439
+ >
440
+ <div
441
+ className={`${
442
+ learnerAnswerState === "EMPTY"
443
+ ? "border-catchup-lighter-gray"
444
+ : learnerAnswerState === "CORRECT"
445
+ ? "border-catchup-green"
446
+ : learnerAnswerState === "INCORRECT"
447
+ ? "border-catchup-red"
448
+ : "border-catchup-blue"
449
+ } border-2 rounded-catchup-xlarge h-full flex flex-col items-center justify-center transition-all duration-300 cursor-pointer`}
450
+ onClick={(e) => {
451
+ e.stopPropagation();
452
+ if (checkCanAnswerQuestion()) {
453
+ onChange(
454
+ answer,
455
+ answerMapKey,
456
+ null,
457
+ answerMapIndex
458
+ );
459
+ setSelectedValue(null);
460
+ }
461
+ }}
462
+ >
463
+ {contentMap.type === "TEXT" ? (
464
+ <div className="flex flex-col items-center justify-center transition-all duration-300 min-w-[200px] overflow-y-auto">
465
+ <div className="m-2">
466
+ <p className="text-xl text-center whitespace-pre-wrap">
467
+ {constructInputWithSpecialExpressionList(
468
+ answerMapValue
469
+ ).map((inputPart, index) => (
470
+ <span
471
+ key={index}
472
+ className={`${
473
+ inputPart.isBold
474
+ ? "font-bold"
475
+ : ""
476
+ } ${
477
+ inputPart.isUnderline
478
+ ? "underline"
479
+ : ""
480
+ }`}
481
+ >
482
+ {inputPart.isEquation ? (
483
+ <span className="text-2xl">
484
+ <InlineMath
485
+ math={inputPart.value}
486
+ />
329
487
  </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
- )}
488
+ ) : (
489
+ inputPart.value
490
+ )}
491
+ </span>
492
+ ))}
493
+ </p>
494
+ </div>
342
495
  </div>
343
- </div>
496
+ ) : (
497
+ <ShowMaterialMediaByContentType
498
+ key={`${uniqueValue}-${answerMapIndex}`}
499
+ contentType={contentMap.type}
500
+ src={answerMapValue}
501
+ canFullScreen={false}
502
+ />
503
+ )}
344
504
  </div>
345
- );
346
- }
347
- )}
348
- </div>
349
- </div>
350
- }
351
- />
505
+ </div>
506
+ </div>
507
+ );
508
+ }
509
+ )}
510
+ </div>
511
+ </div>
352
512
  </div>
353
513
  </div>
354
514
  </div>
355
515
  </div>
356
516
  ))}
357
517
  </div>
358
- </>
518
+ </div>
359
519
  );
360
520
  };
361
521