@node-edit-utils/core 2.3.3 → 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 (101) hide show
  1. package/dist/lib/viewport/label/getViewportLabelOverlay.d.ts +1 -0
  2. package/dist/lib/viewport/label/index.d.ts +5 -3
  3. package/dist/lib/viewport/label/isViewportDragging.d.ts +2 -0
  4. package/dist/lib/viewport/label/refreshViewportLabel.d.ts +8 -0
  5. package/dist/lib/viewport/label/removeViewportLabel.d.ts +5 -0
  6. package/dist/lib/viewport/label/setupViewportDrag.d.ts +1 -0
  7. package/dist/node-edit-utils.cjs.js +100 -62
  8. package/dist/node-edit-utils.esm.js +100 -62
  9. package/dist/node-edit-utils.umd.js +100 -62
  10. package/dist/node-edit-utils.umd.min.js +1 -1
  11. package/dist/styles.css +1 -1
  12. package/package.json +7 -2
  13. package/src/lib/canvas/createCanvasObserver.test.ts +242 -0
  14. package/src/lib/canvas/disableCanvasKeyboard.test.ts +53 -0
  15. package/src/lib/canvas/disableCanvasKeyboard.ts +1 -1
  16. package/src/lib/canvas/disableCanvasTextMode.test.ts +53 -0
  17. package/src/lib/canvas/disableCanvasTextMode.ts +1 -1
  18. package/src/lib/canvas/enableCanvasKeyboard.test.ts +53 -0
  19. package/src/lib/canvas/enableCanvasKeyboard.ts +1 -1
  20. package/src/lib/canvas/enableCanvasTextMode.test.ts +53 -0
  21. package/src/lib/canvas/enableCanvasTextMode.ts +1 -1
  22. package/src/lib/canvas/helpers/applyCanvasState.test.ts +119 -0
  23. package/src/lib/canvas/helpers/applyCanvasState.ts +1 -1
  24. package/src/lib/canvas/helpers/getCanvasContainer.test.ts +62 -0
  25. package/src/lib/canvas/helpers/getCanvasContainerOrBody.test.ts +51 -0
  26. package/src/lib/canvas/helpers/getCanvasWindowValue.test.ts +116 -0
  27. package/src/lib/helpers/adjustForZoom.test.ts +65 -0
  28. package/src/lib/helpers/createDragHandler.test.ts +325 -0
  29. package/src/lib/helpers/getNodeProvider.test.ts +71 -0
  30. package/src/lib/helpers/getNodeTools.test.ts +50 -0
  31. package/src/lib/helpers/getViewportDimensions.test.ts +93 -0
  32. package/src/lib/helpers/observer/connectMutationObserver.test.ts +127 -0
  33. package/src/lib/helpers/observer/connectResizeObserver.test.ts +147 -0
  34. package/src/lib/helpers/parseTransform.test.ts +117 -0
  35. package/src/lib/helpers/toggleClass.test.ts +71 -0
  36. package/src/lib/helpers/withRAF.test.ts +439 -0
  37. package/src/lib/node-tools/createNodeTools.test.ts +373 -0
  38. package/src/lib/node-tools/events/click/handleNodeClick.test.ts +109 -0
  39. package/src/lib/node-tools/events/setupEventListener.test.ts +136 -0
  40. package/src/lib/node-tools/highlight/clearHighlightFrame.test.ts +88 -0
  41. package/src/lib/node-tools/highlight/createCornerHandles.test.ts +150 -0
  42. package/src/lib/node-tools/highlight/createHighlightFrame.test.ts +237 -0
  43. package/src/lib/node-tools/highlight/createTagLabel.test.ts +135 -0
  44. package/src/lib/node-tools/highlight/createToolsContainer.test.ts +97 -0
  45. package/src/lib/node-tools/highlight/helpers/getElementBounds.test.ts +158 -0
  46. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.test.ts +78 -0
  47. package/src/lib/node-tools/highlight/helpers/getScreenBounds.test.ts +133 -0
  48. package/src/lib/node-tools/highlight/highlightNode.test.ts +213 -0
  49. package/src/lib/node-tools/highlight/refreshHighlightFrame.test.ts +323 -0
  50. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.test.ts +110 -0
  51. package/src/lib/node-tools/select/helpers/getElementsFromPoint.test.ts +109 -0
  52. package/src/lib/node-tools/select/helpers/isInsideComponent.test.ts +81 -0
  53. package/src/lib/node-tools/select/helpers/isInsideViewport.test.ts +82 -0
  54. package/src/lib/node-tools/select/helpers/targetSameCandidates.test.ts +81 -0
  55. package/src/lib/node-tools/select/selectNode.test.ts +238 -0
  56. package/src/lib/node-tools/text/events/setupKeydownHandler.test.ts +91 -0
  57. package/src/lib/node-tools/text/events/setupMutationObserver.test.ts +213 -0
  58. package/src/lib/node-tools/text/events/setupNodeListeners.test.ts +133 -0
  59. package/src/lib/node-tools/text/helpers/enterTextEditMode.test.ts +50 -0
  60. package/src/lib/node-tools/text/helpers/handleTextChange.test.ts +201 -0
  61. package/src/lib/node-tools/text/helpers/hasTextContent.test.ts +101 -0
  62. package/src/lib/node-tools/text/helpers/insertLineBreak.test.ts +96 -0
  63. package/src/lib/node-tools/text/helpers/makeNodeEditable.test.ts +56 -0
  64. package/src/lib/node-tools/text/helpers/makeNodeNonEditable.test.ts +57 -0
  65. package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.test.ts +61 -0
  66. package/src/lib/node-tools/text/nodeText.test.ts +233 -0
  67. package/src/lib/post-message/processPostMessage.test.ts +218 -0
  68. package/src/lib/post-message/sendPostMessage.test.ts +120 -0
  69. package/src/lib/styles/styles.css +2 -2
  70. package/src/lib/viewport/createViewport.test.ts +267 -0
  71. package/src/lib/viewport/createViewport.ts +7 -4
  72. package/src/lib/viewport/events/setupEventListener.test.ts +103 -0
  73. package/src/lib/viewport/label/getViewportLabelOverlay.test.ts +77 -0
  74. package/src/lib/viewport/label/{getViewportLabelsOverlay.ts → getViewportLabelOverlay.ts} +2 -1
  75. package/src/lib/viewport/label/helpers/getLabelPosition.test.ts +51 -0
  76. package/src/lib/viewport/label/helpers/getTransformValues.test.ts +59 -0
  77. package/src/lib/viewport/label/helpers/getZoomValue.test.ts +53 -0
  78. package/src/lib/viewport/label/helpers/selectFirstViewportNode.test.ts +105 -0
  79. package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +8 -0
  80. package/src/lib/viewport/label/index.ts +5 -3
  81. package/src/lib/viewport/label/isViewportDragging.test.ts +35 -0
  82. package/src/lib/viewport/label/isViewportDragging.ts +9 -0
  83. package/src/lib/viewport/label/refreshViewportLabel.test.ts +105 -0
  84. package/src/lib/viewport/label/refreshViewportLabel.ts +50 -0
  85. package/src/lib/viewport/label/refreshViewportLabels.test.ts +107 -0
  86. package/src/lib/viewport/label/refreshViewportLabels.ts +17 -50
  87. package/src/lib/viewport/label/removeViewportLabel.test.ts +67 -0
  88. package/src/lib/viewport/label/removeViewportLabel.ts +20 -0
  89. package/src/lib/viewport/label/setupViewportDrag.test.ts +249 -0
  90. package/src/lib/viewport/label/{setupViewportLabelDrag.ts → setupViewportDrag.ts} +14 -14
  91. package/src/lib/viewport/resize/createResizeHandle.test.ts +37 -0
  92. package/src/lib/viewport/resize/createResizePresets.test.ts +75 -0
  93. package/src/lib/viewport/resize/updateActivePreset.test.ts +92 -0
  94. package/src/lib/viewport/width/calcConstrainedWidth.test.ts +47 -0
  95. package/src/lib/viewport/width/calcWidth.test.ts +68 -0
  96. package/src/lib/viewport/width/updateWidth.test.ts +78 -0
  97. package/src/lib/window/bindToWindow.test.ts +166 -0
  98. package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +0 -1
  99. package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +0 -2
  100. package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +0 -1
  101. package/src/lib/viewport/label/isViewportLabelDragging.ts +0 -9
@@ -0,0 +1 @@
1
+ export declare const getViewportLabelOverlay: () => SVGSVGElement;
@@ -1,4 +1,6 @@
1
- export { getViewportLabelsOverlay } from "./getViewportLabelsOverlay";
2
- export { isViewportLabelDragging } from "./isViewportLabelDragging";
1
+ export { getViewportLabelOverlay } from "./getViewportLabelOverlay";
2
+ export { isViewportDragging } from "./isViewportDragging";
3
+ export { refreshViewportLabel } from "./refreshViewportLabel";
3
4
  export { refreshViewportLabels } from "./refreshViewportLabels";
4
- export { setupViewportLabelDrag } from "./setupViewportLabelDrag";
5
+ export { removeViewportLabel } from "./removeViewportLabel";
6
+ export { setupViewportDrag } from "./setupViewportDrag";
@@ -0,0 +1,2 @@
1
+ export declare const isViewportDragging: () => boolean;
2
+ export declare const setViewportDragging: (isDragging: boolean) => void;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Refreshes (updates) a viewport label for a single viewport element.
3
+ * Creates the label if it doesn't exist, or updates its position if it does.
4
+ * Similar to refreshHighlightFrame - updates existing elements rather than recreating.
5
+ *
6
+ * @param viewportElement - The viewport element to refresh the label for
7
+ */
8
+ export declare const refreshViewportLabel: (viewportElement: HTMLElement) => void;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Removes a viewport label for a single viewport element.
3
+ * @param viewportElement - The viewport element to remove the label for
4
+ */
5
+ export declare const removeViewportLabel: (viewportElement: HTMLElement) => void;
@@ -0,0 +1 @@
1
+ export declare const setupViewportDrag: (labelElement: SVGTextElement, viewportElement: HTMLElement, viewportName: string) => (() => void);
@@ -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.3
4
+ * @version 2.3.4
5
5
  */
6
6
  'use strict';
7
7
 
@@ -16,16 +16,6 @@ const getViewportDimensions = () => {
16
16
  };
17
17
  };
18
18
 
19
- function getScreenBounds(element) {
20
- const rect = element.getBoundingClientRect();
21
- return {
22
- top: rect.top,
23
- left: rect.left,
24
- width: rect.width,
25
- height: rect.height,
26
- };
27
- }
28
-
29
19
  const getCanvasContainer = () => {
30
20
  return document.querySelector(".canvas-container");
31
21
  };
@@ -34,7 +24,7 @@ const getCanvasContainerOrBody = () => {
34
24
  return getCanvasContainer() || document.body;
35
25
  };
36
26
 
37
- const getViewportLabelsOverlay = () => {
27
+ const getViewportLabelOverlay = () => {
38
28
  const container = getCanvasContainerOrBody();
39
29
  // Check if overlay already exists
40
30
  let overlay = container.querySelector(".viewport-labels-overlay");
@@ -60,11 +50,21 @@ const getViewportLabelsOverlay = () => {
60
50
 
61
51
  // Global flag to prevent refreshViewportLabels during drag
62
52
  let globalIsDragging = false;
63
- const isViewportLabelDragging = () => globalIsDragging;
64
- const setViewportLabelDragging = (isDragging) => {
53
+ const isViewportDragging = () => globalIsDragging;
54
+ const setViewportDragging = (isDragging) => {
65
55
  globalIsDragging = isDragging;
66
56
  };
67
57
 
58
+ function getScreenBounds(element) {
59
+ const rect = element.getBoundingClientRect();
60
+ return {
61
+ top: rect.top,
62
+ left: rect.left,
63
+ width: rect.width,
64
+ height: rect.height,
65
+ };
66
+ }
67
+
68
68
  /**
69
69
  * Creates a reusable drag handler for mouse drag operations
70
70
  * @param element - Element that triggers the drag (mousedown listener attached here)
@@ -198,12 +198,18 @@ const selectFirstViewportNode = (viewportElement) => {
198
198
  if (firstChild) {
199
199
  const nodeTools = getNodeTools();
200
200
  if (nodeTools?.selectNode) {
201
+ const wasAlreadySelected = nodeTools.getSelectedNode() === firstChild;
201
202
  nodeTools.selectNode(firstChild);
203
+ // Always emit postMessage when selecting via viewport label click,
204
+ // even if the node was already selected (to match behavior of direct node clicks)
205
+ if (wasAlreadySelected) {
206
+ sendPostMessage("selectedNodeChanged", firstChild.getAttribute("data-node-id") ?? null);
207
+ }
202
208
  }
203
209
  }
204
210
  };
205
211
 
206
- const setupViewportLabelDrag = (labelElement, viewportElement, viewportName) => {
212
+ const setupViewportDrag = (labelElement, viewportElement, viewportName) => {
207
213
  // Get the parent group element that contains the label
208
214
  const labelGroup = labelElement.parentElement;
209
215
  // Track initial positions for calculations
@@ -211,7 +217,7 @@ const setupViewportLabelDrag = (labelElement, viewportElement, viewportName) =>
211
217
  let initialLabelPosition = { x: 0, y: 0 };
212
218
  return createDragHandler(labelElement, {
213
219
  onStart: () => {
214
- setViewportLabelDragging(true);
220
+ setViewportDragging(true);
215
221
  initialTransform = getTransformValues(viewportElement);
216
222
  initialLabelPosition = getLabelPosition(labelGroup);
217
223
  selectFirstViewportNode(viewportElement);
@@ -232,66 +238,51 @@ const setupViewportLabelDrag = (labelElement, viewportElement, viewportName) =>
232
238
  viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
233
239
  },
234
240
  onStop: (_event, { hasDragged }) => {
235
- setViewportLabelDragging(false);
241
+ setViewportDragging(false);
242
+ const finalTransform = getTransformValues(viewportElement);
236
243
  // If it was a drag, handle drag completion
237
244
  if (hasDragged) {
238
- const finalTransform = getTransformValues(viewportElement);
239
245
  // Trigger refresh after drag completes to update highlight frame and labels
240
246
  const nodeTools = getNodeTools();
241
247
  if (nodeTools?.refreshHighlightFrame) {
242
248
  nodeTools.refreshHighlightFrame();
243
249
  }
244
- // Notify parent about the new position
245
- sendPostMessage("viewport-position-changed", {
246
- viewportName,
247
- x: finalTransform.x,
248
- y: finalTransform.y,
249
- });
250
250
  }
251
+ // Always notify parent about the new position on drag stop
252
+ sendPostMessage("viewport-position-changed", {
253
+ viewportName,
254
+ x: finalTransform.x,
255
+ y: finalTransform.y,
256
+ });
251
257
  },
252
258
  onCancel: () => {
253
- setViewportLabelDragging(false);
259
+ setViewportDragging(false);
254
260
  },
255
261
  onPreventClick: () => { },
256
262
  });
257
263
  };
258
264
 
259
- // Store cleanup functions for drag listeners
260
- const dragCleanupFunctions = new Map();
261
- const refreshViewportLabels = () => {
262
- // Skip refresh if a viewport label is being dragged
263
- if (isViewportLabelDragging()) {
265
+ /**
266
+ * Refreshes (updates) a viewport label for a single viewport element.
267
+ * Creates the label if it doesn't exist, or updates its position if it does.
268
+ * Similar to refreshHighlightFrame - updates existing elements rather than recreating.
269
+ *
270
+ * @param viewportElement - The viewport element to refresh the label for
271
+ */
272
+ const refreshViewportLabel = (viewportElement) => {
273
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
274
+ if (!viewportName) {
264
275
  return;
265
276
  }
266
- const overlay = getViewportLabelsOverlay();
267
- // Update SVG dimensions to match current viewport
268
- const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
269
- overlay.setAttribute("width", viewportWidth.toString());
270
- overlay.setAttribute("height", viewportHeight.toString());
271
- // Find all viewports with names
272
- const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
273
- // Clean up existing drag listeners
274
- dragCleanupFunctions.forEach((cleanup) => {
275
- cleanup();
276
- });
277
- dragCleanupFunctions.clear();
278
- // Remove existing label groups
279
- const existingGroups = overlay.querySelectorAll(".viewport-label-group");
280
- existingGroups.forEach((group) => {
281
- group.remove();
282
- });
283
- // Create/update labels for each viewport
284
- viewports.forEach((viewport) => {
285
- const viewportElement = viewport;
286
- const viewportName = viewportElement.getAttribute("data-viewport-name");
287
- if (!viewportName)
288
- return;
289
- const bounds = getScreenBounds(viewportElement);
277
+ const overlay = getViewportLabelOverlay();
278
+ const bounds = getScreenBounds(viewportElement);
279
+ // Get existing label group or create if it doesn't exist
280
+ let group = overlay.querySelector(`[data-viewport-name="${viewportName}"]`);
281
+ if (!group) {
290
282
  // Create group for this viewport label
291
- const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
283
+ group = document.createElementNS("http://www.w3.org/2000/svg", "g");
292
284
  group.classList.add("viewport-label-group");
293
285
  group.setAttribute("data-viewport-name", viewportName);
294
- group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
295
286
  // Create text element
296
287
  const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
297
288
  text.classList.add("viewport-label-text");
@@ -302,9 +293,38 @@ const refreshViewportLabels = () => {
302
293
  text.textContent = viewportName;
303
294
  group.appendChild(text);
304
295
  overlay.appendChild(group);
305
- // Setup drag functionality for this label
306
- const cleanup = setupViewportLabelDrag(text, viewportElement, viewportName);
307
- dragCleanupFunctions.set(viewportName, cleanup);
296
+ // Setup drag functionality only when creating new label
297
+ setupViewportDrag(text, viewportElement, viewportName);
298
+ }
299
+ // Update label position (this is the refresh part - updates existing label)
300
+ group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
301
+ };
302
+
303
+ const refreshViewportLabels = () => {
304
+ // Skip refresh if a viewport label is being dragged
305
+ if (isViewportDragging()) {
306
+ return;
307
+ }
308
+ const overlay = getViewportLabelOverlay();
309
+ // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
310
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
311
+ overlay.setAttribute("width", viewportWidth.toString());
312
+ overlay.setAttribute("height", viewportHeight.toString());
313
+ // Find all viewports with names and refresh each label
314
+ const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
315
+ viewports.forEach((viewport) => {
316
+ refreshViewportLabel(viewport);
317
+ });
318
+ // Remove labels for viewports that no longer exist
319
+ const existingGroups = overlay.querySelectorAll(".viewport-label-group");
320
+ existingGroups.forEach((group) => {
321
+ const viewportName = group.getAttribute("data-viewport-name");
322
+ if (viewportName) {
323
+ const viewportExists = Array.from(viewports).some((viewport) => viewport.getAttribute("data-viewport-name") === viewportName);
324
+ if (!viewportExists) {
325
+ group.remove();
326
+ }
327
+ }
308
328
  });
309
329
  };
310
330
 
@@ -1234,6 +1254,22 @@ const RESIZE_PRESETS = [
1234
1254
  },
1235
1255
  ];
1236
1256
 
1257
+ /**
1258
+ * Removes a viewport label for a single viewport element.
1259
+ * @param viewportElement - The viewport element to remove the label for
1260
+ */
1261
+ const removeViewportLabel = (viewportElement) => {
1262
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
1263
+ if (!viewportName) {
1264
+ return;
1265
+ }
1266
+ const overlay = getViewportLabelOverlay();
1267
+ const labelGroup = overlay.querySelector(`[data-viewport-name="${viewportName}"]`);
1268
+ if (labelGroup) {
1269
+ labelGroup.remove();
1270
+ }
1271
+ };
1272
+
1237
1273
  const createResizeHandle = (container) => {
1238
1274
  const handle = document.createElement("div");
1239
1275
  handle.className = "resize-handle";
@@ -1337,17 +1373,19 @@ const createViewport = (container, initialWidth) => {
1337
1373
  onPreventClick: () => { },
1338
1374
  });
1339
1375
  document.addEventListener("mouseleave", handleMouseLeave);
1340
- refreshViewportLabels();
1376
+ // Create/refresh the label for this viewport
1377
+ refreshViewportLabel(container);
1341
1378
  const cleanup = () => {
1342
1379
  removeDragListeners();
1343
1380
  document.removeEventListener("mouseleave", handleMouseLeave);
1344
1381
  resizeHandle.remove();
1345
- refreshViewportLabels();
1382
+ // Remove the label for this viewport
1383
+ removeViewportLabel(container);
1346
1384
  };
1347
1385
  return {
1348
1386
  setWidth: (width) => {
1349
1387
  updateWidth(container, width);
1350
- refreshViewportLabels();
1388
+ refreshViewportLabel(container);
1351
1389
  const nodeTools = getNodeTools();
1352
1390
  const selectedNode = nodeTools?.getSelectedNode?.();
1353
1391
  const nodeProvider = getNodeProvider();
@@ -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.3
4
+ * @version 2.3.4
5
5
  */
6
6
  const getNodeTools = () => {
7
7
  return window.nodeTools;
@@ -14,16 +14,6 @@ const getViewportDimensions = () => {
14
14
  };
15
15
  };
16
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
17
  const getCanvasContainer = () => {
28
18
  return document.querySelector(".canvas-container");
29
19
  };
@@ -32,7 +22,7 @@ const getCanvasContainerOrBody = () => {
32
22
  return getCanvasContainer() || document.body;
33
23
  };
34
24
 
35
- const getViewportLabelsOverlay = () => {
25
+ const getViewportLabelOverlay = () => {
36
26
  const container = getCanvasContainerOrBody();
37
27
  // Check if overlay already exists
38
28
  let overlay = container.querySelector(".viewport-labels-overlay");
@@ -58,11 +48,21 @@ const getViewportLabelsOverlay = () => {
58
48
 
59
49
  // Global flag to prevent refreshViewportLabels during drag
60
50
  let globalIsDragging = false;
61
- const isViewportLabelDragging = () => globalIsDragging;
62
- const setViewportLabelDragging = (isDragging) => {
51
+ const isViewportDragging = () => globalIsDragging;
52
+ const setViewportDragging = (isDragging) => {
63
53
  globalIsDragging = isDragging;
64
54
  };
65
55
 
56
+ function getScreenBounds(element) {
57
+ const rect = element.getBoundingClientRect();
58
+ return {
59
+ top: rect.top,
60
+ left: rect.left,
61
+ width: rect.width,
62
+ height: rect.height,
63
+ };
64
+ }
65
+
66
66
  /**
67
67
  * Creates a reusable drag handler for mouse drag operations
68
68
  * @param element - Element that triggers the drag (mousedown listener attached here)
@@ -196,12 +196,18 @@ const selectFirstViewportNode = (viewportElement) => {
196
196
  if (firstChild) {
197
197
  const nodeTools = getNodeTools();
198
198
  if (nodeTools?.selectNode) {
199
+ const wasAlreadySelected = nodeTools.getSelectedNode() === firstChild;
199
200
  nodeTools.selectNode(firstChild);
201
+ // Always emit postMessage when selecting via viewport label click,
202
+ // even if the node was already selected (to match behavior of direct node clicks)
203
+ if (wasAlreadySelected) {
204
+ sendPostMessage("selectedNodeChanged", firstChild.getAttribute("data-node-id") ?? null);
205
+ }
200
206
  }
201
207
  }
202
208
  };
203
209
 
204
- const setupViewportLabelDrag = (labelElement, viewportElement, viewportName) => {
210
+ const setupViewportDrag = (labelElement, viewportElement, viewportName) => {
205
211
  // Get the parent group element that contains the label
206
212
  const labelGroup = labelElement.parentElement;
207
213
  // Track initial positions for calculations
@@ -209,7 +215,7 @@ const setupViewportLabelDrag = (labelElement, viewportElement, viewportName) =>
209
215
  let initialLabelPosition = { x: 0, y: 0 };
210
216
  return createDragHandler(labelElement, {
211
217
  onStart: () => {
212
- setViewportLabelDragging(true);
218
+ setViewportDragging(true);
213
219
  initialTransform = getTransformValues(viewportElement);
214
220
  initialLabelPosition = getLabelPosition(labelGroup);
215
221
  selectFirstViewportNode(viewportElement);
@@ -230,66 +236,51 @@ const setupViewportLabelDrag = (labelElement, viewportElement, viewportName) =>
230
236
  viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
231
237
  },
232
238
  onStop: (_event, { hasDragged }) => {
233
- setViewportLabelDragging(false);
239
+ setViewportDragging(false);
240
+ const finalTransform = getTransformValues(viewportElement);
234
241
  // If it was a drag, handle drag completion
235
242
  if (hasDragged) {
236
- const finalTransform = getTransformValues(viewportElement);
237
243
  // Trigger refresh after drag completes to update highlight frame and labels
238
244
  const nodeTools = getNodeTools();
239
245
  if (nodeTools?.refreshHighlightFrame) {
240
246
  nodeTools.refreshHighlightFrame();
241
247
  }
242
- // Notify parent about the new position
243
- sendPostMessage("viewport-position-changed", {
244
- viewportName,
245
- x: finalTransform.x,
246
- y: finalTransform.y,
247
- });
248
248
  }
249
+ // Always notify parent about the new position on drag stop
250
+ sendPostMessage("viewport-position-changed", {
251
+ viewportName,
252
+ x: finalTransform.x,
253
+ y: finalTransform.y,
254
+ });
249
255
  },
250
256
  onCancel: () => {
251
- setViewportLabelDragging(false);
257
+ setViewportDragging(false);
252
258
  },
253
259
  onPreventClick: () => { },
254
260
  });
255
261
  };
256
262
 
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()) {
263
+ /**
264
+ * Refreshes (updates) a viewport label for a single viewport element.
265
+ * Creates the label if it doesn't exist, or updates its position if it does.
266
+ * Similar to refreshHighlightFrame - updates existing elements rather than recreating.
267
+ *
268
+ * @param viewportElement - The viewport element to refresh the label for
269
+ */
270
+ const refreshViewportLabel = (viewportElement) => {
271
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
272
+ if (!viewportName) {
262
273
  return;
263
274
  }
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);
275
+ const overlay = getViewportLabelOverlay();
276
+ const bounds = getScreenBounds(viewportElement);
277
+ // Get existing label group or create if it doesn't exist
278
+ let group = overlay.querySelector(`[data-viewport-name="${viewportName}"]`);
279
+ if (!group) {
288
280
  // Create group for this viewport label
289
- const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
281
+ group = document.createElementNS("http://www.w3.org/2000/svg", "g");
290
282
  group.classList.add("viewport-label-group");
291
283
  group.setAttribute("data-viewport-name", viewportName);
292
- group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
293
284
  // Create text element
294
285
  const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
295
286
  text.classList.add("viewport-label-text");
@@ -300,9 +291,38 @@ const refreshViewportLabels = () => {
300
291
  text.textContent = viewportName;
301
292
  group.appendChild(text);
302
293
  overlay.appendChild(group);
303
- // Setup drag functionality for this label
304
- const cleanup = setupViewportLabelDrag(text, viewportElement, viewportName);
305
- dragCleanupFunctions.set(viewportName, cleanup);
294
+ // Setup drag functionality only when creating new label
295
+ setupViewportDrag(text, viewportElement, viewportName);
296
+ }
297
+ // Update label position (this is the refresh part - updates existing label)
298
+ group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
299
+ };
300
+
301
+ const refreshViewportLabels = () => {
302
+ // Skip refresh if a viewport label is being dragged
303
+ if (isViewportDragging()) {
304
+ return;
305
+ }
306
+ const overlay = getViewportLabelOverlay();
307
+ // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
308
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
309
+ overlay.setAttribute("width", viewportWidth.toString());
310
+ overlay.setAttribute("height", viewportHeight.toString());
311
+ // Find all viewports with names and refresh each label
312
+ const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
313
+ viewports.forEach((viewport) => {
314
+ refreshViewportLabel(viewport);
315
+ });
316
+ // Remove labels for viewports that no longer exist
317
+ const existingGroups = overlay.querySelectorAll(".viewport-label-group");
318
+ existingGroups.forEach((group) => {
319
+ const viewportName = group.getAttribute("data-viewport-name");
320
+ if (viewportName) {
321
+ const viewportExists = Array.from(viewports).some((viewport) => viewport.getAttribute("data-viewport-name") === viewportName);
322
+ if (!viewportExists) {
323
+ group.remove();
324
+ }
325
+ }
306
326
  });
307
327
  };
308
328
 
@@ -1232,6 +1252,22 @@ const RESIZE_PRESETS = [
1232
1252
  },
1233
1253
  ];
1234
1254
 
1255
+ /**
1256
+ * Removes a viewport label for a single viewport element.
1257
+ * @param viewportElement - The viewport element to remove the label for
1258
+ */
1259
+ const removeViewportLabel = (viewportElement) => {
1260
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
1261
+ if (!viewportName) {
1262
+ return;
1263
+ }
1264
+ const overlay = getViewportLabelOverlay();
1265
+ const labelGroup = overlay.querySelector(`[data-viewport-name="${viewportName}"]`);
1266
+ if (labelGroup) {
1267
+ labelGroup.remove();
1268
+ }
1269
+ };
1270
+
1235
1271
  const createResizeHandle = (container) => {
1236
1272
  const handle = document.createElement("div");
1237
1273
  handle.className = "resize-handle";
@@ -1335,17 +1371,19 @@ const createViewport = (container, initialWidth) => {
1335
1371
  onPreventClick: () => { },
1336
1372
  });
1337
1373
  document.addEventListener("mouseleave", handleMouseLeave);
1338
- refreshViewportLabels();
1374
+ // Create/refresh the label for this viewport
1375
+ refreshViewportLabel(container);
1339
1376
  const cleanup = () => {
1340
1377
  removeDragListeners();
1341
1378
  document.removeEventListener("mouseleave", handleMouseLeave);
1342
1379
  resizeHandle.remove();
1343
- refreshViewportLabels();
1380
+ // Remove the label for this viewport
1381
+ removeViewportLabel(container);
1344
1382
  };
1345
1383
  return {
1346
1384
  setWidth: (width) => {
1347
1385
  updateWidth(container, width);
1348
- refreshViewportLabels();
1386
+ refreshViewportLabel(container);
1349
1387
  const nodeTools = getNodeTools();
1350
1388
  const selectedNode = nodeTools?.getSelectedNode?.();
1351
1389
  const nodeProvider = getNodeProvider();