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.
@@ -22,6 +22,10 @@ const OrderingActivityMaterialContent = ({
22
22
  null
23
23
  );
24
24
  const dragElementRef = useRef<HTMLDivElement>(null);
25
+ const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({
26
+ x: 0,
27
+ y: 0,
28
+ });
25
29
  const [touchPosition, setTouchPosition] = useState<{ x: number; y: number }>({
26
30
  x: 0,
27
31
  y: 0,
@@ -90,6 +94,24 @@ const OrderingActivityMaterialContent = ({
90
94
  e.preventDefault();
91
95
  setDraggedKey(materialKey);
92
96
  setSelectedKey(null);
97
+ setMousePosition({ x: e.clientX, y: e.clientY });
98
+ };
99
+
100
+ const handleMouseMove = (e: React.MouseEvent): void => {
101
+ if (!draggedKey) return;
102
+
103
+ setMousePosition({ x: e.clientX, y: e.clientY });
104
+
105
+ // Find the element under the mouse point
106
+ const elementUnder = document.elementFromPoint(e.clientX, e.clientY);
107
+ const dropZone = elementUnder?.closest("[data-ordering-drop-zone]");
108
+
109
+ if (dropZone) {
110
+ const dropKey = dropZone.getAttribute("data-ordering-drop-zone");
111
+ setDropTargetKey(dropKey);
112
+ } else {
113
+ setDropTargetKey(null);
114
+ }
93
115
  };
94
116
 
95
117
  const handleMouseUp = (): void => {
@@ -102,6 +124,7 @@ const OrderingActivityMaterialContent = ({
102
124
  }
103
125
  setDraggedKey(null);
104
126
  setDropTargetKey(null);
127
+ setMousePosition({ x: 0, y: 0 });
105
128
  };
106
129
 
107
130
  // Touch drag handlers
@@ -150,6 +173,7 @@ const OrderingActivityMaterialContent = ({
150
173
  setDraggedKey(null);
151
174
  setDropTargetKey(null);
152
175
  setDraggedElement(null);
176
+ setTouchPosition({ x: 0, y: 0 });
153
177
  };
154
178
 
155
179
  // Click/tap to select (for easier mobile interaction)
@@ -170,7 +194,57 @@ const OrderingActivityMaterialContent = ({
170
194
  const answerMap = retrieveAnswerMap();
171
195
 
172
196
  return (
173
- <div className="flex flex-row flex-wrap" onMouseUp={handleMouseUp}>
197
+ <div
198
+ className="flex flex-row flex-wrap"
199
+ onMouseMove={handleMouseMove}
200
+ onMouseUp={handleMouseUp}
201
+ >
202
+ {/* Floating drag preview for mouse */}
203
+ {draggedKey && mousePosition.x > 0 && (
204
+ <div
205
+ className="fixed pointer-events-none z-50 opacity-80"
206
+ style={{
207
+ left: `${mousePosition.x}px`,
208
+ top: `${mousePosition.y}px`,
209
+ transform: "translate(-50%, -50%)",
210
+ }}
211
+ >
212
+ {contentMap.type === "TEXT" ? (
213
+ <div className="border-catchup-blue border-2 px-3 py-2 rounded-catchup-xlarge bg-white shadow-lg">
214
+ <p className="text-xl whitespace-pre-wrap">
215
+ {constructInputWithSpecialExpressionList(
216
+ materialMap[answerMap[draggedKey]]
217
+ ).map((inputPart, index) => (
218
+ <span
219
+ key={index}
220
+ className={`${inputPart.isBold ? "font-bold" : ""} ${
221
+ inputPart.isUnderline ? "underline" : ""
222
+ }`}
223
+ >
224
+ {inputPart.isEquation ? (
225
+ <span className="text-xl">
226
+ <InlineMath math={inputPart.value} />
227
+ </span>
228
+ ) : (
229
+ inputPart.value
230
+ )}
231
+ </span>
232
+ ))}
233
+ </p>
234
+ </div>
235
+ ) : (
236
+ <div className="border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge bg-white shadow-lg">
237
+ <ShowMaterialMediaByContentType
238
+ key={`${uniqueValue}-drag-mouse`}
239
+ contentType={contentMap.type}
240
+ src={materialMap[answerMap[draggedKey]]}
241
+ canFullScreen={false}
242
+ />
243
+ </div>
244
+ )}
245
+ </div>
246
+ )}
247
+
174
248
  {/* Floating drag preview for touch */}
175
249
  {draggedKey && touchPosition.x > 0 && (
176
250
  <div
@@ -207,7 +281,7 @@ const OrderingActivityMaterialContent = ({
207
281
  ) : (
208
282
  <div className="border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge bg-white shadow-lg">
209
283
  <ShowMaterialMediaByContentType
210
- key={`${uniqueValue}-drag`}
284
+ key={`${uniqueValue}-drag-touch`}
211
285
  contentType={contentMap.type}
212
286
  src={materialMap[answerMap[draggedKey]]}
213
287
  canFullScreen={false}
@@ -1,306 +0,0 @@
1
- import { InlineMath } from "react-katex";
2
- import InputGroup from "../../groups/InputGroup";
3
- import { constructInputWithSpecialExpressionList } from "../../../utilization/CatchtivityUtilization";
4
- import i18n from "../../../language/i18n";
5
- import { useState } from "react";
6
- import { useEffect } from "react";
7
- import { useDrop } from "react-dnd";
8
- import BaseImage from "../../images/BaseImage";
9
- import { shuffleArray } from "../../../utilization/AppUtilization";
10
- import DraggableItem from "../../dnds/DraggableItem";
11
- import DroppableItem from "../../dnds/DroppableItem";
12
- import ShowMaterialMediaByContentType from "./ShowMaterialMediaByContentType";
13
- import DividerLine from "../../dividers/DividerLine";
14
- import { IFillInTheBlanksActivityMaterialProps } from "../../../properties/ActivityProperties";
15
- import InputWithSpecialExpression from "../../texts/InputWithSpecialExpression";
16
-
17
- const FillInTheBlanksActivityMaterialContent = ({
18
- uniqueValue,
19
- answer,
20
- optionList,
21
- materialMap,
22
- contentMap,
23
- checkCanAnswerQuestion,
24
- onChange,
25
- isPreview,
26
- showCorrectAnswer,
27
- }: IFillInTheBlanksActivityMaterialProps) => {
28
- const [shuffleOptionList, setShuffleOptionList] = useState([]);
29
- const [selectedOption, setSelectedOption] = useState(null);
30
- const [pasteOptionIndex, setPasteOptionIndex] = useState(null);
31
- const [{ isOver, canDrop }, drop] = useDrop({
32
- accept: "FILL_IN_THE_BLANKS",
33
- drop: () => {},
34
- collect: (monitor) => ({
35
- isOver: monitor.isOver(),
36
- canDrop: monitor.canDrop(),
37
- }),
38
- });
39
-
40
- useEffect(() => {
41
- setShuffleOptionList(shuffleArray(optionList));
42
- }, []);
43
-
44
- useEffect(() => {
45
- if (!showCorrectAnswer) return;
46
- const foundAnswer = answer.data.find(
47
- (answerData: any) => answerData.type === "FILL_IN_THE_BLANKS"
48
- );
49
- if (foundAnswer.answerMap.length === 0) return;
50
- if (Object.keys(materialMap).length === 0) return;
51
- foundAnswer.answerMap = Object.keys(materialMap).map(
52
- (materialMapKey) => JSON.parse(materialMap[materialMapKey])[0]
53
- );
54
-
55
- onChange(answer, 0, JSON.parse(materialMap[0])[0]);
56
- }, [showCorrectAnswer]);
57
-
58
- const retrieveAnswerMap = () => {
59
- const foundIndex = answer.data.findIndex(
60
- (answerData: any) => answerData.type === "FILL_IN_THE_BLANKS"
61
- );
62
- return answer.data[foundIndex].answerMap;
63
- };
64
-
65
- const checkAnswerState = (correctAnswerList: any, learnerAnswer: string) => {
66
- if (!isPreview) return null;
67
- const foundIndex = correctAnswerList.findIndex(
68
- (correctAnswer: string) => correctAnswer === learnerAnswer
69
- );
70
- if (foundIndex !== -1) {
71
- return "CORRECT";
72
- }
73
- return "INCORRECT";
74
- };
75
-
76
- const checkAnswerProvided = (answerMap: any, option: string) => {
77
- return (
78
- Object.keys(answerMap).findIndex((key) => answerMap[key] === option) !==
79
- -1
80
- );
81
- };
82
-
83
- const handleSelectOption = (option: any) => {
84
- setSelectedOption(option);
85
- setPasteOptionIndex(null);
86
- };
87
-
88
- const answerMap = retrieveAnswerMap();
89
-
90
- return (
91
- <div
92
- className="flex flex-row flex-wrap items-center"
93
- // onMouseUp={() => {}}
94
- // onTouchStart={() => {}}
95
- >
96
- <div className="hidden md:block">
97
- <span className="font-semibold text-xl opacity-60">
98
- {i18n.t("please_select_fill_in_the_blanks_text")}
99
- </span>
100
- </div>
101
- <div className="hidden md:contents">
102
- <DividerLine />
103
- </div>
104
-
105
- <div className="w-full flex flex-row flex-wrap gap-x-2 gap-y-2 my-2">
106
- {shuffleOptionList.map((option, index) =>
107
- checkAnswerProvided(answerMap, option) ? (
108
- <div className="opacity-30" key={index}>
109
- <ShowMaterialMediaByContentType
110
- key={`${uniqueValue}-${index}`}
111
- contentType={contentMap.type}
112
- src={option}
113
- canFullScreen={true}
114
- />
115
- </div>
116
- ) : (
117
- <DraggableItem
118
- key={index}
119
- item={{ index: option }}
120
- type={"FILL_IN_THE_BLANKS"}
121
- component={
122
- contentMap.type === "TEXT" ? (
123
- <div
124
- className="border-catchup-blue border-2 px-2 rounded-catchup-xlarge cursor-pointer select-none"
125
- onClick={() => handleSelectOption(option)}
126
- onMouseDown={() => {
127
- setSelectedOption(option);
128
- setPasteOptionIndex(null);
129
- }}
130
- onTouchEnd={() => {
131
- setSelectedOption(option);
132
- setPasteOptionIndex(null);
133
- }}
134
- >
135
- <p className="italic whitespace-pre-wrap">
136
- <InputWithSpecialExpression
137
- value={option}
138
- showSpecialOnly={false}
139
- />
140
- </p>
141
- </div>
142
- ) : (
143
- <div
144
- className="border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge cursor-pointer select-none"
145
- onClick={() => handleSelectOption(option)}
146
- onMouseDown={() => {
147
- setSelectedOption(option);
148
- setPasteOptionIndex(null);
149
- }}
150
- onTouchEnd={() => {
151
- setSelectedOption(option);
152
- setPasteOptionIndex(null);
153
- }}
154
- >
155
- <ShowMaterialMediaByContentType
156
- key={`${uniqueValue}-${index}`}
157
- contentType={contentMap.type}
158
- src={option}
159
- canFullScreen={true}
160
- />
161
- </div>
162
- )
163
- }
164
- moveCardHandler={() => {
165
- onChange(answer, pasteOptionIndex, selectedOption);
166
- }}
167
- />
168
- )
169
- )}
170
- </div>
171
- <div className="w-full flex flex-row flex-wrap">
172
- {Object.keys(answerMap).map((materialKey, index) => {
173
- const learnerAnswerState = checkAnswerState(
174
- JSON.parse(materialMap[materialKey]),
175
- answerMap[materialKey]
176
- );
177
- return (
178
- <div key={index} className="w-full md:w-1/2">
179
- <div className="mx-2">
180
- <DroppableItem
181
- key={index}
182
- item={{ index }}
183
- type={"FILL_IN_THE_BLANKS"}
184
- target={pasteOptionIndex}
185
- setTarget={setPasteOptionIndex}
186
- dropRef={drop}
187
- component={
188
- <div className="w-full flex flex-row my-2 gap-x-2">
189
- <div className="my-auto">
190
- <p className="text-xl">
191
- {parseFloat(materialKey) + 1}.
192
- </p>
193
- </div>
194
- <div className="flex-1">
195
- {checkCanAnswerQuestion() ? (
196
- contentMap.type === "TEXT" ? (
197
- <div className="relative">
198
- <div className="flex-1">
199
- <div
200
- className={`w-full min-h-[44px] border rounded-lg ${
201
- answerMap[materialKey]
202
- ? "border-catchup-blue-400 px-2"
203
- : "bg-catchup-gray-50 border-catchup-gray-200 border-dashed py-2 px-4"
204
- }`}
205
- onClick={() => {
206
- if (answerMap[materialKey]) {
207
- onChange(answer, materialKey, "");
208
- }
209
- }}
210
- >
211
- {answerMap[materialKey] ? (
212
- <InputWithSpecialExpression
213
- value={answerMap[materialKey]}
214
- showSpecialOnly={false}
215
- />
216
- ) : (
217
- <p className="text-gray-400 italic"></p>
218
- )}
219
- </div>
220
- </div>
221
-
222
- {learnerAnswerState === "CORRECT" ? (
223
- <div className="absolute -top-[10px] right-4 bg-catchup-white">
224
- <BaseImage
225
- src="/icons/checkbox.webp"
226
- alt="checkbox"
227
- size="small"
228
- />
229
- </div>
230
- ) : learnerAnswerState === "INCORRECT" ? (
231
- <div className="absolute -top-[10px] right-4 bg-catchup-white">
232
- <BaseImage
233
- src="/icons/cross-red.webp"
234
- alt="cross-red"
235
- size="small"
236
- />
237
- </div>
238
- ) : null}
239
- </div>
240
- ) : answerMap[materialKey] === "" ? (
241
- <div
242
- className={`w-catchup-activity-media-box-item h-catchup-activity-media-box-item border rounded-catchup-xlarge border-dashed ${
243
- learnerAnswerState === "CORRECT"
244
- ? "border-catchup-green"
245
- : learnerAnswerState === "INCORRECT"
246
- ? "border-catchup-red"
247
- : "border-catchup-blue"
248
- }`}
249
- >
250
- <div className="h-full flex flex-col items-center justify-center px-4 py-2">
251
- <span className="italic">
252
- {i18n.t("please_drop_here")}
253
- </span>
254
- </div>
255
- </div>
256
- ) : (
257
- <div
258
- className="flex-1 cursor-pointer"
259
- onClick={() => {
260
- onChange(answer, materialKey, "");
261
- }}
262
- >
263
- <ShowMaterialMediaByContentType
264
- key={`${uniqueValue}-${index}`}
265
- contentType={contentMap.type}
266
- src={answerMap[materialKey]}
267
- canFullScreen={true}
268
- />
269
- </div>
270
- )
271
- ) : (
272
- <p key={materialKey} className="text-xl">
273
- {constructInputWithSpecialExpressionList(
274
- answerMap[materialKey]
275
- ).map((inputPart, index) => (
276
- <span
277
- key={index}
278
- className={`${
279
- inputPart.isBold ? "font-bold" : ""
280
- } ${inputPart.isUnderline ? "underline" : ""}`}
281
- >
282
- {inputPart.isEquation ? (
283
- <span className="text-xl">
284
- <InlineMath math={inputPart.value} />
285
- </span>
286
- ) : (
287
- inputPart.value
288
- )}
289
- </span>
290
- ))}
291
- </p>
292
- )}
293
- </div>
294
- </div>
295
- }
296
- />
297
- </div>
298
- </div>
299
- );
300
- })}
301
- </div>
302
- </div>
303
- );
304
- };
305
-
306
- export default FillInTheBlanksActivityMaterialContent;
@@ -1,231 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { useDrop } from "react-dnd";
3
- import ShowMaterialMediaByContentType from "./ShowMaterialMediaByContentType";
4
- import { InlineMath } from "react-katex";
5
- import useScreenSize from "../../../hooks/useScreenSize";
6
- import { constructInputWithSpecialExpressionList } from "../../../utilization/CatchtivityUtilization";
7
- import { IOrderingActivityMaterialProps } from "../../../properties/ActivityProperties";
8
- import DraggableDroppableItem from "../../dnds/DraggableDroppableItem";
9
-
10
- const OrderingActivityMaterialContent = ({
11
- uniqueValue,
12
- answer,
13
- materialMap,
14
- contentMap,
15
- checkCanAnswerQuestion,
16
- onChange,
17
- isPreview,
18
- showCorrectAnswer,
19
- }: IOrderingActivityMaterialProps) => {
20
- const [selectedTargetKey, setSelectedTargetKey] = useState(null);
21
- const [selectedKey, setSelectedKey] = useState<string | null>(null);
22
- const { screenSize } = useScreenSize();
23
- const [view, setView] = useState("PC");
24
- const [{ isOver, canDrop }, drop] = useDrop({
25
- accept: "ORDERING",
26
- drop: () => {},
27
- collect: (monitor) => ({
28
- isOver: monitor.isOver(),
29
- canDrop: monitor.canDrop(),
30
- }),
31
- });
32
-
33
- useEffect(() => {
34
- if (!screenSize) return;
35
- if (screenSize.width <= 1024) {
36
- setView("TABLET");
37
- } else {
38
- setView("PC");
39
- }
40
- }, [screenSize]);
41
-
42
- useEffect(() => {
43
- if (!showCorrectAnswer) return;
44
- const answerMap = answer.data.find(
45
- (answerData: any) => answerData.type === "ORDERING"
46
- ).answerMap;
47
- Object.keys(answerMap).forEach((answerKey, index) => {
48
- answerMap[answerKey] = index;
49
- });
50
- }, [showCorrectAnswer]);
51
-
52
- const retrieveAnswerMap = () => {
53
- const foundIndex = answer.data.findIndex(
54
- (answerData: any) => answerData.type === "ORDERING"
55
- );
56
- return answer.data[foundIndex].answerMap;
57
- };
58
-
59
- const checkAnswerState = (correctAnswer: string, learnerAnswer: string) => {
60
- if (!isPreview) return null;
61
- if (correctAnswer === learnerAnswer) {
62
- return "CORRECT";
63
- }
64
- return "INCORRECT";
65
- };
66
-
67
- const handleOrderingActivityItemChange = (
68
- selectedKey: string,
69
- materialKey: string
70
- ) => {
71
- if (checkCanAnswerQuestion()) {
72
- if (selectedKey) {
73
- onChange(answer, selectedKey, materialKey);
74
- setSelectedKey(null);
75
- } else {
76
- setSelectedKey(materialKey);
77
- }
78
- }
79
- };
80
-
81
- const calculateMarginTop = (index: number) => {
82
- if (index === 0) {
83
- if (contentMap.type === "TEXT") {
84
- return 0;
85
- } else {
86
- return 0;
87
- }
88
- } else if (index === 1) {
89
- if (contentMap.type === "TEXT") {
90
- return 65;
91
- } else {
92
- return 130;
93
- }
94
- } else if (index % 2 === 0) {
95
- if (contentMap.type === "TEXT") {
96
- return -65;
97
- } else {
98
- return -130;
99
- }
100
- } else if (index % 2 === 1) {
101
- if (contentMap.type === "TEXT") {
102
- return 0;
103
- } else {
104
- return 0;
105
- }
106
- }
107
- return 0;
108
- };
109
-
110
- const answerMap = retrieveAnswerMap();
111
-
112
- return (
113
- <div className="flex flex-row flex-wrap">
114
- {Object.keys(answerMap).map((materialKey, index) => {
115
- const learnerAnswerState = checkAnswerState(
116
- answerMap[materialKey] + "",
117
- index + ""
118
- );
119
- return (
120
- <div className="w-full lg:w-1/2" key={index}>
121
- <div
122
- className={`flex flex-row items-center my-4 mx-2`}
123
- style={{
124
- marginTop:
125
- view === "PC"
126
- ? calculateMarginTop(parseFloat(materialKey))
127
- : 0,
128
- }}
129
- >
130
- <div className="mr-3">
131
- <div className="h-catchup-activity-box-item w-catchup-activity-box-item flex flex-col items-center justify-center cursor-pointer transition-all duration-300 overflow-y-auto">
132
- <div
133
- className={`${
134
- selectedKey === materialKey
135
- ? "border-2 border-catchup-light-gray"
136
- : "border-2 border-catchup-blue"
137
- } flex flex-col items-center justify-center transition-all duration-300 rounded-catchup-full w-[50px] h-[50px]`}
138
- >
139
- <p className="">{parseFloat(materialKey) + 1}</p>
140
- </div>
141
- </div>
142
- </div>
143
-
144
- <DraggableDroppableItem
145
- key={index}
146
- item={{ index: materialKey }}
147
- type={"ORDERING"}
148
- dropRef={drop}
149
- component={
150
- <div
151
- className={`${
152
- canDrop
153
- ? selectedKey !== materialKey
154
- ? selectedTargetKey === materialKey
155
- ? "bg-catchup-light-blue rounded-catchup-xlarge"
156
- : "bg-catchup-light-blue rounded-catchup-xlarge opacity-40"
157
- : ""
158
- : ""
159
- } ${
160
- contentMap.type === "TEXT"
161
- ? "h-catchup-activity-text-box-item"
162
- : "h-catchup-activity-media-box-item"
163
- } flex flex-col items-center justify-center border-2 rounded-catchup-xlarge cursor-pointer p-3 ${
164
- learnerAnswerState === "CORRECT"
165
- ? "border-catchup-green"
166
- : learnerAnswerState === "INCORRECT"
167
- ? "border-catchup-red"
168
- : "border-catchup-blue"
169
- }`}
170
- onMouseDown={() => {
171
- if (checkCanAnswerQuestion()) {
172
- setSelectedKey(materialKey);
173
- }
174
- }}
175
- onTouchEnd={() => {
176
- if (checkCanAnswerQuestion()) {
177
- setSelectedKey(materialKey);
178
- }
179
- }}
180
- >
181
- {contentMap.type === "TEXT" ? (
182
- <p className="text-xl whitespace-pre-wrap">
183
- {constructInputWithSpecialExpressionList(
184
- materialMap[answerMap[materialKey]]
185
- ).map((inputPart, index) => (
186
- <span
187
- key={index}
188
- className={`${
189
- inputPart.isBold ? "font-bold" : ""
190
- } ${inputPart.isUnderline ? "underline" : ""}`}
191
- >
192
- {inputPart.isEquation ? (
193
- <span className="text-xl">
194
- <InlineMath math={inputPart.value} />
195
- </span>
196
- ) : (
197
- inputPart.value
198
- )}
199
- </span>
200
- ))}
201
- </p>
202
- ) : (
203
- <ShowMaterialMediaByContentType
204
- key={`${uniqueValue}-${index}`}
205
- contentType={contentMap.type}
206
- src={materialMap[answerMap[materialKey]]}
207
- canFullScreen={true}
208
- />
209
- )}
210
- </div>
211
- }
212
- target={selectedTargetKey}
213
- setTarget={setSelectedTargetKey}
214
- moveCardHandler={() => {
215
- if (!selectedKey) return;
216
- if (!selectedTargetKey) return;
217
- handleOrderingActivityItemChange(
218
- selectedKey,
219
- selectedTargetKey
220
- );
221
- }}
222
- />
223
- </div>
224
- </div>
225
- );
226
- })}
227
- </div>
228
- );
229
- };
230
-
231
- export default OrderingActivityMaterialContent;