@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,67 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as getViewportLabelOverlayModule from "./getViewportLabelOverlay";
3
+ import { removeViewportLabel } from "./removeViewportLabel";
4
+
5
+ vi.mock("./getViewportLabelOverlay");
6
+
7
+ describe("removeViewportLabel", () => {
8
+ let overlay: SVGSVGElement;
9
+ let viewportElement: HTMLElement;
10
+
11
+ beforeEach(() => {
12
+ overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
13
+ overlay.classList.add("viewport-labels-overlay");
14
+ document.body.appendChild(overlay);
15
+
16
+ viewportElement = document.createElement("div");
17
+ viewportElement.setAttribute("data-viewport-name", "test-viewport");
18
+ document.body.appendChild(viewportElement);
19
+
20
+ vi.mocked(getViewportLabelOverlayModule.getViewportLabelOverlay).mockReturnValue(overlay);
21
+ });
22
+
23
+ afterEach(() => {
24
+ document.body.removeChild(overlay);
25
+ document.body.removeChild(viewportElement);
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ it("should remove label group when viewport name exists", () => {
30
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
31
+ group.setAttribute("data-viewport-name", "test-viewport");
32
+ overlay.appendChild(group);
33
+
34
+ removeViewportLabel(viewportElement);
35
+
36
+ expect(overlay.querySelector(`[data-viewport-name="test-viewport"]`)).toBeNull();
37
+ });
38
+
39
+ it("should do nothing when viewport name doesn't exist", () => {
40
+ viewportElement.removeAttribute("data-viewport-name");
41
+
42
+ expect(() => {
43
+ removeViewportLabel(viewportElement);
44
+ }).not.toThrow();
45
+ });
46
+
47
+ it("should do nothing when label group doesn't exist", () => {
48
+ removeViewportLabel(viewportElement);
49
+
50
+ expect(overlay.querySelector(`[data-viewport-name="test-viewport"]`)).toBeNull();
51
+ });
52
+
53
+ it("should only remove the matching label group", () => {
54
+ const group1 = document.createElementNS("http://www.w3.org/2000/svg", "g");
55
+ group1.setAttribute("data-viewport-name", "test-viewport");
56
+ overlay.appendChild(group1);
57
+
58
+ const group2 = document.createElementNS("http://www.w3.org/2000/svg", "g");
59
+ group2.setAttribute("data-viewport-name", "other-viewport");
60
+ overlay.appendChild(group2);
61
+
62
+ removeViewportLabel(viewportElement);
63
+
64
+ expect(overlay.querySelector(`[data-viewport-name="test-viewport"]`)).toBeNull();
65
+ expect(overlay.querySelector(`[data-viewport-name="other-viewport"]`)).toBe(group2);
66
+ });
67
+ });
@@ -0,0 +1,20 @@
1
+ import { getViewportLabelOverlay } from "./getViewportLabelOverlay";
2
+
3
+ /**
4
+ * Removes a viewport label for a single viewport element.
5
+ * @param viewportElement - The viewport element to remove the label for
6
+ */
7
+ export const removeViewportLabel = (viewportElement: HTMLElement): void => {
8
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
9
+
10
+ if (!viewportName) {
11
+ return;
12
+ }
13
+
14
+ const overlay = getViewportLabelOverlay();
15
+ const labelGroup = overlay.querySelector(`[data-viewport-name="${viewportName}"]`);
16
+
17
+ if (labelGroup) {
18
+ labelGroup.remove();
19
+ }
20
+ };
@@ -0,0 +1,249 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { DragState } from "../../helpers/createDragHandler";
3
+ import * as createDragHandlerModule from "../../helpers/createDragHandler";
4
+ import * as getNodeToolsModule from "../../helpers/getNodeTools";
5
+ import * as sendPostMessageModule from "../../post-message/sendPostMessage";
6
+ import * as getLabelPositionModule from "./helpers/getLabelPosition";
7
+ import * as getTransformValuesModule from "./helpers/getTransformValues";
8
+ import * as getZoomValueModule from "./helpers/getZoomValue";
9
+ import * as selectFirstViewportNodeModule from "./helpers/selectFirstViewportNode";
10
+ import * as setViewportDraggingModule from "./isViewportDragging";
11
+ import { setupViewportDrag } from "./setupViewportDrag";
12
+
13
+ vi.mock("../../helpers/createDragHandler");
14
+ vi.mock("../../helpers/getNodeTools");
15
+ vi.mock("../../post-message/sendPostMessage");
16
+ vi.mock("./helpers/getLabelPosition");
17
+ vi.mock("./helpers/getTransformValues");
18
+ vi.mock("./helpers/getZoomValue");
19
+ vi.mock("./helpers/selectFirstViewportNode");
20
+ vi.mock("./isViewportDragging");
21
+
22
+ describe("setupViewportDrag", () => {
23
+ let labelElement: SVGTextElement;
24
+ let labelGroup: SVGGElement;
25
+ let viewportElement: HTMLElement;
26
+ let mockCleanup: ReturnType<typeof vi.fn>;
27
+ let mockNodeTools: {
28
+ refreshHighlightFrame: ReturnType<typeof vi.fn>;
29
+ };
30
+
31
+ beforeEach(() => {
32
+ labelGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
33
+ labelElement = document.createElementNS("http://www.w3.org/2000/svg", "text");
34
+ labelGroup.appendChild(labelElement);
35
+
36
+ viewportElement = document.createElement("div");
37
+ viewportElement.style.transform = "translate3d(100px, 200px, 0px)";
38
+ document.body.appendChild(viewportElement);
39
+
40
+ mockCleanup = vi.fn();
41
+ mockNodeTools = {
42
+ refreshHighlightFrame: vi.fn(),
43
+ };
44
+
45
+ vi.mocked(createDragHandlerModule.createDragHandler).mockReturnValue(mockCleanup);
46
+ vi.mocked(getNodeToolsModule.getNodeTools).mockReturnValue(
47
+ mockNodeTools as unknown as ReturnType<typeof getNodeToolsModule.getNodeTools>
48
+ );
49
+ vi.mocked(sendPostMessageModule.sendPostMessage).mockImplementation(() => {});
50
+ vi.mocked(getLabelPositionModule.getLabelPosition).mockReturnValue({ x: 50, y: 100 });
51
+ vi.mocked(getTransformValuesModule.getTransformValues).mockReturnValue({ x: 100, y: 200 });
52
+ vi.mocked(getZoomValueModule.getZoomValue).mockReturnValue(1);
53
+ vi.mocked(selectFirstViewportNodeModule.selectFirstViewportNode).mockImplementation(() => {});
54
+ vi.mocked(setViewportDraggingModule.setViewportDragging).mockImplementation(() => {});
55
+ });
56
+
57
+ afterEach(() => {
58
+ document.body.removeChild(viewportElement);
59
+ vi.clearAllMocks();
60
+ });
61
+
62
+ it("should return cleanup function from createDragHandler", () => {
63
+ const cleanup = setupViewportDrag(labelElement, viewportElement, "test-viewport");
64
+
65
+ expect(cleanup).toBe(mockCleanup);
66
+ });
67
+
68
+ it("should set dragging to true on drag start", () => {
69
+ let onStartCallback: ((event: MouseEvent, state: DragState) => void) | undefined;
70
+
71
+ vi.mocked(createDragHandlerModule.createDragHandler).mockImplementation((_element, callbacks) => {
72
+ onStartCallback = callbacks.onStart;
73
+ return mockCleanup;
74
+ });
75
+
76
+ setupViewportDrag(labelElement, viewportElement, "test-viewport");
77
+
78
+ const mockEvent = new MouseEvent("mousedown");
79
+ const mockState: DragState = { isDragging: true, hasDragged: false, startX: 0, startY: 0 };
80
+ onStartCallback?.(mockEvent, mockState);
81
+
82
+ expect(setViewportDraggingModule.setViewportDragging).toHaveBeenCalledWith(true);
83
+ });
84
+
85
+ it("should select first viewport node on drag start", () => {
86
+ let onStartCallback: ((event: MouseEvent, state: DragState) => void) | undefined;
87
+
88
+ vi.mocked(createDragHandlerModule.createDragHandler).mockImplementation((_element, callbacks) => {
89
+ onStartCallback = callbacks.onStart;
90
+ return mockCleanup;
91
+ });
92
+
93
+ setupViewportDrag(labelElement, viewportElement, "test-viewport");
94
+
95
+ const mockEvent = new MouseEvent("mousedown");
96
+ const mockState: DragState = { isDragging: true, hasDragged: false, startX: 0, startY: 0 };
97
+ onStartCallback?.(mockEvent, mockState);
98
+
99
+ expect(selectFirstViewportNodeModule.selectFirstViewportNode).toHaveBeenCalledWith(viewportElement);
100
+ });
101
+
102
+ it("should update viewport and label positions during drag", () => {
103
+ let onStartCallback: ((event: MouseEvent, state: DragState) => void) | undefined;
104
+ let onDragCallback: ((event: MouseEvent, state: DragState & { deltaX: number; deltaY: number }) => void) | undefined;
105
+
106
+ vi.mocked(createDragHandlerModule.createDragHandler).mockImplementation((_element, callbacks) => {
107
+ onStartCallback = callbacks.onStart;
108
+ onDragCallback = callbacks.onDrag;
109
+ return mockCleanup;
110
+ });
111
+
112
+ setupViewportDrag(labelElement, viewportElement, "test-viewport");
113
+
114
+ // Call onStart first to capture initial positions
115
+ const startEvent = new MouseEvent("mousedown");
116
+ const startState: DragState = { isDragging: true, hasDragged: false, startX: 0, startY: 0 };
117
+ onStartCallback?.(startEvent, startState);
118
+
119
+ const mockEvent = new MouseEvent("mousemove");
120
+ const mockState: DragState & { deltaX: number; deltaY: number } = {
121
+ isDragging: true,
122
+ hasDragged: true,
123
+ startX: 0,
124
+ startY: 0,
125
+ deltaX: 50,
126
+ deltaY: 100,
127
+ };
128
+ onDragCallback?.(mockEvent, mockState);
129
+
130
+ // Label position should use raw delta (screen space)
131
+ expect(labelGroup.getAttribute("transform")).toBe("translate(100, 200)");
132
+ // Viewport position should use zoom-adjusted delta (canvas space)
133
+ expect(viewportElement.style.transform).toBe("translate3d(150px, 300px, 0)");
134
+ });
135
+
136
+ it("should account for zoom level during drag", () => {
137
+ vi.mocked(getZoomValueModule.getZoomValue).mockReturnValue(2);
138
+
139
+ let onStartCallback: ((event: MouseEvent, state: DragState) => void) | undefined;
140
+ let onDragCallback: ((event: MouseEvent, state: DragState & { deltaX: number; deltaY: number }) => void) | undefined;
141
+
142
+ vi.mocked(createDragHandlerModule.createDragHandler).mockImplementation((_element, callbacks) => {
143
+ onStartCallback = callbacks.onStart;
144
+ onDragCallback = callbacks.onDrag;
145
+ return mockCleanup;
146
+ });
147
+
148
+ setupViewportDrag(labelElement, viewportElement, "test-viewport");
149
+
150
+ // Call onStart first to capture initial positions
151
+ const startEvent = new MouseEvent("mousedown");
152
+ const startState: DragState = { isDragging: true, hasDragged: false, startX: 0, startY: 0 };
153
+ onStartCallback?.(startEvent, startState);
154
+
155
+ const mockEvent = new MouseEvent("mousemove");
156
+ const mockState: DragState & { deltaX: number; deltaY: number } = {
157
+ isDragging: true,
158
+ hasDragged: true,
159
+ startX: 0,
160
+ startY: 0,
161
+ deltaX: 100,
162
+ deltaY: 200,
163
+ };
164
+ onDragCallback?.(mockEvent, mockState);
165
+
166
+ // Delta should be divided by zoom (100/2 = 50, 200/2 = 100)
167
+ expect(viewportElement.style.transform).toBe("translate3d(150px, 300px, 0)");
168
+ });
169
+
170
+ it("should set dragging to false and refresh highlight on drag stop with hasDragged", () => {
171
+ let onStopCallback: ((event: MouseEvent, state: DragState) => void) | undefined;
172
+
173
+ vi.mocked(createDragHandlerModule.createDragHandler).mockImplementation((_element, callbacks) => {
174
+ onStopCallback = callbacks.onStop;
175
+ return mockCleanup;
176
+ });
177
+
178
+ setupViewportDrag(labelElement, viewportElement, "test-viewport");
179
+
180
+ const mockEvent = new MouseEvent("mouseup");
181
+ const mockState: DragState = { isDragging: false, hasDragged: true, startX: 0, startY: 0 };
182
+ onStopCallback?.(mockEvent, mockState);
183
+
184
+ expect(setViewportDraggingModule.setViewportDragging).toHaveBeenCalledWith(false);
185
+ expect(mockNodeTools.refreshHighlightFrame).toHaveBeenCalled();
186
+ });
187
+
188
+ it("should send postMessage with final position on drag stop when hasDragged is true", () => {
189
+ vi.mocked(getTransformValuesModule.getTransformValues).mockReturnValue({ x: 150, y: 250 });
190
+
191
+ let onStopCallback: ((event: MouseEvent, state: DragState) => void) | undefined;
192
+
193
+ vi.mocked(createDragHandlerModule.createDragHandler).mockImplementation((_element, callbacks) => {
194
+ onStopCallback = callbacks.onStop;
195
+ return mockCleanup;
196
+ });
197
+
198
+ setupViewportDrag(labelElement, viewportElement, "test-viewport");
199
+
200
+ const mockEvent = new MouseEvent("mouseup");
201
+ const mockState: DragState = { isDragging: false, hasDragged: true, startX: 0, startY: 0 };
202
+ onStopCallback?.(mockEvent, mockState);
203
+
204
+ expect(sendPostMessageModule.sendPostMessage).toHaveBeenCalledWith("viewport-position-changed", {
205
+ viewportName: "test-viewport",
206
+ x: 150,
207
+ y: 250,
208
+ });
209
+ });
210
+
211
+ it("should send postMessage with final position on drag stop even when hasDragged is false", () => {
212
+ vi.mocked(getTransformValuesModule.getTransformValues).mockReturnValue({ x: 100, y: 200 });
213
+
214
+ let onStopCallback: ((event: MouseEvent, state: DragState) => void) | undefined;
215
+
216
+ vi.mocked(createDragHandlerModule.createDragHandler).mockImplementation((_element, callbacks) => {
217
+ onStopCallback = callbacks.onStop;
218
+ return mockCleanup;
219
+ });
220
+
221
+ setupViewportDrag(labelElement, viewportElement, "test-viewport");
222
+
223
+ const mockEvent = new MouseEvent("mouseup");
224
+ const mockState: DragState = { isDragging: false, hasDragged: false, startX: 0, startY: 0 };
225
+ onStopCallback?.(mockEvent, mockState);
226
+
227
+ expect(sendPostMessageModule.sendPostMessage).toHaveBeenCalledWith("viewport-position-changed", {
228
+ viewportName: "test-viewport",
229
+ x: 100,
230
+ y: 200,
231
+ });
232
+ });
233
+
234
+ it("should set dragging to false on cancel", () => {
235
+ let onCancelCallback: ((state: DragState) => void) | undefined;
236
+
237
+ vi.mocked(createDragHandlerModule.createDragHandler).mockImplementation((_element, callbacks) => {
238
+ onCancelCallback = callbacks.onCancel;
239
+ return mockCleanup;
240
+ });
241
+
242
+ setupViewportDrag(labelElement, viewportElement, "test-viewport");
243
+
244
+ const mockState: DragState = { isDragging: false, hasDragged: false, startX: 0, startY: 0 };
245
+ onCancelCallback?.(mockState);
246
+
247
+ expect(setViewportDraggingModule.setViewportDragging).toHaveBeenCalledWith(false);
248
+ });
249
+ });
@@ -0,0 +1,70 @@
1
+ import { createDragHandler } from "../../helpers/createDragHandler";
2
+ import { getNodeTools } from "../../helpers/getNodeTools";
3
+ import { sendPostMessage } from "../../post-message/sendPostMessage";
4
+ import { getLabelPosition } from "./helpers/getLabelPosition";
5
+ import { getTransformValues } from "./helpers/getTransformValues";
6
+ import { getZoomValue } from "./helpers/getZoomValue";
7
+ import { selectFirstViewportNode } from "./helpers/selectFirstViewportNode";
8
+ import { setViewportDragging } from "./isViewportDragging";
9
+
10
+ export const setupViewportDrag = (labelElement: SVGTextElement, viewportElement: HTMLElement, viewportName: string): (() => void) => {
11
+ // Get the parent group element that contains the label
12
+ const labelGroup = labelElement.parentElement as unknown as SVGGElement;
13
+
14
+ // Track initial positions for calculations
15
+ let initialTransform = { x: 0, y: 0 };
16
+ let initialLabelPosition = { x: 0, y: 0 };
17
+
18
+ return createDragHandler(labelElement, {
19
+ onStart: () => {
20
+ setViewportDragging(true);
21
+ initialTransform = getTransformValues(viewportElement);
22
+ initialLabelPosition = getLabelPosition(labelGroup);
23
+ selectFirstViewportNode(viewportElement);
24
+ },
25
+ onDrag: (_event, { deltaX, deltaY }) => {
26
+ const zoom = getZoomValue();
27
+
28
+ // Adjust delta for zoom level (viewport is in canvas space)
29
+ const deltaXZoomed = deltaX / zoom;
30
+ const deltaYZoomed = deltaY / zoom;
31
+
32
+ // Calculate new positions
33
+ const newX = initialTransform.x + deltaXZoomed;
34
+ const newY = initialTransform.y + deltaYZoomed;
35
+
36
+ // Update label position with raw delta (labels are in screen space)
37
+ const newLabelX = initialLabelPosition.x + deltaX;
38
+ const newLabelY = initialLabelPosition.y + deltaY;
39
+ labelGroup.setAttribute("transform", `translate(${newLabelX}, ${newLabelY})`);
40
+
41
+ // Update viewport position with zoom-adjusted delta
42
+ viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
43
+ },
44
+ onStop: (_event, { hasDragged }) => {
45
+ setViewportDragging(false);
46
+
47
+ const finalTransform = getTransformValues(viewportElement);
48
+
49
+ // If it was a drag, handle drag completion
50
+ if (hasDragged) {
51
+ // Trigger refresh after drag completes to update highlight frame and labels
52
+ const nodeTools = getNodeTools();
53
+ if (nodeTools?.refreshHighlightFrame) {
54
+ nodeTools.refreshHighlightFrame();
55
+ }
56
+ }
57
+
58
+ // Always notify parent about the new position on drag stop
59
+ sendPostMessage("viewport-position-changed", {
60
+ viewportName,
61
+ x: finalTransform.x,
62
+ y: finalTransform.y,
63
+ });
64
+ },
65
+ onCancel: () => {
66
+ setViewportDragging(false);
67
+ },
68
+ onPreventClick: () => {},
69
+ });
70
+ };
@@ -0,0 +1,37 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { createResizeHandle } from "./createResizeHandle";
3
+
4
+ describe("createResizeHandle", () => {
5
+ let container: HTMLElement;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement("div");
9
+ document.body.appendChild(container);
10
+ });
11
+
12
+ afterEach(() => {
13
+ document.body.removeChild(container);
14
+ });
15
+
16
+ it("should create a resize handle element", () => {
17
+ const handle = createResizeHandle(container);
18
+ expect(handle).toBeInstanceOf(HTMLElement);
19
+ });
20
+
21
+ it("should have the resize-handle class", () => {
22
+ const handle = createResizeHandle(container);
23
+ expect(handle.classList.contains("resize-handle")).toBe(true);
24
+ });
25
+
26
+ it("should append handle to container", () => {
27
+ const handle = createResizeHandle(container);
28
+ expect(container.contains(handle)).toBe(true);
29
+ expect(container.children).toContain(handle);
30
+ });
31
+
32
+ it("should return the created handle element", () => {
33
+ const handle = createResizeHandle(container);
34
+ const foundHandle = container.querySelector(".resize-handle");
35
+ expect(handle).toBe(foundHandle);
36
+ });
37
+ });
@@ -0,0 +1,75 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { RESIZE_PRESETS } from "../constants";
3
+ import { createResizePresets } from "./createResizePresets";
4
+
5
+ describe("createResizePresets", () => {
6
+ let container: HTMLElement;
7
+ let resizeHandle: HTMLElement;
8
+ const mockUpdateWidth = vi.fn();
9
+
10
+ beforeEach(() => {
11
+ container = document.createElement("div");
12
+ resizeHandle = document.createElement("div");
13
+ container.appendChild(resizeHandle);
14
+ document.body.appendChild(container);
15
+ mockUpdateWidth.mockClear();
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ });
21
+
22
+ it("should create presets container", () => {
23
+ const presets = createResizePresets(resizeHandle, container, mockUpdateWidth);
24
+ expect(presets).toBeInstanceOf(HTMLElement);
25
+ expect(presets.classList.contains("resize-presets")).toBe(true);
26
+ });
27
+
28
+ it("should append presets to resize handle", () => {
29
+ const presets = createResizePresets(resizeHandle, container, mockUpdateWidth);
30
+ expect(resizeHandle.contains(presets)).toBe(true);
31
+ });
32
+
33
+ it("should create buttons for all presets", () => {
34
+ createResizePresets(resizeHandle, container, mockUpdateWidth);
35
+ const buttons = resizeHandle.querySelectorAll(".resize-preset-button");
36
+ expect(buttons).toHaveLength(RESIZE_PRESETS.length);
37
+ });
38
+
39
+ it("should set correct text content for each button", () => {
40
+ createResizePresets(resizeHandle, container, mockUpdateWidth);
41
+ const buttons = resizeHandle.querySelectorAll<HTMLButtonElement>(".resize-preset-button");
42
+ RESIZE_PRESETS.forEach((preset, index) => {
43
+ expect(buttons[index].textContent).toBe(preset.name);
44
+ });
45
+ });
46
+
47
+ it("should call updateWidth with correct rawValue when button is clicked", () => {
48
+ createResizePresets(resizeHandle, container, mockUpdateWidth);
49
+ const buttons = resizeHandle.querySelectorAll<HTMLButtonElement>(".resize-preset-button");
50
+
51
+ const firstPreset = RESIZE_PRESETS[0];
52
+ buttons[0].click();
53
+
54
+ expect(mockUpdateWidth).toHaveBeenCalledTimes(1);
55
+ expect(mockUpdateWidth).toHaveBeenCalledWith(container, firstPreset.rawValue);
56
+ });
57
+
58
+ it("should handle clicks on all preset buttons", () => {
59
+ createResizePresets(resizeHandle, container, mockUpdateWidth);
60
+ const buttons = resizeHandle.querySelectorAll<HTMLButtonElement>(".resize-preset-button");
61
+
62
+ buttons.forEach((button, index) => {
63
+ button.click();
64
+ expect(mockUpdateWidth).toHaveBeenCalledWith(container, RESIZE_PRESETS[index].rawValue);
65
+ });
66
+
67
+ expect(mockUpdateWidth).toHaveBeenCalledTimes(RESIZE_PRESETS.length);
68
+ });
69
+
70
+ it("should return the presets element", () => {
71
+ const presets = createResizePresets(resizeHandle, container, mockUpdateWidth);
72
+ const foundPresets = resizeHandle.querySelector(".resize-presets");
73
+ expect(presets).toBe(foundPresets);
74
+ });
75
+ });
@@ -0,0 +1,92 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { RESIZE_PRESETS } from "../constants";
3
+ import { updateActivePreset } from "./updateActivePreset";
4
+
5
+ describe("updateActivePreset", () => {
6
+ let container: HTMLElement;
7
+
8
+ beforeEach(() => {
9
+ container = document.createElement("div");
10
+ document.body.appendChild(container);
11
+ });
12
+
13
+ afterEach(() => {
14
+ document.body.removeChild(container);
15
+ });
16
+
17
+ it("should add is-active class to matching preset button", () => {
18
+ const presets = document.createElement("div");
19
+ RESIZE_PRESETS.forEach(() => {
20
+ const button = document.createElement("button");
21
+ button.className = "resize-preset-button";
22
+ presets.appendChild(button);
23
+ });
24
+ container.appendChild(presets);
25
+
26
+ const mobilePreset = RESIZE_PRESETS[0];
27
+ updateActivePreset(container, mobilePreset.rawValue);
28
+
29
+ const buttons = container.querySelectorAll<HTMLButtonElement>(".resize-preset-button");
30
+ expect(buttons[0].classList.contains("is-active")).toBe(true);
31
+ });
32
+
33
+ it("should remove is-active class from non-matching buttons", () => {
34
+ const presets = document.createElement("div");
35
+ RESIZE_PRESETS.forEach(() => {
36
+ const button = document.createElement("button");
37
+ button.className = "resize-preset-button";
38
+ presets.appendChild(button);
39
+ });
40
+ container.appendChild(presets);
41
+
42
+ const buttons = container.querySelectorAll<HTMLButtonElement>(".resize-preset-button");
43
+ buttons[0].classList.add("is-active");
44
+ buttons[1].classList.add("is-active");
45
+
46
+ const tabletPreset = RESIZE_PRESETS[1];
47
+ updateActivePreset(container, tabletPreset.rawValue);
48
+
49
+ expect(buttons[0].classList.contains("is-active")).toBe(false);
50
+ expect(buttons[1].classList.contains("is-active")).toBe(true);
51
+ });
52
+
53
+ it("should handle width that doesn't match any preset", () => {
54
+ const presets = document.createElement("div");
55
+ RESIZE_PRESETS.forEach(() => {
56
+ const button = document.createElement("button");
57
+ button.className = "resize-preset-button";
58
+ presets.appendChild(button);
59
+ });
60
+ container.appendChild(presets);
61
+
62
+ const buttons = container.querySelectorAll<HTMLButtonElement>(".resize-preset-button");
63
+ buttons[0].classList.add("is-active");
64
+
65
+ updateActivePreset(container, 999);
66
+
67
+ buttons.forEach((button) => {
68
+ expect(button.classList.contains("is-active")).toBe(false);
69
+ });
70
+ });
71
+
72
+ it("should handle empty container", () => {
73
+ expect(() => {
74
+ updateActivePreset(container, 100);
75
+ }).not.toThrow();
76
+ });
77
+
78
+ it("should handle container with fewer buttons than presets", () => {
79
+ const presets = document.createElement("div");
80
+ // Create only 2 buttons when there are 5 presets
81
+ for (let i = 0; i < 2; i++) {
82
+ const button = document.createElement("button");
83
+ button.className = "resize-preset-button";
84
+ presets.appendChild(button);
85
+ }
86
+ container.appendChild(presets);
87
+
88
+ expect(() => {
89
+ updateActivePreset(container, RESIZE_PRESETS[0].rawValue);
90
+ }).not.toThrow();
91
+ });
92
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { RESIZE_CONFIG } from "../constants";
3
+ import { calcConstrainedWidth } from "./calcConstrainedWidth";
4
+
5
+ describe("calcConstrainedWidth", () => {
6
+ it("should return startWidth + deltaX when within bounds", () => {
7
+ const startWidth = 100;
8
+ const deltaX = 50;
9
+ const result = calcConstrainedWidth(startWidth, deltaX);
10
+ expect(result).toBe(150);
11
+ });
12
+
13
+ it("should return minWidth when result is below minimum", () => {
14
+ const startWidth = 10;
15
+ const deltaX = -10;
16
+ const result = calcConstrainedWidth(startWidth, deltaX);
17
+ expect(result).toBe(RESIZE_CONFIG.minWidth);
18
+ });
19
+
20
+ it("should return maxWidth when result is above maximum", () => {
21
+ const startWidth = 2500;
22
+ const deltaX = 100;
23
+ const result = calcConstrainedWidth(startWidth, deltaX);
24
+ expect(result).toBe(RESIZE_CONFIG.maxWidth);
25
+ });
26
+
27
+ it("should round the result", () => {
28
+ const startWidth = 100;
29
+ const deltaX = 50.7;
30
+ const result = calcConstrainedWidth(startWidth, deltaX);
31
+ expect(result).toBe(151);
32
+ });
33
+
34
+ it("should handle negative deltaX correctly", () => {
35
+ const startWidth = 200;
36
+ const deltaX = -50;
37
+ const result = calcConstrainedWidth(startWidth, deltaX);
38
+ expect(result).toBe(150);
39
+ });
40
+
41
+ it("should handle zero deltaX", () => {
42
+ const startWidth = 100;
43
+ const deltaX = 0;
44
+ const result = calcConstrainedWidth(startWidth, deltaX);
45
+ expect(result).toBe(100);
46
+ });
47
+ });