@node-edit-utils/core 2.3.2 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/lib/canvas/helpers/getCanvasContainerOrBody.d.ts +1 -0
  2. package/dist/lib/canvas/helpers/getCanvasWindowValue.d.ts +1 -1
  3. package/dist/lib/helpers/adjustForZoom.d.ts +1 -0
  4. package/dist/lib/helpers/createDragHandler.d.ts +69 -0
  5. package/dist/lib/helpers/getNodeProvider.d.ts +1 -0
  6. package/dist/lib/helpers/getNodeTools.d.ts +2 -0
  7. package/dist/lib/helpers/getViewportDimensions.d.ts +4 -0
  8. package/dist/lib/helpers/index.d.ts +9 -1
  9. package/dist/lib/helpers/parseTransform.d.ts +8 -0
  10. package/dist/lib/helpers/toggleClass.d.ts +1 -0
  11. package/dist/lib/viewport/label/getViewportLabelOverlay.d.ts +1 -0
  12. package/dist/lib/viewport/label/helpers/selectFirstViewportNode.d.ts +5 -0
  13. package/dist/lib/viewport/label/index.d.ts +5 -3
  14. package/dist/lib/viewport/label/isViewportDragging.d.ts +2 -0
  15. package/dist/lib/viewport/label/refreshViewportLabel.d.ts +8 -0
  16. package/dist/lib/viewport/label/removeViewportLabel.d.ts +5 -0
  17. package/dist/lib/viewport/label/setupViewportDrag.d.ts +1 -0
  18. package/dist/node-edit-utils.cjs.js +342 -280
  19. package/dist/node-edit-utils.esm.js +342 -280
  20. package/dist/node-edit-utils.umd.js +342 -280
  21. package/dist/node-edit-utils.umd.min.js +1 -1
  22. package/dist/styles.css +1 -1
  23. package/package.json +7 -2
  24. package/src/lib/canvas/createCanvasObserver.test.ts +242 -0
  25. package/src/lib/canvas/createCanvasObserver.ts +2 -2
  26. package/src/lib/canvas/disableCanvasKeyboard.test.ts +53 -0
  27. package/src/lib/canvas/disableCanvasKeyboard.ts +1 -1
  28. package/src/lib/canvas/disableCanvasTextMode.test.ts +53 -0
  29. package/src/lib/canvas/disableCanvasTextMode.ts +1 -1
  30. package/src/lib/canvas/enableCanvasKeyboard.test.ts +53 -0
  31. package/src/lib/canvas/enableCanvasKeyboard.ts +1 -1
  32. package/src/lib/canvas/enableCanvasTextMode.test.ts +53 -0
  33. package/src/lib/canvas/enableCanvasTextMode.ts +1 -1
  34. package/src/lib/canvas/helpers/applyCanvasState.test.ts +119 -0
  35. package/src/lib/canvas/helpers/applyCanvasState.ts +1 -1
  36. package/src/lib/canvas/helpers/getCanvasContainer.test.ts +62 -0
  37. package/src/lib/canvas/helpers/getCanvasContainerOrBody.test.ts +51 -0
  38. package/src/lib/canvas/helpers/getCanvasContainerOrBody.ts +6 -0
  39. package/src/lib/canvas/helpers/getCanvasWindowValue.test.ts +116 -0
  40. package/src/lib/canvas/helpers/getCanvasWindowValue.ts +2 -3
  41. package/src/lib/helpers/adjustForZoom.test.ts +65 -0
  42. package/src/lib/helpers/adjustForZoom.ts +4 -0
  43. package/src/lib/helpers/createDragHandler.test.ts +325 -0
  44. package/src/lib/helpers/createDragHandler.ts +171 -0
  45. package/src/lib/helpers/getNodeProvider.test.ts +71 -0
  46. package/src/lib/helpers/getNodeProvider.ts +4 -0
  47. package/src/lib/helpers/getNodeTools.test.ts +50 -0
  48. package/src/lib/helpers/getNodeTools.ts +6 -0
  49. package/src/lib/helpers/getViewportDimensions.test.ts +93 -0
  50. package/src/lib/helpers/getViewportDimensions.ts +7 -0
  51. package/src/lib/helpers/index.ts +9 -1
  52. package/src/lib/helpers/observer/connectMutationObserver.test.ts +127 -0
  53. package/src/lib/helpers/observer/connectResizeObserver.test.ts +147 -0
  54. package/src/lib/helpers/parseTransform.test.ts +117 -0
  55. package/src/lib/helpers/parseTransform.ts +9 -0
  56. package/src/lib/helpers/toggleClass.test.ts +71 -0
  57. package/src/lib/helpers/toggleClass.ts +9 -0
  58. package/src/lib/helpers/withRAF.test.ts +439 -0
  59. package/src/lib/node-tools/createNodeTools.test.ts +373 -0
  60. package/src/lib/node-tools/createNodeTools.ts +0 -1
  61. package/src/lib/node-tools/events/click/handleNodeClick.test.ts +109 -0
  62. package/src/lib/node-tools/events/setupEventListener.test.ts +136 -0
  63. package/src/lib/node-tools/highlight/clearHighlightFrame.test.ts +88 -0
  64. package/src/lib/node-tools/highlight/clearHighlightFrame.ts +2 -3
  65. package/src/lib/node-tools/highlight/createCornerHandles.test.ts +150 -0
  66. package/src/lib/node-tools/highlight/createHighlightFrame.test.ts +237 -0
  67. package/src/lib/node-tools/highlight/createHighlightFrame.ts +5 -9
  68. package/src/lib/node-tools/highlight/createTagLabel.test.ts +135 -0
  69. package/src/lib/node-tools/highlight/createToolsContainer.test.ts +97 -0
  70. package/src/lib/node-tools/highlight/createToolsContainer.ts +3 -6
  71. package/src/lib/node-tools/highlight/helpers/getElementBounds.test.ts +158 -0
  72. package/src/lib/node-tools/highlight/helpers/getElementBounds.ts +6 -5
  73. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.test.ts +78 -0
  74. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +2 -3
  75. package/src/lib/node-tools/highlight/helpers/getScreenBounds.test.ts +133 -0
  76. package/src/lib/node-tools/highlight/highlightNode.test.ts +213 -0
  77. package/src/lib/node-tools/highlight/highlightNode.ts +7 -15
  78. package/src/lib/node-tools/highlight/refreshHighlightFrame.test.ts +323 -0
  79. package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +12 -42
  80. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.test.ts +110 -0
  81. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +2 -3
  82. package/src/lib/node-tools/select/helpers/getElementsFromPoint.test.ts +109 -0
  83. package/src/lib/node-tools/select/helpers/isInsideComponent.test.ts +81 -0
  84. package/src/lib/node-tools/select/helpers/isInsideViewport.test.ts +82 -0
  85. package/src/lib/node-tools/select/helpers/targetSameCandidates.test.ts +81 -0
  86. package/src/lib/node-tools/select/selectNode.test.ts +238 -0
  87. package/src/lib/node-tools/text/events/setupKeydownHandler.test.ts +91 -0
  88. package/src/lib/node-tools/text/events/setupMutationObserver.test.ts +213 -0
  89. package/src/lib/node-tools/text/events/setupNodeListeners.test.ts +133 -0
  90. package/src/lib/node-tools/text/helpers/enterTextEditMode.test.ts +50 -0
  91. package/src/lib/node-tools/text/helpers/handleTextChange.test.ts +201 -0
  92. package/src/lib/node-tools/text/helpers/hasTextContent.test.ts +101 -0
  93. package/src/lib/node-tools/text/helpers/insertLineBreak.test.ts +96 -0
  94. package/src/lib/node-tools/text/helpers/makeNodeEditable.test.ts +56 -0
  95. package/src/lib/node-tools/text/helpers/makeNodeNonEditable.test.ts +57 -0
  96. package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.test.ts +61 -0
  97. package/src/lib/node-tools/text/nodeText.test.ts +233 -0
  98. package/src/lib/post-message/processPostMessage.test.ts +218 -0
  99. package/src/lib/post-message/sendPostMessage.test.ts +120 -0
  100. package/src/lib/styles/styles.css +3 -3
  101. package/src/lib/viewport/createViewport.test.ts +267 -0
  102. package/src/lib/viewport/createViewport.ts +51 -51
  103. package/src/lib/viewport/events/setupEventListener.test.ts +103 -0
  104. package/src/lib/viewport/label/getViewportLabelOverlay.test.ts +77 -0
  105. package/src/lib/viewport/label/{getViewportLabelsOverlay.ts → getViewportLabelOverlay.ts} +6 -6
  106. package/src/lib/viewport/label/helpers/getLabelPosition.test.ts +51 -0
  107. package/src/lib/viewport/label/helpers/getLabelPosition.ts +3 -5
  108. package/src/lib/viewport/label/helpers/getTransformValues.test.ts +59 -0
  109. package/src/lib/viewport/label/helpers/getTransformValues.ts +3 -5
  110. package/src/lib/viewport/label/helpers/getZoomValue.test.ts +53 -0
  111. package/src/lib/viewport/label/helpers/selectFirstViewportNode.test.ts +105 -0
  112. package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +26 -0
  113. package/src/lib/viewport/label/index.ts +5 -3
  114. package/src/lib/viewport/label/isViewportDragging.test.ts +35 -0
  115. package/src/lib/viewport/label/isViewportDragging.ts +9 -0
  116. package/src/lib/viewport/label/refreshViewportLabel.test.ts +105 -0
  117. package/src/lib/viewport/label/refreshViewportLabel.ts +50 -0
  118. package/src/lib/viewport/label/refreshViewportLabels.test.ts +107 -0
  119. package/src/lib/viewport/label/refreshViewportLabels.ts +19 -52
  120. package/src/lib/viewport/label/removeViewportLabel.test.ts +67 -0
  121. package/src/lib/viewport/label/removeViewportLabel.ts +20 -0
  122. package/src/lib/viewport/label/setupViewportDrag.test.ts +249 -0
  123. package/src/lib/viewport/label/setupViewportDrag.ts +70 -0
  124. package/src/lib/viewport/resize/createResizeHandle.test.ts +37 -0
  125. package/src/lib/viewport/resize/createResizePresets.test.ts +75 -0
  126. package/src/lib/viewport/resize/updateActivePreset.test.ts +92 -0
  127. package/src/lib/viewport/width/calcConstrainedWidth.test.ts +47 -0
  128. package/src/lib/viewport/width/calcWidth.test.ts +68 -0
  129. package/src/lib/viewport/width/updateWidth.test.ts +78 -0
  130. package/src/lib/window/bindToWindow.test.ts +166 -0
  131. package/src/lib/window/bindToWindow.ts +1 -2
  132. package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +0 -1
  133. package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +0 -2
  134. package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +0 -1
  135. package/src/lib/viewport/label/isViewportLabelDragging.ts +0 -9
  136. package/src/lib/viewport/label/setupViewportLabelDrag.ts +0 -98
@@ -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.4
5
5
  */
6
6
  (function (global, factory) {
7
7
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
@@ -9,23 +9,27 @@
9
9
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MarkupCanvas = {}));
10
10
  })(this, (function (exports) { 'use strict';
11
11
 
12
- function getScreenBounds(element) {
13
- const rect = element.getBoundingClientRect();
12
+ const getNodeTools = () => {
13
+ return window.nodeTools;
14
+ };
15
+
16
+ const getViewportDimensions = () => {
14
17
  return {
15
- top: rect.top,
16
- left: rect.left,
17
- width: rect.width,
18
- height: rect.height,
18
+ width: document.documentElement.clientWidth || window.innerWidth,
19
+ height: document.documentElement.clientHeight || window.innerHeight,
19
20
  };
20
- }
21
+ };
21
22
 
22
23
  const getCanvasContainer = () => {
23
24
  return document.querySelector(".canvas-container");
24
25
  };
25
26
 
26
- const getViewportLabelsOverlay = () => {
27
- const canvasContainer = getCanvasContainer();
28
- const container = canvasContainer || document.body;
27
+ const getCanvasContainerOrBody = () => {
28
+ return getCanvasContainer() || document.body;
29
+ };
30
+
31
+ const getViewportLabelOverlay = () => {
32
+ const container = getCanvasContainerOrBody();
29
33
  // Check if overlay already exists
30
34
  let overlay = container.querySelector(".viewport-labels-overlay");
31
35
  if (!overlay) {
@@ -40,8 +44,7 @@
40
44
  overlay.style.height = "100vh";
41
45
  overlay.style.pointerEvents = "none";
42
46
  overlay.style.zIndex = "500";
43
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
44
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
47
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
45
48
  overlay.setAttribute("width", viewportWidth.toString());
46
49
  overlay.setAttribute("height", viewportHeight.toString());
47
50
  container.appendChild(overlay);
@@ -51,11 +54,112 @@
51
54
 
52
55
  // Global flag to prevent refreshViewportLabels during drag
53
56
  let globalIsDragging = false;
54
- const isViewportLabelDragging = () => globalIsDragging;
55
- const setViewportLabelDragging = (isDragging) => {
57
+ const isViewportDragging = () => globalIsDragging;
58
+ const setViewportDragging = (isDragging) => {
56
59
  globalIsDragging = isDragging;
57
60
  };
58
61
 
62
+ function getScreenBounds(element) {
63
+ const rect = element.getBoundingClientRect();
64
+ return {
65
+ top: rect.top,
66
+ left: rect.left,
67
+ width: rect.width,
68
+ height: rect.height,
69
+ };
70
+ }
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,119 +193,100 @@
88
193
  return zoomValue ? parseFloat(zoomValue) : 1;
89
194
  };
90
195
 
91
- 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 };
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
+ const wasAlreadySelected = nodeTools.getSelectedNode() === firstChild;
206
+ nodeTools.selectNode(firstChild);
207
+ // Always emit postMessage when selecting via viewport label click,
208
+ // even if the node was already selected (to match behavior of direct node clicks)
209
+ if (wasAlreadySelected) {
210
+ sendPostMessage("selectedNodeChanged", firstChild.getAttribute("data-node-id") ?? null);
211
+ }
212
+ }
213
+ }
214
+ };
215
+
216
+ const setupViewportDrag = (labelElement, viewportElement, viewportName) => {
97
217
  // Get the parent group element that contains the label
98
218
  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
- };
219
+ // Track initial positions for calculations
220
+ let initialTransform = { x: 0, y: 0 };
221
+ let initialLabelPosition = { x: 0, y: 0 };
222
+ return createDragHandler(labelElement, {
223
+ onStart: () => {
224
+ setViewportDragging(true);
225
+ initialTransform = getTransformValues(viewportElement);
226
+ initialLabelPosition = getLabelPosition(labelGroup);
227
+ selectFirstViewportNode(viewportElement);
228
+ },
229
+ onDrag: (_event, { deltaX, deltaY }) => {
230
+ const zoom = getZoomValue();
231
+ // Adjust delta for zoom level (viewport is in canvas space)
232
+ const deltaXZoomed = deltaX / zoom;
233
+ const deltaYZoomed = deltaY / zoom;
234
+ // Calculate new positions
235
+ const newX = initialTransform.x + deltaXZoomed;
236
+ const newY = initialTransform.y + deltaYZoomed;
237
+ // Update label position with raw delta (labels are in screen space)
238
+ const newLabelX = initialLabelPosition.x + deltaX;
239
+ const newLabelY = initialLabelPosition.y + deltaY;
240
+ labelGroup.setAttribute("transform", `translate(${newLabelX}, ${newLabelY})`);
241
+ // Update viewport position with zoom-adjusted delta
242
+ viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
243
+ },
244
+ onStop: (_event, { hasDragged }) => {
245
+ setViewportDragging(false);
246
+ const finalTransform = getTransformValues(viewportElement);
247
+ // If it was a drag, handle drag completion
248
+ if (hasDragged) {
249
+ // Trigger refresh after drag completes to update highlight frame and labels
250
+ const nodeTools = getNodeTools();
251
+ if (nodeTools?.refreshHighlightFrame) {
252
+ nodeTools.refreshHighlightFrame();
253
+ }
254
+ }
255
+ // Always notify parent about the new position on drag stop
256
+ sendPostMessage("viewport-position-changed", {
257
+ viewportName,
258
+ x: finalTransform.x,
259
+ y: finalTransform.y,
260
+ });
261
+ },
262
+ onCancel: () => {
263
+ setViewportDragging(false);
264
+ },
265
+ onPreventClick: () => { },
266
+ });
165
267
  };
166
268
 
167
- // Store cleanup functions for drag listeners
168
- const dragCleanupFunctions = new Map();
169
- const refreshViewportLabels = () => {
170
- // Skip refresh if a viewport label is being dragged
171
- if (isViewportLabelDragging()) {
269
+ /**
270
+ * Refreshes (updates) a viewport label for a single viewport element.
271
+ * Creates the label if it doesn't exist, or updates its position if it does.
272
+ * Similar to refreshHighlightFrame - updates existing elements rather than recreating.
273
+ *
274
+ * @param viewportElement - The viewport element to refresh the label for
275
+ */
276
+ const refreshViewportLabel = (viewportElement) => {
277
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
278
+ if (!viewportName) {
172
279
  return;
173
280
  }
174
- const overlay = getViewportLabelsOverlay();
175
- // Update SVG dimensions to match current viewport
176
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
177
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
178
- overlay.setAttribute("width", viewportWidth.toString());
179
- overlay.setAttribute("height", viewportHeight.toString());
180
- // Find all viewports with names
181
- const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
182
- // Clean up existing drag listeners
183
- dragCleanupFunctions.forEach((cleanup) => {
184
- cleanup();
185
- });
186
- dragCleanupFunctions.clear();
187
- // Remove existing label groups
188
- const existingGroups = overlay.querySelectorAll(".viewport-label-group");
189
- existingGroups.forEach((group) => {
190
- group.remove();
191
- });
192
- // Create/update labels for each viewport
193
- viewports.forEach((viewport) => {
194
- const viewportElement = viewport;
195
- const viewportName = viewportElement.getAttribute("data-viewport-name");
196
- if (!viewportName)
197
- return;
198
- const bounds = getScreenBounds(viewportElement);
281
+ const overlay = getViewportLabelOverlay();
282
+ const bounds = getScreenBounds(viewportElement);
283
+ // Get existing label group or create if it doesn't exist
284
+ let group = overlay.querySelector(`[data-viewport-name="${viewportName}"]`);
285
+ if (!group) {
199
286
  // Create group for this viewport label
200
- const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
287
+ group = document.createElementNS("http://www.w3.org/2000/svg", "g");
201
288
  group.classList.add("viewport-label-group");
202
289
  group.setAttribute("data-viewport-name", viewportName);
203
- group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
204
290
  // Create text element
205
291
  const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
206
292
  text.classList.add("viewport-label-text");
@@ -211,14 +297,42 @@
211
297
  text.textContent = viewportName;
212
298
  group.appendChild(text);
213
299
  overlay.appendChild(group);
214
- // Setup drag functionality for this label
215
- const cleanup = setupViewportLabelDrag(text, viewportElement, viewportName);
216
- dragCleanupFunctions.set(viewportName, cleanup);
300
+ // Setup drag functionality only when creating new label
301
+ setupViewportDrag(text, viewportElement, viewportName);
302
+ }
303
+ // Update label position (this is the refresh part - updates existing label)
304
+ group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
305
+ };
306
+
307
+ const refreshViewportLabels = () => {
308
+ // Skip refresh if a viewport label is being dragged
309
+ if (isViewportDragging()) {
310
+ return;
311
+ }
312
+ const overlay = getViewportLabelOverlay();
313
+ // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
314
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
315
+ overlay.setAttribute("width", viewportWidth.toString());
316
+ overlay.setAttribute("height", viewportHeight.toString());
317
+ // Find all viewports with names and refresh each label
318
+ const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
319
+ viewports.forEach((viewport) => {
320
+ refreshViewportLabel(viewport);
321
+ });
322
+ // Remove labels for viewports that no longer exist
323
+ const existingGroups = overlay.querySelectorAll(".viewport-label-group");
324
+ existingGroups.forEach((group) => {
325
+ const viewportName = group.getAttribute("data-viewport-name");
326
+ if (viewportName) {
327
+ const viewportExists = Array.from(viewports).some((viewport) => viewport.getAttribute("data-viewport-name") === viewportName);
328
+ if (!viewportExists) {
329
+ group.remove();
330
+ }
331
+ }
217
332
  });
218
333
  };
219
334
 
220
335
  const getCanvasWindowValue = (path, canvasName = "canvas") => {
221
- // biome-ignore lint/suspicious/noExplicitAny: global window extension
222
336
  const canvas = window[canvasName];
223
337
  return path.reduce((obj, prop) => obj?.[prop], canvas);
224
338
  };
@@ -241,8 +355,7 @@
241
355
  const observer = new MutationObserver(() => {
242
356
  applyCanvasState(canvasName);
243
357
  // Refresh highlight frame (throttled via withRAFThrottle)
244
- // biome-ignore lint/suspicious/noExplicitAny: global window extension
245
- const nodeTools = window.nodeTools;
358
+ const nodeTools = getNodeTools();
246
359
  if (nodeTools?.refreshHighlightFrame) {
247
360
  nodeTools.refreshHighlightFrame();
248
361
  }
@@ -281,7 +394,6 @@
281
394
 
282
395
  const bindToWindow = (key, value) => {
283
396
  if (typeof window !== "undefined") {
284
- // biome-ignore lint/suspicious/noExplicitAny: global window extension requires flexibility
285
397
  window[key] = value;
286
398
  }
287
399
  };
@@ -307,8 +419,7 @@
307
419
  };
308
420
 
309
421
  const clearHighlightFrame = () => {
310
- const canvasContainer = getCanvasContainer();
311
- const container = canvasContainer || document.body;
422
+ const container = getCanvasContainerOrBody();
312
423
  const frame = container.querySelector(".highlight-frame-overlay");
313
424
  if (frame) {
314
425
  frame.remove();
@@ -437,7 +548,7 @@
437
548
  onNodeSelected(selectedNode);
438
549
  };
439
550
 
440
- const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
551
+ const setupEventListener = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
441
552
  const messageHandler = (event) => {
442
553
  processPostMessage(event, onNodeSelected);
443
554
  };
@@ -461,6 +572,17 @@
461
572
  };
462
573
  };
463
574
 
575
+ const toggleClass = (element, className, condition) => {
576
+ if (!element)
577
+ return;
578
+ if (condition) {
579
+ element.classList.add(className);
580
+ }
581
+ else {
582
+ element.classList.remove(className);
583
+ }
584
+ };
585
+
464
586
  const isComponentInstance = (element) => {
465
587
  return element.getAttribute("data-instance") === "true";
466
588
  };
@@ -526,8 +648,7 @@
526
648
  svg.style.height = "100vh";
527
649
  svg.style.pointerEvents = "none";
528
650
  svg.style.zIndex = "500";
529
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
530
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
651
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
531
652
  svg.setAttribute("width", viewportWidth.toString());
532
653
  svg.setAttribute("height", viewportHeight.toString());
533
654
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
@@ -550,13 +671,8 @@
550
671
  group.appendChild(rect);
551
672
  createCornerHandles(group, minWidth, height, isInstance, isTextEdit);
552
673
  svg.appendChild(group);
553
- const canvasContainer = getCanvasContainer();
554
- if (canvasContainer) {
555
- canvasContainer.appendChild(svg);
556
- }
557
- else {
558
- document.body.appendChild(svg);
559
- }
674
+ const container = getCanvasContainerOrBody();
675
+ container.appendChild(svg);
560
676
  return svg;
561
677
  };
562
678
 
@@ -593,19 +709,14 @@
593
709
  const createToolsContainer = (node, highlightFrame, isInstance = false, isTextEdit = false) => {
594
710
  const nodeTools = document.createElement("div");
595
711
  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
- }
712
+ toggleClass(nodeTools, "is-instance", isInstance);
713
+ toggleClass(nodeTools, "is-text-edit", isTextEdit);
602
714
  highlightFrame.appendChild(nodeTools);
603
715
  createTagLabel(node, nodeTools);
604
716
  };
605
717
 
606
718
  function getHighlightFrameElement() {
607
- const canvasContainer = getCanvasContainer();
608
- const container = canvasContainer || document.body;
719
+ const container = getCanvasContainerOrBody();
609
720
  return container.querySelector(".highlight-frame-overlay");
610
721
  }
611
722
 
@@ -613,8 +724,8 @@
613
724
  if (!node)
614
725
  return;
615
726
  const existingHighlightFrame = getHighlightFrameElement();
616
- const canvasContainer = getCanvasContainer();
617
- const existingToolsWrapper = canvasContainer?.querySelector(".highlight-frame-tools-wrapper") || document.body.querySelector(".highlight-frame-tools-wrapper");
727
+ const container = getCanvasContainerOrBody();
728
+ const existingToolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
618
729
  if (existingHighlightFrame) {
619
730
  existingHighlightFrame.remove();
620
731
  }
@@ -633,24 +744,15 @@
633
744
  // Create tools wrapper using CSS transform (GPU-accelerated)
634
745
  const toolsWrapper = document.createElement("div");
635
746
  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
- }
747
+ toggleClass(toolsWrapper, "is-instance", isInstance);
748
+ toggleClass(toolsWrapper, "is-text-edit", isTextEdit);
642
749
  toolsWrapper.style.position = "absolute";
643
750
  toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
644
751
  toolsWrapper.style.transformOrigin = "left center";
645
752
  toolsWrapper.style.pointerEvents = "none";
646
753
  toolsWrapper.style.zIndex = "500";
647
754
  createToolsContainer(node, toolsWrapper, isInstance, isTextEdit);
648
- if (canvasContainer) {
649
- canvasContainer.appendChild(toolsWrapper);
650
- }
651
- else {
652
- document.body.appendChild(toolsWrapper);
653
- }
755
+ container.appendChild(toolsWrapper);
654
756
  };
655
757
 
656
758
  const getComponentColor = () => {
@@ -668,24 +770,13 @@
668
770
  const isTextEdit = node.contentEditable === "true";
669
771
  // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
670
772
  // 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;
773
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
673
774
  frame.setAttribute("width", viewportWidth.toString());
674
775
  frame.setAttribute("height", viewportHeight.toString());
675
776
  // Update instance class
676
- if (isInstance) {
677
- frame.classList.add("is-instance");
678
- }
679
- else {
680
- frame.classList.remove("is-instance");
681
- }
777
+ toggleClass(frame, "is-instance", isInstance);
682
778
  // 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
- }
779
+ toggleClass(frame, "is-text-edit", isTextEdit);
689
780
  const group = frame.querySelector(".highlight-frame-group");
690
781
  if (!group)
691
782
  return;
@@ -702,8 +793,7 @@
702
793
  else {
703
794
  rect.removeAttribute("stroke"); // Use CSS default
704
795
  }
705
- const canvasContainer = getCanvasContainer();
706
- const container = canvasContainer || document.body;
796
+ const container = getCanvasContainerOrBody();
707
797
  const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
708
798
  const nodeTools = toolsWrapper?.querySelector(".node-tools");
709
799
  const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
@@ -714,36 +804,10 @@
714
804
  const minWidth = Math.max(width, 3);
715
805
  const bottomY = top + height;
716
806
  // 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
- }
807
+ toggleClass(toolsWrapper, "is-instance", isInstance);
808
+ toggleClass(toolsWrapper, "is-text-edit", isTextEdit);
809
+ toggleClass(nodeTools, "is-instance", isInstance);
810
+ toggleClass(nodeTools, "is-text-edit", isTextEdit);
747
811
  // Batch all DOM writes (single paint pass)
748
812
  // Update group transform to move entire group (rect + handles) at once
749
813
  group.setAttribute("transform", `translate(${left}, ${top})`);
@@ -809,8 +873,7 @@
809
873
  const displayValue = hasHiddenClass ? "none" : "";
810
874
  // Batch DOM writes
811
875
  frame.style.display = displayValue;
812
- const canvasContainer = getCanvasContainer();
813
- const container = canvasContainer || document.body;
876
+ const container = getCanvasContainerOrBody();
814
877
  const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
815
878
  if (toolsWrapper) {
816
879
  toolsWrapper.style.display = displayValue;
@@ -1116,7 +1179,6 @@
1116
1179
  highlightNode(node);
1117
1180
  if (nodeProvider) {
1118
1181
  updateHighlightFrameVisibility(node);
1119
- updateHighlightFrameVisibility(node);
1120
1182
  }
1121
1183
  }
1122
1184
  else {
@@ -1124,7 +1186,7 @@
1124
1186
  }
1125
1187
  };
1126
1188
  // Setup event listener
1127
- const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
1189
+ const removeListeners = setupEventListener(nodeProvider, selectNode, handleEscape, text);
1128
1190
  const cleanup = () => {
1129
1191
  removeListeners();
1130
1192
  resizeObserver?.disconnect();
@@ -1159,6 +1221,10 @@
1159
1221
  return nodeTools;
1160
1222
  };
1161
1223
 
1224
+ const getNodeProvider = () => {
1225
+ return document.querySelector('[data-role="node-provider"]');
1226
+ };
1227
+
1162
1228
  const DEFAULT_WIDTH = 400;
1163
1229
  const RESIZE_CONFIG = {
1164
1230
  minWidth: 4,
@@ -1192,25 +1258,20 @@
1192
1258
  },
1193
1259
  ];
1194
1260
 
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
- };
1261
+ /**
1262
+ * Removes a viewport label for a single viewport element.
1263
+ * @param viewportElement - The viewport element to remove the label for
1264
+ */
1265
+ const removeViewportLabel = (viewportElement) => {
1266
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
1267
+ if (!viewportName) {
1268
+ return;
1269
+ }
1270
+ const overlay = getViewportLabelOverlay();
1271
+ const labelGroup = overlay.querySelector(`[data-viewport-name="${viewportName}"]`);
1272
+ if (labelGroup) {
1273
+ labelGroup.remove();
1274
+ }
1214
1275
  };
1215
1276
 
1216
1277
  const createResizeHandle = (container) => {
@@ -1279,58 +1340,59 @@
1279
1340
  const width = initialWidth ?? DEFAULT_WIDTH;
1280
1341
  container.style.setProperty("--container-width", `${width}px`);
1281
1342
  createResizePresets(resizeHandle, container, updateWidth);
1282
- let isDragging = false;
1343
+ // Track initial values for resize calculation
1283
1344
  let startX = 0;
1284
1345
  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";
1346
+ // Handle mouse leave for resize (specific to resize use case)
1347
+ const handleMouseLeave = (event) => {
1348
+ // Check if mouse is leaving the window/document
1349
+ if (!event.relatedTarget && (event.target === document || event.target === document.documentElement)) {
1350
+ if (canvas) {
1351
+ canvas.style.cursor = "default";
1352
+ }
1312
1353
  }
1313
- isDragging = false;
1314
1354
  };
1315
- const removeListeners = setupEventListener(resizeHandle, startResize, handleResize, stopResize, blurResize);
1316
- // Refresh viewport labels when viewport is created
1317
- refreshViewportLabels();
1355
+ const removeDragListeners = createDragHandler(resizeHandle, {
1356
+ onStart: (_event, { startX: dragStartX }) => {
1357
+ startX = dragStartX;
1358
+ startWidth = container.offsetWidth;
1359
+ },
1360
+ onDrag: (event) => {
1361
+ if (canvas) {
1362
+ canvas.style.cursor = "ew-resize";
1363
+ }
1364
+ const width = calcWidth(event, startX, startWidth);
1365
+ updateWidth(container, width);
1366
+ },
1367
+ onStop: () => {
1368
+ if (canvas) {
1369
+ canvas.style.cursor = "default";
1370
+ }
1371
+ },
1372
+ onCancel: () => {
1373
+ if (canvas) {
1374
+ canvas.style.cursor = "default";
1375
+ }
1376
+ },
1377
+ onPreventClick: () => { },
1378
+ });
1379
+ document.addEventListener("mouseleave", handleMouseLeave);
1380
+ // Create/refresh the label for this viewport
1381
+ refreshViewportLabel(container);
1318
1382
  const cleanup = () => {
1319
- isDragging = false;
1320
- removeListeners();
1383
+ removeDragListeners();
1384
+ document.removeEventListener("mouseleave", handleMouseLeave);
1321
1385
  resizeHandle.remove();
1322
- // Refresh labels after cleanup to remove this viewport's label if needed
1323
- refreshViewportLabels();
1386
+ // Remove the label for this viewport
1387
+ removeViewportLabel(container);
1324
1388
  };
1325
1389
  return {
1326
1390
  setWidth: (width) => {
1327
1391
  updateWidth(container, width);
1328
- 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;
1392
+ refreshViewportLabel(container);
1393
+ const nodeTools = getNodeTools();
1332
1394
  const selectedNode = nodeTools?.getSelectedNode?.();
1333
- const nodeProvider = document.querySelector('[data-role="node-provider"]');
1395
+ const nodeProvider = getNodeProvider();
1334
1396
  if (selectedNode && nodeProvider) {
1335
1397
  refreshHighlightFrame(selectedNode, nodeProvider);
1336
1398
  }