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/dist/index.js +445 -388
- package/dist/index.mjs +395 -338
- package/package.json +1 -1
- package/src/components/activities/material-contents/FillInTheBlanksActivityMaterialContent.tsx +136 -54
- package/src/components/activities/material-contents/OrderingActivityMaterialContent.tsx +214 -115
- package/src/components/activities/material-contents/OrderingActivityMaterialContent2.tsx +231 -0
package/package.json
CHANGED
package/src/components/activities/material-contents/FillInTheBlanksActivityMaterialContent.tsx
CHANGED
|
@@ -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 = (
|
|
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 = (
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
130
|
-
onDragStart={() => handleDragStart(option)}
|
|
131
|
-
onDragEnd={handleDragEnd}
|
|
215
|
+
ref={draggedOption === option ? dragElementRef : null}
|
|
132
216
|
className={`${
|
|
133
|
-
draggedOption === option
|
|
134
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
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
|
>
|