@node-edit-utils/core 2.3.1 → 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 (63) 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/node-tools/select/helpers/isInsideViewport.d.ts +1 -0
  12. package/dist/lib/viewport/constants.d.ts +2 -2
  13. package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +1 -0
  14. package/dist/lib/viewport/label/helpers/getLabelPosition.d.ts +4 -0
  15. package/dist/lib/viewport/label/helpers/getTransformValues.d.ts +4 -0
  16. package/dist/lib/viewport/label/helpers/getZoomValue.d.ts +1 -0
  17. package/dist/lib/viewport/label/helpers/selectFirstViewportNode.d.ts +5 -0
  18. package/dist/lib/viewport/label/index.d.ts +4 -0
  19. package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +2 -0
  20. package/dist/lib/viewport/label/refreshViewportLabels.d.ts +1 -0
  21. package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +1 -0
  22. package/dist/node-edit-utils.cjs.js +437 -172
  23. package/dist/node-edit-utils.esm.js +437 -172
  24. package/dist/node-edit-utils.umd.js +437 -172
  25. package/dist/node-edit-utils.umd.min.js +1 -1
  26. package/dist/styles.css +1 -1
  27. package/package.json +1 -1
  28. package/src/lib/canvas/createCanvasObserver.ts +16 -2
  29. package/src/lib/canvas/helpers/getCanvasContainerOrBody.ts +6 -0
  30. package/src/lib/canvas/helpers/getCanvasWindowValue.ts +2 -3
  31. package/src/lib/helpers/adjustForZoom.ts +4 -0
  32. package/src/lib/helpers/createDragHandler.ts +171 -0
  33. package/src/lib/helpers/getNodeProvider.ts +4 -0
  34. package/src/lib/helpers/getNodeTools.ts +6 -0
  35. package/src/lib/helpers/getViewportDimensions.ts +7 -0
  36. package/src/lib/helpers/index.ts +9 -1
  37. package/src/lib/helpers/parseTransform.ts +9 -0
  38. package/src/lib/helpers/toggleClass.ts +9 -0
  39. package/src/lib/node-tools/createNodeTools.ts +11 -5
  40. package/src/lib/node-tools/highlight/clearHighlightFrame.ts +2 -3
  41. package/src/lib/node-tools/highlight/createHighlightFrame.ts +5 -9
  42. package/src/lib/node-tools/highlight/createToolsContainer.ts +3 -6
  43. package/src/lib/node-tools/highlight/helpers/getElementBounds.ts +6 -5
  44. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +2 -3
  45. package/src/lib/node-tools/highlight/highlightNode.ts +7 -15
  46. package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +12 -42
  47. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +2 -3
  48. package/src/lib/node-tools/select/helpers/isInsideViewport.ts +19 -0
  49. package/src/lib/node-tools/select/selectNode.ts +13 -1
  50. package/src/lib/node-tools/text/events/setupMutationObserver.ts +1 -0
  51. package/src/lib/styles/styles.css +48 -1
  52. package/src/lib/viewport/constants.ts +2 -2
  53. package/src/lib/viewport/createViewport.ts +56 -41
  54. package/src/lib/viewport/label/getViewportLabelsOverlay.ts +32 -0
  55. package/src/lib/viewport/label/helpers/getLabelPosition.ts +6 -0
  56. package/src/lib/viewport/label/helpers/getTransformValues.ts +6 -0
  57. package/src/lib/viewport/label/helpers/getZoomValue.ts +4 -0
  58. package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +18 -0
  59. package/src/lib/viewport/label/index.ts +4 -0
  60. package/src/lib/viewport/label/isViewportLabelDragging.ts +9 -0
  61. package/src/lib/viewport/label/refreshViewportLabels.ts +69 -0
  62. package/src/lib/viewport/label/setupViewportLabelDrag.ts +70 -0
  63. package/src/lib/window/bindToWindow.ts +1 -2
@@ -1,10 +1,312 @@
1
1
  /**
2
2
  * Markup Canvas
3
3
  * High-performance markup canvas with zoom and pan capabilities
4
- * @version 2.3.1
4
+ * @version 2.3.3
5
5
  */
6
+ const getNodeTools = () => {
7
+ return window.nodeTools;
8
+ };
9
+
10
+ const getViewportDimensions = () => {
11
+ return {
12
+ width: document.documentElement.clientWidth || window.innerWidth,
13
+ height: document.documentElement.clientHeight || window.innerHeight,
14
+ };
15
+ };
16
+
17
+ function getScreenBounds(element) {
18
+ const rect = element.getBoundingClientRect();
19
+ return {
20
+ top: rect.top,
21
+ left: rect.left,
22
+ width: rect.width,
23
+ height: rect.height,
24
+ };
25
+ }
26
+
27
+ const getCanvasContainer = () => {
28
+ return document.querySelector(".canvas-container");
29
+ };
30
+
31
+ const getCanvasContainerOrBody = () => {
32
+ return getCanvasContainer() || document.body;
33
+ };
34
+
35
+ const getViewportLabelsOverlay = () => {
36
+ const container = getCanvasContainerOrBody();
37
+ // Check if overlay already exists
38
+ let overlay = container.querySelector(".viewport-labels-overlay");
39
+ if (!overlay) {
40
+ // Create new SVG overlay
41
+ overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
42
+ overlay.classList.add("viewport-labels-overlay");
43
+ // Set fixed positioning
44
+ overlay.style.position = "absolute";
45
+ overlay.style.top = "0";
46
+ overlay.style.left = "0";
47
+ overlay.style.width = "100vw";
48
+ overlay.style.height = "100vh";
49
+ overlay.style.pointerEvents = "none";
50
+ overlay.style.zIndex = "500";
51
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
52
+ overlay.setAttribute("width", viewportWidth.toString());
53
+ overlay.setAttribute("height", viewportHeight.toString());
54
+ container.appendChild(overlay);
55
+ }
56
+ return overlay;
57
+ };
58
+
59
+ // Global flag to prevent refreshViewportLabels during drag
60
+ let globalIsDragging = false;
61
+ const isViewportLabelDragging = () => globalIsDragging;
62
+ const setViewportLabelDragging = (isDragging) => {
63
+ globalIsDragging = isDragging;
64
+ };
65
+
66
+ /**
67
+ * Creates a reusable drag handler for mouse drag operations
68
+ * @param element - Element that triggers the drag (mousedown listener attached here)
69
+ * @param callbacks - Callbacks for drag lifecycle events
70
+ * @param options - Configuration options
71
+ * @returns Cleanup function to remove all event listeners
72
+ */
73
+ function createDragHandler(element, callbacks, options = {}) {
74
+ const { preventDefault = true, stopPropagation = true } = options;
75
+ const state = {
76
+ isDragging: false,
77
+ hasDragged: false,
78
+ startX: 0,
79
+ startY: 0,
80
+ };
81
+ const startDrag = (event) => {
82
+ if (preventDefault) {
83
+ event.preventDefault();
84
+ }
85
+ if (stopPropagation) {
86
+ event.stopPropagation();
87
+ }
88
+ state.isDragging = true;
89
+ state.hasDragged = false;
90
+ state.startX = event.clientX;
91
+ state.startY = event.clientY;
92
+ callbacks.onStart?.(event, state);
93
+ };
94
+ const handleDrag = (event) => {
95
+ if (!state.isDragging)
96
+ return;
97
+ const deltaX = event.clientX - state.startX;
98
+ const deltaY = event.clientY - state.startY;
99
+ state.hasDragged = true;
100
+ callbacks.onDrag(event, {
101
+ ...state,
102
+ deltaX,
103
+ deltaY,
104
+ });
105
+ };
106
+ const stopDrag = (event) => {
107
+ if (!state.isDragging)
108
+ return;
109
+ if (preventDefault) {
110
+ event.preventDefault();
111
+ }
112
+ if (stopPropagation) {
113
+ event.stopPropagation();
114
+ }
115
+ state.isDragging = false;
116
+ callbacks.onStop?.(event, state);
117
+ };
118
+ const cancelDrag = () => {
119
+ if (!state.isDragging)
120
+ return;
121
+ state.isDragging = false;
122
+ callbacks.onCancel?.(state);
123
+ };
124
+ const preventClick = (event) => {
125
+ if (preventDefault) {
126
+ event.preventDefault();
127
+ }
128
+ if (stopPropagation) {
129
+ event.stopPropagation();
130
+ }
131
+ callbacks.onPreventClick?.(event, state);
132
+ // Reset hasDragged flag after handling the click
133
+ if (state.hasDragged) {
134
+ state.hasDragged = false;
135
+ }
136
+ };
137
+ // Attach event listeners
138
+ element.addEventListener("mousedown", startDrag);
139
+ if (callbacks.onPreventClick) {
140
+ element.addEventListener("click", preventClick);
141
+ }
142
+ document.addEventListener("mousemove", handleDrag);
143
+ document.addEventListener("mouseup", stopDrag);
144
+ window.addEventListener("blur", cancelDrag);
145
+ // Return cleanup function
146
+ return () => {
147
+ element.removeEventListener("mousedown", startDrag);
148
+ if (callbacks.onPreventClick) {
149
+ element.removeEventListener("click", preventClick);
150
+ }
151
+ document.removeEventListener("mousemove", handleDrag);
152
+ document.removeEventListener("mouseup", stopDrag);
153
+ window.removeEventListener("blur", cancelDrag);
154
+ };
155
+ }
156
+
157
+ function sendPostMessage(action, data) {
158
+ window.parent.postMessage({
159
+ source: "node-edit-utils",
160
+ action,
161
+ data,
162
+ timestamp: Date.now(),
163
+ }, "*");
164
+ }
165
+
166
+ const parseTransform3d = (transform) => {
167
+ const match = transform.match(/translate3d\((-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px\)/);
168
+ return match ? { x: parseFloat(match[1]), y: parseFloat(match[2]) } : { x: 0, y: 0 };
169
+ };
170
+ const parseTransform2d = (transform) => {
171
+ const match = transform?.match(/translate\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)/);
172
+ return match ? { x: parseFloat(match[1]), y: parseFloat(match[2]) } : { x: 0, y: 0 };
173
+ };
174
+
175
+ const getLabelPosition = (labelGroup) => {
176
+ const transform = labelGroup.getAttribute("transform");
177
+ return parseTransform2d(transform);
178
+ };
179
+
180
+ const getTransformValues = (element) => {
181
+ const style = element.style.transform;
182
+ return parseTransform3d(style);
183
+ };
184
+
185
+ const getZoomValue = () => {
186
+ const zoomValue = getComputedStyle(document.body).getPropertyValue("--zoom").trim();
187
+ return zoomValue ? parseFloat(zoomValue) : 1;
188
+ };
189
+
190
+ /**
191
+ * Selects the first child node inside a viewport element.
192
+ * Skips the resize-handle element if present.
193
+ */
194
+ const selectFirstViewportNode = (viewportElement) => {
195
+ const firstChild = Array.from(viewportElement.children).find((child) => !child.classList.contains("resize-handle"));
196
+ if (firstChild) {
197
+ const nodeTools = getNodeTools();
198
+ if (nodeTools?.selectNode) {
199
+ nodeTools.selectNode(firstChild);
200
+ }
201
+ }
202
+ };
203
+
204
+ const setupViewportLabelDrag = (labelElement, viewportElement, viewportName) => {
205
+ // Get the parent group element that contains the label
206
+ const labelGroup = labelElement.parentElement;
207
+ // Track initial positions for calculations
208
+ let initialTransform = { x: 0, y: 0 };
209
+ let initialLabelPosition = { x: 0, y: 0 };
210
+ return createDragHandler(labelElement, {
211
+ onStart: () => {
212
+ setViewportLabelDragging(true);
213
+ initialTransform = getTransformValues(viewportElement);
214
+ initialLabelPosition = getLabelPosition(labelGroup);
215
+ selectFirstViewportNode(viewportElement);
216
+ },
217
+ onDrag: (_event, { deltaX, deltaY }) => {
218
+ const zoom = getZoomValue();
219
+ // Adjust delta for zoom level (viewport is in canvas space)
220
+ const deltaXZoomed = deltaX / zoom;
221
+ const deltaYZoomed = deltaY / zoom;
222
+ // Calculate new positions
223
+ const newX = initialTransform.x + deltaXZoomed;
224
+ const newY = initialTransform.y + deltaYZoomed;
225
+ // Update label position with raw delta (labels are in screen space)
226
+ const newLabelX = initialLabelPosition.x + deltaX;
227
+ const newLabelY = initialLabelPosition.y + deltaY;
228
+ labelGroup.setAttribute("transform", `translate(${newLabelX}, ${newLabelY})`);
229
+ // Update viewport position with zoom-adjusted delta
230
+ viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
231
+ },
232
+ onStop: (_event, { hasDragged }) => {
233
+ setViewportLabelDragging(false);
234
+ // If it was a drag, handle drag completion
235
+ if (hasDragged) {
236
+ const finalTransform = getTransformValues(viewportElement);
237
+ // Trigger refresh after drag completes to update highlight frame and labels
238
+ const nodeTools = getNodeTools();
239
+ if (nodeTools?.refreshHighlightFrame) {
240
+ nodeTools.refreshHighlightFrame();
241
+ }
242
+ // Notify parent about the new position
243
+ sendPostMessage("viewport-position-changed", {
244
+ viewportName,
245
+ x: finalTransform.x,
246
+ y: finalTransform.y,
247
+ });
248
+ }
249
+ },
250
+ onCancel: () => {
251
+ setViewportLabelDragging(false);
252
+ },
253
+ onPreventClick: () => { },
254
+ });
255
+ };
256
+
257
+ // Store cleanup functions for drag listeners
258
+ const dragCleanupFunctions = new Map();
259
+ const refreshViewportLabels = () => {
260
+ // Skip refresh if a viewport label is being dragged
261
+ if (isViewportLabelDragging()) {
262
+ return;
263
+ }
264
+ const overlay = getViewportLabelsOverlay();
265
+ // Update SVG dimensions to match current viewport
266
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
267
+ overlay.setAttribute("width", viewportWidth.toString());
268
+ overlay.setAttribute("height", viewportHeight.toString());
269
+ // Find all viewports with names
270
+ const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
271
+ // Clean up existing drag listeners
272
+ dragCleanupFunctions.forEach((cleanup) => {
273
+ cleanup();
274
+ });
275
+ dragCleanupFunctions.clear();
276
+ // Remove existing label groups
277
+ const existingGroups = overlay.querySelectorAll(".viewport-label-group");
278
+ existingGroups.forEach((group) => {
279
+ group.remove();
280
+ });
281
+ // Create/update labels for each viewport
282
+ viewports.forEach((viewport) => {
283
+ const viewportElement = viewport;
284
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
285
+ if (!viewportName)
286
+ return;
287
+ const bounds = getScreenBounds(viewportElement);
288
+ // Create group for this viewport label
289
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
290
+ group.classList.add("viewport-label-group");
291
+ group.setAttribute("data-viewport-name", viewportName);
292
+ group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
293
+ // Create text element
294
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
295
+ text.classList.add("viewport-label-text");
296
+ text.setAttribute("x", "0");
297
+ text.setAttribute("y", "-8");
298
+ text.setAttribute("vector-effect", "non-scaling-stroke");
299
+ text.setAttribute("pointer-events", "auto");
300
+ text.textContent = viewportName;
301
+ group.appendChild(text);
302
+ overlay.appendChild(group);
303
+ // Setup drag functionality for this label
304
+ const cleanup = setupViewportLabelDrag(text, viewportElement, viewportName);
305
+ dragCleanupFunctions.set(viewportName, cleanup);
306
+ });
307
+ };
308
+
6
309
  const getCanvasWindowValue = (path, canvasName = "canvas") => {
7
- // biome-ignore lint/suspicious/noExplicitAny: global window extension
8
310
  const canvas = window[canvasName];
9
311
  return path.reduce((obj, prop) => obj?.[prop], canvas);
10
312
  };
@@ -27,11 +329,12 @@ function createCanvasObserver(canvasName = "canvas") {
27
329
  const observer = new MutationObserver(() => {
28
330
  applyCanvasState(canvasName);
29
331
  // Refresh highlight frame (throttled via withRAFThrottle)
30
- // biome-ignore lint/suspicious/noExplicitAny: global window extension
31
- const nodeTools = window.nodeTools;
332
+ const nodeTools = getNodeTools();
32
333
  if (nodeTools?.refreshHighlightFrame) {
33
334
  nodeTools.refreshHighlightFrame();
34
335
  }
336
+ // Refresh viewport labels
337
+ refreshViewportLabels();
35
338
  });
36
339
  observer.observe(transformLayer, {
37
340
  attributes: true,
@@ -39,8 +342,16 @@ function createCanvasObserver(canvasName = "canvas") {
39
342
  subtree: true,
40
343
  childList: true,
41
344
  });
345
+ // Handle window resize for viewport labels
346
+ const handleResize = () => {
347
+ refreshViewportLabels();
348
+ };
349
+ window.addEventListener("resize", handleResize);
350
+ // Initial refresh of viewport labels
351
+ refreshViewportLabels();
42
352
  function disconnect() {
43
353
  observer.disconnect();
354
+ window.removeEventListener("resize", handleResize);
44
355
  }
45
356
  return {
46
357
  disconnect,
@@ -55,18 +366,8 @@ const connectResizeObserver = (element, handler) => {
55
366
  return resizeObserver;
56
367
  };
57
368
 
58
- function sendPostMessage(action, data) {
59
- window.parent.postMessage({
60
- source: "node-edit-utils",
61
- action,
62
- data,
63
- timestamp: Date.now(),
64
- }, "*");
65
- }
66
-
67
369
  const bindToWindow = (key, value) => {
68
370
  if (typeof window !== "undefined") {
69
- // biome-ignore lint/suspicious/noExplicitAny: global window extension requires flexibility
70
371
  window[key] = value;
71
372
  }
72
373
  };
@@ -91,13 +392,8 @@ const processPostMessage = (event, onNodeSelected) => {
91
392
  }
92
393
  };
93
394
 
94
- const getCanvasContainer = () => {
95
- return document.querySelector(".canvas-container");
96
- };
97
-
98
395
  const clearHighlightFrame = () => {
99
- const canvasContainer = getCanvasContainer();
100
- const container = canvasContainer || document.body;
396
+ const container = getCanvasContainerOrBody();
101
397
  const frame = container.querySelector(".highlight-frame-overlay");
102
398
  if (frame) {
103
399
  frame.remove();
@@ -146,6 +442,21 @@ const isInsideComponent = (element) => {
146
442
  return false;
147
443
  };
148
444
 
445
+ const isInsideViewport = (element) => {
446
+ let current = element;
447
+ while (current) {
448
+ if (current.classList.contains("viewport")) {
449
+ return true;
450
+ }
451
+ // Stop at node-provider to avoid checking beyond the editable area
452
+ if (current.getAttribute("data-role") === "node-provider") {
453
+ break;
454
+ }
455
+ current = current.parentElement;
456
+ }
457
+ return false;
458
+ };
459
+
149
460
  const targetSameCandidates = (cache, current) => cache.length === current.length && cache.every((el, i) => el === current[i]);
150
461
 
151
462
  let candidateCache = [];
@@ -158,7 +469,16 @@ const selectNode = (event, nodeProvider, text) => {
158
469
  const clickThrough = event.metaKey || event.ctrlKey;
159
470
  const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
160
471
  !element.classList.contains("select-none") &&
161
- !isInsideComponent(element));
472
+ !element.classList.contains("content-layer") &&
473
+ !element.classList.contains("resize-handle") &&
474
+ !element.classList.contains("resize-presets") &&
475
+ !isInsideComponent(element) &&
476
+ isInsideViewport(element));
477
+ console.log("candidates", candidates);
478
+ if (candidates.length === 0) {
479
+ lastSelectedNode = null;
480
+ return null;
481
+ }
162
482
  const editableNode = text.getEditableNode();
163
483
  if (editableNode && candidates.includes(editableNode)) {
164
484
  selectedNode = editableNode;
@@ -202,7 +522,7 @@ const handleNodeClick = (event, nodeProvider, text, onNodeSelected) => {
202
522
  onNodeSelected(selectedNode);
203
523
  };
204
524
 
205
- const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
525
+ const setupEventListener = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
206
526
  const messageHandler = (event) => {
207
527
  processPostMessage(event, onNodeSelected);
208
528
  };
@@ -226,6 +546,17 @@ const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, tex
226
546
  };
227
547
  };
228
548
 
549
+ const toggleClass = (element, className, condition) => {
550
+ if (!element)
551
+ return;
552
+ if (condition) {
553
+ element.classList.add(className);
554
+ }
555
+ else {
556
+ element.classList.remove(className);
557
+ }
558
+ };
559
+
229
560
  const isComponentInstance = (element) => {
230
561
  return element.getAttribute("data-instance") === "true";
231
562
  };
@@ -263,16 +594,6 @@ const createCornerHandles = (group, width, height, isInstance = false, isTextEdi
263
594
  createCornerHandle(group, 0, height, "handle-bottom-left", isInstance, isTextEdit);
264
595
  };
265
596
 
266
- function getScreenBounds(element) {
267
- const rect = element.getBoundingClientRect();
268
- return {
269
- top: rect.top,
270
- left: rect.left,
271
- width: rect.width,
272
- height: rect.height,
273
- };
274
- }
275
-
276
597
  const getComponentColor$1 = () => {
277
598
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
278
599
  };
@@ -301,8 +622,7 @@ const createHighlightFrame = (node, isInstance = false, isTextEdit = false) => {
301
622
  svg.style.height = "100vh";
302
623
  svg.style.pointerEvents = "none";
303
624
  svg.style.zIndex = "500";
304
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
305
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
625
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
306
626
  svg.setAttribute("width", viewportWidth.toString());
307
627
  svg.setAttribute("height", viewportHeight.toString());
308
628
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
@@ -325,13 +645,8 @@ const createHighlightFrame = (node, isInstance = false, isTextEdit = false) => {
325
645
  group.appendChild(rect);
326
646
  createCornerHandles(group, minWidth, height, isInstance, isTextEdit);
327
647
  svg.appendChild(group);
328
- const canvasContainer = getCanvasContainer();
329
- if (canvasContainer) {
330
- canvasContainer.appendChild(svg);
331
- }
332
- else {
333
- document.body.appendChild(svg);
334
- }
648
+ const container = getCanvasContainerOrBody();
649
+ container.appendChild(svg);
335
650
  return svg;
336
651
  };
337
652
 
@@ -368,19 +683,14 @@ const createTagLabel = (node, nodeTools) => {
368
683
  const createToolsContainer = (node, highlightFrame, isInstance = false, isTextEdit = false) => {
369
684
  const nodeTools = document.createElement("div");
370
685
  nodeTools.className = "node-tools";
371
- if (isInstance) {
372
- nodeTools.classList.add("is-instance");
373
- }
374
- if (isTextEdit) {
375
- nodeTools.classList.add("is-text-edit");
376
- }
686
+ toggleClass(nodeTools, "is-instance", isInstance);
687
+ toggleClass(nodeTools, "is-text-edit", isTextEdit);
377
688
  highlightFrame.appendChild(nodeTools);
378
689
  createTagLabel(node, nodeTools);
379
690
  };
380
691
 
381
692
  function getHighlightFrameElement() {
382
- const canvasContainer = getCanvasContainer();
383
- const container = canvasContainer || document.body;
693
+ const container = getCanvasContainerOrBody();
384
694
  return container.querySelector(".highlight-frame-overlay");
385
695
  }
386
696
 
@@ -388,8 +698,8 @@ const highlightNode = (node) => {
388
698
  if (!node)
389
699
  return;
390
700
  const existingHighlightFrame = getHighlightFrameElement();
391
- const canvasContainer = getCanvasContainer();
392
- const existingToolsWrapper = canvasContainer?.querySelector(".highlight-frame-tools-wrapper") || document.body.querySelector(".highlight-frame-tools-wrapper");
701
+ const container = getCanvasContainerOrBody();
702
+ const existingToolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
393
703
  if (existingHighlightFrame) {
394
704
  existingHighlightFrame.remove();
395
705
  }
@@ -408,24 +718,15 @@ const highlightNode = (node) => {
408
718
  // Create tools wrapper using CSS transform (GPU-accelerated)
409
719
  const toolsWrapper = document.createElement("div");
410
720
  toolsWrapper.classList.add("highlight-frame-tools-wrapper");
411
- if (isInstance) {
412
- toolsWrapper.classList.add("is-instance");
413
- }
414
- if (isTextEdit) {
415
- toolsWrapper.classList.add("is-text-edit");
416
- }
721
+ toggleClass(toolsWrapper, "is-instance", isInstance);
722
+ toggleClass(toolsWrapper, "is-text-edit", isTextEdit);
417
723
  toolsWrapper.style.position = "absolute";
418
724
  toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
419
725
  toolsWrapper.style.transformOrigin = "left center";
420
726
  toolsWrapper.style.pointerEvents = "none";
421
727
  toolsWrapper.style.zIndex = "500";
422
728
  createToolsContainer(node, toolsWrapper, isInstance, isTextEdit);
423
- if (canvasContainer) {
424
- canvasContainer.appendChild(toolsWrapper);
425
- }
426
- else {
427
- document.body.appendChild(toolsWrapper);
428
- }
729
+ container.appendChild(toolsWrapper);
429
730
  };
430
731
 
431
732
  const getComponentColor = () => {
@@ -443,24 +744,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
443
744
  const isTextEdit = node.contentEditable === "true";
444
745
  // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
445
746
  // Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
446
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
447
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
747
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
448
748
  frame.setAttribute("width", viewportWidth.toString());
449
749
  frame.setAttribute("height", viewportHeight.toString());
450
750
  // Update instance class
451
- if (isInstance) {
452
- frame.classList.add("is-instance");
453
- }
454
- else {
455
- frame.classList.remove("is-instance");
456
- }
751
+ toggleClass(frame, "is-instance", isInstance);
457
752
  // Update text edit class
458
- if (isTextEdit) {
459
- frame.classList.add("is-text-edit");
460
- }
461
- else {
462
- frame.classList.remove("is-text-edit");
463
- }
753
+ toggleClass(frame, "is-text-edit", isTextEdit);
464
754
  const group = frame.querySelector(".highlight-frame-group");
465
755
  if (!group)
466
756
  return;
@@ -477,8 +767,7 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
477
767
  else {
478
768
  rect.removeAttribute("stroke"); // Use CSS default
479
769
  }
480
- const canvasContainer = getCanvasContainer();
481
- const container = canvasContainer || document.body;
770
+ const container = getCanvasContainerOrBody();
482
771
  const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
483
772
  const nodeTools = toolsWrapper?.querySelector(".node-tools");
484
773
  const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
@@ -489,36 +778,10 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
489
778
  const minWidth = Math.max(width, 3);
490
779
  const bottomY = top + height;
491
780
  // Update instance classes on tools wrapper and node tools
492
- if (toolsWrapper) {
493
- if (isInstance) {
494
- toolsWrapper.classList.add("is-instance");
495
- }
496
- else {
497
- toolsWrapper.classList.remove("is-instance");
498
- }
499
- // Update text edit class
500
- if (isTextEdit) {
501
- toolsWrapper.classList.add("is-text-edit");
502
- }
503
- else {
504
- toolsWrapper.classList.remove("is-text-edit");
505
- }
506
- }
507
- if (nodeTools) {
508
- if (isInstance) {
509
- nodeTools.classList.add("is-instance");
510
- }
511
- else {
512
- nodeTools.classList.remove("is-instance");
513
- }
514
- // Update text edit class
515
- if (isTextEdit) {
516
- nodeTools.classList.add("is-text-edit");
517
- }
518
- else {
519
- nodeTools.classList.remove("is-text-edit");
520
- }
521
- }
781
+ toggleClass(toolsWrapper, "is-instance", isInstance);
782
+ toggleClass(toolsWrapper, "is-text-edit", isTextEdit);
783
+ toggleClass(nodeTools, "is-instance", isInstance);
784
+ toggleClass(nodeTools, "is-text-edit", isTextEdit);
522
785
  // Batch all DOM writes (single paint pass)
523
786
  // Update group transform to move entire group (rect + handles) at once
524
787
  group.setAttribute("transform", `translate(${left}, ${top})`);
@@ -584,8 +847,7 @@ const updateHighlightFrameVisibility = (node) => {
584
847
  const displayValue = hasHiddenClass ? "none" : "";
585
848
  // Batch DOM writes
586
849
  frame.style.display = displayValue;
587
- const canvasContainer = getCanvasContainer();
588
- const container = canvasContainer || document.body;
850
+ const container = getCanvasContainerOrBody();
589
851
  const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
590
852
  if (toolsWrapper) {
591
853
  toolsWrapper.style.display = displayValue;
@@ -705,6 +967,7 @@ const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
705
967
  // Accumulate mutations instead of replacing
706
968
  pendingMutations.push(...mutations);
707
969
  scheduleProcess();
970
+ console.log("refreshHighlightFrame in mutationObserver");
708
971
  refreshHighlightFrame(node, nodeProvider, canvasName);
709
972
  });
710
973
  return () => {
@@ -842,14 +1105,15 @@ const createNodeTools = (element, canvasName = "canvas") => {
842
1105
  checkNodeExists();
843
1106
  if (!document.contains(node))
844
1107
  return;
1108
+ console.log("refreshHighlightFrame in mutationObserver 2");
845
1109
  refreshHighlightFrame(node, nodeProvider, canvasName);
846
1110
  updateHighlightFrameVisibility(node);
847
1111
  });
848
1112
  mutationObserver.observe(node, {
849
1113
  attributes: true,
850
1114
  characterData: true,
851
- childList: true,
852
- subtree: true,
1115
+ //childList: true,
1116
+ //subtree: true,
853
1117
  });
854
1118
  // Also observe parent node to catch when this node is removed
855
1119
  const parentNode = node.parentElement;
@@ -878,19 +1142,25 @@ const createNodeTools = (element, canvasName = "canvas") => {
878
1142
  if (!document.contains(node))
879
1143
  return; // Exit early if node was removed
880
1144
  refreshHighlightFrame(node, nodeProvider, canvasName);
1145
+ console.log("refreshHighlightFrame in resizeObserver");
881
1146
  updateHighlightFrameVisibility(node);
882
1147
  });
883
1148
  }
884
1149
  selectedNode = node;
885
1150
  sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
886
1151
  highlightNode(node) ?? null;
887
- if (node && nodeProvider) {
888
- updateHighlightFrameVisibility(node);
889
- updateHighlightFrameVisibility(node);
1152
+ if (node) {
1153
+ highlightNode(node);
1154
+ if (nodeProvider) {
1155
+ updateHighlightFrameVisibility(node);
1156
+ }
1157
+ }
1158
+ else {
1159
+ clearHighlightFrame();
890
1160
  }
891
1161
  };
892
1162
  // Setup event listener
893
- const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
1163
+ const removeListeners = setupEventListener(nodeProvider, selectNode, handleEscape, text);
894
1164
  const cleanup = () => {
895
1165
  removeListeners();
896
1166
  resizeObserver?.disconnect();
@@ -925,10 +1195,14 @@ const createNodeTools = (element, canvasName = "canvas") => {
925
1195
  return nodeTools;
926
1196
  };
927
1197
 
1198
+ const getNodeProvider = () => {
1199
+ return document.querySelector('[data-role="node-provider"]');
1200
+ };
1201
+
928
1202
  const DEFAULT_WIDTH = 400;
929
1203
  const RESIZE_CONFIG = {
930
- minWidth: 320,
931
- maxWidth: 1680,
1204
+ minWidth: 4,
1205
+ maxWidth: 2560,
932
1206
  };
933
1207
  const RESIZE_PRESETS = [
934
1208
  {
@@ -958,27 +1232,6 @@ const RESIZE_PRESETS = [
958
1232
  },
959
1233
  ];
960
1234
 
961
- const setupEventListener = (resizeHandle, startResize, handleResize, stopResize, blurResize) => {
962
- const handleMouseLeave = (event) => {
963
- // Check if mouse is leaving the window/document
964
- if (!event.relatedTarget && (event.target === document || event.target === document.documentElement)) {
965
- blurResize();
966
- }
967
- };
968
- resizeHandle.addEventListener("mousedown", startResize);
969
- document.addEventListener("mousemove", handleResize);
970
- document.addEventListener("mouseup", stopResize);
971
- document.addEventListener("mouseleave", handleMouseLeave);
972
- window.addEventListener("blur", blurResize);
973
- return () => {
974
- resizeHandle.removeEventListener("mousedown", startResize);
975
- document.removeEventListener("mousemove", handleResize);
976
- document.removeEventListener("mouseup", stopResize);
977
- document.removeEventListener("mouseleave", handleMouseLeave);
978
- window.removeEventListener("blur", blurResize);
979
- };
980
- };
981
-
982
1235
  const createResizeHandle = (container) => {
983
1236
  const handle = document.createElement("div");
984
1237
  handle.className = "resize-handle";
@@ -1045,48 +1298,60 @@ const createViewport = (container, initialWidth) => {
1045
1298
  const width = initialWidth ?? DEFAULT_WIDTH;
1046
1299
  container.style.setProperty("--container-width", `${width}px`);
1047
1300
  createResizePresets(resizeHandle, container, updateWidth);
1048
- let isDragging = false;
1301
+ // Track initial values for resize calculation
1049
1302
  let startX = 0;
1050
1303
  let startWidth = 0;
1051
- const startResize = (event) => {
1052
- event.preventDefault();
1053
- event.stopPropagation();
1054
- isDragging = true;
1055
- startX = event.clientX;
1056
- startWidth = container.offsetWidth;
1057
- };
1058
- const handleResize = (event) => {
1059
- if (!isDragging)
1060
- return;
1061
- if (canvas) {
1062
- canvas.style.cursor = "ew-resize";
1063
- }
1064
- const width = calcWidth(event, startX, startWidth);
1065
- updateWidth(container, width);
1066
- };
1067
- const stopResize = (event) => {
1068
- event.preventDefault();
1069
- event.stopPropagation();
1070
- if (canvas) {
1071
- canvas.style.cursor = "default";
1072
- }
1073
- isDragging = false;
1074
- };
1075
- const blurResize = () => {
1076
- if (canvas) {
1077
- canvas.style.cursor = "default";
1304
+ // Handle mouse leave for resize (specific to resize use case)
1305
+ const handleMouseLeave = (event) => {
1306
+ // Check if mouse is leaving the window/document
1307
+ if (!event.relatedTarget && (event.target === document || event.target === document.documentElement)) {
1308
+ if (canvas) {
1309
+ canvas.style.cursor = "default";
1310
+ }
1078
1311
  }
1079
- isDragging = false;
1080
1312
  };
1081
- const removeListeners = setupEventListener(resizeHandle, startResize, handleResize, stopResize, blurResize);
1313
+ const removeDragListeners = createDragHandler(resizeHandle, {
1314
+ onStart: (_event, { startX: dragStartX }) => {
1315
+ startX = dragStartX;
1316
+ startWidth = container.offsetWidth;
1317
+ },
1318
+ onDrag: (event) => {
1319
+ if (canvas) {
1320
+ canvas.style.cursor = "ew-resize";
1321
+ }
1322
+ const width = calcWidth(event, startX, startWidth);
1323
+ updateWidth(container, width);
1324
+ },
1325
+ onStop: () => {
1326
+ if (canvas) {
1327
+ canvas.style.cursor = "default";
1328
+ }
1329
+ },
1330
+ onCancel: () => {
1331
+ if (canvas) {
1332
+ canvas.style.cursor = "default";
1333
+ }
1334
+ },
1335
+ onPreventClick: () => { },
1336
+ });
1337
+ document.addEventListener("mouseleave", handleMouseLeave);
1338
+ refreshViewportLabels();
1082
1339
  const cleanup = () => {
1083
- isDragging = false;
1084
- removeListeners();
1340
+ removeDragListeners();
1341
+ document.removeEventListener("mouseleave", handleMouseLeave);
1085
1342
  resizeHandle.remove();
1343
+ refreshViewportLabels();
1086
1344
  };
1087
1345
  return {
1088
1346
  setWidth: (width) => {
1089
1347
  updateWidth(container, width);
1348
+ refreshViewportLabels();
1349
+ const nodeTools = getNodeTools();
1350
+ const selectedNode = nodeTools?.getSelectedNode?.();
1351
+ const nodeProvider = getNodeProvider();
1352
+ if (selectedNode && nodeProvider) {
1353
+ refreshHighlightFrame(selectedNode, nodeProvider);
1354
+ }
1090
1355
  },
1091
1356
  cleanup,
1092
1357
  };