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.
@@ -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,18 @@ 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 [touchPosition, setTouchPosition] = useState<{ x: number; y: number }>({
28
+ x: 0,
29
+ y: 0,
32
30
  });
33
31
  const itemsRef = useRef<HTMLDivElement>(null);
34
32
 
@@ -50,32 +48,20 @@ const MatchingActivityMaterialContent = ({
50
48
  materialList.push(materialMap[materialKey]);
51
49
  });
52
50
  setShuffledMaterialList(shuffleArray(materialList));
53
- }, []);
51
+ }, [materialMap, isShuffled]);
54
52
 
55
53
  useEffect(() => {
56
54
  if (!showCorrectAnswer) return;
57
55
  answer.data.find(
58
56
  (answerData: any) => answerData.type === "MATCHING"
59
57
  ).answerMap = materialMap;
60
- }, [showCorrectAnswer]);
58
+ }, [showCorrectAnswer, answer, materialMap]);
61
59
 
62
60
  const retrieveAnswerMap = () => {
63
61
  const foundIndex = answer.data.findIndex(
64
62
  (answerData: any) => answerData.type === "MATCHING"
65
63
  );
66
64
  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
65
  };
80
66
 
81
67
  const retrieveFilteredMaterialList = (answerMap: any) => {
@@ -100,12 +86,79 @@ const MatchingActivityMaterialContent = ({
100
86
  return "INCORRECT";
101
87
  };
102
88
 
103
- const handleMatchingActivityItemOnChange = (
104
- selectedTargetKey: string,
105
- selectedValue: string | null
106
- ) => {
107
- if (checkCanAnswerQuestion()) {
108
- onChange(answer, selectedTargetKey, selectedValue);
89
+ // Mouse drag handlers
90
+ const handleMouseDown = (
91
+ e: React.MouseEvent,
92
+ materialValue: string
93
+ ): void => {
94
+ if (!checkCanAnswerQuestion()) return;
95
+ e.preventDefault();
96
+ setDraggedValue(materialValue);
97
+ setSelectedValue(null);
98
+ };
99
+
100
+ const handleMouseUp = (): void => {
101
+ if (dropTargetKey !== null && draggedValue !== null) {
102
+ onChange(answer, dropTargetKey, draggedValue);
103
+ }
104
+ setDraggedValue(null);
105
+ setDropTargetKey(null);
106
+ };
107
+
108
+ // Touch drag handlers
109
+ const handleTouchStart = (
110
+ e: React.TouchEvent,
111
+ materialValue: string,
112
+ element: HTMLElement
113
+ ): void => {
114
+ if (!checkCanAnswerQuestion()) return;
115
+ const touch = e.touches[0];
116
+ setDraggedValue(materialValue);
117
+ setDraggedElement(element);
118
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
119
+ setSelectedValue(null);
120
+ };
121
+
122
+ const handleTouchMove = (e: React.TouchEvent): void => {
123
+ if (!draggedValue) return;
124
+
125
+ const touch = e.touches[0];
126
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
127
+
128
+ // Find the element under the touch point
129
+ const elementUnder = document.elementFromPoint(
130
+ touch.clientX,
131
+ touch.clientY
132
+ );
133
+ const dropZone = elementUnder?.closest("[data-matching-drop-zone]");
134
+
135
+ if (dropZone) {
136
+ const dropKey = dropZone.getAttribute("data-matching-drop-zone");
137
+ setDropTargetKey(dropKey);
138
+ } else {
139
+ setDropTargetKey(null);
140
+ }
141
+ };
142
+
143
+ const handleTouchEnd = (): void => {
144
+ if (dropTargetKey !== null && draggedValue !== null) {
145
+ onChange(answer, dropTargetKey, draggedValue);
146
+ }
147
+ setDraggedValue(null);
148
+ setDropTargetKey(null);
149
+ setDraggedElement(null);
150
+ };
151
+
152
+ // Click/tap to select (for easier mobile interaction)
153
+ const handleSelectItem = (materialValue: string): void => {
154
+ if (!checkCanAnswerQuestion()) return;
155
+ setSelectedValue(materialValue);
156
+ setDraggedValue(null);
157
+ };
158
+
159
+ const handleDropZoneClick = (answerMapKey: string): void => {
160
+ if (selectedValue !== null) {
161
+ onChange(answer, answerMapKey, selectedValue);
109
162
  setSelectedValue(null);
110
163
  }
111
164
  };
@@ -114,7 +167,55 @@ const MatchingActivityMaterialContent = ({
114
167
  const filteredMaterialList = retrieveFilteredMaterialList(answerMap);
115
168
 
116
169
  return (
117
- <>
170
+ <div onMouseUp={handleMouseUp}>
171
+ {/* Floating drag preview for touch */}
172
+ {draggedValue && touchPosition.x > 0 && (
173
+ <div
174
+ className="fixed pointer-events-none z-50 opacity-80"
175
+ style={{
176
+ left: `${touchPosition.x}px`,
177
+ top: `${touchPosition.y}px`,
178
+ transform: "translate(-50%, -50%)",
179
+ }}
180
+ >
181
+ {contentMap.type === "TEXT" ? (
182
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
183
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px] px-4">
184
+ <p className="text-lg whitespace-pre-wrap">
185
+ {constructInputWithSpecialExpressionList(draggedValue).map(
186
+ (inputPart, index) => (
187
+ <span
188
+ key={index}
189
+ className={`${inputPart.isBold ? "font-bold" : ""} ${
190
+ inputPart.isUnderline ? "underline" : ""
191
+ }`}
192
+ >
193
+ {inputPart.isEquation ? (
194
+ <span className="text-xl">
195
+ <InlineMath math={inputPart.value} />
196
+ </span>
197
+ ) : (
198
+ inputPart.value
199
+ )}
200
+ </span>
201
+ )
202
+ )}
203
+ </p>
204
+ </div>
205
+ </div>
206
+ ) : (
207
+ <div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
208
+ <ShowMaterialMediaByContentType
209
+ key={`${uniqueValue}-drag`}
210
+ contentType={contentMap.type}
211
+ src={draggedValue}
212
+ canFullScreen={false}
213
+ />
214
+ </div>
215
+ )}
216
+ </div>
217
+ )}
218
+
118
219
  {filteredMaterialList.length > 0 ? (
119
220
  <>
120
221
  <div
@@ -122,84 +223,68 @@ const MatchingActivityMaterialContent = ({
122
223
  className="flex-shrink-0 flex flex-row gap-x-4 gap-y-4 overflow-x-auto py-2"
123
224
  >
124
225
  {filteredMaterialList.map((materialValue, index) => (
125
- <DraggableItem
226
+ <div
126
227
  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>
228
+ ref={draggedValue === materialValue ? dragElementRef : null}
229
+ className={`${
230
+ draggedValue === materialValue
231
+ ? "opacity-40"
232
+ : selectedValue === materialValue
233
+ ? "ring-2 ring-blue-500"
234
+ : "opacity-100"
235
+ } transition-all duration-200`}
236
+ onMouseDown={(e) => handleMouseDown(e, materialValue)}
237
+ onTouchStart={(e) =>
238
+ handleTouchStart(e, materialValue, e.currentTarget)
193
239
  }
194
- moveCardHandler={() => {
195
- if (!selectedTargetKey) return;
196
- if (!selectedValue) return;
197
- handleMatchingActivityItemOnChange(
198
- selectedTargetKey,
199
- selectedValue
200
- );
201
- }}
202
- />
240
+ onTouchMove={handleTouchMove}
241
+ onTouchEnd={handleTouchEnd}
242
+ >
243
+ <div
244
+ className={`${
245
+ selectedValue === materialValue
246
+ ? "border-catchup-blue"
247
+ : "border-catchup-lighter-gray"
248
+ } ${
249
+ contentMap.type === "TEXT"
250
+ ? "h-catchup-activity-text-box-item"
251
+ : "h-catchup-activity-media-box-item"
252
+ } flex flex-col items-center justify-center border-2 rounded-catchup-xlarge cursor-pointer transition-all duration-300`}
253
+ onClick={() => handleSelectItem(materialValue)}
254
+ >
255
+ {contentMap.type === "TEXT" ? (
256
+ <div className="flex flex-col items-center justify-center m-2 min-w-[200px] overflow-y-auto px-4">
257
+ <p className="text-lg whitespace-pre-wrap">
258
+ {constructInputWithSpecialExpressionList(
259
+ materialValue
260
+ ).map((inputPart, index) => (
261
+ <span
262
+ key={index}
263
+ className={`${
264
+ inputPart.isBold ? "font-bold" : ""
265
+ } ${inputPart.isUnderline ? "underline" : ""}`}
266
+ >
267
+ {inputPart.isEquation ? (
268
+ <span className="text-xl">
269
+ <InlineMath math={inputPart.value} />
270
+ </span>
271
+ ) : (
272
+ inputPart.value
273
+ )}
274
+ </span>
275
+ ))}
276
+ </p>
277
+ </div>
278
+ ) : (
279
+ <ShowMaterialMediaByContentType
280
+ key={`${uniqueValue}-${index}`}
281
+ contentType={contentMap.type}
282
+ src={materialValue}
283
+ canFullScreen={true}
284
+ />
285
+ )}
286
+ </div>
287
+ </div>
203
288
  ))}
204
289
  </div>
205
290
  <div className="flex-shrink-0">
@@ -260,11 +345,15 @@ const MatchingActivityMaterialContent = ({
260
345
  <div className="mx-4 w-[2px] bg-catchup-lighter-gray"></div>
261
346
  <div className="flex-1">
262
347
  <div
348
+ data-matching-drop-zone={answerMapKey}
349
+ onMouseEnter={() =>
350
+ draggedValue && setDropTargetKey(answerMapKey)
351
+ }
352
+ onMouseLeave={() => setDropTargetKey(null)}
353
+ onClick={() => handleDropZoneClick(answerMapKey)}
263
354
  className={`${
264
- canDrop
265
- ? selectedTargetKey === answerMapKey
266
- ? "bg-catchup-light-blue"
267
- : "bg-catchup-light-blue opacity-40"
355
+ dropTargetKey === answerMapKey
356
+ ? "bg-catchup-light-blue ring-2 ring-blue-400"
268
357
  : ""
269
358
  } ${
270
359
  contentMap.type === "TEXT"
@@ -279,71 +368,56 @@ const MatchingActivityMaterialContent = ({
279
368
  ? "border-catchup-red"
280
369
  : "border-catchup-blue"
281
370
  }`}
282
- onClick={() => {
283
- if (checkCanAnswerQuestion()) {
284
- setSelectedValue(null);
285
- }
286
- }}
287
371
  >
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
- />
372
+ <div
373
+ className="h-full flex-1 flex flex-row items-center justify-center px-4"
374
+ onClick={(e) => {
375
+ e.stopPropagation();
376
+ if (checkCanAnswerQuestion() && answerMap[answerMapKey]) {
377
+ onChange(answer, answerMapKey, null);
378
+ setSelectedValue(null);
379
+ }
380
+ }}
381
+ >
382
+ {answerMap[answerMapKey] ? (
383
+ contentMap.type === "TEXT" ? (
384
+ <p className="text-lg whitespace-pre-wrap">
385
+ {constructInputWithSpecialExpressionList(
386
+ answerMap[answerMapKey]
387
+ ).map((inputPart, index) => (
388
+ <span
389
+ key={index}
390
+ className={`${
391
+ inputPart.isBold ? "font-bold" : ""
392
+ } ${inputPart.isUnderline ? "underline" : ""}`}
393
+ >
394
+ {inputPart.isEquation ? (
395
+ <span className="text-xl">
396
+ <InlineMath math={inputPart.value} />
397
+ </span>
398
+ ) : (
399
+ inputPart.value
400
+ )}
401
+ </span>
402
+ ))}
403
+ </p>
404
+ ) : (
405
+ <ShowMaterialMediaByContentType
406
+ key={`${uniqueValue}-${index}`}
407
+ contentType={contentMap.type}
408
+ src={answerMap[answerMapKey]}
409
+ canFullScreen={false}
410
+ />
411
+ )
412
+ ) : null}
413
+ </div>
340
414
  </div>
341
415
  </div>
342
416
  </div>
343
417
  );
344
418
  })}
345
419
  </div>
346
- </>
420
+ </div>
347
421
  );
348
422
  };
349
423