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