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