@node-edit-utils/core 2.3.1 → 2.3.2

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 (32) hide show
  1. package/dist/lib/node-tools/select/helpers/isInsideViewport.d.ts +1 -0
  2. package/dist/lib/viewport/constants.d.ts +2 -2
  3. package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +1 -0
  4. package/dist/lib/viewport/label/helpers/getLabelPosition.d.ts +4 -0
  5. package/dist/lib/viewport/label/helpers/getTransformValues.d.ts +4 -0
  6. package/dist/lib/viewport/label/helpers/getZoomValue.d.ts +1 -0
  7. package/dist/lib/viewport/label/index.d.ts +4 -0
  8. package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +2 -0
  9. package/dist/lib/viewport/label/refreshViewportLabels.d.ts +1 -0
  10. package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +1 -0
  11. package/dist/node-edit-utils.cjs.js +273 -32
  12. package/dist/node-edit-utils.esm.js +273 -32
  13. package/dist/node-edit-utils.umd.js +273 -32
  14. package/dist/node-edit-utils.umd.min.js +1 -1
  15. package/dist/styles.css +1 -1
  16. package/package.json +1 -1
  17. package/src/lib/canvas/createCanvasObserver.ts +14 -0
  18. package/src/lib/node-tools/createNodeTools.ts +12 -5
  19. package/src/lib/node-tools/select/helpers/isInsideViewport.ts +19 -0
  20. package/src/lib/node-tools/select/selectNode.ts +13 -1
  21. package/src/lib/node-tools/text/events/setupMutationObserver.ts +1 -0
  22. package/src/lib/styles/styles.css +48 -1
  23. package/src/lib/viewport/constants.ts +2 -2
  24. package/src/lib/viewport/createViewport.ts +18 -0
  25. package/src/lib/viewport/label/getViewportLabelsOverlay.ts +33 -0
  26. package/src/lib/viewport/label/helpers/getLabelPosition.ts +8 -0
  27. package/src/lib/viewport/label/helpers/getTransformValues.ts +8 -0
  28. package/src/lib/viewport/label/helpers/getZoomValue.ts +4 -0
  29. package/src/lib/viewport/label/index.ts +4 -0
  30. package/src/lib/viewport/label/isViewportLabelDragging.ts +9 -0
  31. package/src/lib/viewport/label/refreshViewportLabels.ts +69 -0
  32. package/src/lib/viewport/label/setupViewportLabelDrag.ts +98 -0
@@ -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.3 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,8 @@
1
1
  import { getCanvasContainer } from "../canvas/helpers/getCanvasContainer";
2
+ import { refreshHighlightFrame } from "../node-tools/highlight/refreshHighlightFrame";
2
3
  import { DEFAULT_WIDTH } from "./constants";
3
4
  import { setupEventListener } from "./events/setupEventListener";
5
+ import { refreshViewportLabels } from "./label/refreshViewportLabels";
4
6
  import { createResizeHandle } from "./resize/createResizeHandle";
5
7
  import { createResizePresets } from "./resize/createResizePresets";
6
8
  import type { Viewport } from "./types";
@@ -67,15 +69,31 @@ export const createViewport = (container: HTMLElement, initialWidth?: number): V
67
69
 
68
70
  const removeListeners = setupEventListener(resizeHandle, startResize, handleResize, stopResize, blurResize);
69
71
 
72
+ // Refresh viewport labels when viewport is created
73
+ refreshViewportLabels();
74
+
70
75
  const cleanup = (): void => {
71
76
  isDragging = false;
72
77
  removeListeners();
73
78
  resizeHandle.remove();
79
+ // Refresh labels after cleanup to remove this viewport's label if needed
80
+ refreshViewportLabels();
74
81
  };
75
82
 
76
83
  return {
77
84
  setWidth: (width: number): void => {
78
85
  updateWidth(container, width);
86
+ refreshViewportLabels();
87
+
88
+ // Refresh highlight frame when viewport width changes to update node positions
89
+ // biome-ignore lint/suspicious/noExplicitAny: global window extension
90
+ const nodeTools = (window as any).nodeTools;
91
+ const selectedNode = nodeTools?.getSelectedNode?.();
92
+ const nodeProvider = document.querySelector('[data-role="node-provider"]') as HTMLElement | null;
93
+
94
+ if (selectedNode && nodeProvider) {
95
+ refreshHighlightFrame(selectedNode, nodeProvider);
96
+ }
79
97
  },
80
98
  cleanup,
81
99
  };
@@ -0,0 +1,33 @@
1
+ import { getCanvasContainer } from "../../canvas/helpers/getCanvasContainer";
2
+
3
+ export const getViewportLabelsOverlay = (): SVGSVGElement => {
4
+ const canvasContainer = getCanvasContainer();
5
+ const container = canvasContainer || document.body;
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 viewportWidth = document.documentElement.clientWidth || window.innerWidth;
25
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
26
+ overlay.setAttribute("width", viewportWidth.toString());
27
+ overlay.setAttribute("height", viewportHeight.toString());
28
+
29
+ container.appendChild(overlay);
30
+ }
31
+
32
+ return overlay;
33
+ };
@@ -0,0 +1,8 @@
1
+ export const getLabelPosition = (labelGroup: SVGGElement): { x: number; y: number } => {
2
+ const transform = labelGroup.getAttribute("transform");
3
+ const match = transform?.match(/translate\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)/);
4
+ if (match) {
5
+ return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
6
+ }
7
+ return { x: 0, y: 0 };
8
+ };
@@ -0,0 +1,8 @@
1
+ export const getTransformValues = (element: HTMLElement): { x: number; y: number } => {
2
+ const style = element.style.transform;
3
+ const match = style.match(/translate3d\((-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px\)/);
4
+ if (match) {
5
+ return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
6
+ }
7
+ return { x: 0, y: 0 };
8
+ };
@@ -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,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 { getScreenBounds } from "../../node-tools/highlight/helpers/getScreenBounds";
2
+ import { getViewportLabelsOverlay } from "./getViewportLabelsOverlay";
3
+ import { isViewportLabelDragging } from "./isViewportLabelDragging";
4
+ import { setupViewportLabelDrag } from "./setupViewportLabelDrag";
5
+
6
+ // Store cleanup functions for drag listeners
7
+ const dragCleanupFunctions = new Map<string, () => void>();
8
+
9
+ export const refreshViewportLabels = (): void => {
10
+ // Skip refresh if a viewport label is being dragged
11
+ if (isViewportLabelDragging()) {
12
+ return;
13
+ }
14
+
15
+ const overlay = getViewportLabelsOverlay();
16
+
17
+ // Update SVG dimensions to match current viewport
18
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
19
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
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,98 @@
1
+ import { sendPostMessage } from "../../post-message/sendPostMessage";
2
+ import { getLabelPosition } from "./helpers/getLabelPosition";
3
+ import { getTransformValues } from "./helpers/getTransformValues";
4
+ import { getZoomValue } from "./helpers/getZoomValue";
5
+ import { setViewportLabelDragging } from "./isViewportLabelDragging";
6
+
7
+ export const setupViewportLabelDrag = (labelElement: SVGTextElement, viewportElement: HTMLElement, viewportName: string): (() => void) => {
8
+ let isDragging = false;
9
+ let startX = 0;
10
+ let startY = 0;
11
+ let initialTransform = { x: 0, y: 0 };
12
+ let initialLabelPosition = { x: 0, y: 0 };
13
+
14
+ // Get the parent group element that contains the label
15
+ const labelGroup = labelElement.parentElement as unknown as SVGGElement;
16
+
17
+ const startDrag = (event: MouseEvent): void => {
18
+ event.preventDefault();
19
+ event.stopPropagation();
20
+
21
+ isDragging = true;
22
+ setViewportLabelDragging(true);
23
+ startX = event.clientX;
24
+ startY = event.clientY;
25
+ initialTransform = getTransformValues(viewportElement);
26
+ initialLabelPosition = getLabelPosition(labelGroup);
27
+ };
28
+
29
+ const handleDrag = (event: MouseEvent): void => {
30
+ if (!isDragging) return;
31
+
32
+ const zoom = getZoomValue();
33
+
34
+ // Calculate mouse delta
35
+ const rawDeltaX = event.clientX - startX;
36
+ const rawDeltaY = event.clientY - startY;
37
+
38
+ // Adjust delta for zoom level
39
+ const deltaX = rawDeltaX / zoom;
40
+ const deltaY = rawDeltaY / zoom;
41
+
42
+ const newX = initialTransform.x + deltaX;
43
+ const newY = initialTransform.y + deltaY;
44
+
45
+ // Update label position with raw delta (labels are in screen space)
46
+ const newLabelX = initialLabelPosition.x + rawDeltaX;
47
+ const newLabelY = initialLabelPosition.y + rawDeltaY;
48
+ labelGroup.setAttribute("transform", `translate(${newLabelX}, ${newLabelY})`);
49
+
50
+ // Update viewport position with zoom-adjusted delta
51
+ viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
52
+ };
53
+
54
+ const stopDrag = (event: MouseEvent): void => {
55
+ if (!isDragging) return;
56
+
57
+ event.preventDefault();
58
+ event.stopPropagation();
59
+
60
+ isDragging = false;
61
+ setViewportLabelDragging(false);
62
+
63
+ const finalTransform = getTransformValues(viewportElement);
64
+
65
+ // Trigger refresh after drag completes to update highlight frame and labels
66
+ // biome-ignore lint/suspicious/noExplicitAny: global window extension
67
+ const nodeTools = (window as any).nodeTools;
68
+ if (nodeTools?.refreshHighlightFrame) {
69
+ nodeTools.refreshHighlightFrame();
70
+ }
71
+
72
+ // Notify parent about the new position
73
+ sendPostMessage("viewport-position-changed", {
74
+ viewportName,
75
+ x: finalTransform.x,
76
+ y: finalTransform.y,
77
+ });
78
+ };
79
+
80
+ const cancelDrag = (): void => {
81
+ isDragging = false;
82
+ setViewportLabelDragging(false);
83
+ };
84
+
85
+ // Attach event listeners
86
+ labelElement.addEventListener("mousedown", startDrag);
87
+ document.addEventListener("mousemove", handleDrag);
88
+ document.addEventListener("mouseup", stopDrag);
89
+ window.addEventListener("blur", cancelDrag);
90
+
91
+ // Return cleanup function
92
+ return () => {
93
+ labelElement.removeEventListener("mousedown", startDrag);
94
+ document.removeEventListener("mousemove", handleDrag);
95
+ document.removeEventListener("mouseup", stopDrag);
96
+ window.removeEventListener("blur", cancelDrag);
97
+ };
98
+ };