@node-edit-utils/core 2.1.8 → 2.2.0
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.
- package/dist/lib/node-tools/highlight/clearHighlightFrame.d.ts +1 -1
- package/dist/lib/node-tools/highlight/createCornerHandles.d.ts +1 -0
- package/dist/lib/node-tools/highlight/createHighlightFrame.d.ts +1 -1
- package/dist/lib/node-tools/highlight/createToolsContainer.d.ts +1 -1
- package/dist/lib/node-tools/highlight/helpers/getHighlightFrameElement.d.ts +1 -1
- package/dist/lib/node-tools/highlight/helpers/getScreenBounds.d.ts +6 -0
- package/dist/lib/node-tools/highlight/highlightNode.d.ts +1 -1
- package/dist/lib/node-tools/highlight/updateHighlightFrameVisibility.d.ts +1 -1
- package/dist/lib/node-tools/select/helpers/isComponentInstance.d.ts +1 -0
- package/dist/lib/node-tools/select/helpers/isInsideComponent.d.ts +1 -0
- package/dist/node-edit-utils.cjs.js +300 -159
- package/dist/node-edit-utils.esm.js +300 -159
- package/dist/node-edit-utils.umd.js +300 -159
- package/dist/node-edit-utils.umd.min.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/lib/canvas/createCanvasObserver.ts +8 -6
- package/src/lib/node-tools/createNodeTools.ts +27 -24
- package/src/lib/node-tools/events/click/handleNodeClick.ts +1 -1
- package/src/lib/node-tools/highlight/clearHighlightFrame.ts +7 -8
- package/src/lib/node-tools/highlight/createCornerHandles.ts +31 -0
- package/src/lib/node-tools/highlight/createHighlightFrame.ts +45 -24
- package/src/lib/node-tools/highlight/createToolsContainer.ts +4 -1
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +2 -4
- package/src/lib/node-tools/highlight/helpers/getScreenBounds.ts +16 -0
- package/src/lib/node-tools/highlight/highlightNode.ts +28 -5
- package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +114 -11
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +5 -5
- package/src/lib/node-tools/select/helpers/isComponentInstance.ts +3 -0
- package/src/lib/node-tools/select/helpers/isInsideComponent.ts +18 -0
- package/src/lib/node-tools/select/selectNode.ts +5 -1
- package/src/lib/post-message/processPostMessage.ts +0 -6
- package/src/lib/styles/styles.css +57 -21
|
@@ -1,72 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Markup Canvas
|
|
3
3
|
* High-performance markup canvas with zoom and pan capabilities
|
|
4
|
-
* @version 2.
|
|
4
|
+
* @version 2.2.0
|
|
5
5
|
*/
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
// biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
|
|
9
|
-
function withRAFThrottle(func) {
|
|
10
|
-
let rafId = null;
|
|
11
|
-
let lastArgs = null;
|
|
12
|
-
const throttled = (...args) => {
|
|
13
|
-
lastArgs = args;
|
|
14
|
-
if (rafId === null) {
|
|
15
|
-
rafId = requestAnimationFrame(() => {
|
|
16
|
-
if (lastArgs) {
|
|
17
|
-
func(...lastArgs);
|
|
18
|
-
}
|
|
19
|
-
rafId = null;
|
|
20
|
-
lastArgs = null;
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
throttled.cleanup = () => {
|
|
25
|
-
if (rafId !== null) {
|
|
26
|
-
cancelAnimationFrame(rafId);
|
|
27
|
-
rafId = null;
|
|
28
|
-
lastArgs = null;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
return throttled;
|
|
32
|
-
}
|
|
33
|
-
// biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
|
|
34
|
-
function withDoubleRAF(func) {
|
|
35
|
-
let rafId1 = null;
|
|
36
|
-
let rafId2 = null;
|
|
37
|
-
let lastArgs = null;
|
|
38
|
-
const throttled = (...args) => {
|
|
39
|
-
lastArgs = args;
|
|
40
|
-
if (rafId1 === null) {
|
|
41
|
-
rafId1 = requestAnimationFrame(() => {
|
|
42
|
-
// First RAF: let browser complete layout
|
|
43
|
-
rafId2 = requestAnimationFrame(() => {
|
|
44
|
-
// Second RAF: read bounds after layout is complete
|
|
45
|
-
if (lastArgs) {
|
|
46
|
-
const currentArgs = lastArgs;
|
|
47
|
-
rafId1 = null;
|
|
48
|
-
rafId2 = null;
|
|
49
|
-
lastArgs = null;
|
|
50
|
-
func(...currentArgs);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
throttled.cleanup = () => {
|
|
57
|
-
if (rafId1 !== null) {
|
|
58
|
-
cancelAnimationFrame(rafId1);
|
|
59
|
-
rafId1 = null;
|
|
60
|
-
}
|
|
61
|
-
if (rafId2 !== null) {
|
|
62
|
-
cancelAnimationFrame(rafId2);
|
|
63
|
-
rafId2 = null;
|
|
64
|
-
}
|
|
65
|
-
lastArgs = null;
|
|
66
|
-
};
|
|
67
|
-
return throttled;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
8
|
const getCanvasWindowValue = (path) => {
|
|
71
9
|
// biome-ignore lint/suspicious/noExplicitAny: global window extension
|
|
72
10
|
const canvas = window.canvas;
|
|
@@ -88,11 +26,14 @@ function createCanvasObserver() {
|
|
|
88
26
|
disconnect: () => { },
|
|
89
27
|
};
|
|
90
28
|
}
|
|
91
|
-
const throttledUpdate = withRAFThrottle(() => {
|
|
92
|
-
applyCanvasState();
|
|
93
|
-
});
|
|
94
29
|
const observer = new MutationObserver(() => {
|
|
95
|
-
|
|
30
|
+
applyCanvasState();
|
|
31
|
+
// Refresh highlight frame (throttled via withRAFThrottle)
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: global window extension
|
|
33
|
+
const nodeTools = window.nodeTools;
|
|
34
|
+
if (nodeTools?.refreshHighlightFrame) {
|
|
35
|
+
nodeTools.refreshHighlightFrame();
|
|
36
|
+
}
|
|
96
37
|
});
|
|
97
38
|
observer.observe(transformLayer, {
|
|
98
39
|
attributes: true,
|
|
@@ -101,7 +42,6 @@ function createCanvasObserver() {
|
|
|
101
42
|
childList: true,
|
|
102
43
|
});
|
|
103
44
|
function disconnect() {
|
|
104
|
-
throttledUpdate.cleanup();
|
|
105
45
|
observer.disconnect();
|
|
106
46
|
}
|
|
107
47
|
return {
|
|
@@ -117,6 +57,32 @@ const connectResizeObserver = (element, handler) => {
|
|
|
117
57
|
return resizeObserver;
|
|
118
58
|
};
|
|
119
59
|
|
|
60
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
|
|
61
|
+
function withRAFThrottle(func) {
|
|
62
|
+
let rafId = null;
|
|
63
|
+
let lastArgs = null;
|
|
64
|
+
const throttled = (...args) => {
|
|
65
|
+
lastArgs = args;
|
|
66
|
+
if (rafId === null) {
|
|
67
|
+
rafId = requestAnimationFrame(() => {
|
|
68
|
+
if (lastArgs) {
|
|
69
|
+
func(...lastArgs);
|
|
70
|
+
}
|
|
71
|
+
rafId = null;
|
|
72
|
+
lastArgs = null;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
throttled.cleanup = () => {
|
|
77
|
+
if (rafId !== null) {
|
|
78
|
+
cancelAnimationFrame(rafId);
|
|
79
|
+
rafId = null;
|
|
80
|
+
lastArgs = null;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return throttled;
|
|
84
|
+
}
|
|
85
|
+
|
|
120
86
|
function sendPostMessage(action, data) {
|
|
121
87
|
window.parent.postMessage({
|
|
122
88
|
source: "node-edit-utils",
|
|
@@ -138,11 +104,6 @@ const isLocked = (node) => {
|
|
|
138
104
|
};
|
|
139
105
|
|
|
140
106
|
const processPostMessage = (event, onNodeSelected) => {
|
|
141
|
-
// if (event.data.source === "markup-canvas" && event.data.canvasName === "canvas") {
|
|
142
|
-
// if (event.data.action === "zoom") {
|
|
143
|
-
// // Zoom handling can be implemented here if needed
|
|
144
|
-
// }
|
|
145
|
-
// }
|
|
146
107
|
if (event.data.source === "application") {
|
|
147
108
|
if (event.data.action === "selectedNodeChanged") {
|
|
148
109
|
const nodeId = event.data.data;
|
|
@@ -158,17 +119,14 @@ const processPostMessage = (event, onNodeSelected) => {
|
|
|
158
119
|
}
|
|
159
120
|
};
|
|
160
121
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const clearHighlightFrame = (nodeProvider) => {
|
|
166
|
-
if (!nodeProvider) {
|
|
167
|
-
return;
|
|
122
|
+
const clearHighlightFrame = () => {
|
|
123
|
+
const frame = document.body.querySelector(".highlight-frame-overlay");
|
|
124
|
+
if (frame) {
|
|
125
|
+
frame.remove();
|
|
168
126
|
}
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
|
|
127
|
+
const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
|
|
128
|
+
if (toolsWrapper) {
|
|
129
|
+
toolsWrapper.remove();
|
|
172
130
|
}
|
|
173
131
|
};
|
|
174
132
|
|
|
@@ -188,6 +146,21 @@ const getElementsFromPoint = (clickX, clickY) => {
|
|
|
188
146
|
}, { elements: [], found: false }).elements;
|
|
189
147
|
};
|
|
190
148
|
|
|
149
|
+
const isInsideComponent = (element) => {
|
|
150
|
+
let current = element.parentElement;
|
|
151
|
+
while (current) {
|
|
152
|
+
if (current.getAttribute("data-instance") === "true") {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
// Stop at node-provider to avoid checking beyond the editable area
|
|
156
|
+
if (current.getAttribute("data-role") === "node-provider") {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
current = current.parentElement;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
|
|
191
164
|
const targetSameCandidates = (cache, current) => cache.length === current.length && cache.every((el, i) => el === current[i]);
|
|
192
165
|
|
|
193
166
|
let candidateCache = [];
|
|
@@ -197,7 +170,9 @@ const selectNode = (event, editableNode) => {
|
|
|
197
170
|
const clickX = event.clientX;
|
|
198
171
|
const clickY = event.clientY;
|
|
199
172
|
const clickThrough = event.metaKey || event.ctrlKey;
|
|
200
|
-
const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
|
|
173
|
+
const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
|
|
174
|
+
!element.classList.contains("select-none") &&
|
|
175
|
+
!isInsideComponent(element));
|
|
201
176
|
if (editableNode && candidates.includes(editableNode)) {
|
|
202
177
|
return editableNode;
|
|
203
178
|
}
|
|
@@ -222,7 +197,7 @@ const handleNodeClick = (event, nodeProvider, editableNode, onNodeSelected) => {
|
|
|
222
197
|
event.preventDefault();
|
|
223
198
|
event.stopPropagation();
|
|
224
199
|
if (nodeProvider && !nodeProvider.contains(event.target)) {
|
|
225
|
-
clearHighlightFrame(
|
|
200
|
+
clearHighlightFrame();
|
|
226
201
|
onNodeSelected(null);
|
|
227
202
|
return;
|
|
228
203
|
}
|
|
@@ -254,48 +229,90 @@ const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, get
|
|
|
254
229
|
};
|
|
255
230
|
};
|
|
256
231
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
232
|
+
const isComponentInstance = (element) => {
|
|
233
|
+
return element.getAttribute("data-instance") === "true";
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const HANDLE_SIZE = 6;
|
|
237
|
+
const getComponentColor$2 = () => {
|
|
238
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
239
|
+
};
|
|
240
|
+
const createCornerHandle = (group, x, y, className, isInstance = false) => {
|
|
241
|
+
const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
242
|
+
// Position relative to group (offset by half handle size to center on corner)
|
|
243
|
+
handle.setAttribute("x", (x - HANDLE_SIZE / 2).toString());
|
|
244
|
+
handle.setAttribute("y", (y - HANDLE_SIZE / 2).toString());
|
|
245
|
+
handle.setAttribute("width", HANDLE_SIZE.toString());
|
|
246
|
+
handle.setAttribute("height", HANDLE_SIZE.toString());
|
|
247
|
+
handle.setAttribute("vector-effect", "non-scaling-stroke");
|
|
248
|
+
handle.classList.add("highlight-frame-handle", className);
|
|
249
|
+
if (isInstance) {
|
|
250
|
+
handle.setAttribute("stroke", getComponentColor$2());
|
|
251
|
+
}
|
|
252
|
+
group.appendChild(handle);
|
|
253
|
+
return handle;
|
|
254
|
+
};
|
|
255
|
+
const createCornerHandles = (group, width, height, isInstance = false) => {
|
|
256
|
+
// Create corner handles using relative coordinates (group handles positioning)
|
|
257
|
+
createCornerHandle(group, 0, 0, "handle-top-left", isInstance);
|
|
258
|
+
createCornerHandle(group, width, 0, "handle-top-right", isInstance);
|
|
259
|
+
createCornerHandle(group, width, height, "handle-bottom-right", isInstance);
|
|
260
|
+
createCornerHandle(group, 0, height, "handle-bottom-left", isInstance);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
function getScreenBounds(element) {
|
|
264
|
+
const rect = element.getBoundingClientRect();
|
|
267
265
|
return {
|
|
268
|
-
top,
|
|
269
|
-
left,
|
|
270
|
-
width,
|
|
271
|
-
height,
|
|
266
|
+
top: rect.top,
|
|
267
|
+
left: rect.left,
|
|
268
|
+
width: rect.width,
|
|
269
|
+
height: rect.height,
|
|
272
270
|
};
|
|
273
271
|
}
|
|
274
272
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
frame.classList.add("highlight-frame");
|
|
282
|
-
frame.style.setProperty("--frame-top", `${top}px`);
|
|
283
|
-
frame.style.setProperty("--frame-left", `${left}px`);
|
|
284
|
-
frame.style.setProperty("--frame-width", `${width}px`);
|
|
285
|
-
frame.style.setProperty("--frame-height", `${height}px`);
|
|
286
|
-
// Create SVG overlay for outline
|
|
273
|
+
const getComponentColor$1 = () => {
|
|
274
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
275
|
+
};
|
|
276
|
+
const createHighlightFrame = (node, isInstance = false) => {
|
|
277
|
+
const { top, left, width, height } = getScreenBounds(node);
|
|
278
|
+
// Create fixed SVG overlay
|
|
287
279
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
288
|
-
svg.classList.add("highlight-frame-
|
|
280
|
+
svg.classList.add("highlight-frame-overlay");
|
|
281
|
+
if (isInstance) {
|
|
282
|
+
svg.classList.add("is-instance");
|
|
283
|
+
}
|
|
284
|
+
svg.setAttribute("data-node-id", node.getAttribute("data-node-id") || "");
|
|
285
|
+
// Set fixed positioning
|
|
286
|
+
svg.style.position = "fixed";
|
|
287
|
+
svg.style.top = "0";
|
|
288
|
+
svg.style.left = "0";
|
|
289
|
+
svg.style.width = "100vw";
|
|
290
|
+
svg.style.height = "100vh";
|
|
291
|
+
svg.style.pointerEvents = "none";
|
|
292
|
+
svg.style.zIndex = "5000";
|
|
293
|
+
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
294
|
+
const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
|
295
|
+
svg.setAttribute("width", viewportWidth.toString());
|
|
296
|
+
svg.setAttribute("height", viewportHeight.toString());
|
|
297
|
+
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
298
|
+
group.classList.add("highlight-frame-group");
|
|
299
|
+
group.setAttribute("transform", `translate(${left}, ${top})`);
|
|
289
300
|
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
290
301
|
rect.setAttribute("x", "0");
|
|
291
302
|
rect.setAttribute("y", "0");
|
|
292
|
-
rect.setAttribute("width",
|
|
293
|
-
rect.setAttribute("height",
|
|
303
|
+
rect.setAttribute("width", width.toString());
|
|
304
|
+
rect.setAttribute("height", height.toString());
|
|
305
|
+
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
294
306
|
rect.classList.add("highlight-frame-rect");
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
307
|
+
// Apply instance color if it's an instance
|
|
308
|
+
if (isInstance) {
|
|
309
|
+
rect.setAttribute("stroke", getComponentColor$1());
|
|
310
|
+
}
|
|
311
|
+
group.appendChild(rect);
|
|
312
|
+
createCornerHandles(group, width, height, isInstance);
|
|
313
|
+
svg.appendChild(group);
|
|
314
|
+
document.body.appendChild(svg);
|
|
315
|
+
return svg;
|
|
299
316
|
};
|
|
300
317
|
|
|
301
318
|
const createTagLabel = (node, nodeTools) => {
|
|
@@ -305,56 +322,177 @@ const createTagLabel = (node, nodeTools) => {
|
|
|
305
322
|
nodeTools.appendChild(tagLabel);
|
|
306
323
|
};
|
|
307
324
|
|
|
308
|
-
const createToolsContainer = (node, highlightFrame) => {
|
|
325
|
+
const createToolsContainer = (node, highlightFrame, isInstance = false) => {
|
|
309
326
|
const nodeTools = document.createElement("div");
|
|
310
327
|
nodeTools.className = "node-tools";
|
|
328
|
+
if (isInstance) {
|
|
329
|
+
nodeTools.classList.add("is-instance");
|
|
330
|
+
}
|
|
311
331
|
highlightFrame.appendChild(nodeTools);
|
|
312
332
|
createTagLabel(node, nodeTools);
|
|
313
333
|
};
|
|
314
334
|
|
|
315
|
-
|
|
335
|
+
function getHighlightFrameElement() {
|
|
336
|
+
return document.body.querySelector(".highlight-frame-overlay");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const highlightNode = (node) => {
|
|
316
340
|
if (!node)
|
|
317
341
|
return;
|
|
318
|
-
const existingHighlightFrame = getHighlightFrameElement(
|
|
342
|
+
const existingHighlightFrame = getHighlightFrameElement();
|
|
343
|
+
const existingToolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
|
|
319
344
|
if (existingHighlightFrame) {
|
|
320
345
|
existingHighlightFrame.remove();
|
|
321
346
|
}
|
|
322
|
-
|
|
347
|
+
if (existingToolsWrapper) {
|
|
348
|
+
existingToolsWrapper.remove();
|
|
349
|
+
}
|
|
350
|
+
const isInstance = isComponentInstance(node);
|
|
351
|
+
const highlightFrame = createHighlightFrame(node, isInstance);
|
|
323
352
|
if (node.contentEditable === "true") {
|
|
324
353
|
highlightFrame.classList.add("is-editable");
|
|
325
354
|
}
|
|
326
|
-
|
|
327
|
-
|
|
355
|
+
// Create tools wrapper with tag label - centered using translateX(-50%)
|
|
356
|
+
const { left, top, height } = getScreenBounds(node);
|
|
357
|
+
const bottomY = top + height;
|
|
358
|
+
const toolsWrapper = document.createElement("div");
|
|
359
|
+
toolsWrapper.classList.add("highlight-frame-tools-wrapper");
|
|
360
|
+
if (isInstance) {
|
|
361
|
+
toolsWrapper.classList.add("is-instance");
|
|
362
|
+
}
|
|
363
|
+
toolsWrapper.style.position = "fixed";
|
|
364
|
+
toolsWrapper.style.left = `${left}px`;
|
|
365
|
+
toolsWrapper.style.top = `${bottomY}px`;
|
|
366
|
+
toolsWrapper.style.transform = "translateX(-50%)";
|
|
367
|
+
toolsWrapper.style.transformOrigin = "center";
|
|
368
|
+
toolsWrapper.style.pointerEvents = "none";
|
|
369
|
+
toolsWrapper.style.zIndex = "5000"; // Match --z-index-highlight (below canvas rulers)
|
|
370
|
+
createToolsContainer(node, toolsWrapper, isInstance);
|
|
371
|
+
document.body.appendChild(toolsWrapper);
|
|
328
372
|
};
|
|
329
373
|
|
|
374
|
+
const getComponentColor = () => {
|
|
375
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
376
|
+
};
|
|
330
377
|
const refreshHighlightFrame = (node, nodeProvider) => {
|
|
331
|
-
|
|
332
|
-
const
|
|
378
|
+
// Batch all DOM reads first (single layout pass)
|
|
379
|
+
const frame = getHighlightFrameElement();
|
|
333
380
|
if (!frame)
|
|
334
381
|
return;
|
|
335
|
-
|
|
382
|
+
const isInstance = isComponentInstance(node);
|
|
383
|
+
// Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
|
|
384
|
+
// Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
|
|
385
|
+
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
386
|
+
const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
|
387
|
+
frame.setAttribute("width", viewportWidth.toString());
|
|
388
|
+
frame.setAttribute("height", viewportHeight.toString());
|
|
389
|
+
// Update instance class
|
|
390
|
+
if (isInstance) {
|
|
391
|
+
frame.classList.add("is-instance");
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
frame.classList.remove("is-instance");
|
|
395
|
+
}
|
|
396
|
+
const group = frame.querySelector(".highlight-frame-group");
|
|
397
|
+
if (!group)
|
|
398
|
+
return;
|
|
399
|
+
const rect = group.querySelector("rect");
|
|
400
|
+
if (!rect)
|
|
401
|
+
return;
|
|
402
|
+
// Update instance color
|
|
403
|
+
if (isInstance) {
|
|
404
|
+
rect.setAttribute("stroke", getComponentColor());
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
rect.removeAttribute("stroke"); // Use CSS default
|
|
408
|
+
}
|
|
409
|
+
const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
|
|
410
|
+
const nodeTools = toolsWrapper?.querySelector(".node-tools");
|
|
411
|
+
const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
|
|
412
|
+
const bounds = getScreenBounds(node);
|
|
413
|
+
// Calculate all values before any DOM writes
|
|
414
|
+
const { top, left, width, height } = bounds;
|
|
415
|
+
const bottomY = top + height;
|
|
416
|
+
// Update instance classes on tools wrapper and node tools
|
|
417
|
+
if (toolsWrapper) {
|
|
418
|
+
if (isInstance) {
|
|
419
|
+
toolsWrapper.classList.add("is-instance");
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
toolsWrapper.classList.remove("is-instance");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (nodeTools) {
|
|
426
|
+
if (isInstance) {
|
|
427
|
+
nodeTools.classList.add("is-instance");
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
nodeTools.classList.remove("is-instance");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// Batch all DOM writes (single paint pass)
|
|
434
|
+
// Update group transform to move entire group (rect + handles) at once
|
|
435
|
+
group.setAttribute("transform", `translate(${left}, ${top})`);
|
|
436
|
+
// Update rect dimensions (position is handled by group transform)
|
|
437
|
+
rect.setAttribute("width", width.toString());
|
|
438
|
+
rect.setAttribute("height", height.toString());
|
|
439
|
+
// Update corner handles positions (relative to group, so only width/height matter)
|
|
440
|
+
const topLeft = group.querySelector(".handle-top-left");
|
|
441
|
+
const topRight = group.querySelector(".handle-top-right");
|
|
442
|
+
const bottomRight = group.querySelector(".handle-bottom-right");
|
|
443
|
+
const bottomLeft = group.querySelector(".handle-bottom-left");
|
|
444
|
+
const HANDLE_SIZE = 6;
|
|
445
|
+
// Update handle colors and positions
|
|
446
|
+
const handles = [topLeft, topRight, bottomRight, bottomLeft];
|
|
447
|
+
handles.forEach((handle) => {
|
|
448
|
+
if (handle) {
|
|
449
|
+
if (isInstance) {
|
|
450
|
+
handle.setAttribute("stroke", getComponentColor());
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
handle.removeAttribute("stroke"); // Use CSS default
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
if (topLeft) {
|
|
458
|
+
topLeft.setAttribute("x", (-HANDLE_SIZE / 2).toString());
|
|
459
|
+
topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
460
|
+
}
|
|
461
|
+
if (topRight) {
|
|
462
|
+
topRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
|
|
463
|
+
topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
464
|
+
}
|
|
465
|
+
if (bottomRight) {
|
|
466
|
+
bottomRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
|
|
467
|
+
bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
468
|
+
}
|
|
469
|
+
if (bottomLeft) {
|
|
470
|
+
bottomLeft.setAttribute("x", (-HANDLE_SIZE / 2).toString());
|
|
471
|
+
bottomLeft.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
472
|
+
}
|
|
473
|
+
// Update tools wrapper position (use calculated bounds, not rect attributes)
|
|
474
|
+
if (toolsWrapper) {
|
|
475
|
+
toolsWrapper.style.left = `${left}px`;
|
|
476
|
+
toolsWrapper.style.top = `${bottomY}px`;
|
|
477
|
+
}
|
|
478
|
+
if (zoom <= 10) {
|
|
336
479
|
nodeProvider.style.setProperty("--tool-opacity", `1`);
|
|
337
480
|
}
|
|
338
481
|
else {
|
|
339
482
|
nodeProvider.style.setProperty("--tool-opacity", `0`);
|
|
340
483
|
}
|
|
341
|
-
const { top, left, width, height } = getElementBounds(node, nodeProvider);
|
|
342
|
-
frame.style.setProperty("--frame-top", `${top}px`);
|
|
343
|
-
frame.style.setProperty("--frame-left", `${left}px`);
|
|
344
|
-
frame.style.setProperty("--frame-width", `${width}px`);
|
|
345
|
-
frame.style.setProperty("--frame-height", `${height}px`);
|
|
346
484
|
};
|
|
347
485
|
|
|
348
|
-
const updateHighlightFrameVisibility = (node
|
|
349
|
-
const frame = getHighlightFrameElement(
|
|
486
|
+
const updateHighlightFrameVisibility = (node) => {
|
|
487
|
+
const frame = getHighlightFrameElement();
|
|
350
488
|
if (!frame)
|
|
351
489
|
return;
|
|
352
490
|
const hasHiddenClass = node.classList.contains("hidden") || node.classList.contains("select-none");
|
|
353
491
|
const displayValue = hasHiddenClass ? "none" : "";
|
|
354
492
|
frame.style.display = displayValue;
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
|
|
493
|
+
const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
|
|
494
|
+
if (toolsWrapper) {
|
|
495
|
+
toolsWrapper.style.display = displayValue;
|
|
358
496
|
}
|
|
359
497
|
};
|
|
360
498
|
|
|
@@ -494,21 +632,23 @@ const nodeText = () => {
|
|
|
494
632
|
const createNodeTools = (element) => {
|
|
495
633
|
const nodeProvider = element;
|
|
496
634
|
let resizeObserver = null;
|
|
497
|
-
let nodeResizeObserver = null;
|
|
498
635
|
let mutationObserver = null;
|
|
499
636
|
let selectedNode = null;
|
|
500
637
|
const text = nodeText();
|
|
501
|
-
|
|
638
|
+
// Combined throttled function for refresh + visibility update
|
|
639
|
+
const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
|
|
640
|
+
refreshHighlightFrame(node, nodeProvider);
|
|
641
|
+
updateHighlightFrameVisibility(node);
|
|
642
|
+
});
|
|
502
643
|
const handleEscape = () => {
|
|
503
644
|
if (text.isEditing()) {
|
|
504
645
|
text.blurEditMode();
|
|
505
646
|
}
|
|
506
647
|
if (selectedNode) {
|
|
507
648
|
if (nodeProvider) {
|
|
508
|
-
clearHighlightFrame(
|
|
649
|
+
clearHighlightFrame();
|
|
509
650
|
selectedNode = null;
|
|
510
651
|
resizeObserver?.disconnect();
|
|
511
|
-
nodeResizeObserver?.disconnect();
|
|
512
652
|
mutationObserver?.disconnect();
|
|
513
653
|
}
|
|
514
654
|
}
|
|
@@ -524,33 +664,31 @@ const createNodeTools = (element) => {
|
|
|
524
664
|
}
|
|
525
665
|
}
|
|
526
666
|
resizeObserver?.disconnect();
|
|
527
|
-
nodeResizeObserver?.disconnect();
|
|
528
667
|
mutationObserver?.disconnect();
|
|
529
668
|
if (node && nodeProvider) {
|
|
530
669
|
text.enableEditMode(node, nodeProvider);
|
|
531
|
-
resizeObserver = connectResizeObserver(nodeProvider, () => {
|
|
532
|
-
throttledFrameRefresh(node, nodeProvider);
|
|
533
|
-
});
|
|
534
670
|
mutationObserver = new MutationObserver(() => {
|
|
535
|
-
|
|
536
|
-
|
|
671
|
+
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
672
|
+
console.log("mutationObserver", node);
|
|
673
|
+
refreshHighlightFrame(node, nodeProvider);
|
|
674
|
+
updateHighlightFrameVisibility(node);
|
|
537
675
|
});
|
|
538
676
|
mutationObserver.observe(node, {
|
|
539
677
|
attributes: true,
|
|
540
|
-
subtree: true,
|
|
541
|
-
childList: true,
|
|
542
678
|
characterData: true,
|
|
543
679
|
});
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
680
|
+
resizeObserver = connectResizeObserver(node, () => {
|
|
681
|
+
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
682
|
+
console.log("resizeObserver", node);
|
|
683
|
+
refreshHighlightFrame(node, nodeProvider);
|
|
684
|
+
updateHighlightFrameVisibility(node);
|
|
547
685
|
});
|
|
548
686
|
}
|
|
549
687
|
selectedNode = node;
|
|
550
688
|
sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
|
|
551
|
-
highlightNode(node
|
|
689
|
+
highlightNode(node) ?? null;
|
|
552
690
|
if (node && nodeProvider) {
|
|
553
|
-
updateHighlightFrameVisibility(node
|
|
691
|
+
updateHighlightFrameVisibility(node);
|
|
554
692
|
}
|
|
555
693
|
};
|
|
556
694
|
// Setup event listener
|
|
@@ -558,22 +696,25 @@ const createNodeTools = (element) => {
|
|
|
558
696
|
const cleanup = () => {
|
|
559
697
|
removeListeners();
|
|
560
698
|
resizeObserver?.disconnect();
|
|
561
|
-
nodeResizeObserver?.disconnect();
|
|
562
699
|
mutationObserver?.disconnect();
|
|
563
700
|
text.blurEditMode();
|
|
564
|
-
|
|
701
|
+
throttledRefreshAndVisibility.cleanup();
|
|
565
702
|
};
|
|
566
703
|
const nodeTools = {
|
|
567
704
|
selectNode,
|
|
568
705
|
getSelectedNode: () => selectedNode,
|
|
569
706
|
refreshHighlightFrame: () => {
|
|
570
|
-
|
|
707
|
+
if (selectedNode && nodeProvider) {
|
|
708
|
+
// Call directly (not throttled) since this is typically called from already-throttled contexts
|
|
709
|
+
// to avoid double RAF
|
|
710
|
+
refreshHighlightFrame(selectedNode, nodeProvider);
|
|
711
|
+
updateHighlightFrameVisibility(selectedNode);
|
|
712
|
+
}
|
|
571
713
|
},
|
|
572
714
|
clearSelectedNode: () => {
|
|
573
|
-
clearHighlightFrame(
|
|
715
|
+
clearHighlightFrame();
|
|
574
716
|
selectedNode = null;
|
|
575
717
|
resizeObserver?.disconnect();
|
|
576
|
-
nodeResizeObserver?.disconnect();
|
|
577
718
|
mutationObserver?.disconnect();
|
|
578
719
|
},
|
|
579
720
|
getEditableNode: () => text.getEditableNode(),
|