@node-edit-utils/core 2.3.2 → 2.3.3

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 (46) 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/helpers/selectFirstViewportNode.d.ts +5 -0
  12. package/dist/node-edit-utils.cjs.js +259 -235
  13. package/dist/node-edit-utils.esm.js +259 -235
  14. package/dist/node-edit-utils.umd.js +259 -235
  15. package/dist/node-edit-utils.umd.min.js +1 -1
  16. package/dist/styles.css +1 -1
  17. package/package.json +1 -1
  18. package/src/lib/canvas/createCanvasObserver.ts +2 -2
  19. package/src/lib/canvas/helpers/getCanvasContainerOrBody.ts +6 -0
  20. package/src/lib/canvas/helpers/getCanvasWindowValue.ts +2 -3
  21. package/src/lib/helpers/adjustForZoom.ts +4 -0
  22. package/src/lib/helpers/createDragHandler.ts +171 -0
  23. package/src/lib/helpers/getNodeProvider.ts +4 -0
  24. package/src/lib/helpers/getNodeTools.ts +6 -0
  25. package/src/lib/helpers/getViewportDimensions.ts +7 -0
  26. package/src/lib/helpers/index.ts +9 -1
  27. package/src/lib/helpers/parseTransform.ts +9 -0
  28. package/src/lib/helpers/toggleClass.ts +9 -0
  29. package/src/lib/node-tools/createNodeTools.ts +0 -1
  30. package/src/lib/node-tools/highlight/clearHighlightFrame.ts +2 -3
  31. package/src/lib/node-tools/highlight/createHighlightFrame.ts +5 -9
  32. package/src/lib/node-tools/highlight/createToolsContainer.ts +3 -6
  33. package/src/lib/node-tools/highlight/helpers/getElementBounds.ts +6 -5
  34. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +2 -3
  35. package/src/lib/node-tools/highlight/highlightNode.ts +7 -15
  36. package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +12 -42
  37. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +2 -3
  38. package/src/lib/styles/styles.css +1 -1
  39. package/src/lib/viewport/createViewport.ts +44 -47
  40. package/src/lib/viewport/label/getViewportLabelsOverlay.ts +4 -5
  41. package/src/lib/viewport/label/helpers/getLabelPosition.ts +3 -5
  42. package/src/lib/viewport/label/helpers/getTransformValues.ts +3 -5
  43. package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +18 -0
  44. package/src/lib/viewport/label/refreshViewportLabels.ts +2 -2
  45. package/src/lib/viewport/label/setupViewportLabelDrag.ts +58 -86
  46. package/src/lib/window/bindToWindow.ts +1 -2
@@ -1,8 +1,19 @@
1
1
  /**
2
2
  * Markup Canvas
3
3
  * High-performance markup canvas with zoom and pan capabilities
4
- * @version 2.3.2
4
+ * @version 2.3.3
5
5
  */
6
+ const getNodeTools = () => {
7
+ return window.nodeTools;
8
+ };
9
+
10
+ const getViewportDimensions = () => {
11
+ return {
12
+ width: document.documentElement.clientWidth || window.innerWidth,
13
+ height: document.documentElement.clientHeight || window.innerHeight,
14
+ };
15
+ };
16
+
6
17
  function getScreenBounds(element) {
7
18
  const rect = element.getBoundingClientRect();
8
19
  return {
@@ -17,9 +28,12 @@ const getCanvasContainer = () => {
17
28
  return document.querySelector(".canvas-container");
18
29
  };
19
30
 
31
+ const getCanvasContainerOrBody = () => {
32
+ return getCanvasContainer() || document.body;
33
+ };
34
+
20
35
  const getViewportLabelsOverlay = () => {
21
- const canvasContainer = getCanvasContainer();
22
- const container = canvasContainer || document.body;
36
+ const container = getCanvasContainerOrBody();
23
37
  // Check if overlay already exists
24
38
  let overlay = container.querySelector(".viewport-labels-overlay");
25
39
  if (!overlay) {
@@ -34,8 +48,7 @@ const getViewportLabelsOverlay = () => {
34
48
  overlay.style.height = "100vh";
35
49
  overlay.style.pointerEvents = "none";
36
50
  overlay.style.zIndex = "500";
37
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
38
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
51
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
39
52
  overlay.setAttribute("width", viewportWidth.toString());
40
53
  overlay.setAttribute("height", viewportHeight.toString());
41
54
  container.appendChild(overlay);
@@ -50,6 +63,97 @@ const setViewportLabelDragging = (isDragging) => {
50
63
  globalIsDragging = isDragging;
51
64
  };
52
65
 
66
+ /**
67
+ * Creates a reusable drag handler for mouse drag operations
68
+ * @param element - Element that triggers the drag (mousedown listener attached here)
69
+ * @param callbacks - Callbacks for drag lifecycle events
70
+ * @param options - Configuration options
71
+ * @returns Cleanup function to remove all event listeners
72
+ */
73
+ function createDragHandler(element, callbacks, options = {}) {
74
+ const { preventDefault = true, stopPropagation = true } = options;
75
+ const state = {
76
+ isDragging: false,
77
+ hasDragged: false,
78
+ startX: 0,
79
+ startY: 0,
80
+ };
81
+ const startDrag = (event) => {
82
+ if (preventDefault) {
83
+ event.preventDefault();
84
+ }
85
+ if (stopPropagation) {
86
+ event.stopPropagation();
87
+ }
88
+ state.isDragging = true;
89
+ state.hasDragged = false;
90
+ state.startX = event.clientX;
91
+ state.startY = event.clientY;
92
+ callbacks.onStart?.(event, state);
93
+ };
94
+ const handleDrag = (event) => {
95
+ if (!state.isDragging)
96
+ return;
97
+ const deltaX = event.clientX - state.startX;
98
+ const deltaY = event.clientY - state.startY;
99
+ state.hasDragged = true;
100
+ callbacks.onDrag(event, {
101
+ ...state,
102
+ deltaX,
103
+ deltaY,
104
+ });
105
+ };
106
+ const stopDrag = (event) => {
107
+ if (!state.isDragging)
108
+ return;
109
+ if (preventDefault) {
110
+ event.preventDefault();
111
+ }
112
+ if (stopPropagation) {
113
+ event.stopPropagation();
114
+ }
115
+ state.isDragging = false;
116
+ callbacks.onStop?.(event, state);
117
+ };
118
+ const cancelDrag = () => {
119
+ if (!state.isDragging)
120
+ return;
121
+ state.isDragging = false;
122
+ callbacks.onCancel?.(state);
123
+ };
124
+ const preventClick = (event) => {
125
+ if (preventDefault) {
126
+ event.preventDefault();
127
+ }
128
+ if (stopPropagation) {
129
+ event.stopPropagation();
130
+ }
131
+ callbacks.onPreventClick?.(event, state);
132
+ // Reset hasDragged flag after handling the click
133
+ if (state.hasDragged) {
134
+ state.hasDragged = false;
135
+ }
136
+ };
137
+ // Attach event listeners
138
+ element.addEventListener("mousedown", startDrag);
139
+ if (callbacks.onPreventClick) {
140
+ element.addEventListener("click", preventClick);
141
+ }
142
+ document.addEventListener("mousemove", handleDrag);
143
+ document.addEventListener("mouseup", stopDrag);
144
+ window.addEventListener("blur", cancelDrag);
145
+ // Return cleanup function
146
+ return () => {
147
+ element.removeEventListener("mousedown", startDrag);
148
+ if (callbacks.onPreventClick) {
149
+ element.removeEventListener("click", preventClick);
150
+ }
151
+ document.removeEventListener("mousemove", handleDrag);
152
+ document.removeEventListener("mouseup", stopDrag);
153
+ window.removeEventListener("blur", cancelDrag);
154
+ };
155
+ }
156
+
53
157
  function sendPostMessage(action, data) {
54
158
  window.parent.postMessage({
55
159
  source: "node-edit-utils",
@@ -59,22 +163,23 @@ function sendPostMessage(action, data) {
59
163
  }, "*");
60
164
  }
61
165
 
166
+ const parseTransform3d = (transform) => {
167
+ const match = transform.match(/translate3d\((-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px\)/);
168
+ return match ? { x: parseFloat(match[1]), y: parseFloat(match[2]) } : { x: 0, y: 0 };
169
+ };
170
+ const parseTransform2d = (transform) => {
171
+ const match = transform?.match(/translate\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)/);
172
+ return match ? { x: parseFloat(match[1]), y: parseFloat(match[2]) } : { x: 0, y: 0 };
173
+ };
174
+
62
175
  const getLabelPosition = (labelGroup) => {
63
176
  const transform = labelGroup.getAttribute("transform");
64
- const match = transform?.match(/translate\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)/);
65
- if (match) {
66
- return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
67
- }
68
- return { x: 0, y: 0 };
177
+ return parseTransform2d(transform);
69
178
  };
70
179
 
71
180
  const getTransformValues = (element) => {
72
181
  const style = element.style.transform;
73
- const match = style.match(/translate3d\((-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px\)/);
74
- if (match) {
75
- return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
76
- }
77
- return { x: 0, y: 0 };
182
+ return parseTransform3d(style);
78
183
  };
79
184
 
80
185
  const getZoomValue = () => {
@@ -82,80 +187,71 @@ const getZoomValue = () => {
82
187
  return zoomValue ? parseFloat(zoomValue) : 1;
83
188
  };
84
189
 
190
+ /**
191
+ * Selects the first child node inside a viewport element.
192
+ * Skips the resize-handle element if present.
193
+ */
194
+ const selectFirstViewportNode = (viewportElement) => {
195
+ const firstChild = Array.from(viewportElement.children).find((child) => !child.classList.contains("resize-handle"));
196
+ if (firstChild) {
197
+ const nodeTools = getNodeTools();
198
+ if (nodeTools?.selectNode) {
199
+ nodeTools.selectNode(firstChild);
200
+ }
201
+ }
202
+ };
203
+
85
204
  const setupViewportLabelDrag = (labelElement, viewportElement, viewportName) => {
86
- let isDragging = false;
87
- let startX = 0;
88
- let startY = 0;
89
- let initialTransform = { x: 0, y: 0 };
90
- let initialLabelPosition = { x: 0, y: 0 };
91
205
  // Get the parent group element that contains the label
92
206
  const labelGroup = labelElement.parentElement;
93
- const startDrag = (event) => {
94
- event.preventDefault();
95
- event.stopPropagation();
96
- isDragging = true;
97
- setViewportLabelDragging(true);
98
- startX = event.clientX;
99
- startY = event.clientY;
100
- initialTransform = getTransformValues(viewportElement);
101
- initialLabelPosition = getLabelPosition(labelGroup);
102
- };
103
- const handleDrag = (event) => {
104
- if (!isDragging)
105
- return;
106
- const zoom = getZoomValue();
107
- // Calculate mouse delta
108
- const rawDeltaX = event.clientX - startX;
109
- const rawDeltaY = event.clientY - startY;
110
- // Adjust delta for zoom level
111
- const deltaX = rawDeltaX / zoom;
112
- const deltaY = rawDeltaY / zoom;
113
- const newX = initialTransform.x + deltaX;
114
- const newY = initialTransform.y + deltaY;
115
- // Update label position with raw delta (labels are in screen space)
116
- const newLabelX = initialLabelPosition.x + rawDeltaX;
117
- const newLabelY = initialLabelPosition.y + rawDeltaY;
118
- labelGroup.setAttribute("transform", `translate(${newLabelX}, ${newLabelY})`);
119
- // Update viewport position with zoom-adjusted delta
120
- viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
121
- };
122
- const stopDrag = (event) => {
123
- if (!isDragging)
124
- return;
125
- event.preventDefault();
126
- event.stopPropagation();
127
- isDragging = false;
128
- setViewportLabelDragging(false);
129
- const finalTransform = getTransformValues(viewportElement);
130
- // Trigger refresh after drag completes to update highlight frame and labels
131
- // biome-ignore lint/suspicious/noExplicitAny: global window extension
132
- const nodeTools = window.nodeTools;
133
- if (nodeTools?.refreshHighlightFrame) {
134
- nodeTools.refreshHighlightFrame();
135
- }
136
- // Notify parent about the new position
137
- sendPostMessage("viewport-position-changed", {
138
- viewportName,
139
- x: finalTransform.x,
140
- y: finalTransform.y,
141
- });
142
- };
143
- const cancelDrag = () => {
144
- isDragging = false;
145
- setViewportLabelDragging(false);
146
- };
147
- // Attach event listeners
148
- labelElement.addEventListener("mousedown", startDrag);
149
- document.addEventListener("mousemove", handleDrag);
150
- document.addEventListener("mouseup", stopDrag);
151
- window.addEventListener("blur", cancelDrag);
152
- // Return cleanup function
153
- return () => {
154
- labelElement.removeEventListener("mousedown", startDrag);
155
- document.removeEventListener("mousemove", handleDrag);
156
- document.removeEventListener("mouseup", stopDrag);
157
- window.removeEventListener("blur", cancelDrag);
158
- };
207
+ // Track initial positions for calculations
208
+ let initialTransform = { x: 0, y: 0 };
209
+ let initialLabelPosition = { x: 0, y: 0 };
210
+ return createDragHandler(labelElement, {
211
+ onStart: () => {
212
+ setViewportLabelDragging(true);
213
+ initialTransform = getTransformValues(viewportElement);
214
+ initialLabelPosition = getLabelPosition(labelGroup);
215
+ selectFirstViewportNode(viewportElement);
216
+ },
217
+ onDrag: (_event, { deltaX, deltaY }) => {
218
+ const zoom = getZoomValue();
219
+ // Adjust delta for zoom level (viewport is in canvas space)
220
+ const deltaXZoomed = deltaX / zoom;
221
+ const deltaYZoomed = deltaY / zoom;
222
+ // Calculate new positions
223
+ const newX = initialTransform.x + deltaXZoomed;
224
+ const newY = initialTransform.y + deltaYZoomed;
225
+ // Update label position with raw delta (labels are in screen space)
226
+ const newLabelX = initialLabelPosition.x + deltaX;
227
+ const newLabelY = initialLabelPosition.y + deltaY;
228
+ labelGroup.setAttribute("transform", `translate(${newLabelX}, ${newLabelY})`);
229
+ // Update viewport position with zoom-adjusted delta
230
+ viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
231
+ },
232
+ onStop: (_event, { hasDragged }) => {
233
+ setViewportLabelDragging(false);
234
+ // If it was a drag, handle drag completion
235
+ if (hasDragged) {
236
+ const finalTransform = getTransformValues(viewportElement);
237
+ // Trigger refresh after drag completes to update highlight frame and labels
238
+ const nodeTools = getNodeTools();
239
+ if (nodeTools?.refreshHighlightFrame) {
240
+ nodeTools.refreshHighlightFrame();
241
+ }
242
+ // Notify parent about the new position
243
+ sendPostMessage("viewport-position-changed", {
244
+ viewportName,
245
+ x: finalTransform.x,
246
+ y: finalTransform.y,
247
+ });
248
+ }
249
+ },
250
+ onCancel: () => {
251
+ setViewportLabelDragging(false);
252
+ },
253
+ onPreventClick: () => { },
254
+ });
159
255
  };
160
256
 
161
257
  // Store cleanup functions for drag listeners
@@ -167,8 +263,7 @@ const refreshViewportLabels = () => {
167
263
  }
168
264
  const overlay = getViewportLabelsOverlay();
169
265
  // Update SVG dimensions to match current viewport
170
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
171
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
266
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
172
267
  overlay.setAttribute("width", viewportWidth.toString());
173
268
  overlay.setAttribute("height", viewportHeight.toString());
174
269
  // Find all viewports with names
@@ -212,7 +307,6 @@ const refreshViewportLabels = () => {
212
307
  };
213
308
 
214
309
  const getCanvasWindowValue = (path, canvasName = "canvas") => {
215
- // biome-ignore lint/suspicious/noExplicitAny: global window extension
216
310
  const canvas = window[canvasName];
217
311
  return path.reduce((obj, prop) => obj?.[prop], canvas);
218
312
  };
@@ -235,8 +329,7 @@ function createCanvasObserver(canvasName = "canvas") {
235
329
  const observer = new MutationObserver(() => {
236
330
  applyCanvasState(canvasName);
237
331
  // Refresh highlight frame (throttled via withRAFThrottle)
238
- // biome-ignore lint/suspicious/noExplicitAny: global window extension
239
- const nodeTools = window.nodeTools;
332
+ const nodeTools = getNodeTools();
240
333
  if (nodeTools?.refreshHighlightFrame) {
241
334
  nodeTools.refreshHighlightFrame();
242
335
  }
@@ -275,7 +368,6 @@ const connectResizeObserver = (element, handler) => {
275
368
 
276
369
  const bindToWindow = (key, value) => {
277
370
  if (typeof window !== "undefined") {
278
- // biome-ignore lint/suspicious/noExplicitAny: global window extension requires flexibility
279
371
  window[key] = value;
280
372
  }
281
373
  };
@@ -301,8 +393,7 @@ const processPostMessage = (event, onNodeSelected) => {
301
393
  };
302
394
 
303
395
  const clearHighlightFrame = () => {
304
- const canvasContainer = getCanvasContainer();
305
- const container = canvasContainer || document.body;
396
+ const container = getCanvasContainerOrBody();
306
397
  const frame = container.querySelector(".highlight-frame-overlay");
307
398
  if (frame) {
308
399
  frame.remove();
@@ -431,7 +522,7 @@ const handleNodeClick = (event, nodeProvider, text, onNodeSelected) => {
431
522
  onNodeSelected(selectedNode);
432
523
  };
433
524
 
434
- const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
525
+ const setupEventListener = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
435
526
  const messageHandler = (event) => {
436
527
  processPostMessage(event, onNodeSelected);
437
528
  };
@@ -455,6 +546,17 @@ const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, tex
455
546
  };
456
547
  };
457
548
 
549
+ const toggleClass = (element, className, condition) => {
550
+ if (!element)
551
+ return;
552
+ if (condition) {
553
+ element.classList.add(className);
554
+ }
555
+ else {
556
+ element.classList.remove(className);
557
+ }
558
+ };
559
+
458
560
  const isComponentInstance = (element) => {
459
561
  return element.getAttribute("data-instance") === "true";
460
562
  };
@@ -520,8 +622,7 @@ const createHighlightFrame = (node, isInstance = false, isTextEdit = false) => {
520
622
  svg.style.height = "100vh";
521
623
  svg.style.pointerEvents = "none";
522
624
  svg.style.zIndex = "500";
523
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
524
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
625
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
525
626
  svg.setAttribute("width", viewportWidth.toString());
526
627
  svg.setAttribute("height", viewportHeight.toString());
527
628
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
@@ -544,13 +645,8 @@ const createHighlightFrame = (node, isInstance = false, isTextEdit = false) => {
544
645
  group.appendChild(rect);
545
646
  createCornerHandles(group, minWidth, height, isInstance, isTextEdit);
546
647
  svg.appendChild(group);
547
- const canvasContainer = getCanvasContainer();
548
- if (canvasContainer) {
549
- canvasContainer.appendChild(svg);
550
- }
551
- else {
552
- document.body.appendChild(svg);
553
- }
648
+ const container = getCanvasContainerOrBody();
649
+ container.appendChild(svg);
554
650
  return svg;
555
651
  };
556
652
 
@@ -587,19 +683,14 @@ const createTagLabel = (node, nodeTools) => {
587
683
  const createToolsContainer = (node, highlightFrame, isInstance = false, isTextEdit = false) => {
588
684
  const nodeTools = document.createElement("div");
589
685
  nodeTools.className = "node-tools";
590
- if (isInstance) {
591
- nodeTools.classList.add("is-instance");
592
- }
593
- if (isTextEdit) {
594
- nodeTools.classList.add("is-text-edit");
595
- }
686
+ toggleClass(nodeTools, "is-instance", isInstance);
687
+ toggleClass(nodeTools, "is-text-edit", isTextEdit);
596
688
  highlightFrame.appendChild(nodeTools);
597
689
  createTagLabel(node, nodeTools);
598
690
  };
599
691
 
600
692
  function getHighlightFrameElement() {
601
- const canvasContainer = getCanvasContainer();
602
- const container = canvasContainer || document.body;
693
+ const container = getCanvasContainerOrBody();
603
694
  return container.querySelector(".highlight-frame-overlay");
604
695
  }
605
696
 
@@ -607,8 +698,8 @@ const highlightNode = (node) => {
607
698
  if (!node)
608
699
  return;
609
700
  const existingHighlightFrame = getHighlightFrameElement();
610
- const canvasContainer = getCanvasContainer();
611
- const existingToolsWrapper = canvasContainer?.querySelector(".highlight-frame-tools-wrapper") || document.body.querySelector(".highlight-frame-tools-wrapper");
701
+ const container = getCanvasContainerOrBody();
702
+ const existingToolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
612
703
  if (existingHighlightFrame) {
613
704
  existingHighlightFrame.remove();
614
705
  }
@@ -627,24 +718,15 @@ const highlightNode = (node) => {
627
718
  // Create tools wrapper using CSS transform (GPU-accelerated)
628
719
  const toolsWrapper = document.createElement("div");
629
720
  toolsWrapper.classList.add("highlight-frame-tools-wrapper");
630
- if (isInstance) {
631
- toolsWrapper.classList.add("is-instance");
632
- }
633
- if (isTextEdit) {
634
- toolsWrapper.classList.add("is-text-edit");
635
- }
721
+ toggleClass(toolsWrapper, "is-instance", isInstance);
722
+ toggleClass(toolsWrapper, "is-text-edit", isTextEdit);
636
723
  toolsWrapper.style.position = "absolute";
637
724
  toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
638
725
  toolsWrapper.style.transformOrigin = "left center";
639
726
  toolsWrapper.style.pointerEvents = "none";
640
727
  toolsWrapper.style.zIndex = "500";
641
728
  createToolsContainer(node, toolsWrapper, isInstance, isTextEdit);
642
- if (canvasContainer) {
643
- canvasContainer.appendChild(toolsWrapper);
644
- }
645
- else {
646
- document.body.appendChild(toolsWrapper);
647
- }
729
+ container.appendChild(toolsWrapper);
648
730
  };
649
731
 
650
732
  const getComponentColor = () => {
@@ -662,24 +744,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
662
744
  const isTextEdit = node.contentEditable === "true";
663
745
  // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
664
746
  // Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
665
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
666
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
747
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
667
748
  frame.setAttribute("width", viewportWidth.toString());
668
749
  frame.setAttribute("height", viewportHeight.toString());
669
750
  // Update instance class
670
- if (isInstance) {
671
- frame.classList.add("is-instance");
672
- }
673
- else {
674
- frame.classList.remove("is-instance");
675
- }
751
+ toggleClass(frame, "is-instance", isInstance);
676
752
  // Update text edit class
677
- if (isTextEdit) {
678
- frame.classList.add("is-text-edit");
679
- }
680
- else {
681
- frame.classList.remove("is-text-edit");
682
- }
753
+ toggleClass(frame, "is-text-edit", isTextEdit);
683
754
  const group = frame.querySelector(".highlight-frame-group");
684
755
  if (!group)
685
756
  return;
@@ -696,8 +767,7 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
696
767
  else {
697
768
  rect.removeAttribute("stroke"); // Use CSS default
698
769
  }
699
- const canvasContainer = getCanvasContainer();
700
- const container = canvasContainer || document.body;
770
+ const container = getCanvasContainerOrBody();
701
771
  const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
702
772
  const nodeTools = toolsWrapper?.querySelector(".node-tools");
703
773
  const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
@@ -708,36 +778,10 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
708
778
  const minWidth = Math.max(width, 3);
709
779
  const bottomY = top + height;
710
780
  // Update instance classes on tools wrapper and node tools
711
- if (toolsWrapper) {
712
- if (isInstance) {
713
- toolsWrapper.classList.add("is-instance");
714
- }
715
- else {
716
- toolsWrapper.classList.remove("is-instance");
717
- }
718
- // Update text edit class
719
- if (isTextEdit) {
720
- toolsWrapper.classList.add("is-text-edit");
721
- }
722
- else {
723
- toolsWrapper.classList.remove("is-text-edit");
724
- }
725
- }
726
- if (nodeTools) {
727
- if (isInstance) {
728
- nodeTools.classList.add("is-instance");
729
- }
730
- else {
731
- nodeTools.classList.remove("is-instance");
732
- }
733
- // Update text edit class
734
- if (isTextEdit) {
735
- nodeTools.classList.add("is-text-edit");
736
- }
737
- else {
738
- nodeTools.classList.remove("is-text-edit");
739
- }
740
- }
781
+ toggleClass(toolsWrapper, "is-instance", isInstance);
782
+ toggleClass(toolsWrapper, "is-text-edit", isTextEdit);
783
+ toggleClass(nodeTools, "is-instance", isInstance);
784
+ toggleClass(nodeTools, "is-text-edit", isTextEdit);
741
785
  // Batch all DOM writes (single paint pass)
742
786
  // Update group transform to move entire group (rect + handles) at once
743
787
  group.setAttribute("transform", `translate(${left}, ${top})`);
@@ -803,8 +847,7 @@ const updateHighlightFrameVisibility = (node) => {
803
847
  const displayValue = hasHiddenClass ? "none" : "";
804
848
  // Batch DOM writes
805
849
  frame.style.display = displayValue;
806
- const canvasContainer = getCanvasContainer();
807
- const container = canvasContainer || document.body;
850
+ const container = getCanvasContainerOrBody();
808
851
  const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
809
852
  if (toolsWrapper) {
810
853
  toolsWrapper.style.display = displayValue;
@@ -1110,7 +1153,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
1110
1153
  highlightNode(node);
1111
1154
  if (nodeProvider) {
1112
1155
  updateHighlightFrameVisibility(node);
1113
- updateHighlightFrameVisibility(node);
1114
1156
  }
1115
1157
  }
1116
1158
  else {
@@ -1118,7 +1160,7 @@ const createNodeTools = (element, canvasName = "canvas") => {
1118
1160
  }
1119
1161
  };
1120
1162
  // Setup event listener
1121
- const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
1163
+ const removeListeners = setupEventListener(nodeProvider, selectNode, handleEscape, text);
1122
1164
  const cleanup = () => {
1123
1165
  removeListeners();
1124
1166
  resizeObserver?.disconnect();
@@ -1153,6 +1195,10 @@ const createNodeTools = (element, canvasName = "canvas") => {
1153
1195
  return nodeTools;
1154
1196
  };
1155
1197
 
1198
+ const getNodeProvider = () => {
1199
+ return document.querySelector('[data-role="node-provider"]');
1200
+ };
1201
+
1156
1202
  const DEFAULT_WIDTH = 400;
1157
1203
  const RESIZE_CONFIG = {
1158
1204
  minWidth: 4,
@@ -1186,27 +1232,6 @@ const RESIZE_PRESETS = [
1186
1232
  },
1187
1233
  ];
1188
1234
 
1189
- const setupEventListener = (resizeHandle, startResize, handleResize, stopResize, blurResize) => {
1190
- const handleMouseLeave = (event) => {
1191
- // Check if mouse is leaving the window/document
1192
- if (!event.relatedTarget && (event.target === document || event.target === document.documentElement)) {
1193
- blurResize();
1194
- }
1195
- };
1196
- resizeHandle.addEventListener("mousedown", startResize);
1197
- document.addEventListener("mousemove", handleResize);
1198
- document.addEventListener("mouseup", stopResize);
1199
- document.addEventListener("mouseleave", handleMouseLeave);
1200
- window.addEventListener("blur", blurResize);
1201
- return () => {
1202
- resizeHandle.removeEventListener("mousedown", startResize);
1203
- document.removeEventListener("mousemove", handleResize);
1204
- document.removeEventListener("mouseup", stopResize);
1205
- document.removeEventListener("mouseleave", handleMouseLeave);
1206
- window.removeEventListener("blur", blurResize);
1207
- };
1208
- };
1209
-
1210
1235
  const createResizeHandle = (container) => {
1211
1236
  const handle = document.createElement("div");
1212
1237
  handle.className = "resize-handle";
@@ -1273,58 +1298,57 @@ const createViewport = (container, initialWidth) => {
1273
1298
  const width = initialWidth ?? DEFAULT_WIDTH;
1274
1299
  container.style.setProperty("--container-width", `${width}px`);
1275
1300
  createResizePresets(resizeHandle, container, updateWidth);
1276
- let isDragging = false;
1301
+ // Track initial values for resize calculation
1277
1302
  let startX = 0;
1278
1303
  let startWidth = 0;
1279
- const startResize = (event) => {
1280
- event.preventDefault();
1281
- event.stopPropagation();
1282
- isDragging = true;
1283
- startX = event.clientX;
1284
- startWidth = container.offsetWidth;
1285
- };
1286
- const handleResize = (event) => {
1287
- if (!isDragging)
1288
- return;
1289
- if (canvas) {
1290
- canvas.style.cursor = "ew-resize";
1291
- }
1292
- const width = calcWidth(event, startX, startWidth);
1293
- updateWidth(container, width);
1294
- };
1295
- const stopResize = (event) => {
1296
- event.preventDefault();
1297
- event.stopPropagation();
1298
- if (canvas) {
1299
- canvas.style.cursor = "default";
1300
- }
1301
- isDragging = false;
1302
- };
1303
- const blurResize = () => {
1304
- if (canvas) {
1305
- canvas.style.cursor = "default";
1304
+ // Handle mouse leave for resize (specific to resize use case)
1305
+ const handleMouseLeave = (event) => {
1306
+ // Check if mouse is leaving the window/document
1307
+ if (!event.relatedTarget && (event.target === document || event.target === document.documentElement)) {
1308
+ if (canvas) {
1309
+ canvas.style.cursor = "default";
1310
+ }
1306
1311
  }
1307
- isDragging = false;
1308
1312
  };
1309
- const removeListeners = setupEventListener(resizeHandle, startResize, handleResize, stopResize, blurResize);
1310
- // Refresh viewport labels when viewport is created
1313
+ const removeDragListeners = createDragHandler(resizeHandle, {
1314
+ onStart: (_event, { startX: dragStartX }) => {
1315
+ startX = dragStartX;
1316
+ startWidth = container.offsetWidth;
1317
+ },
1318
+ onDrag: (event) => {
1319
+ if (canvas) {
1320
+ canvas.style.cursor = "ew-resize";
1321
+ }
1322
+ const width = calcWidth(event, startX, startWidth);
1323
+ updateWidth(container, width);
1324
+ },
1325
+ onStop: () => {
1326
+ if (canvas) {
1327
+ canvas.style.cursor = "default";
1328
+ }
1329
+ },
1330
+ onCancel: () => {
1331
+ if (canvas) {
1332
+ canvas.style.cursor = "default";
1333
+ }
1334
+ },
1335
+ onPreventClick: () => { },
1336
+ });
1337
+ document.addEventListener("mouseleave", handleMouseLeave);
1311
1338
  refreshViewportLabels();
1312
1339
  const cleanup = () => {
1313
- isDragging = false;
1314
- removeListeners();
1340
+ removeDragListeners();
1341
+ document.removeEventListener("mouseleave", handleMouseLeave);
1315
1342
  resizeHandle.remove();
1316
- // Refresh labels after cleanup to remove this viewport's label if needed
1317
1343
  refreshViewportLabels();
1318
1344
  };
1319
1345
  return {
1320
1346
  setWidth: (width) => {
1321
1347
  updateWidth(container, width);
1322
1348
  refreshViewportLabels();
1323
- // Refresh highlight frame when viewport width changes to update node positions
1324
- // biome-ignore lint/suspicious/noExplicitAny: global window extension
1325
- const nodeTools = window.nodeTools;
1349
+ const nodeTools = getNodeTools();
1326
1350
  const selectedNode = nodeTools?.getSelectedNode?.();
1327
- const nodeProvider = document.querySelector('[data-role="node-provider"]');
1351
+ const nodeProvider = getNodeProvider();
1328
1352
  if (selectedNode && nodeProvider) {
1329
1353
  refreshHighlightFrame(selectedNode, nodeProvider);
1330
1354
  }