@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,5 +1,7 @@
1
- import { getCanvasContainer } from "@/lib/canvas/helpers/getCanvasContainer";
1
+ import { getCanvasContainerOrBody } from "@/lib/canvas/helpers/getCanvasContainerOrBody";
2
2
  import { getCanvasWindowValue } from "@/lib/canvas/helpers/getCanvasWindowValue";
3
+ import { getViewportDimensions } from "@/lib/helpers/getViewportDimensions";
4
+ import { toggleClass } from "@/lib/helpers/toggleClass";
3
5
  import { isComponentInstance } from "../select/helpers/isComponentInstance";
4
6
  import { getHighlightFrameElement } from "./helpers/getHighlightFrameElement";
5
7
  import { getScreenBounds } from "./helpers/getScreenBounds";
@@ -22,24 +24,15 @@ export const refreshHighlightFrame = (node: HTMLElement, nodeProvider: HTMLEleme
22
24
 
23
25
  // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
24
26
  // Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
25
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
26
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
27
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
27
28
  frame.setAttribute("width", viewportWidth.toString());
28
29
  frame.setAttribute("height", viewportHeight.toString());
29
30
 
30
31
  // Update instance class
31
- if (isInstance) {
32
- frame.classList.add("is-instance");
33
- } else {
34
- frame.classList.remove("is-instance");
35
- }
32
+ toggleClass(frame, "is-instance", isInstance);
36
33
 
37
34
  // Update text edit class
38
- if (isTextEdit) {
39
- frame.classList.add("is-text-edit");
40
- } else {
41
- frame.classList.remove("is-text-edit");
42
- }
35
+ toggleClass(frame, "is-text-edit", isTextEdit);
43
36
 
44
37
  const group = frame.querySelector(".highlight-frame-group") as SVGGElement | null;
45
38
  if (!group) return;
@@ -56,12 +49,11 @@ export const refreshHighlightFrame = (node: HTMLElement, nodeProvider: HTMLEleme
56
49
  rect.removeAttribute("stroke"); // Use CSS default
57
50
  }
58
51
 
59
- const canvasContainer = getCanvasContainer();
60
- const container = canvasContainer || document.body;
52
+ const container = getCanvasContainerOrBody();
61
53
  const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper") as HTMLElement | null;
62
54
  const nodeTools = toolsWrapper?.querySelector(".node-tools") as HTMLElement | null;
63
55
 
64
- const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
56
+ const zoom = (getCanvasWindowValue(["zoom", "current"], canvasName) as number | undefined) ?? 1;
65
57
  const bounds = getScreenBounds(node);
66
58
 
67
59
  // Calculate all values before any DOM writes
@@ -71,32 +63,10 @@ export const refreshHighlightFrame = (node: HTMLElement, nodeProvider: HTMLEleme
71
63
  const bottomY = top + height;
72
64
 
73
65
  // Update instance classes on tools wrapper and node tools
74
- if (toolsWrapper) {
75
- if (isInstance) {
76
- toolsWrapper.classList.add("is-instance");
77
- } else {
78
- toolsWrapper.classList.remove("is-instance");
79
- }
80
- // Update text edit class
81
- if (isTextEdit) {
82
- toolsWrapper.classList.add("is-text-edit");
83
- } else {
84
- toolsWrapper.classList.remove("is-text-edit");
85
- }
86
- }
87
- if (nodeTools) {
88
- if (isInstance) {
89
- nodeTools.classList.add("is-instance");
90
- } else {
91
- nodeTools.classList.remove("is-instance");
92
- }
93
- // Update text edit class
94
- if (isTextEdit) {
95
- nodeTools.classList.add("is-text-edit");
96
- } else {
97
- nodeTools.classList.remove("is-text-edit");
98
- }
99
- }
66
+ toggleClass(toolsWrapper, "is-instance", isInstance);
67
+ toggleClass(toolsWrapper, "is-text-edit", isTextEdit);
68
+ toggleClass(nodeTools, "is-instance", isInstance);
69
+ toggleClass(nodeTools, "is-text-edit", isTextEdit);
100
70
 
101
71
  // Batch all DOM writes (single paint pass)
102
72
  // Update group transform to move entire group (rect + handles) at once
@@ -1,4 +1,4 @@
1
- import { getCanvasContainer } from "@/lib/canvas/helpers/getCanvasContainer";
1
+ import { getCanvasContainerOrBody } from "@/lib/canvas/helpers/getCanvasContainerOrBody";
2
2
  import { getHighlightFrameElement } from "./helpers/getHighlightFrameElement";
3
3
 
4
4
  export const updateHighlightFrameVisibility = (node: HTMLElement): void => {
@@ -12,8 +12,7 @@ export const updateHighlightFrameVisibility = (node: HTMLElement): void => {
12
12
  // Batch DOM writes
13
13
  frame.style.display = displayValue;
14
14
 
15
- const canvasContainer = getCanvasContainer();
16
- const container = canvasContainer || document.body;
15
+ const container = getCanvasContainerOrBody();
17
16
  const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper") as HTMLElement | null;
18
17
  if (toolsWrapper) {
19
18
  toolsWrapper.style.display = displayValue;
@@ -0,0 +1,19 @@
1
+ export const isInsideViewport = (element: Element): boolean => {
2
+ let current: Element | null = element;
3
+
4
+ while (current) {
5
+ if (current.classList.contains("viewport")) {
6
+ return true;
7
+ }
8
+
9
+ // Stop at node-provider to avoid checking beyond the editable area
10
+ if (current.getAttribute("data-role") === "node-provider") {
11
+ break;
12
+ }
13
+
14
+ current = current.parentElement;
15
+ }
16
+
17
+ return false;
18
+ };
19
+
@@ -3,6 +3,7 @@ import type { NodeText } from "../text/types";
3
3
  import { IGNORED_DOM_ELEMENTS } from "./constants";
4
4
  import { getElementsFromPoint } from "./helpers/getElementsFromPoint";
5
5
  import { isInsideComponent } from "./helpers/isInsideComponent";
6
+ import { isInsideViewport } from "./helpers/isInsideViewport";
6
7
  import { targetSameCandidates } from "./helpers/targetSameCandidates";
7
8
 
8
9
  let candidateCache: Element[] = [];
@@ -22,9 +23,20 @@ export const selectNode = (event: MouseEvent, nodeProvider: HTMLElement | null,
22
23
  (element) =>
23
24
  !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
24
25
  !element.classList.contains("select-none") &&
25
- !isInsideComponent(element)
26
+ !element.classList.contains("content-layer") &&
27
+ !element.classList.contains("resize-handle") &&
28
+ !element.classList.contains("resize-presets") &&
29
+ !isInsideComponent(element) &&
30
+ isInsideViewport(element)
26
31
  );
27
32
 
33
+ console.log("candidates", candidates);
34
+
35
+ if (candidates.length === 0) {
36
+ lastSelectedNode = null;
37
+ return null;
38
+ }
39
+
28
40
  const editableNode = text.getEditableNode();
29
41
  if (editableNode && candidates.includes(editableNode)) {
30
42
  selectedNode = editableNode;
@@ -50,6 +50,7 @@ export const setupMutationObserver = (
50
50
  // Accumulate mutations instead of replacing
51
51
  pendingMutations.push(...mutations);
52
52
  scheduleProcess();
53
+ console.log("refreshHighlightFrame in mutationObserver");
53
54
  refreshHighlightFrame(node, nodeProvider, canvasName);
54
55
  });
55
56
 
@@ -71,6 +71,51 @@
71
71
  will-change: transform;
72
72
  }
73
73
 
74
+ .viewport-labels-overlay {
75
+ position: absolute;
76
+ inset: 0;
77
+ width: 100vw;
78
+ height: 100vh;
79
+ pointer-events: none;
80
+ z-index: var(--z-index-highlight);
81
+ overflow: visible;
82
+ contain: layout style paint;
83
+ will-change: transform;
84
+ }
85
+
86
+ .viewport-label-text {
87
+ font-size: 0.6875rem;
88
+ font-weight: 500;
89
+ font-family: var(--font-family-primary);
90
+ letter-spacing: var(--letter-spacing);
91
+ fill: oklch(0.5 0 0);
92
+ pointer-events: auto;
93
+ user-select: none;
94
+ -webkit-user-select: none;
95
+ -moz-user-select: none;
96
+ -ms-user-select: none;
97
+ }
98
+
99
+ @media (prefers-color-scheme: dark) {
100
+ .viewport-label-text {
101
+ fill: oklch(0.5 0 0);
102
+ }
103
+
104
+ .viewport-label-text:hover {
105
+ fill: oklch(0 0 0);
106
+ }
107
+ }
108
+
109
+ @media (prefers-color-scheme: light) {
110
+ .viewport-label-text {
111
+ fill: oklch(0.7 0 0);
112
+ }
113
+
114
+ .viewport-label-text:hover {
115
+ fill: oklch(0.3 0 0);
116
+ }
117
+ }
118
+
74
119
  .highlight-frame-rect {
75
120
  fill: none;
76
121
  stroke: var(--primary-color);
@@ -152,7 +197,9 @@
152
197
  }
153
198
 
154
199
  .viewport {
155
- position: relative;
200
+ position: absolute;
201
+ top: 0;
202
+ left: 0;
156
203
  width: var(--container-width);
157
204
  }
158
205
 
@@ -1,8 +1,8 @@
1
1
  export const DEFAULT_WIDTH = 400;
2
2
 
3
3
  export const RESIZE_CONFIG = {
4
- minWidth: 320,
5
- maxWidth: 1680,
4
+ minWidth: 4,
5
+ maxWidth: 2560,
6
6
  } as const;
7
7
 
8
8
  export const RESIZE_PRESETS = [
@@ -1,6 +1,10 @@
1
1
  import { getCanvasContainer } from "../canvas/helpers/getCanvasContainer";
2
+ import { createDragHandler } from "../helpers/createDragHandler";
3
+ import { getNodeProvider } from "../helpers/getNodeProvider";
4
+ import { getNodeTools } from "../helpers/getNodeTools";
5
+ import { refreshHighlightFrame } from "../node-tools/highlight/refreshHighlightFrame";
2
6
  import { DEFAULT_WIDTH } from "./constants";
3
- import { setupEventListener } from "./events/setupEventListener";
7
+ import { refreshViewportLabels } from "./label/refreshViewportLabels";
4
8
  import { createResizeHandle } from "./resize/createResizeHandle";
5
9
  import { createResizePresets } from "./resize/createResizePresets";
6
10
  import type { Viewport } from "./types";
@@ -12,6 +16,7 @@ export const createViewport = (container: HTMLElement, initialWidth?: number): V
12
16
 
13
17
  // Remove any existing resize handle to prevent duplicates
14
18
  const existingHandle = container.querySelector(".resize-handle");
19
+
15
20
  if (existingHandle) {
16
21
  existingHandle.remove();
17
22
  }
@@ -22,60 +27,70 @@ export const createViewport = (container: HTMLElement, initialWidth?: number): V
22
27
 
23
28
  createResizePresets(resizeHandle, container, updateWidth);
24
29
 
25
- let isDragging: boolean = false;
26
- let startX: number = 0;
27
- let startWidth: number = 0;
28
-
29
- const startResize = (event: MouseEvent): void => {
30
- event.preventDefault();
31
- event.stopPropagation();
32
-
33
- isDragging = true;
34
- startX = event.clientX;
35
- startWidth = container.offsetWidth;
36
- };
37
-
38
- const handleResize = (event: MouseEvent): void => {
39
- if (!isDragging) return;
40
-
41
- if (canvas) {
42
- canvas.style.cursor = "ew-resize";
30
+ // Track initial values for resize calculation
31
+ let startX = 0;
32
+ let startWidth = 0;
33
+
34
+ // Handle mouse leave for resize (specific to resize use case)
35
+ const handleMouseLeave = (event: MouseEvent): void => {
36
+ // Check if mouse is leaving the window/document
37
+ if (!event.relatedTarget && (event.target === document || event.target === document.documentElement)) {
38
+ if (canvas) {
39
+ canvas.style.cursor = "default";
40
+ }
43
41
  }
44
-
45
- const width = calcWidth(event, startX, startWidth);
46
- updateWidth(container, width);
47
42
  };
48
43
 
49
- const stopResize = (event: MouseEvent): void => {
50
- event.preventDefault();
51
- event.stopPropagation();
52
-
53
- if (canvas) {
54
- canvas.style.cursor = "default";
55
- }
56
-
57
- isDragging = false;
58
- };
44
+ const removeDragListeners = createDragHandler(resizeHandle, {
45
+ onStart: (_event, { startX: dragStartX }) => {
46
+ startX = dragStartX;
47
+ startWidth = container.offsetWidth;
48
+ },
49
+ onDrag: (event) => {
50
+ if (canvas) {
51
+ canvas.style.cursor = "ew-resize";
52
+ }
59
53
 
60
- const blurResize = (): void => {
61
- if (canvas) {
62
- canvas.style.cursor = "default";
63
- }
54
+ const width = calcWidth(event, startX, startWidth);
55
+ updateWidth(container, width);
56
+ },
57
+ onStop: () => {
58
+ if (canvas) {
59
+ canvas.style.cursor = "default";
60
+ }
61
+ },
62
+ onCancel: () => {
63
+ if (canvas) {
64
+ canvas.style.cursor = "default";
65
+ }
66
+ },
67
+ onPreventClick: () => {},
68
+ });
64
69
 
65
- isDragging = false;
66
- };
70
+ document.addEventListener("mouseleave", handleMouseLeave);
67
71
 
68
- const removeListeners = setupEventListener(resizeHandle, startResize, handleResize, stopResize, blurResize);
72
+ refreshViewportLabels();
69
73
 
70
74
  const cleanup = (): void => {
71
- isDragging = false;
72
- removeListeners();
75
+ removeDragListeners();
76
+ document.removeEventListener("mouseleave", handleMouseLeave);
73
77
  resizeHandle.remove();
78
+
79
+ refreshViewportLabels();
74
80
  };
75
81
 
76
82
  return {
77
83
  setWidth: (width: number): void => {
78
84
  updateWidth(container, width);
85
+ refreshViewportLabels();
86
+
87
+ const nodeTools = getNodeTools();
88
+ const selectedNode = nodeTools?.getSelectedNode?.();
89
+ const nodeProvider = getNodeProvider();
90
+
91
+ if (selectedNode && nodeProvider) {
92
+ refreshHighlightFrame(selectedNode, nodeProvider);
93
+ }
79
94
  },
80
95
  cleanup,
81
96
  };
@@ -0,0 +1,32 @@
1
+ import { getCanvasContainerOrBody } from "../../canvas/helpers/getCanvasContainerOrBody";
2
+ import { getViewportDimensions } from "../../helpers/getViewportDimensions";
3
+
4
+ export const getViewportLabelsOverlay = (): SVGSVGElement => {
5
+ const container = getCanvasContainerOrBody();
6
+
7
+ // Check if overlay already exists
8
+ let overlay = container.querySelector(".viewport-labels-overlay") as SVGSVGElement | null;
9
+
10
+ if (!overlay) {
11
+ // Create new SVG overlay
12
+ overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
13
+ overlay.classList.add("viewport-labels-overlay");
14
+
15
+ // Set fixed positioning
16
+ overlay.style.position = "absolute";
17
+ overlay.style.top = "0";
18
+ overlay.style.left = "0";
19
+ overlay.style.width = "100vw";
20
+ overlay.style.height = "100vh";
21
+ overlay.style.pointerEvents = "none";
22
+ overlay.style.zIndex = "500";
23
+
24
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
25
+ overlay.setAttribute("width", viewportWidth.toString());
26
+ overlay.setAttribute("height", viewportHeight.toString());
27
+
28
+ container.appendChild(overlay);
29
+ }
30
+
31
+ return overlay;
32
+ };
@@ -0,0 +1,6 @@
1
+ import { parseTransform2d } from "../../../helpers/parseTransform";
2
+
3
+ export const getLabelPosition = (labelGroup: SVGGElement): { x: number; y: number } => {
4
+ const transform = labelGroup.getAttribute("transform");
5
+ return parseTransform2d(transform);
6
+ };
@@ -0,0 +1,6 @@
1
+ import { parseTransform3d } from "../../../helpers/parseTransform";
2
+
3
+ export const getTransformValues = (element: HTMLElement): { x: number; y: number } => {
4
+ const style = element.style.transform;
5
+ return parseTransform3d(style);
6
+ };
@@ -0,0 +1,4 @@
1
+ export const getZoomValue = (): number => {
2
+ const zoomValue = getComputedStyle(document.body).getPropertyValue("--zoom").trim();
3
+ return zoomValue ? parseFloat(zoomValue) : 1;
4
+ };
@@ -0,0 +1,18 @@
1
+ import { getNodeTools } from "../../../helpers/getNodeTools";
2
+
3
+ /**
4
+ * Selects the first child node inside a viewport element.
5
+ * Skips the resize-handle element if present.
6
+ */
7
+ export const selectFirstViewportNode = (viewportElement: HTMLElement): void => {
8
+ const firstChild = Array.from(viewportElement.children).find((child) => !child.classList.contains("resize-handle")) as
9
+ | HTMLElement
10
+ | undefined;
11
+
12
+ if (firstChild) {
13
+ const nodeTools = getNodeTools();
14
+ if (nodeTools?.selectNode) {
15
+ nodeTools.selectNode(firstChild);
16
+ }
17
+ }
18
+ };
@@ -0,0 +1,4 @@
1
+ export { getViewportLabelsOverlay } from "./getViewportLabelsOverlay";
2
+ export { isViewportLabelDragging } from "./isViewportLabelDragging";
3
+ export { refreshViewportLabels } from "./refreshViewportLabels";
4
+ export { setupViewportLabelDrag } from "./setupViewportLabelDrag";
@@ -0,0 +1,9 @@
1
+ // Global flag to prevent refreshViewportLabels during drag
2
+ let globalIsDragging = false;
3
+
4
+ export const isViewportLabelDragging = (): boolean => globalIsDragging;
5
+
6
+ export const setViewportLabelDragging = (isDragging: boolean): void => {
7
+ globalIsDragging = isDragging;
8
+ };
9
+
@@ -0,0 +1,69 @@
1
+ import { getViewportDimensions } from "../../helpers/getViewportDimensions";
2
+ import { getScreenBounds } from "../../node-tools/highlight/helpers/getScreenBounds";
3
+ import { getViewportLabelsOverlay } from "./getViewportLabelsOverlay";
4
+ import { isViewportLabelDragging } from "./isViewportLabelDragging";
5
+ import { setupViewportLabelDrag } from "./setupViewportLabelDrag";
6
+
7
+ // Store cleanup functions for drag listeners
8
+ const dragCleanupFunctions = new Map<string, () => void>();
9
+
10
+ export const refreshViewportLabels = (): void => {
11
+ // Skip refresh if a viewport label is being dragged
12
+ if (isViewportLabelDragging()) {
13
+ return;
14
+ }
15
+
16
+ const overlay = getViewportLabelsOverlay();
17
+
18
+ // Update SVG dimensions to match current viewport
19
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
20
+ overlay.setAttribute("width", viewportWidth.toString());
21
+ overlay.setAttribute("height", viewportHeight.toString());
22
+
23
+ // Find all viewports with names
24
+ const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
25
+
26
+ // Clean up existing drag listeners
27
+ dragCleanupFunctions.forEach((cleanup) => {
28
+ cleanup();
29
+ });
30
+ dragCleanupFunctions.clear();
31
+
32
+ // Remove existing label groups
33
+ const existingGroups = overlay.querySelectorAll(".viewport-label-group");
34
+ existingGroups.forEach((group) => {
35
+ group.remove();
36
+ });
37
+
38
+ // Create/update labels for each viewport
39
+ viewports.forEach((viewport) => {
40
+ const viewportElement = viewport as HTMLElement;
41
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
42
+
43
+ if (!viewportName) return;
44
+
45
+ const bounds = getScreenBounds(viewportElement);
46
+
47
+ // Create group for this viewport label
48
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
49
+ group.classList.add("viewport-label-group");
50
+ group.setAttribute("data-viewport-name", viewportName);
51
+ group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
52
+
53
+ // Create text element
54
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
55
+ text.classList.add("viewport-label-text");
56
+ text.setAttribute("x", "0");
57
+ text.setAttribute("y", "-8");
58
+ text.setAttribute("vector-effect", "non-scaling-stroke");
59
+ text.setAttribute("pointer-events", "auto");
60
+ text.textContent = viewportName;
61
+
62
+ group.appendChild(text);
63
+ overlay.appendChild(group);
64
+
65
+ // Setup drag functionality for this label
66
+ const cleanup = setupViewportLabelDrag(text, viewportElement, viewportName);
67
+ dragCleanupFunctions.set(viewportName, cleanup);
68
+ });
69
+ };
@@ -0,0 +1,70 @@
1
+ import { createDragHandler } from "../../helpers/createDragHandler";
2
+ import { getNodeTools } from "../../helpers/getNodeTools";
3
+ import { sendPostMessage } from "../../post-message/sendPostMessage";
4
+ import { getLabelPosition } from "./helpers/getLabelPosition";
5
+ import { getTransformValues } from "./helpers/getTransformValues";
6
+ import { getZoomValue } from "./helpers/getZoomValue";
7
+ import { selectFirstViewportNode } from "./helpers/selectFirstViewportNode";
8
+ import { setViewportLabelDragging } from "./isViewportLabelDragging";
9
+
10
+ export const setupViewportLabelDrag = (labelElement: SVGTextElement, viewportElement: HTMLElement, viewportName: string): (() => void) => {
11
+ // Get the parent group element that contains the label
12
+ const labelGroup = labelElement.parentElement as unknown as SVGGElement;
13
+
14
+ // Track initial positions for calculations
15
+ let initialTransform = { x: 0, y: 0 };
16
+ let initialLabelPosition = { x: 0, y: 0 };
17
+
18
+ return createDragHandler(labelElement, {
19
+ onStart: () => {
20
+ setViewportLabelDragging(true);
21
+ initialTransform = getTransformValues(viewportElement);
22
+ initialLabelPosition = getLabelPosition(labelGroup);
23
+ selectFirstViewportNode(viewportElement);
24
+ },
25
+ onDrag: (_event, { deltaX, deltaY }) => {
26
+ const zoom = getZoomValue();
27
+
28
+ // Adjust delta for zoom level (viewport is in canvas space)
29
+ const deltaXZoomed = deltaX / zoom;
30
+ const deltaYZoomed = deltaY / zoom;
31
+
32
+ // Calculate new positions
33
+ const newX = initialTransform.x + deltaXZoomed;
34
+ const newY = initialTransform.y + deltaYZoomed;
35
+
36
+ // Update label position with raw delta (labels are in screen space)
37
+ const newLabelX = initialLabelPosition.x + deltaX;
38
+ const newLabelY = initialLabelPosition.y + deltaY;
39
+ labelGroup.setAttribute("transform", `translate(${newLabelX}, ${newLabelY})`);
40
+
41
+ // Update viewport position with zoom-adjusted delta
42
+ viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
43
+ },
44
+ onStop: (_event, { hasDragged }) => {
45
+ setViewportLabelDragging(false);
46
+
47
+ // If it was a drag, handle drag completion
48
+ if (hasDragged) {
49
+ const finalTransform = getTransformValues(viewportElement);
50
+
51
+ // Trigger refresh after drag completes to update highlight frame and labels
52
+ const nodeTools = getNodeTools();
53
+ if (nodeTools?.refreshHighlightFrame) {
54
+ nodeTools.refreshHighlightFrame();
55
+ }
56
+
57
+ // Notify parent about the new position
58
+ sendPostMessage("viewport-position-changed", {
59
+ viewportName,
60
+ x: finalTransform.x,
61
+ y: finalTransform.y,
62
+ });
63
+ }
64
+ },
65
+ onCancel: () => {
66
+ setViewportLabelDragging(false);
67
+ },
68
+ onPreventClick: () => {},
69
+ });
70
+ };
@@ -1,6 +1,5 @@
1
1
  export const bindToWindow = <T>(key: string, value: T): void => {
2
2
  if (typeof window !== "undefined") {
3
- // biome-ignore lint/suspicious/noExplicitAny: global window extension requires flexibility
4
- (window as any)[key] = value;
3
+ (window as unknown as Record<string, unknown>)[key] = value;
5
4
  }
6
5
  };