@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.
Files changed (136) hide show
  1. package/dist/lib/canvas/helpers/getCanvasContainerOrBody.d.ts +1 -0
  2. package/dist/lib/canvas/helpers/getCanvasWindowValue.d.ts +1 -1
  3. package/dist/lib/helpers/adjustForZoom.d.ts +1 -0
  4. package/dist/lib/helpers/createDragHandler.d.ts +69 -0
  5. package/dist/lib/helpers/getNodeProvider.d.ts +1 -0
  6. package/dist/lib/helpers/getNodeTools.d.ts +2 -0
  7. package/dist/lib/helpers/getViewportDimensions.d.ts +4 -0
  8. package/dist/lib/helpers/index.d.ts +9 -1
  9. package/dist/lib/helpers/parseTransform.d.ts +8 -0
  10. package/dist/lib/helpers/toggleClass.d.ts +1 -0
  11. package/dist/lib/viewport/label/getViewportLabelOverlay.d.ts +1 -0
  12. package/dist/lib/viewport/label/helpers/selectFirstViewportNode.d.ts +5 -0
  13. package/dist/lib/viewport/label/index.d.ts +5 -3
  14. package/dist/lib/viewport/label/isViewportDragging.d.ts +2 -0
  15. package/dist/lib/viewport/label/refreshViewportLabel.d.ts +8 -0
  16. package/dist/lib/viewport/label/removeViewportLabel.d.ts +5 -0
  17. package/dist/lib/viewport/label/setupViewportDrag.d.ts +1 -0
  18. package/dist/node-edit-utils.cjs.js +342 -280
  19. package/dist/node-edit-utils.esm.js +342 -280
  20. package/dist/node-edit-utils.umd.js +342 -280
  21. package/dist/node-edit-utils.umd.min.js +1 -1
  22. package/dist/styles.css +1 -1
  23. package/package.json +7 -2
  24. package/src/lib/canvas/createCanvasObserver.test.ts +242 -0
  25. package/src/lib/canvas/createCanvasObserver.ts +2 -2
  26. package/src/lib/canvas/disableCanvasKeyboard.test.ts +53 -0
  27. package/src/lib/canvas/disableCanvasKeyboard.ts +1 -1
  28. package/src/lib/canvas/disableCanvasTextMode.test.ts +53 -0
  29. package/src/lib/canvas/disableCanvasTextMode.ts +1 -1
  30. package/src/lib/canvas/enableCanvasKeyboard.test.ts +53 -0
  31. package/src/lib/canvas/enableCanvasKeyboard.ts +1 -1
  32. package/src/lib/canvas/enableCanvasTextMode.test.ts +53 -0
  33. package/src/lib/canvas/enableCanvasTextMode.ts +1 -1
  34. package/src/lib/canvas/helpers/applyCanvasState.test.ts +119 -0
  35. package/src/lib/canvas/helpers/applyCanvasState.ts +1 -1
  36. package/src/lib/canvas/helpers/getCanvasContainer.test.ts +62 -0
  37. package/src/lib/canvas/helpers/getCanvasContainerOrBody.test.ts +51 -0
  38. package/src/lib/canvas/helpers/getCanvasContainerOrBody.ts +6 -0
  39. package/src/lib/canvas/helpers/getCanvasWindowValue.test.ts +116 -0
  40. package/src/lib/canvas/helpers/getCanvasWindowValue.ts +2 -3
  41. package/src/lib/helpers/adjustForZoom.test.ts +65 -0
  42. package/src/lib/helpers/adjustForZoom.ts +4 -0
  43. package/src/lib/helpers/createDragHandler.test.ts +325 -0
  44. package/src/lib/helpers/createDragHandler.ts +171 -0
  45. package/src/lib/helpers/getNodeProvider.test.ts +71 -0
  46. package/src/lib/helpers/getNodeProvider.ts +4 -0
  47. package/src/lib/helpers/getNodeTools.test.ts +50 -0
  48. package/src/lib/helpers/getNodeTools.ts +6 -0
  49. package/src/lib/helpers/getViewportDimensions.test.ts +93 -0
  50. package/src/lib/helpers/getViewportDimensions.ts +7 -0
  51. package/src/lib/helpers/index.ts +9 -1
  52. package/src/lib/helpers/observer/connectMutationObserver.test.ts +127 -0
  53. package/src/lib/helpers/observer/connectResizeObserver.test.ts +147 -0
  54. package/src/lib/helpers/parseTransform.test.ts +117 -0
  55. package/src/lib/helpers/parseTransform.ts +9 -0
  56. package/src/lib/helpers/toggleClass.test.ts +71 -0
  57. package/src/lib/helpers/toggleClass.ts +9 -0
  58. package/src/lib/helpers/withRAF.test.ts +439 -0
  59. package/src/lib/node-tools/createNodeTools.test.ts +373 -0
  60. package/src/lib/node-tools/createNodeTools.ts +0 -1
  61. package/src/lib/node-tools/events/click/handleNodeClick.test.ts +109 -0
  62. package/src/lib/node-tools/events/setupEventListener.test.ts +136 -0
  63. package/src/lib/node-tools/highlight/clearHighlightFrame.test.ts +88 -0
  64. package/src/lib/node-tools/highlight/clearHighlightFrame.ts +2 -3
  65. package/src/lib/node-tools/highlight/createCornerHandles.test.ts +150 -0
  66. package/src/lib/node-tools/highlight/createHighlightFrame.test.ts +237 -0
  67. package/src/lib/node-tools/highlight/createHighlightFrame.ts +5 -9
  68. package/src/lib/node-tools/highlight/createTagLabel.test.ts +135 -0
  69. package/src/lib/node-tools/highlight/createToolsContainer.test.ts +97 -0
  70. package/src/lib/node-tools/highlight/createToolsContainer.ts +3 -6
  71. package/src/lib/node-tools/highlight/helpers/getElementBounds.test.ts +158 -0
  72. package/src/lib/node-tools/highlight/helpers/getElementBounds.ts +6 -5
  73. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.test.ts +78 -0
  74. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +2 -3
  75. package/src/lib/node-tools/highlight/helpers/getScreenBounds.test.ts +133 -0
  76. package/src/lib/node-tools/highlight/highlightNode.test.ts +213 -0
  77. package/src/lib/node-tools/highlight/highlightNode.ts +7 -15
  78. package/src/lib/node-tools/highlight/refreshHighlightFrame.test.ts +323 -0
  79. package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +12 -42
  80. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.test.ts +110 -0
  81. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +2 -3
  82. package/src/lib/node-tools/select/helpers/getElementsFromPoint.test.ts +109 -0
  83. package/src/lib/node-tools/select/helpers/isInsideComponent.test.ts +81 -0
  84. package/src/lib/node-tools/select/helpers/isInsideViewport.test.ts +82 -0
  85. package/src/lib/node-tools/select/helpers/targetSameCandidates.test.ts +81 -0
  86. package/src/lib/node-tools/select/selectNode.test.ts +238 -0
  87. package/src/lib/node-tools/text/events/setupKeydownHandler.test.ts +91 -0
  88. package/src/lib/node-tools/text/events/setupMutationObserver.test.ts +213 -0
  89. package/src/lib/node-tools/text/events/setupNodeListeners.test.ts +133 -0
  90. package/src/lib/node-tools/text/helpers/enterTextEditMode.test.ts +50 -0
  91. package/src/lib/node-tools/text/helpers/handleTextChange.test.ts +201 -0
  92. package/src/lib/node-tools/text/helpers/hasTextContent.test.ts +101 -0
  93. package/src/lib/node-tools/text/helpers/insertLineBreak.test.ts +96 -0
  94. package/src/lib/node-tools/text/helpers/makeNodeEditable.test.ts +56 -0
  95. package/src/lib/node-tools/text/helpers/makeNodeNonEditable.test.ts +57 -0
  96. package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.test.ts +61 -0
  97. package/src/lib/node-tools/text/nodeText.test.ts +233 -0
  98. package/src/lib/post-message/processPostMessage.test.ts +218 -0
  99. package/src/lib/post-message/sendPostMessage.test.ts +120 -0
  100. package/src/lib/styles/styles.css +3 -3
  101. package/src/lib/viewport/createViewport.test.ts +267 -0
  102. package/src/lib/viewport/createViewport.ts +51 -51
  103. package/src/lib/viewport/events/setupEventListener.test.ts +103 -0
  104. package/src/lib/viewport/label/getViewportLabelOverlay.test.ts +77 -0
  105. package/src/lib/viewport/label/{getViewportLabelsOverlay.ts → getViewportLabelOverlay.ts} +6 -6
  106. package/src/lib/viewport/label/helpers/getLabelPosition.test.ts +51 -0
  107. package/src/lib/viewport/label/helpers/getLabelPosition.ts +3 -5
  108. package/src/lib/viewport/label/helpers/getTransformValues.test.ts +59 -0
  109. package/src/lib/viewport/label/helpers/getTransformValues.ts +3 -5
  110. package/src/lib/viewport/label/helpers/getZoomValue.test.ts +53 -0
  111. package/src/lib/viewport/label/helpers/selectFirstViewportNode.test.ts +105 -0
  112. package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +26 -0
  113. package/src/lib/viewport/label/index.ts +5 -3
  114. package/src/lib/viewport/label/isViewportDragging.test.ts +35 -0
  115. package/src/lib/viewport/label/isViewportDragging.ts +9 -0
  116. package/src/lib/viewport/label/refreshViewportLabel.test.ts +105 -0
  117. package/src/lib/viewport/label/refreshViewportLabel.ts +50 -0
  118. package/src/lib/viewport/label/refreshViewportLabels.test.ts +107 -0
  119. package/src/lib/viewport/label/refreshViewportLabels.ts +19 -52
  120. package/src/lib/viewport/label/removeViewportLabel.test.ts +67 -0
  121. package/src/lib/viewport/label/removeViewportLabel.ts +20 -0
  122. package/src/lib/viewport/label/setupViewportDrag.test.ts +249 -0
  123. package/src/lib/viewport/label/setupViewportDrag.ts +70 -0
  124. package/src/lib/viewport/resize/createResizeHandle.test.ts +37 -0
  125. package/src/lib/viewport/resize/createResizePresets.test.ts +75 -0
  126. package/src/lib/viewport/resize/updateActivePreset.test.ts +92 -0
  127. package/src/lib/viewport/width/calcConstrainedWidth.test.ts +47 -0
  128. package/src/lib/viewport/width/calcWidth.test.ts +68 -0
  129. package/src/lib/viewport/width/updateWidth.test.ts +78 -0
  130. package/src/lib/window/bindToWindow.test.ts +166 -0
  131. package/src/lib/window/bindToWindow.ts +1 -2
  132. package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +0 -1
  133. package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +0 -2
  134. package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +0 -1
  135. package/src/lib/viewport/label/isViewportLabelDragging.ts +0 -9
  136. 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,4 @@
1
+ export const getNodeProvider = (): HTMLElement | null => {
2
+ return document.querySelector('[data-role="node-provider"]') as HTMLElement | null;
3
+ };
4
+
@@ -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
+ });
@@ -0,0 +1,6 @@
1
+ import type { NodeTools } from "../node-tools/types";
2
+
3
+ export const getNodeTools = (): NodeTools | undefined => {
4
+ return (window as Window & { nodeTools?: NodeTools }).nodeTools;
5
+ };
6
+