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,11 +1,8 @@
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 { IMatchingActivityMaterialProps } from "../../../properties/ActivityProperties";
7
- import DraggableItem from "../../dnds/DraggableItem";
8
- import DroppableItem from "../../dnds/DroppableItem";
9
6
  import DividerLine from "../../dividers/DividerLine";
10
7
 
11
8
  const MatchingActivityMaterialContent = ({
@@ -18,17 +15,22 @@ const MatchingActivityMaterialContent = ({
18
15
  isPreview,
19
16
  showCorrectAnswer,
20
17
  }: IMatchingActivityMaterialProps) => {
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: "MATCHING",
27
- drop: () => {},
28
- collect: (monitor) => ({
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 itemsRef = useRef<HTMLDivElement>(null);
34
36
 
@@ -50,32 +52,20 @@ const MatchingActivityMaterialContent = ({
50
52
  materialList.push(materialMap[materialKey]);
51
53
  });
52
54
  setShuffledMaterialList(shuffleArray(materialList));
53
- }, []);
55
+ }, [materialMap, isShuffled]);
54
56
 
55
57
  useEffect(() => {
56
58
  if (!showCorrectAnswer) return;
57
59
  answer.data.find(
58
60
  (answerData: any) => answerData.type === "MATCHING"
59
61
  ).answerMap = materialMap;
60
- }, [showCorrectAnswer]);
62
+ }, [showCorrectAnswer, answer, materialMap]);
61
63
 
62
64
  const retrieveAnswerMap = () => {
63
65
  const foundIndex = answer.data.findIndex(
64
66
  (answerData: any) => answerData.type === "MATCHING"
65
67
  );
66
68
  return answer.data[foundIndex].answerMap;
67
- // const sortedAnswerMapKeys = Object.keys(answerMap).sort((a, b) =>
68
- // answerMap[a]
69
- // ? answerMap[b]
70
- // ? answerMap[a].localeCompare(answerMap[b])
71
- // : 1
72
- // : -1
73
- // );
74
- // const sortedAnswerMap: any = {};
75
- // for (const answerMapKey of sortedAnswerMapKeys) {
76
- // sortedAnswerMap[answerMapKey] = answerMap[answerMapKey];
77
- // }
78
- // return sortedAnswerMap;
79
69
  };
80
70
 
81
71
  const retrieveFilteredMaterialList = (answerMap: any) => {
@@ -100,12 +90,99 @@ const MatchingActivityMaterialContent = ({
100
90
  return "INCORRECT";
101
91
  };
102
92
 
103
- const handleMatchingActivityItemOnChange = (
104
- selectedTargetKey: string,
105
- selectedValue: string | null
106
- ) => {
107
- if (checkCanAnswerQuestion()) {
108
- onChange(answer, selectedTargetKey, selectedValue);
93
+ // Mouse drag handlers
94
+ const handleMouseDown = (
95
+ e: React.MouseEvent,
96
+ materialValue: string
97
+ ): void => {
98
+ if (!checkCanAnswerQuestion()) return;
99
+ e.preventDefault();
100
+ setDraggedValue(materialValue);
101
+ setSelectedValue(null);
102
+ setMousePosition({ x: e.clientX, y: e.clientY });
103
+ };
104
+
105
+ const handleMouseMove = (e: React.MouseEvent): void => {
106
+ if (!draggedValue) return;
107
+
108
+ setMousePosition({ x: e.clientX, y: e.clientY });
109
+
110
+ // Find the element under the mouse point
111
+ const elementUnder = document.elementFromPoint(e.clientX, e.clientY);
112
+ const dropZone = elementUnder?.closest("[data-matching-drop-zone]");
113
+
114
+ if (dropZone) {
115
+ const dropKey = dropZone.getAttribute("data-matching-drop-zone");
116
+ setDropTargetKey(dropKey);
117
+ } else {
118
+ setDropTargetKey(null);
119
+ }
120
+ };
121
+
122
+ const handleMouseUp = (): void => {
123
+ if (dropTargetKey !== null && draggedValue !== null) {
124
+ onChange(answer, dropTargetKey, draggedValue);
125
+ }
126
+ setDraggedValue(null);
127
+ setDropTargetKey(null);
128
+ setMousePosition({ x: 0, y: 0 });
129
+ };
130
+
131
+ // Touch drag handlers
132
+ const handleTouchStart = (
133
+ e: React.TouchEvent,
134
+ materialValue: string,
135
+ element: HTMLElement
136
+ ): void => {
137
+ if (!checkCanAnswerQuestion()) return;
138
+ const touch = e.touches[0];
139
+ setDraggedValue(materialValue);
140
+ setDraggedElement(element);
141
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
142
+ setSelectedValue(null);
143
+ };
144
+
145
+ const handleTouchMove = (e: React.TouchEvent): void => {
146
+ if (!draggedValue) return;
147
+
148
+ const touch = e.touches[0];
149
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
150
+
151
+ // Find the element under the touch point
152
+ const elementUnder = document.elementFromPoint(
153
+ touch.clientX,
154
+ touch.clientY
155
+ );
156
+ const dropZone = elementUnder?.closest("[data-matching-drop-zone]");
157
+
158
+ if (dropZone) {
159
+ const dropKey = dropZone.getAttribute("data-matching-drop-zone");
160
+ setDropTargetKey(dropKey);
161
+ } else {
162
+ setDropTargetKey(null);
163
+ }
164
+ };
165
+
166
+ const handleTouchEnd = (): void => {
167
+ if (dropTargetKey !== null && draggedValue !== null) {
168
+ onChange(answer, dropTargetKey, draggedValue);
169
+ }
170
+ setDraggedValue(null);
171
+ setDropTargetKey(null);
172
+ setDraggedElement(null);
173
+ setTouchPosition({ x: 0, y: 0 });
174
+ };
175
+
176
+ // Click/tap to select (for easier mobile interaction)
177
+ const handleSelectItem = (materialValue: string): void => {
178
+ if (!checkCanAnswerQuestion()) return;
179
+ setSelectedValue(materialValue);
180
+ setDraggedValue(null);
181
+ };
182
+
183
+ const handleDropZoneClick = (answerMapKey: string): void => {
184
+ if (selectedValue !== null) {
185
+ onChange(answer, answerMapKey, selectedValue);
109
186
  setSelectedValue(null);
110
187
  }
111
188
  };
@@ -114,7 +191,103 @@ const MatchingActivityMaterialContent = ({
114
191
  const filteredMaterialList = retrieveFilteredMaterialList(answerMap);
115
192
 
116
193
  return (
117
- <>
194
+ <div onMouseMove={handleMouseMove} onMouseUp={handleMouseUp}>
195
+ {/* Floating drag preview for mouse */}
196
+ {draggedValue && mousePosition.x > 0 && (
197
+ <div
198
+ className="fixed pointer-events-none z-50 opacity-80"
199
+ style={{
200
+ left: `${mousePosition.x}px`,
201
+ top: `${mousePosition.y}px`,
202
+ transform: "translate(-50%, -50%)",
203
+ }}
204
+ >
205
+ {contentMap.type === "TEXT" ? (
206
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
207
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px] px-4">
208
+ <p className="text-lg whitespace-pre-wrap">
209
+ {constructInputWithSpecialExpressionList(draggedValue).map(
210
+ (inputPart, index) => (
211
+ <span
212
+ key={index}
213
+ className={`${inputPart.isBold ? "font-bold" : ""} ${
214
+ inputPart.isUnderline ? "underline" : ""
215
+ }`}
216
+ >
217
+ {inputPart.isEquation ? (
218
+ <span className="text-xl">
219
+ <InlineMath math={inputPart.value} />
220
+ </span>
221
+ ) : (
222
+ inputPart.value
223
+ )}
224
+ </span>
225
+ )
226
+ )}
227
+ </p>
228
+ </div>
229
+ </div>
230
+ ) : (
231
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
232
+ <ShowMaterialMediaByContentType
233
+ key={`${uniqueValue}-drag-mouse`}
234
+ contentType={contentMap.type}
235
+ src={draggedValue}
236
+ canFullScreen={false}
237
+ />
238
+ </div>
239
+ )}
240
+ </div>
241
+ )}
242
+
243
+ {/* Floating drag preview for touch */}
244
+ {draggedValue && touchPosition.x > 0 && (
245
+ <div
246
+ className="fixed pointer-events-none z-50 opacity-80"
247
+ style={{
248
+ left: `${touchPosition.x}px`,
249
+ top: `${touchPosition.y}px`,
250
+ transform: "translate(-50%, -50%)",
251
+ }}
252
+ >
253
+ {contentMap.type === "TEXT" ? (
254
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
255
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px] px-4">
256
+ <p className="text-lg whitespace-pre-wrap">
257
+ {constructInputWithSpecialExpressionList(draggedValue).map(
258
+ (inputPart, index) => (
259
+ <span
260
+ key={index}
261
+ className={`${inputPart.isBold ? "font-bold" : ""} ${
262
+ inputPart.isUnderline ? "underline" : ""
263
+ }`}
264
+ >
265
+ {inputPart.isEquation ? (
266
+ <span className="text-xl">
267
+ <InlineMath math={inputPart.value} />
268
+ </span>
269
+ ) : (
270
+ inputPart.value
271
+ )}
272
+ </span>
273
+ )
274
+ )}
275
+ </p>
276
+ </div>
277
+ </div>
278
+ ) : (
279
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
280
+ <ShowMaterialMediaByContentType
281
+ key={`${uniqueValue}-drag-touch`}
282
+ contentType={contentMap.type}
283
+ src={draggedValue}
284
+ canFullScreen={false}
285
+ />
286
+ </div>
287
+ )}
288
+ </div>
289
+ )}
290
+
118
291
  {filteredMaterialList.length > 0 ? (
119
292
  <>
120
293
  <div
@@ -122,84 +295,68 @@ const MatchingActivityMaterialContent = ({
122
295
  className="flex-shrink-0 flex flex-row gap-x-4 gap-y-4 overflow-x-auto py-2"
123
296
  >
124
297
  {filteredMaterialList.map((materialValue, index) => (
125
- <DraggableItem
298
+ <div
126
299
  key={index}
127
- item={{ index: materialValue }}
128
- type={"MATCHING"}
129
- component={
130
- <div
131
- className={`${
132
- selectedValue === materialValue
133
- ? "border-catchup-blue"
134
- : "border-catchup-lighter-gray"
135
- } ${
136
- contentMap.type === "TEXT"
137
- ? "h-catchup-activity-text-box-item"
138
- : "h-catchup-activity-media-box-item"
139
- } flex flex-col items-center justify-center border-2 rounded-catchup-xlarge cursor-pointer transition-all duration-300`}
140
- onMouseDown={() => {
141
- if (checkCanAnswerQuestion()) {
142
- setSelectedValue(materialValue);
143
- }
144
- }}
145
- onTouchEnd={() => {
146
- if (checkCanAnswerQuestion()) {
147
- setSelectedValue(materialValue);
148
- }
149
- }}
150
- onMouseUp={() => {
151
- if (checkCanAnswerQuestion()) {
152
- setSelectedValue(null);
153
- }
154
- }}
155
- onTouchStart={() => {
156
- if (checkCanAnswerQuestion()) {
157
- setSelectedValue(null);
158
- }
159
- }}
160
- >
161
- {contentMap.type === "TEXT" ? (
162
- <div className="flex flex-col items-center justify-center m-2 min-w-[200px] overflow-y-auto px-4">
163
- <p className="text-lg whitespace-pre-wrap">
164
- {constructInputWithSpecialExpressionList(
165
- materialValue
166
- ).map((inputPart, index) => (
167
- <span
168
- key={index}
169
- className={`${
170
- inputPart.isBold ? "font-bold" : ""
171
- } ${inputPart.isUnderline ? "underline" : ""}`}
172
- >
173
- {inputPart.isEquation ? (
174
- <span className="text-xl">
175
- <InlineMath math={inputPart.value} />
176
- </span>
177
- ) : (
178
- inputPart.value
179
- )}
180
- </span>
181
- ))}
182
- </p>
183
- </div>
184
- ) : (
185
- <ShowMaterialMediaByContentType
186
- key={`${uniqueValue}-${index}`}
187
- contentType={contentMap.type}
188
- src={materialValue}
189
- canFullScreen={true}
190
- />
191
- )}
192
- </div>
300
+ ref={draggedValue === materialValue ? dragElementRef : null}
301
+ className={`${
302
+ draggedValue === materialValue
303
+ ? "opacity-40"
304
+ : selectedValue === materialValue
305
+ ? "ring-2 ring-blue-500"
306
+ : "opacity-100"
307
+ } transition-all duration-200`}
308
+ onMouseDown={(e) => handleMouseDown(e, materialValue)}
309
+ onTouchStart={(e) =>
310
+ handleTouchStart(e, materialValue, e.currentTarget)
193
311
  }
194
- moveCardHandler={() => {
195
- if (!selectedTargetKey) return;
196
- if (!selectedValue) return;
197
- handleMatchingActivityItemOnChange(
198
- selectedTargetKey,
199
- selectedValue
200
- );
201
- }}
202
- />
312
+ onTouchMove={handleTouchMove}
313
+ onTouchEnd={handleTouchEnd}
314
+ >
315
+ <div
316
+ className={`${
317
+ selectedValue === materialValue
318
+ ? "border-catchup-blue"
319
+ : "border-catchup-lighter-gray"
320
+ } ${
321
+ contentMap.type === "TEXT"
322
+ ? "h-catchup-activity-text-box-item"
323
+ : "h-catchup-activity-media-box-item"
324
+ } flex flex-col items-center justify-center border-2 rounded-catchup-xlarge cursor-pointer transition-all duration-300`}
325
+ onClick={() => handleSelectItem(materialValue)}
326
+ >
327
+ {contentMap.type === "TEXT" ? (
328
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px] overflow-y-auto px-4">
329
+ <p className="text-lg whitespace-pre-wrap">
330
+ {constructInputWithSpecialExpressionList(
331
+ materialValue
332
+ ).map((inputPart, index) => (
333
+ <span
334
+ key={index}
335
+ className={`${
336
+ inputPart.isBold ? "font-bold" : ""
337
+ } ${inputPart.isUnderline ? "underline" : ""}`}
338
+ >
339
+ {inputPart.isEquation ? (
340
+ <span className="text-xl">
341
+ <InlineMath math={inputPart.value} />
342
+ </span>
343
+ ) : (
344
+ inputPart.value
345
+ )}
346
+ </span>
347
+ ))}
348
+ </p>
349
+ </div>
350
+ ) : (
351
+ <ShowMaterialMediaByContentType
352
+ key={`${uniqueValue}-${index}`}
353
+ contentType={contentMap.type}
354
+ src={materialValue}
355
+ canFullScreen={true}
356
+ />
357
+ )}
358
+ </div>
359
+ </div>
203
360
  ))}
204
361
  </div>
205
362
  <div className="flex-shrink-0">
@@ -260,11 +417,15 @@ const MatchingActivityMaterialContent = ({
260
417
  <div className="mx-4 w-[2px] bg-catchup-lighter-gray"></div>
261
418
  <div className="flex-1">
262
419
  <div
420
+ data-matching-drop-zone={answerMapKey}
421
+ onMouseEnter={() =>
422
+ draggedValue && setDropTargetKey(answerMapKey)
423
+ }
424
+ onMouseLeave={() => setDropTargetKey(null)}
425
+ onClick={() => handleDropZoneClick(answerMapKey)}
263
426
  className={`${
264
- canDrop
265
- ? selectedTargetKey === answerMapKey
266
- ? "bg-catchup-light-blue"
267
- : "bg-catchup-light-blue opacity-40"
427
+ dropTargetKey === answerMapKey
428
+ ? "bg-catchup-light-blue ring-2 ring-blue-400"
268
429
  : ""
269
430
  } ${
270
431
  contentMap.type === "TEXT"
@@ -279,71 +440,56 @@ const MatchingActivityMaterialContent = ({
279
440
  ? "border-catchup-red"
280
441
  : "border-catchup-blue"
281
442
  }`}
282
- onClick={() => {
283
- if (checkCanAnswerQuestion()) {
284
- setSelectedValue(null);
285
- }
286
- }}
287
443
  >
288
- <DroppableItem
289
- key={index}
290
- item={{ index: answerMapKey }}
291
- type={"MATCHING"}
292
- target={selectedTargetKey}
293
- setTarget={setSelectedTargetKey}
294
- dropRef={drop}
295
- component={
296
- <div
297
- className="h-full flex-1 flex flex-row items-center justify-center px-4"
298
- onClick={(e) => {
299
- e.preventDefault();
300
- if (checkCanAnswerQuestion()) {
301
- handleMatchingActivityItemOnChange(
302
- answerMapKey,
303
- null
304
- );
305
- }
306
- }}
307
- >
308
- {contentMap.type === "TEXT" ? (
309
- <p className="text-lg whitespace-pre-wrap">
310
- {constructInputWithSpecialExpressionList(
311
- answerMap[answerMapKey]
312
- ).map((inputPart, index) => (
313
- <span
314
- key={index}
315
- className={`${
316
- inputPart.isBold ? "font-bold" : ""
317
- } ${inputPart.isUnderline ? "underline" : ""}`}
318
- >
319
- {inputPart.isEquation ? (
320
- <span className="text-xl">
321
- <InlineMath math={inputPart.value} />
322
- </span>
323
- ) : (
324
- inputPart.value
325
- )}
326
- </span>
327
- ))}
328
- </p>
329
- ) : (
330
- <ShowMaterialMediaByContentType
331
- key={`${uniqueValue}-${index}`}
332
- contentType={contentMap.type}
333
- src={answerMap[answerMapKey]}
334
- canFullScreen={false}
335
- />
336
- )}
337
- </div>
338
- }
339
- />
444
+ <div
445
+ className="h-full flex-1 flex flex-row items-center justify-center px-4"
446
+ onClick={(e) => {
447
+ e.stopPropagation();
448
+ if (checkCanAnswerQuestion() && answerMap[answerMapKey]) {
449
+ onChange(answer, answerMapKey, null);
450
+ setSelectedValue(null);
451
+ }
452
+ }}
453
+ >
454
+ {answerMap[answerMapKey] ? (
455
+ contentMap.type === "TEXT" ? (
456
+ <p className="text-lg whitespace-pre-wrap">
457
+ {constructInputWithSpecialExpressionList(
458
+ answerMap[answerMapKey]
459
+ ).map((inputPart, index) => (
460
+ <span
461
+ key={index}
462
+ className={`${
463
+ inputPart.isBold ? "font-bold" : ""
464
+ } ${inputPart.isUnderline ? "underline" : ""}`}
465
+ >
466
+ {inputPart.isEquation ? (
467
+ <span className="text-xl">
468
+ <InlineMath math={inputPart.value} />
469
+ </span>
470
+ ) : (
471
+ inputPart.value
472
+ )}
473
+ </span>
474
+ ))}
475
+ </p>
476
+ ) : (
477
+ <ShowMaterialMediaByContentType
478
+ key={`${uniqueValue}-${index}`}
479
+ contentType={contentMap.type}
480
+ src={answerMap[answerMapKey]}
481
+ canFullScreen={false}
482
+ />
483
+ )
484
+ ) : null}
485
+ </div>
340
486
  </div>
341
487
  </div>
342
488
  </div>
343
489
  );
344
490
  })}
345
491
  </div>
346
- </>
492
+ </div>
347
493
  );
348
494
  };
349
495