@node-edit-utils/core 2.3.2 → 2.3.4
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/lib/canvas/helpers/getCanvasContainerOrBody.d.ts +1 -0
- package/dist/lib/canvas/helpers/getCanvasWindowValue.d.ts +1 -1
- package/dist/lib/helpers/adjustForZoom.d.ts +1 -0
- package/dist/lib/helpers/createDragHandler.d.ts +69 -0
- package/dist/lib/helpers/getNodeProvider.d.ts +1 -0
- package/dist/lib/helpers/getNodeTools.d.ts +2 -0
- package/dist/lib/helpers/getViewportDimensions.d.ts +4 -0
- package/dist/lib/helpers/index.d.ts +9 -1
- package/dist/lib/helpers/parseTransform.d.ts +8 -0
- package/dist/lib/helpers/toggleClass.d.ts +1 -0
- package/dist/lib/viewport/label/getViewportLabelOverlay.d.ts +1 -0
- package/dist/lib/viewport/label/helpers/selectFirstViewportNode.d.ts +5 -0
- package/dist/lib/viewport/label/index.d.ts +5 -3
- package/dist/lib/viewport/label/isViewportDragging.d.ts +2 -0
- package/dist/lib/viewport/label/refreshViewportLabel.d.ts +8 -0
- package/dist/lib/viewport/label/removeViewportLabel.d.ts +5 -0
- package/dist/lib/viewport/label/setupViewportDrag.d.ts +1 -0
- package/dist/node-edit-utils.cjs.js +342 -280
- package/dist/node-edit-utils.esm.js +342 -280
- package/dist/node-edit-utils.umd.js +342 -280
- package/dist/node-edit-utils.umd.min.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -2
- package/src/lib/canvas/createCanvasObserver.test.ts +242 -0
- package/src/lib/canvas/createCanvasObserver.ts +2 -2
- package/src/lib/canvas/disableCanvasKeyboard.test.ts +53 -0
- package/src/lib/canvas/disableCanvasKeyboard.ts +1 -1
- package/src/lib/canvas/disableCanvasTextMode.test.ts +53 -0
- package/src/lib/canvas/disableCanvasTextMode.ts +1 -1
- package/src/lib/canvas/enableCanvasKeyboard.test.ts +53 -0
- package/src/lib/canvas/enableCanvasKeyboard.ts +1 -1
- package/src/lib/canvas/enableCanvasTextMode.test.ts +53 -0
- package/src/lib/canvas/enableCanvasTextMode.ts +1 -1
- package/src/lib/canvas/helpers/applyCanvasState.test.ts +119 -0
- package/src/lib/canvas/helpers/applyCanvasState.ts +1 -1
- package/src/lib/canvas/helpers/getCanvasContainer.test.ts +62 -0
- package/src/lib/canvas/helpers/getCanvasContainerOrBody.test.ts +51 -0
- package/src/lib/canvas/helpers/getCanvasContainerOrBody.ts +6 -0
- package/src/lib/canvas/helpers/getCanvasWindowValue.test.ts +116 -0
- package/src/lib/canvas/helpers/getCanvasWindowValue.ts +2 -3
- package/src/lib/helpers/adjustForZoom.test.ts +65 -0
- package/src/lib/helpers/adjustForZoom.ts +4 -0
- package/src/lib/helpers/createDragHandler.test.ts +325 -0
- package/src/lib/helpers/createDragHandler.ts +171 -0
- package/src/lib/helpers/getNodeProvider.test.ts +71 -0
- package/src/lib/helpers/getNodeProvider.ts +4 -0
- package/src/lib/helpers/getNodeTools.test.ts +50 -0
- package/src/lib/helpers/getNodeTools.ts +6 -0
- package/src/lib/helpers/getViewportDimensions.test.ts +93 -0
- package/src/lib/helpers/getViewportDimensions.ts +7 -0
- package/src/lib/helpers/index.ts +9 -1
- package/src/lib/helpers/observer/connectMutationObserver.test.ts +127 -0
- package/src/lib/helpers/observer/connectResizeObserver.test.ts +147 -0
- package/src/lib/helpers/parseTransform.test.ts +117 -0
- package/src/lib/helpers/parseTransform.ts +9 -0
- package/src/lib/helpers/toggleClass.test.ts +71 -0
- package/src/lib/helpers/toggleClass.ts +9 -0
- package/src/lib/helpers/withRAF.test.ts +439 -0
- package/src/lib/node-tools/createNodeTools.test.ts +373 -0
- package/src/lib/node-tools/createNodeTools.ts +0 -1
- package/src/lib/node-tools/events/click/handleNodeClick.test.ts +109 -0
- package/src/lib/node-tools/events/setupEventListener.test.ts +136 -0
- package/src/lib/node-tools/highlight/clearHighlightFrame.test.ts +88 -0
- package/src/lib/node-tools/highlight/clearHighlightFrame.ts +2 -3
- package/src/lib/node-tools/highlight/createCornerHandles.test.ts +150 -0
- package/src/lib/node-tools/highlight/createHighlightFrame.test.ts +237 -0
- package/src/lib/node-tools/highlight/createHighlightFrame.ts +5 -9
- package/src/lib/node-tools/highlight/createTagLabel.test.ts +135 -0
- package/src/lib/node-tools/highlight/createToolsContainer.test.ts +97 -0
- package/src/lib/node-tools/highlight/createToolsContainer.ts +3 -6
- package/src/lib/node-tools/highlight/helpers/getElementBounds.test.ts +158 -0
- package/src/lib/node-tools/highlight/helpers/getElementBounds.ts +6 -5
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.test.ts +78 -0
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +2 -3
- package/src/lib/node-tools/highlight/helpers/getScreenBounds.test.ts +133 -0
- package/src/lib/node-tools/highlight/highlightNode.test.ts +213 -0
- package/src/lib/node-tools/highlight/highlightNode.ts +7 -15
- package/src/lib/node-tools/highlight/refreshHighlightFrame.test.ts +323 -0
- package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +12 -42
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.test.ts +110 -0
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +2 -3
- package/src/lib/node-tools/select/helpers/getElementsFromPoint.test.ts +109 -0
- package/src/lib/node-tools/select/helpers/isInsideComponent.test.ts +81 -0
- package/src/lib/node-tools/select/helpers/isInsideViewport.test.ts +82 -0
- package/src/lib/node-tools/select/helpers/targetSameCandidates.test.ts +81 -0
- package/src/lib/node-tools/select/selectNode.test.ts +238 -0
- package/src/lib/node-tools/text/events/setupKeydownHandler.test.ts +91 -0
- package/src/lib/node-tools/text/events/setupMutationObserver.test.ts +213 -0
- package/src/lib/node-tools/text/events/setupNodeListeners.test.ts +133 -0
- package/src/lib/node-tools/text/helpers/enterTextEditMode.test.ts +50 -0
- package/src/lib/node-tools/text/helpers/handleTextChange.test.ts +201 -0
- package/src/lib/node-tools/text/helpers/hasTextContent.test.ts +101 -0
- package/src/lib/node-tools/text/helpers/insertLineBreak.test.ts +96 -0
- package/src/lib/node-tools/text/helpers/makeNodeEditable.test.ts +56 -0
- package/src/lib/node-tools/text/helpers/makeNodeNonEditable.test.ts +57 -0
- package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.test.ts +61 -0
- package/src/lib/node-tools/text/nodeText.test.ts +233 -0
- package/src/lib/post-message/processPostMessage.test.ts +218 -0
- package/src/lib/post-message/sendPostMessage.test.ts +120 -0
- package/src/lib/styles/styles.css +3 -3
- package/src/lib/viewport/createViewport.test.ts +267 -0
- package/src/lib/viewport/createViewport.ts +51 -51
- package/src/lib/viewport/events/setupEventListener.test.ts +103 -0
- package/src/lib/viewport/label/getViewportLabelOverlay.test.ts +77 -0
- package/src/lib/viewport/label/{getViewportLabelsOverlay.ts → getViewportLabelOverlay.ts} +6 -6
- package/src/lib/viewport/label/helpers/getLabelPosition.test.ts +51 -0
- package/src/lib/viewport/label/helpers/getLabelPosition.ts +3 -5
- package/src/lib/viewport/label/helpers/getTransformValues.test.ts +59 -0
- package/src/lib/viewport/label/helpers/getTransformValues.ts +3 -5
- package/src/lib/viewport/label/helpers/getZoomValue.test.ts +53 -0
- package/src/lib/viewport/label/helpers/selectFirstViewportNode.test.ts +105 -0
- package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +26 -0
- package/src/lib/viewport/label/index.ts +5 -3
- package/src/lib/viewport/label/isViewportDragging.test.ts +35 -0
- package/src/lib/viewport/label/isViewportDragging.ts +9 -0
- package/src/lib/viewport/label/refreshViewportLabel.test.ts +105 -0
- package/src/lib/viewport/label/refreshViewportLabel.ts +50 -0
- package/src/lib/viewport/label/refreshViewportLabels.test.ts +107 -0
- package/src/lib/viewport/label/refreshViewportLabels.ts +19 -52
- package/src/lib/viewport/label/removeViewportLabel.test.ts +67 -0
- package/src/lib/viewport/label/removeViewportLabel.ts +20 -0
- package/src/lib/viewport/label/setupViewportDrag.test.ts +249 -0
- package/src/lib/viewport/label/setupViewportDrag.ts +70 -0
- package/src/lib/viewport/resize/createResizeHandle.test.ts +37 -0
- package/src/lib/viewport/resize/createResizePresets.test.ts +75 -0
- package/src/lib/viewport/resize/updateActivePreset.test.ts +92 -0
- package/src/lib/viewport/width/calcConstrainedWidth.test.ts +47 -0
- package/src/lib/viewport/width/calcWidth.test.ts +68 -0
- package/src/lib/viewport/width/updateWidth.test.ts +78 -0
- package/src/lib/window/bindToWindow.test.ts +166 -0
- package/src/lib/window/bindToWindow.ts +1 -2
- package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +0 -1
- package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +0 -2
- package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +0 -1
- package/src/lib/viewport/label/isViewportLabelDragging.ts +0 -9
- package/src/lib/viewport/label/setupViewportLabelDrag.ts +0 -98
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { DragCallbacks } from "./createDragHandler";
|
|
3
|
+
import { createDragHandler } from "./createDragHandler";
|
|
4
|
+
|
|
5
|
+
describe("createDragHandler", () => {
|
|
6
|
+
let element: HTMLElement;
|
|
7
|
+
let callbacks: DragCallbacks;
|
|
8
|
+
let cleanup: () => void;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
element = document.createElement("div");
|
|
12
|
+
document.body.appendChild(element);
|
|
13
|
+
|
|
14
|
+
callbacks = {
|
|
15
|
+
onStart: vi.fn(),
|
|
16
|
+
onDrag: vi.fn(),
|
|
17
|
+
onStop: vi.fn(),
|
|
18
|
+
onCancel: vi.fn(),
|
|
19
|
+
onPreventClick: vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
if (cleanup) {
|
|
25
|
+
cleanup();
|
|
26
|
+
}
|
|
27
|
+
if (document.body.contains(element)) {
|
|
28
|
+
document.body.removeChild(element);
|
|
29
|
+
}
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return cleanup function", () => {
|
|
34
|
+
cleanup = createDragHandler(element, callbacks);
|
|
35
|
+
|
|
36
|
+
expect(typeof cleanup).toBe("function");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should call onStart on mousedown", () => {
|
|
40
|
+
cleanup = createDragHandler(element, callbacks);
|
|
41
|
+
|
|
42
|
+
const event = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
43
|
+
element.dispatchEvent(event);
|
|
44
|
+
|
|
45
|
+
expect(callbacks.onStart).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(callbacks.onStart).toHaveBeenCalledWith(
|
|
47
|
+
expect.any(MouseEvent),
|
|
48
|
+
expect.objectContaining({
|
|
49
|
+
isDragging: true,
|
|
50
|
+
hasDragged: false,
|
|
51
|
+
startX: 100,
|
|
52
|
+
startY: 200,
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should call onDrag on mousemove after drag starts", () => {
|
|
58
|
+
cleanup = createDragHandler(element, callbacks);
|
|
59
|
+
|
|
60
|
+
// Start drag
|
|
61
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
62
|
+
element.dispatchEvent(startEvent);
|
|
63
|
+
|
|
64
|
+
// Move mouse
|
|
65
|
+
const moveEvent = new MouseEvent("mousemove", { clientX: 150, clientY: 250, bubbles: true });
|
|
66
|
+
document.dispatchEvent(moveEvent);
|
|
67
|
+
|
|
68
|
+
expect(callbacks.onDrag).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(callbacks.onDrag).toHaveBeenCalledWith(
|
|
70
|
+
expect.any(MouseEvent),
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
isDragging: true,
|
|
73
|
+
hasDragged: true,
|
|
74
|
+
startX: 100,
|
|
75
|
+
startY: 200,
|
|
76
|
+
deltaX: 50,
|
|
77
|
+
deltaY: 50,
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should not call onDrag before drag starts", () => {
|
|
83
|
+
cleanup = createDragHandler(element, callbacks);
|
|
84
|
+
|
|
85
|
+
const moveEvent = new MouseEvent("mousemove", { clientX: 150, clientY: 250, bubbles: true });
|
|
86
|
+
document.dispatchEvent(moveEvent);
|
|
87
|
+
|
|
88
|
+
expect(callbacks.onDrag).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should call onStop on mouseup after drag starts", () => {
|
|
92
|
+
cleanup = createDragHandler(element, callbacks);
|
|
93
|
+
|
|
94
|
+
// Start drag
|
|
95
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
96
|
+
element.dispatchEvent(startEvent);
|
|
97
|
+
|
|
98
|
+
// Stop drag
|
|
99
|
+
const stopEvent = new MouseEvent("mouseup", { bubbles: true });
|
|
100
|
+
document.dispatchEvent(stopEvent);
|
|
101
|
+
|
|
102
|
+
expect(callbacks.onStop).toHaveBeenCalledTimes(1);
|
|
103
|
+
expect(callbacks.onStop).toHaveBeenCalledWith(
|
|
104
|
+
expect.any(MouseEvent),
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
isDragging: false,
|
|
107
|
+
hasDragged: false,
|
|
108
|
+
startX: 100,
|
|
109
|
+
startY: 200,
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should not call onStop if drag hasn't started", () => {
|
|
115
|
+
cleanup = createDragHandler(element, callbacks);
|
|
116
|
+
|
|
117
|
+
const stopEvent = new MouseEvent("mouseup", { bubbles: true });
|
|
118
|
+
document.dispatchEvent(stopEvent);
|
|
119
|
+
|
|
120
|
+
expect(callbacks.onStop).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should call onCancel on window blur during drag", () => {
|
|
124
|
+
cleanup = createDragHandler(element, callbacks);
|
|
125
|
+
|
|
126
|
+
// Start drag
|
|
127
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
128
|
+
element.dispatchEvent(startEvent);
|
|
129
|
+
|
|
130
|
+
// Blur window
|
|
131
|
+
window.dispatchEvent(new Event("blur"));
|
|
132
|
+
|
|
133
|
+
expect(callbacks.onCancel).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(callbacks.onCancel).toHaveBeenCalledWith(
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
isDragging: false,
|
|
137
|
+
hasDragged: false,
|
|
138
|
+
startX: 100,
|
|
139
|
+
startY: 200,
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should not call onCancel if drag hasn't started", () => {
|
|
145
|
+
cleanup = createDragHandler(element, callbacks);
|
|
146
|
+
|
|
147
|
+
window.dispatchEvent(new Event("blur"));
|
|
148
|
+
|
|
149
|
+
expect(callbacks.onCancel).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should call onPreventClick on click after drag", () => {
|
|
153
|
+
cleanup = createDragHandler(element, callbacks);
|
|
154
|
+
|
|
155
|
+
// Start drag
|
|
156
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
157
|
+
element.dispatchEvent(startEvent);
|
|
158
|
+
|
|
159
|
+
// Move mouse to set hasDragged
|
|
160
|
+
const moveEvent = new MouseEvent("mousemove", { clientX: 150, clientY: 250, bubbles: true });
|
|
161
|
+
document.dispatchEvent(moveEvent);
|
|
162
|
+
|
|
163
|
+
// Stop drag
|
|
164
|
+
const stopEvent = new MouseEvent("mouseup", { bubbles: true });
|
|
165
|
+
document.dispatchEvent(stopEvent);
|
|
166
|
+
|
|
167
|
+
// Click
|
|
168
|
+
const clickEvent = new MouseEvent("click", { bubbles: true });
|
|
169
|
+
element.dispatchEvent(clickEvent);
|
|
170
|
+
|
|
171
|
+
expect(callbacks.onPreventClick).toHaveBeenCalledTimes(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should calculate correct deltaX and deltaY", () => {
|
|
175
|
+
cleanup = createDragHandler(element, callbacks);
|
|
176
|
+
|
|
177
|
+
// Start at (100, 200)
|
|
178
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
179
|
+
element.dispatchEvent(startEvent);
|
|
180
|
+
|
|
181
|
+
// Move to (250, 350)
|
|
182
|
+
const moveEvent = new MouseEvent("mousemove", { clientX: 250, clientY: 350, bubbles: true });
|
|
183
|
+
document.dispatchEvent(moveEvent);
|
|
184
|
+
|
|
185
|
+
expect(callbacks.onDrag).toHaveBeenCalledWith(
|
|
186
|
+
expect.any(MouseEvent),
|
|
187
|
+
expect.objectContaining({
|
|
188
|
+
deltaX: 150,
|
|
189
|
+
deltaY: 150,
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should handle negative deltas", () => {
|
|
195
|
+
cleanup = createDragHandler(element, callbacks);
|
|
196
|
+
|
|
197
|
+
// Start at (200, 200)
|
|
198
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 200, clientY: 200, bubbles: true });
|
|
199
|
+
element.dispatchEvent(startEvent);
|
|
200
|
+
|
|
201
|
+
// Move to (100, 100)
|
|
202
|
+
const moveEvent = new MouseEvent("mousemove", { clientX: 100, clientY: 100, bubbles: true });
|
|
203
|
+
document.dispatchEvent(moveEvent);
|
|
204
|
+
|
|
205
|
+
expect(callbacks.onDrag).toHaveBeenCalledWith(
|
|
206
|
+
expect.any(MouseEvent),
|
|
207
|
+
expect.objectContaining({
|
|
208
|
+
deltaX: -100,
|
|
209
|
+
deltaY: -100,
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should prevent default and stop propagation by default", () => {
|
|
215
|
+
cleanup = createDragHandler(element, callbacks);
|
|
216
|
+
|
|
217
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true, cancelable: true });
|
|
218
|
+
const preventDefaultSpy = vi.spyOn(startEvent, "preventDefault");
|
|
219
|
+
const stopPropagationSpy = vi.spyOn(startEvent, "stopPropagation");
|
|
220
|
+
|
|
221
|
+
element.dispatchEvent(startEvent);
|
|
222
|
+
|
|
223
|
+
expect(preventDefaultSpy).toHaveBeenCalled();
|
|
224
|
+
expect(stopPropagationSpy).toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should not prevent default when preventDefault option is false", () => {
|
|
228
|
+
cleanup = createDragHandler(element, callbacks, { preventDefault: false });
|
|
229
|
+
|
|
230
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true, cancelable: true });
|
|
231
|
+
const preventDefaultSpy = vi.spyOn(startEvent, "preventDefault");
|
|
232
|
+
|
|
233
|
+
element.dispatchEvent(startEvent);
|
|
234
|
+
|
|
235
|
+
expect(preventDefaultSpy).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should not stop propagation when stopPropagation option is false", () => {
|
|
239
|
+
cleanup = createDragHandler(element, callbacks, { stopPropagation: false });
|
|
240
|
+
|
|
241
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true, cancelable: true });
|
|
242
|
+
const stopPropagationSpy = vi.spyOn(startEvent, "stopPropagation");
|
|
243
|
+
|
|
244
|
+
element.dispatchEvent(startEvent);
|
|
245
|
+
|
|
246
|
+
expect(stopPropagationSpy).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should remove all event listeners on cleanup", () => {
|
|
250
|
+
cleanup = createDragHandler(element, callbacks);
|
|
251
|
+
|
|
252
|
+
cleanup();
|
|
253
|
+
|
|
254
|
+
// Clear mocks
|
|
255
|
+
vi.clearAllMocks();
|
|
256
|
+
|
|
257
|
+
// Try to trigger events - they should not be called
|
|
258
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
259
|
+
element.dispatchEvent(startEvent);
|
|
260
|
+
|
|
261
|
+
const moveEvent = new MouseEvent("mousemove", { clientX: 150, clientY: 250, bubbles: true });
|
|
262
|
+
document.dispatchEvent(moveEvent);
|
|
263
|
+
|
|
264
|
+
window.dispatchEvent(new Event("blur"));
|
|
265
|
+
|
|
266
|
+
expect(callbacks.onStart).not.toHaveBeenCalled();
|
|
267
|
+
expect(callbacks.onDrag).not.toHaveBeenCalled();
|
|
268
|
+
expect(callbacks.onCancel).not.toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should work without optional callbacks", () => {
|
|
272
|
+
const minimalCallbacks: DragCallbacks = {
|
|
273
|
+
onDrag: vi.fn(),
|
|
274
|
+
};
|
|
275
|
+
cleanup = createDragHandler(element, minimalCallbacks);
|
|
276
|
+
|
|
277
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
278
|
+
element.dispatchEvent(startEvent);
|
|
279
|
+
|
|
280
|
+
const moveEvent = new MouseEvent("mousemove", { clientX: 150, clientY: 250, bubbles: true });
|
|
281
|
+
document.dispatchEvent(moveEvent);
|
|
282
|
+
|
|
283
|
+
expect(minimalCallbacks.onDrag).toHaveBeenCalled();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should reset hasDragged flag after preventClick", () => {
|
|
287
|
+
cleanup = createDragHandler(element, callbacks);
|
|
288
|
+
|
|
289
|
+
// Start drag
|
|
290
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
291
|
+
element.dispatchEvent(startEvent);
|
|
292
|
+
|
|
293
|
+
// Move mouse
|
|
294
|
+
const moveEvent = new MouseEvent("mousemove", { clientX: 150, clientY: 250, bubbles: true });
|
|
295
|
+
document.dispatchEvent(moveEvent);
|
|
296
|
+
|
|
297
|
+
// Stop drag
|
|
298
|
+
const stopEvent = new MouseEvent("mouseup", { bubbles: true });
|
|
299
|
+
document.dispatchEvent(stopEvent);
|
|
300
|
+
|
|
301
|
+
// Click - should reset hasDragged
|
|
302
|
+
const clickEvent = new MouseEvent("click", { bubbles: true });
|
|
303
|
+
element.dispatchEvent(clickEvent);
|
|
304
|
+
|
|
305
|
+
// The hasDragged flag should be reset after preventClick
|
|
306
|
+
// This is tested implicitly by the fact that preventClick is called
|
|
307
|
+
expect(callbacks.onPreventClick).toHaveBeenCalled();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should work with SVGElement", () => {
|
|
311
|
+
const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
312
|
+
document.body.appendChild(svgElement);
|
|
313
|
+
|
|
314
|
+
cleanup = createDragHandler(svgElement, callbacks);
|
|
315
|
+
|
|
316
|
+
const startEvent = new MouseEvent("mousedown", { clientX: 100, clientY: 200, bubbles: true });
|
|
317
|
+
svgElement.dispatchEvent(startEvent);
|
|
318
|
+
|
|
319
|
+
expect(callbacks.onStart).toHaveBeenCalled();
|
|
320
|
+
|
|
321
|
+
cleanup();
|
|
322
|
+
document.body.removeChild(svgElement);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State tracked during a drag operation
|
|
3
|
+
*/
|
|
4
|
+
export interface DragState {
|
|
5
|
+
isDragging: boolean;
|
|
6
|
+
hasDragged: boolean;
|
|
7
|
+
startX: number;
|
|
8
|
+
startY: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Callbacks for drag operations
|
|
13
|
+
*/
|
|
14
|
+
export interface DragCallbacks {
|
|
15
|
+
/**
|
|
16
|
+
* Called when drag starts
|
|
17
|
+
* @param event - Mouse event that started the drag
|
|
18
|
+
* @param state - Current drag state
|
|
19
|
+
*/
|
|
20
|
+
onStart?: (event: MouseEvent, state: DragState) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Called during drag movement
|
|
23
|
+
* @param event - Mouse move event
|
|
24
|
+
* @param state - Current drag state with updated deltas
|
|
25
|
+
*/
|
|
26
|
+
onDrag: (event: MouseEvent, state: DragState & { deltaX: number; deltaY: number }) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Called when drag stops
|
|
29
|
+
* @param event - Mouse event that stopped the drag
|
|
30
|
+
* @param state - Final drag state
|
|
31
|
+
*/
|
|
32
|
+
onStop?: (event: MouseEvent, state: DragState) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Called when drag is cancelled (e.g., window blur)
|
|
35
|
+
* @param state - Current drag state
|
|
36
|
+
*/
|
|
37
|
+
onCancel?: (state: DragState) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Called on click events to prevent default behavior
|
|
40
|
+
* @param event - Click event
|
|
41
|
+
* @param state - Current drag state
|
|
42
|
+
*/
|
|
43
|
+
onPreventClick?: (event: MouseEvent, state: DragState) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Options for configuring the drag handler
|
|
48
|
+
*/
|
|
49
|
+
export interface DragHandlerOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Whether to prevent default behavior on start/stop events
|
|
52
|
+
* @default true
|
|
53
|
+
*/
|
|
54
|
+
preventDefault?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Whether to stop event propagation on start/stop events
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
stopPropagation?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a reusable drag handler for mouse drag operations
|
|
64
|
+
* @param element - Element that triggers the drag (mousedown listener attached here)
|
|
65
|
+
* @param callbacks - Callbacks for drag lifecycle events
|
|
66
|
+
* @param options - Configuration options
|
|
67
|
+
* @returns Cleanup function to remove all event listeners
|
|
68
|
+
*/
|
|
69
|
+
export function createDragHandler(
|
|
70
|
+
element: HTMLElement | SVGElement,
|
|
71
|
+
callbacks: DragCallbacks,
|
|
72
|
+
options: DragHandlerOptions = {}
|
|
73
|
+
): () => void {
|
|
74
|
+
const { preventDefault = true, stopPropagation = true } = options;
|
|
75
|
+
|
|
76
|
+
const state: DragState = {
|
|
77
|
+
isDragging: false,
|
|
78
|
+
hasDragged: false,
|
|
79
|
+
startX: 0,
|
|
80
|
+
startY: 0,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const startDrag = (event: MouseEvent): void => {
|
|
84
|
+
if (preventDefault) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
}
|
|
87
|
+
if (stopPropagation) {
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
state.isDragging = true;
|
|
92
|
+
state.hasDragged = false;
|
|
93
|
+
state.startX = event.clientX;
|
|
94
|
+
state.startY = event.clientY;
|
|
95
|
+
|
|
96
|
+
callbacks.onStart?.(event, state);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleDrag = (event: MouseEvent): void => {
|
|
100
|
+
if (!state.isDragging) return;
|
|
101
|
+
|
|
102
|
+
const deltaX = event.clientX - state.startX;
|
|
103
|
+
const deltaY = event.clientY - state.startY;
|
|
104
|
+
|
|
105
|
+
state.hasDragged = true;
|
|
106
|
+
|
|
107
|
+
callbacks.onDrag(event, {
|
|
108
|
+
...state,
|
|
109
|
+
deltaX,
|
|
110
|
+
deltaY,
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const stopDrag = (event: MouseEvent): void => {
|
|
115
|
+
if (!state.isDragging) return;
|
|
116
|
+
|
|
117
|
+
if (preventDefault) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
}
|
|
120
|
+
if (stopPropagation) {
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
state.isDragging = false;
|
|
125
|
+
|
|
126
|
+
callbacks.onStop?.(event, state);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const cancelDrag = (): void => {
|
|
130
|
+
if (!state.isDragging) return;
|
|
131
|
+
|
|
132
|
+
state.isDragging = false;
|
|
133
|
+
callbacks.onCancel?.(state);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const preventClick = (event: MouseEvent): void => {
|
|
137
|
+
if (preventDefault) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
}
|
|
140
|
+
if (stopPropagation) {
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
callbacks.onPreventClick?.(event, state);
|
|
145
|
+
|
|
146
|
+
// Reset hasDragged flag after handling the click
|
|
147
|
+
if (state.hasDragged) {
|
|
148
|
+
state.hasDragged = false;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Attach event listeners
|
|
153
|
+
element.addEventListener("mousedown", startDrag as EventListener);
|
|
154
|
+
if (callbacks.onPreventClick) {
|
|
155
|
+
element.addEventListener("click", preventClick as EventListener);
|
|
156
|
+
}
|
|
157
|
+
document.addEventListener("mousemove", handleDrag as EventListener);
|
|
158
|
+
document.addEventListener("mouseup", stopDrag as EventListener);
|
|
159
|
+
window.addEventListener("blur", cancelDrag);
|
|
160
|
+
|
|
161
|
+
// Return cleanup function
|
|
162
|
+
return () => {
|
|
163
|
+
element.removeEventListener("mousedown", startDrag as EventListener);
|
|
164
|
+
if (callbacks.onPreventClick) {
|
|
165
|
+
element.removeEventListener("click", preventClick as EventListener);
|
|
166
|
+
}
|
|
167
|
+
document.removeEventListener("mousemove", handleDrag as EventListener);
|
|
168
|
+
document.removeEventListener("mouseup", stopDrag as EventListener);
|
|
169
|
+
window.removeEventListener("blur", cancelDrag);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { getNodeProvider } from "./getNodeProvider";
|
|
3
|
+
|
|
4
|
+
describe("getNodeProvider", () => {
|
|
5
|
+
let nodeProvider: HTMLElement;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
nodeProvider = document.createElement("div");
|
|
9
|
+
nodeProvider.setAttribute("data-role", "node-provider");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
if (document.body.contains(nodeProvider)) {
|
|
14
|
+
document.body.removeChild(nodeProvider);
|
|
15
|
+
}
|
|
16
|
+
// Remove any other node providers
|
|
17
|
+
const existingProviders = document.querySelectorAll('[data-role="node-provider"]');
|
|
18
|
+
existingProviders.forEach((provider) => {
|
|
19
|
+
if (document.body.contains(provider)) {
|
|
20
|
+
document.body.removeChild(provider);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should return node provider element when it exists", () => {
|
|
26
|
+
document.body.appendChild(nodeProvider);
|
|
27
|
+
|
|
28
|
+
const result = getNodeProvider();
|
|
29
|
+
|
|
30
|
+
expect(result).toBe(nodeProvider);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return null when node provider does not exist", () => {
|
|
34
|
+
const result = getNodeProvider();
|
|
35
|
+
|
|
36
|
+
expect(result).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return first node provider when multiple exist", () => {
|
|
40
|
+
const firstProvider = document.createElement("div");
|
|
41
|
+
firstProvider.setAttribute("data-role", "node-provider");
|
|
42
|
+
const secondProvider = document.createElement("div");
|
|
43
|
+
secondProvider.setAttribute("data-role", "node-provider");
|
|
44
|
+
|
|
45
|
+
document.body.appendChild(firstProvider);
|
|
46
|
+
document.body.appendChild(secondProvider);
|
|
47
|
+
|
|
48
|
+
const result = getNodeProvider();
|
|
49
|
+
|
|
50
|
+
expect(result).toBe(firstProvider);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return null after provider is removed", () => {
|
|
54
|
+
document.body.appendChild(nodeProvider);
|
|
55
|
+
const result1 = getNodeProvider();
|
|
56
|
+
expect(result1).toBe(nodeProvider);
|
|
57
|
+
|
|
58
|
+
document.body.removeChild(nodeProvider);
|
|
59
|
+
const result2 = getNodeProvider();
|
|
60
|
+
expect(result2).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should return element with correct data-role attribute", () => {
|
|
64
|
+
document.body.appendChild(nodeProvider);
|
|
65
|
+
|
|
66
|
+
const result = getNodeProvider();
|
|
67
|
+
|
|
68
|
+
expect(result?.getAttribute("data-role")).toBe("node-provider");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { getNodeTools } from "./getNodeTools";
|
|
3
|
+
|
|
4
|
+
describe("getNodeTools", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
delete (window as Window & { nodeTools?: unknown }).nodeTools;
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
delete (window as Window & { nodeTools?: unknown }).nodeTools;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should return nodeTools when it exists on window", () => {
|
|
14
|
+
const mockNodeTools = { test: "value" };
|
|
15
|
+
(window as Window & { nodeTools?: unknown }).nodeTools = mockNodeTools;
|
|
16
|
+
|
|
17
|
+
const result = getNodeTools();
|
|
18
|
+
|
|
19
|
+
expect(result).toBe(mockNodeTools);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should return undefined when nodeTools does not exist", () => {
|
|
23
|
+
const result = getNodeTools();
|
|
24
|
+
|
|
25
|
+
expect(result).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return updated value when nodeTools changes", () => {
|
|
29
|
+
const mockNodeTools1 = { test: "value1" };
|
|
30
|
+
(window as Window & { nodeTools?: unknown }).nodeTools = mockNodeTools1;
|
|
31
|
+
const result1 = getNodeTools();
|
|
32
|
+
expect(result1).toBe(mockNodeTools1);
|
|
33
|
+
|
|
34
|
+
const mockNodeTools2 = { test: "value2" };
|
|
35
|
+
(window as Window & { nodeTools?: unknown }).nodeTools = mockNodeTools2;
|
|
36
|
+
const result2 = getNodeTools();
|
|
37
|
+
expect(result2).toBe(mockNodeTools2);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return undefined after nodeTools is deleted", () => {
|
|
41
|
+
const mockNodeTools = { test: "value" };
|
|
42
|
+
(window as Window & { nodeTools?: unknown }).nodeTools = mockNodeTools;
|
|
43
|
+
const result1 = getNodeTools();
|
|
44
|
+
expect(result1).toBe(mockNodeTools);
|
|
45
|
+
|
|
46
|
+
delete (window as Window & { nodeTools?: unknown }).nodeTools;
|
|
47
|
+
const result2 = getNodeTools();
|
|
48
|
+
expect(result2).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|