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