@node-edit-utils/core 2.3.0 → 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 (34) 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/createViewport.d.ts +1 -1
  4. package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +1 -0
  5. package/dist/lib/viewport/label/helpers/getLabelPosition.d.ts +4 -0
  6. package/dist/lib/viewport/label/helpers/getTransformValues.d.ts +4 -0
  7. package/dist/lib/viewport/label/helpers/getZoomValue.d.ts +1 -0
  8. package/dist/lib/viewport/label/index.d.ts +4 -0
  9. package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +2 -0
  10. package/dist/lib/viewport/label/refreshViewportLabels.d.ts +1 -0
  11. package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +1 -0
  12. package/dist/node-edit-utils.cjs.js +288 -63
  13. package/dist/node-edit-utils.esm.js +288 -63
  14. package/dist/node-edit-utils.umd.js +288 -63
  15. package/dist/node-edit-utils.umd.min.js +1 -1
  16. package/dist/styles.css +1 -1
  17. package/package.json +1 -1
  18. package/src/lib/canvas/createCanvasObserver.ts +14 -0
  19. package/src/lib/node-tools/createNodeTools.ts +12 -5
  20. package/src/lib/node-tools/select/helpers/isInsideViewport.ts +19 -0
  21. package/src/lib/node-tools/select/selectNode.ts +13 -1
  22. package/src/lib/node-tools/text/events/setupMutationObserver.ts +1 -0
  23. package/src/lib/styles/styles.css +48 -1
  24. package/src/lib/viewport/constants.ts +2 -2
  25. package/src/lib/viewport/createViewport.ts +26 -7
  26. package/src/lib/viewport/events/setupEventListener.ts +9 -0
  27. package/src/lib/viewport/label/getViewportLabelsOverlay.ts +33 -0
  28. package/src/lib/viewport/label/helpers/getLabelPosition.ts +8 -0
  29. package/src/lib/viewport/label/helpers/getTransformValues.ts +8 -0
  30. package/src/lib/viewport/label/helpers/getZoomValue.ts +4 -0
  31. package/src/lib/viewport/label/index.ts +4 -0
  32. package/src/lib/viewport/label/isViewportLabelDragging.ts +9 -0
  33. package/src/lib/viewport/label/refreshViewportLabels.ts +69 -0
  34. package/src/lib/viewport/label/setupViewportLabelDrag.ts +98 -0
@@ -1,8 +1,216 @@
1
1
  /**
2
2
  * Markup Canvas
3
3
  * High-performance markup canvas with zoom and pan capabilities
4
- * @version 2.3.0
4
+ * @version 2.3.2
5
5
  */
6
+ function getScreenBounds(element) {
7
+ const rect = element.getBoundingClientRect();
8
+ return {
9
+ top: rect.top,
10
+ left: rect.left,
11
+ width: rect.width,
12
+ height: rect.height,
13
+ };
14
+ }
15
+
16
+ const getCanvasContainer = () => {
17
+ return document.querySelector(".canvas-container");
18
+ };
19
+
20
+ const getViewportLabelsOverlay = () => {
21
+ const canvasContainer = getCanvasContainer();
22
+ const container = canvasContainer || document.body;
23
+ // Check if overlay already exists
24
+ let overlay = container.querySelector(".viewport-labels-overlay");
25
+ if (!overlay) {
26
+ // Create new SVG overlay
27
+ overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
28
+ overlay.classList.add("viewport-labels-overlay");
29
+ // Set fixed positioning
30
+ overlay.style.position = "absolute";
31
+ overlay.style.top = "0";
32
+ overlay.style.left = "0";
33
+ overlay.style.width = "100vw";
34
+ overlay.style.height = "100vh";
35
+ overlay.style.pointerEvents = "none";
36
+ overlay.style.zIndex = "500";
37
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
38
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
39
+ overlay.setAttribute("width", viewportWidth.toString());
40
+ overlay.setAttribute("height", viewportHeight.toString());
41
+ container.appendChild(overlay);
42
+ }
43
+ return overlay;
44
+ };
45
+
46
+ // Global flag to prevent refreshViewportLabels during drag
47
+ let globalIsDragging = false;
48
+ const isViewportLabelDragging = () => globalIsDragging;
49
+ const setViewportLabelDragging = (isDragging) => {
50
+ globalIsDragging = isDragging;
51
+ };
52
+
53
+ function sendPostMessage(action, data) {
54
+ window.parent.postMessage({
55
+ source: "node-edit-utils",
56
+ action,
57
+ data,
58
+ timestamp: Date.now(),
59
+ }, "*");
60
+ }
61
+
62
+ const getLabelPosition = (labelGroup) => {
63
+ const transform = labelGroup.getAttribute("transform");
64
+ const match = transform?.match(/translate\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)/);
65
+ if (match) {
66
+ return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
67
+ }
68
+ return { x: 0, y: 0 };
69
+ };
70
+
71
+ const getTransformValues = (element) => {
72
+ const style = element.style.transform;
73
+ const match = style.match(/translate3d\((-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px\)/);
74
+ if (match) {
75
+ return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
76
+ }
77
+ return { x: 0, y: 0 };
78
+ };
79
+
80
+ const getZoomValue = () => {
81
+ const zoomValue = getComputedStyle(document.body).getPropertyValue("--zoom").trim();
82
+ return zoomValue ? parseFloat(zoomValue) : 1;
83
+ };
84
+
85
+ const setupViewportLabelDrag = (labelElement, viewportElement, viewportName) => {
86
+ let isDragging = false;
87
+ let startX = 0;
88
+ let startY = 0;
89
+ let initialTransform = { x: 0, y: 0 };
90
+ let initialLabelPosition = { x: 0, y: 0 };
91
+ // Get the parent group element that contains the label
92
+ const labelGroup = labelElement.parentElement;
93
+ const startDrag = (event) => {
94
+ event.preventDefault();
95
+ event.stopPropagation();
96
+ isDragging = true;
97
+ setViewportLabelDragging(true);
98
+ startX = event.clientX;
99
+ startY = event.clientY;
100
+ initialTransform = getTransformValues(viewportElement);
101
+ initialLabelPosition = getLabelPosition(labelGroup);
102
+ };
103
+ const handleDrag = (event) => {
104
+ if (!isDragging)
105
+ return;
106
+ const zoom = getZoomValue();
107
+ // Calculate mouse delta
108
+ const rawDeltaX = event.clientX - startX;
109
+ const rawDeltaY = event.clientY - startY;
110
+ // Adjust delta for zoom level
111
+ const deltaX = rawDeltaX / zoom;
112
+ const deltaY = rawDeltaY / zoom;
113
+ const newX = initialTransform.x + deltaX;
114
+ const newY = initialTransform.y + deltaY;
115
+ // Update label position with raw delta (labels are in screen space)
116
+ const newLabelX = initialLabelPosition.x + rawDeltaX;
117
+ const newLabelY = initialLabelPosition.y + rawDeltaY;
118
+ labelGroup.setAttribute("transform", `translate(${newLabelX}, ${newLabelY})`);
119
+ // Update viewport position with zoom-adjusted delta
120
+ viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
121
+ };
122
+ const stopDrag = (event) => {
123
+ if (!isDragging)
124
+ return;
125
+ event.preventDefault();
126
+ event.stopPropagation();
127
+ isDragging = false;
128
+ setViewportLabelDragging(false);
129
+ const finalTransform = getTransformValues(viewportElement);
130
+ // Trigger refresh after drag completes to update highlight frame and labels
131
+ // biome-ignore lint/suspicious/noExplicitAny: global window extension
132
+ const nodeTools = window.nodeTools;
133
+ if (nodeTools?.refreshHighlightFrame) {
134
+ nodeTools.refreshHighlightFrame();
135
+ }
136
+ // Notify parent about the new position
137
+ sendPostMessage("viewport-position-changed", {
138
+ viewportName,
139
+ x: finalTransform.x,
140
+ y: finalTransform.y,
141
+ });
142
+ };
143
+ const cancelDrag = () => {
144
+ isDragging = false;
145
+ setViewportLabelDragging(false);
146
+ };
147
+ // Attach event listeners
148
+ labelElement.addEventListener("mousedown", startDrag);
149
+ document.addEventListener("mousemove", handleDrag);
150
+ document.addEventListener("mouseup", stopDrag);
151
+ window.addEventListener("blur", cancelDrag);
152
+ // Return cleanup function
153
+ return () => {
154
+ labelElement.removeEventListener("mousedown", startDrag);
155
+ document.removeEventListener("mousemove", handleDrag);
156
+ document.removeEventListener("mouseup", stopDrag);
157
+ window.removeEventListener("blur", cancelDrag);
158
+ };
159
+ };
160
+
161
+ // Store cleanup functions for drag listeners
162
+ const dragCleanupFunctions = new Map();
163
+ const refreshViewportLabels = () => {
164
+ // Skip refresh if a viewport label is being dragged
165
+ if (isViewportLabelDragging()) {
166
+ return;
167
+ }
168
+ const overlay = getViewportLabelsOverlay();
169
+ // Update SVG dimensions to match current viewport
170
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
171
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
172
+ overlay.setAttribute("width", viewportWidth.toString());
173
+ overlay.setAttribute("height", viewportHeight.toString());
174
+ // Find all viewports with names
175
+ const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
176
+ // Clean up existing drag listeners
177
+ dragCleanupFunctions.forEach((cleanup) => {
178
+ cleanup();
179
+ });
180
+ dragCleanupFunctions.clear();
181
+ // Remove existing label groups
182
+ const existingGroups = overlay.querySelectorAll(".viewport-label-group");
183
+ existingGroups.forEach((group) => {
184
+ group.remove();
185
+ });
186
+ // Create/update labels for each viewport
187
+ viewports.forEach((viewport) => {
188
+ const viewportElement = viewport;
189
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
190
+ if (!viewportName)
191
+ return;
192
+ const bounds = getScreenBounds(viewportElement);
193
+ // Create group for this viewport label
194
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
195
+ group.classList.add("viewport-label-group");
196
+ group.setAttribute("data-viewport-name", viewportName);
197
+ group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
198
+ // Create text element
199
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
200
+ text.classList.add("viewport-label-text");
201
+ text.setAttribute("x", "0");
202
+ text.setAttribute("y", "-8");
203
+ text.setAttribute("vector-effect", "non-scaling-stroke");
204
+ text.setAttribute("pointer-events", "auto");
205
+ text.textContent = viewportName;
206
+ group.appendChild(text);
207
+ overlay.appendChild(group);
208
+ // Setup drag functionality for this label
209
+ const cleanup = setupViewportLabelDrag(text, viewportElement, viewportName);
210
+ dragCleanupFunctions.set(viewportName, cleanup);
211
+ });
212
+ };
213
+
6
214
  const getCanvasWindowValue = (path, canvasName = "canvas") => {
7
215
  // biome-ignore lint/suspicious/noExplicitAny: global window extension
8
216
  const canvas = window[canvasName];
@@ -32,6 +240,8 @@ function createCanvasObserver(canvasName = "canvas") {
32
240
  if (nodeTools?.refreshHighlightFrame) {
33
241
  nodeTools.refreshHighlightFrame();
34
242
  }
243
+ // Refresh viewport labels
244
+ refreshViewportLabels();
35
245
  });
36
246
  observer.observe(transformLayer, {
37
247
  attributes: true,
@@ -39,8 +249,16 @@ function createCanvasObserver(canvasName = "canvas") {
39
249
  subtree: true,
40
250
  childList: true,
41
251
  });
252
+ // Handle window resize for viewport labels
253
+ const handleResize = () => {
254
+ refreshViewportLabels();
255
+ };
256
+ window.addEventListener("resize", handleResize);
257
+ // Initial refresh of viewport labels
258
+ refreshViewportLabels();
42
259
  function disconnect() {
43
260
  observer.disconnect();
261
+ window.removeEventListener("resize", handleResize);
44
262
  }
45
263
  return {
46
264
  disconnect,
@@ -55,15 +273,6 @@ const connectResizeObserver = (element, handler) => {
55
273
  return resizeObserver;
56
274
  };
57
275
 
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
276
  const bindToWindow = (key, value) => {
68
277
  if (typeof window !== "undefined") {
69
278
  // biome-ignore lint/suspicious/noExplicitAny: global window extension requires flexibility
@@ -91,10 +300,6 @@ const processPostMessage = (event, onNodeSelected) => {
91
300
  }
92
301
  };
93
302
 
94
- const getCanvasContainer = () => {
95
- return document.querySelector(".canvas-container");
96
- };
97
-
98
303
  const clearHighlightFrame = () => {
99
304
  const canvasContainer = getCanvasContainer();
100
305
  const container = canvasContainer || document.body;
@@ -146,6 +351,21 @@ const isInsideComponent = (element) => {
146
351
  return false;
147
352
  };
148
353
 
354
+ const isInsideViewport = (element) => {
355
+ let current = element;
356
+ while (current) {
357
+ if (current.classList.contains("viewport")) {
358
+ return true;
359
+ }
360
+ // Stop at node-provider to avoid checking beyond the editable area
361
+ if (current.getAttribute("data-role") === "node-provider") {
362
+ break;
363
+ }
364
+ current = current.parentElement;
365
+ }
366
+ return false;
367
+ };
368
+
149
369
  const targetSameCandidates = (cache, current) => cache.length === current.length && cache.every((el, i) => el === current[i]);
150
370
 
151
371
  let candidateCache = [];
@@ -158,7 +378,16 @@ const selectNode = (event, nodeProvider, text) => {
158
378
  const clickThrough = event.metaKey || event.ctrlKey;
159
379
  const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
160
380
  !element.classList.contains("select-none") &&
161
- !isInsideComponent(element));
381
+ !element.classList.contains("content-layer") &&
382
+ !element.classList.contains("resize-handle") &&
383
+ !element.classList.contains("resize-presets") &&
384
+ !isInsideComponent(element) &&
385
+ isInsideViewport(element));
386
+ console.log("candidates", candidates);
387
+ if (candidates.length === 0) {
388
+ lastSelectedNode = null;
389
+ return null;
390
+ }
162
391
  const editableNode = text.getEditableNode();
163
392
  if (editableNode && candidates.includes(editableNode)) {
164
393
  selectedNode = editableNode;
@@ -263,16 +492,6 @@ const createCornerHandles = (group, width, height, isInstance = false, isTextEdi
263
492
  createCornerHandle(group, 0, height, "handle-bottom-left", isInstance, isTextEdit);
264
493
  };
265
494
 
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
495
  const getComponentColor$1 = () => {
277
496
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
278
497
  };
@@ -705,6 +924,7 @@ const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
705
924
  // Accumulate mutations instead of replacing
706
925
  pendingMutations.push(...mutations);
707
926
  scheduleProcess();
927
+ console.log("refreshHighlightFrame in mutationObserver");
708
928
  refreshHighlightFrame(node, nodeProvider, canvasName);
709
929
  });
710
930
  return () => {
@@ -842,14 +1062,15 @@ const createNodeTools = (element, canvasName = "canvas") => {
842
1062
  checkNodeExists();
843
1063
  if (!document.contains(node))
844
1064
  return;
1065
+ console.log("refreshHighlightFrame in mutationObserver 2");
845
1066
  refreshHighlightFrame(node, nodeProvider, canvasName);
846
1067
  updateHighlightFrameVisibility(node);
847
1068
  });
848
1069
  mutationObserver.observe(node, {
849
1070
  attributes: true,
850
1071
  characterData: true,
851
- childList: true,
852
- subtree: true,
1072
+ //childList: true,
1073
+ //subtree: true,
853
1074
  });
854
1075
  // Also observe parent node to catch when this node is removed
855
1076
  const parentNode = node.parentElement;
@@ -878,15 +1099,22 @@ const createNodeTools = (element, canvasName = "canvas") => {
878
1099
  if (!document.contains(node))
879
1100
  return; // Exit early if node was removed
880
1101
  refreshHighlightFrame(node, nodeProvider, canvasName);
1102
+ console.log("refreshHighlightFrame in resizeObserver");
881
1103
  updateHighlightFrameVisibility(node);
882
1104
  });
883
1105
  }
884
1106
  selectedNode = node;
885
1107
  sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
886
1108
  highlightNode(node) ?? null;
887
- if (node && nodeProvider) {
888
- updateHighlightFrameVisibility(node);
889
- updateHighlightFrameVisibility(node);
1109
+ if (node) {
1110
+ highlightNode(node);
1111
+ if (nodeProvider) {
1112
+ updateHighlightFrameVisibility(node);
1113
+ updateHighlightFrameVisibility(node);
1114
+ }
1115
+ }
1116
+ else {
1117
+ clearHighlightFrame();
890
1118
  }
891
1119
  };
892
1120
  // Setup event listener
@@ -925,36 +1153,10 @@ const createNodeTools = (element, canvasName = "canvas") => {
925
1153
  return nodeTools;
926
1154
  };
927
1155
 
928
- // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
929
- function withRAFThrottle(func) {
930
- let rafId = null;
931
- let lastArgs = null;
932
- const throttled = (...args) => {
933
- lastArgs = args;
934
- if (rafId === null) {
935
- rafId = requestAnimationFrame(() => {
936
- if (lastArgs) {
937
- func(...lastArgs);
938
- }
939
- rafId = null;
940
- lastArgs = null;
941
- });
942
- }
943
- };
944
- throttled.cleanup = () => {
945
- if (rafId !== null) {
946
- cancelAnimationFrame(rafId);
947
- rafId = null;
948
- lastArgs = null;
949
- }
950
- };
951
- return throttled;
952
- }
953
-
954
1156
  const DEFAULT_WIDTH = 400;
955
1157
  const RESIZE_CONFIG = {
956
- minWidth: 320,
957
- maxWidth: 1680,
1158
+ minWidth: 4,
1159
+ maxWidth: 2560,
958
1160
  };
959
1161
  const RESIZE_PRESETS = [
960
1162
  {
@@ -985,14 +1187,22 @@ const RESIZE_PRESETS = [
985
1187
  ];
986
1188
 
987
1189
  const setupEventListener = (resizeHandle, startResize, handleResize, stopResize, blurResize) => {
1190
+ const handleMouseLeave = (event) => {
1191
+ // Check if mouse is leaving the window/document
1192
+ if (!event.relatedTarget && (event.target === document || event.target === document.documentElement)) {
1193
+ blurResize();
1194
+ }
1195
+ };
988
1196
  resizeHandle.addEventListener("mousedown", startResize);
989
1197
  document.addEventListener("mousemove", handleResize);
990
1198
  document.addEventListener("mouseup", stopResize);
1199
+ document.addEventListener("mouseleave", handleMouseLeave);
991
1200
  window.addEventListener("blur", blurResize);
992
1201
  return () => {
993
1202
  resizeHandle.removeEventListener("mousedown", startResize);
994
1203
  document.removeEventListener("mousemove", handleResize);
995
1204
  document.removeEventListener("mouseup", stopResize);
1205
+ document.removeEventListener("mouseleave", handleMouseLeave);
996
1206
  window.removeEventListener("blur", blurResize);
997
1207
  };
998
1208
  };
@@ -1052,7 +1262,7 @@ const updateWidth = (container, width) => {
1052
1262
  updateActivePreset(container, width);
1053
1263
  };
1054
1264
 
1055
- const createViewport = (container) => {
1265
+ const createViewport = (container, initialWidth) => {
1056
1266
  const canvas = getCanvasContainer();
1057
1267
  // Remove any existing resize handle to prevent duplicates
1058
1268
  const existingHandle = container.querySelector(".resize-handle");
@@ -1060,7 +1270,8 @@ const createViewport = (container) => {
1060
1270
  existingHandle.remove();
1061
1271
  }
1062
1272
  const resizeHandle = createResizeHandle(container);
1063
- container.style.setProperty("--container-width", `${DEFAULT_WIDTH}px`);
1273
+ const width = initialWidth ?? DEFAULT_WIDTH;
1274
+ container.style.setProperty("--container-width", `${width}px`);
1064
1275
  createResizePresets(resizeHandle, container, updateWidth);
1065
1276
  let isDragging = false;
1066
1277
  let startX = 0;
@@ -1081,7 +1292,6 @@ const createViewport = (container) => {
1081
1292
  const width = calcWidth(event, startX, startWidth);
1082
1293
  updateWidth(container, width);
1083
1294
  };
1084
- const throttledHandleResize = withRAFThrottle(handleResize);
1085
1295
  const stopResize = (event) => {
1086
1296
  event.preventDefault();
1087
1297
  event.stopPropagation();
@@ -1091,18 +1301,33 @@ const createViewport = (container) => {
1091
1301
  isDragging = false;
1092
1302
  };
1093
1303
  const blurResize = () => {
1304
+ if (canvas) {
1305
+ canvas.style.cursor = "default";
1306
+ }
1094
1307
  isDragging = false;
1095
1308
  };
1096
- const removeListeners = setupEventListener(resizeHandle, startResize, throttledHandleResize, stopResize, blurResize);
1309
+ const removeListeners = setupEventListener(resizeHandle, startResize, handleResize, stopResize, blurResize);
1310
+ // Refresh viewport labels when viewport is created
1311
+ refreshViewportLabels();
1097
1312
  const cleanup = () => {
1098
1313
  isDragging = false;
1099
- throttledHandleResize?.cleanup();
1100
1314
  removeListeners();
1101
1315
  resizeHandle.remove();
1316
+ // Refresh labels after cleanup to remove this viewport's label if needed
1317
+ refreshViewportLabels();
1102
1318
  };
1103
1319
  return {
1104
1320
  setWidth: (width) => {
1105
1321
  updateWidth(container, width);
1322
+ refreshViewportLabels();
1323
+ // Refresh highlight frame when viewport width changes to update node positions
1324
+ // biome-ignore lint/suspicious/noExplicitAny: global window extension
1325
+ const nodeTools = window.nodeTools;
1326
+ const selectedNode = nodeTools?.getSelectedNode?.();
1327
+ const nodeProvider = document.querySelector('[data-role="node-provider"]');
1328
+ if (selectedNode && nodeProvider) {
1329
+ refreshHighlightFrame(selectedNode, nodeProvider);
1330
+ }
1106
1331
  },
1107
1332
  cleanup,
1108
1333
  };