react-matchings 0.0.6 → 0.0.8
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/LICENSE +21 -21
- package/README.md +215 -610
- package/dist/index.css +2 -2
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -13
- package/dist/index.css.map +0 -1
- package/src/index.css +0 -1
- package/src/index.ts +0 -3
- package/src/lib/utils.ts +0 -6
- package/src/matching.tsx +0 -266
- package/tsconfig.json +0 -15
- package/tsup.config.ts +0 -13
package/src/matching.tsx
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { cn } from "./lib/utils";
|
|
3
|
-
|
|
4
|
-
export type TMatch = {
|
|
5
|
-
questionId: number;
|
|
6
|
-
answerId: number;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
type Props = {
|
|
10
|
-
questions: { id: number; text: string }[];
|
|
11
|
-
answers: { id: number; text: string }[];
|
|
12
|
-
className?: string;
|
|
13
|
-
questionClassName?: string;
|
|
14
|
-
answerClassName?: string;
|
|
15
|
-
lineColor?: string;
|
|
16
|
-
circleColor?: string;
|
|
17
|
-
circleRadius?: number;
|
|
18
|
-
offset?: number;
|
|
19
|
-
disabled?: boolean;
|
|
20
|
-
onChange?: (matches: TMatch[]) => void;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type Line = {
|
|
24
|
-
qId: string;
|
|
25
|
-
aId: string;
|
|
26
|
-
start: { x: number; y: number };
|
|
27
|
-
end: { x: number; y: number };
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function Matching({
|
|
31
|
-
questions,
|
|
32
|
-
answers,
|
|
33
|
-
className,
|
|
34
|
-
questionClassName,
|
|
35
|
-
answerClassName,
|
|
36
|
-
lineColor = "black",
|
|
37
|
-
circleColor = "white",
|
|
38
|
-
circleRadius = 8,
|
|
39
|
-
offset = 10,
|
|
40
|
-
disabled,
|
|
41
|
-
onChange,
|
|
42
|
-
}: Props) {
|
|
43
|
-
const [matches, setMatches] = useState<Record<number, number>>({});
|
|
44
|
-
const [lines, setLines] = useState<Line[]>([]);
|
|
45
|
-
const [dragging, setDragging] = useState<number | null>(null);
|
|
46
|
-
const [dragLine, setDragLine] = useState<{
|
|
47
|
-
start: { x: number; y: number };
|
|
48
|
-
end: { x: number; y: number };
|
|
49
|
-
questionId: number;
|
|
50
|
-
} | null>(null);
|
|
51
|
-
|
|
52
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
53
|
-
const questionRefs = useRef<Record<number, HTMLButtonElement | null>>({});
|
|
54
|
-
const answerRefs = useRef<Record<number, HTMLButtonElement | null>>({});
|
|
55
|
-
|
|
56
|
-
const getElementCenter = useCallback(
|
|
57
|
-
(el: HTMLElement | null, isAnswer = false) => {
|
|
58
|
-
if (!el || !containerRef.current) return null;
|
|
59
|
-
const rect = el.getBoundingClientRect();
|
|
60
|
-
const containerRect = containerRef.current.getBoundingClientRect();
|
|
61
|
-
return {
|
|
62
|
-
x: isAnswer
|
|
63
|
-
? rect.left - containerRect.left + circleRadius + offset
|
|
64
|
-
: rect.right - containerRect.left - circleRadius - offset,
|
|
65
|
-
y: rect.top + rect.height / 2 - containerRect.top,
|
|
66
|
-
};
|
|
67
|
-
},
|
|
68
|
-
[circleRadius, offset]
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const handleMouseDown = (qId: number) => {
|
|
72
|
-
if (disabled) return;
|
|
73
|
-
setDragging(qId);
|
|
74
|
-
requestAnimationFrame(() => {
|
|
75
|
-
const start = getElementCenter(questionRefs.current[qId]);
|
|
76
|
-
if (start) setDragLine({ start, end: start, questionId: qId });
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const handleMouseMove = useCallback(
|
|
81
|
-
(e: MouseEvent) => {
|
|
82
|
-
if (!dragging || !containerRef.current) return;
|
|
83
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
84
|
-
setDragLine((prev) =>
|
|
85
|
-
prev
|
|
86
|
-
? {
|
|
87
|
-
...prev,
|
|
88
|
-
end: { x: e.clientX - rect.left, y: e.clientY - rect.top },
|
|
89
|
-
}
|
|
90
|
-
: null
|
|
91
|
-
);
|
|
92
|
-
},
|
|
93
|
-
[dragging]
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
const handleMouseUp = (aId: number) => {
|
|
97
|
-
if (dragging != null) {
|
|
98
|
-
setMatches((prev) => {
|
|
99
|
-
const newMatches = { ...prev, [dragging]: aId };
|
|
100
|
-
if (onChange) {
|
|
101
|
-
// notify parent immediately on every change
|
|
102
|
-
queueMicrotask(() =>
|
|
103
|
-
onChange(
|
|
104
|
-
Object.entries(newMatches).map(([qId, aId]) => ({
|
|
105
|
-
questionId: Number(qId),
|
|
106
|
-
answerId: aId,
|
|
107
|
-
}))
|
|
108
|
-
)
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
return newMatches;
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
setDragging(null);
|
|
115
|
-
setDragLine(null);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const removeMatch = (qId: number) => {
|
|
119
|
-
setMatches((prev) => {
|
|
120
|
-
const newMatches = { ...prev };
|
|
121
|
-
delete newMatches[qId];
|
|
122
|
-
if (onChange) {
|
|
123
|
-
queueMicrotask(() =>
|
|
124
|
-
onChange(
|
|
125
|
-
Object.entries(newMatches).map(([qId, aId]) => ({
|
|
126
|
-
questionId: Number(qId),
|
|
127
|
-
answerId: aId,
|
|
128
|
-
}))
|
|
129
|
-
)
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
return newMatches;
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
if (!dragging) return;
|
|
138
|
-
const handleUp = () => {
|
|
139
|
-
setDragging(null);
|
|
140
|
-
setDragLine(null);
|
|
141
|
-
};
|
|
142
|
-
document.addEventListener("mousemove", handleMouseMove);
|
|
143
|
-
document.addEventListener("mouseup", handleUp);
|
|
144
|
-
return () => {
|
|
145
|
-
document.removeEventListener("mousemove", handleMouseMove);
|
|
146
|
-
document.removeEventListener("mouseup", handleUp);
|
|
147
|
-
};
|
|
148
|
-
}, [dragging, handleMouseMove]);
|
|
149
|
-
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
if (!containerRef.current) return;
|
|
152
|
-
|
|
153
|
-
const newLines: Line[] = Object.entries(matches)
|
|
154
|
-
.map(([qId, aId]) => {
|
|
155
|
-
const startEl = questionRefs.current[Number(qId)];
|
|
156
|
-
const endEl = answerRefs.current[Number(aId)];
|
|
157
|
-
if (!startEl || !endEl) return null;
|
|
158
|
-
const start = getElementCenter(startEl, false);
|
|
159
|
-
const end = getElementCenter(endEl, true);
|
|
160
|
-
if (!start || !end) return null;
|
|
161
|
-
return { qId, aId: aId.toString(), start, end };
|
|
162
|
-
})
|
|
163
|
-
.filter(Boolean) as Line[];
|
|
164
|
-
|
|
165
|
-
setLines(newLines);
|
|
166
|
-
}, [matches, getElementCenter]);
|
|
167
|
-
|
|
168
|
-
return (
|
|
169
|
-
<div
|
|
170
|
-
className={cn("grid relative grid-cols-2 gap-10 select-none", className)}
|
|
171
|
-
ref={containerRef}
|
|
172
|
-
>
|
|
173
|
-
<svg className="absolute z-20 w-full h-full pointer-events-none">
|
|
174
|
-
{lines.map(({ qId, aId, start, end }) => (
|
|
175
|
-
<g key={`${qId}-${aId}`}>
|
|
176
|
-
<line
|
|
177
|
-
x1={start.x}
|
|
178
|
-
y1={start.y}
|
|
179
|
-
x2={end.x}
|
|
180
|
-
y2={end.y}
|
|
181
|
-
stroke={lineColor}
|
|
182
|
-
strokeWidth={3}
|
|
183
|
-
strokeLinecap="round"
|
|
184
|
-
/>
|
|
185
|
-
<circle
|
|
186
|
-
cx={start.x}
|
|
187
|
-
cy={start.y}
|
|
188
|
-
r={circleRadius}
|
|
189
|
-
fill={circleColor}
|
|
190
|
-
/>
|
|
191
|
-
<circle cx={end.x} cy={end.y} r={circleRadius} fill={circleColor} />
|
|
192
|
-
</g>
|
|
193
|
-
))}
|
|
194
|
-
{dragLine && (
|
|
195
|
-
<g>
|
|
196
|
-
<line
|
|
197
|
-
x1={dragLine.start.x}
|
|
198
|
-
y1={dragLine.start.y}
|
|
199
|
-
x2={dragLine.end.x}
|
|
200
|
-
y2={dragLine.end.y}
|
|
201
|
-
stroke={lineColor}
|
|
202
|
-
strokeWidth={3}
|
|
203
|
-
strokeDasharray="5,5"
|
|
204
|
-
strokeLinecap="round"
|
|
205
|
-
/>
|
|
206
|
-
<circle
|
|
207
|
-
cx={dragLine.start.x}
|
|
208
|
-
cy={dragLine.start.y}
|
|
209
|
-
r={circleRadius}
|
|
210
|
-
fill={circleColor}
|
|
211
|
-
/>
|
|
212
|
-
<circle
|
|
213
|
-
cx={dragLine.end.x}
|
|
214
|
-
cy={dragLine.end.y}
|
|
215
|
-
r={circleRadius}
|
|
216
|
-
fill={circleColor}
|
|
217
|
-
/>
|
|
218
|
-
</g>
|
|
219
|
-
)}
|
|
220
|
-
</svg>
|
|
221
|
-
|
|
222
|
-
<div className="relative z-10 space-y-3">
|
|
223
|
-
{questions.map((q) => {
|
|
224
|
-
const isMatched = matches[q.id] !== undefined;
|
|
225
|
-
return (
|
|
226
|
-
<button
|
|
227
|
-
key={q.id}
|
|
228
|
-
ref={(el) => void (questionRefs.current[q.id] = el)}
|
|
229
|
-
type="button"
|
|
230
|
-
onMouseDown={() => handleMouseDown(q.id)}
|
|
231
|
-
onClick={() => isMatched && removeMatch(q.id)}
|
|
232
|
-
className={cn(
|
|
233
|
-
"p-4 rounded bg-black text-white w-full font-medium focus:outline-none focus:ring-2 focus:ring-gray-500",
|
|
234
|
-
isMatched && "bg-gray-700",
|
|
235
|
-
questionClassName
|
|
236
|
-
)}
|
|
237
|
-
>
|
|
238
|
-
{q.text}
|
|
239
|
-
</button>
|
|
240
|
-
);
|
|
241
|
-
})}
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
<div className="relative z-10 space-y-3">
|
|
245
|
-
{answers.map((a) => {
|
|
246
|
-
const isMatched = Object.values(matches).includes(a.id);
|
|
247
|
-
return (
|
|
248
|
-
<button
|
|
249
|
-
key={a.id}
|
|
250
|
-
ref={(el) => void (answerRefs.current[a.id] = el)}
|
|
251
|
-
type="button"
|
|
252
|
-
onMouseUp={() => handleMouseUp(a.id)}
|
|
253
|
-
className={cn(
|
|
254
|
-
"p-4 rounded bg-black text-white w-full font-medium focus:outline-none focus:ring-2 focus:ring-gray-500",
|
|
255
|
-
isMatched && "bg-gray-700",
|
|
256
|
-
answerClassName
|
|
257
|
-
)}
|
|
258
|
-
>
|
|
259
|
-
{a.text}
|
|
260
|
-
</button>
|
|
261
|
-
);
|
|
262
|
-
})}
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
);
|
|
266
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2019",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Node",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"allowSyntheticDefaultImports": true,
|
|
11
|
-
"strict": true,
|
|
12
|
-
"skipLibCheck": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src"]
|
|
15
|
-
}
|
package/tsup.config.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "tsup";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ["src/index.ts"],
|
|
5
|
-
format: ["cjs", "esm"],
|
|
6
|
-
dts: true,
|
|
7
|
-
sourcemap: true,
|
|
8
|
-
clean: true,
|
|
9
|
-
splitting: false,
|
|
10
|
-
minify: false,
|
|
11
|
-
target: "esnext",
|
|
12
|
-
external: ["react"],
|
|
13
|
-
});
|