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