@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,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",
|
|
@@ -136,11 +102,6 @@ const isLocked = (node) => {
|
|
|
136
102
|
};
|
|
137
103
|
|
|
138
104
|
const processPostMessage = (event, onNodeSelected) => {
|
|
139
|
-
// if (event.data.source === "markup-canvas" && event.data.canvasName === "canvas") {
|
|
140
|
-
// if (event.data.action === "zoom") {
|
|
141
|
-
// // Zoom handling can be implemented here if needed
|
|
142
|
-
// }
|
|
143
|
-
// }
|
|
144
105
|
if (event.data.source === "application") {
|
|
145
106
|
if (event.data.action === "selectedNodeChanged") {
|
|
146
107
|
const nodeId = event.data.data;
|
|
@@ -156,17 +117,14 @@ const processPostMessage = (event, onNodeSelected) => {
|
|
|
156
117
|
}
|
|
157
118
|
};
|
|
158
119
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const clearHighlightFrame = (nodeProvider) => {
|
|
164
|
-
if (!nodeProvider) {
|
|
165
|
-
return;
|
|
120
|
+
const clearHighlightFrame = () => {
|
|
121
|
+
const frame = document.body.querySelector(".highlight-frame-overlay");
|
|
122
|
+
if (frame) {
|
|
123
|
+
frame.remove();
|
|
166
124
|
}
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
|
|
125
|
+
const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
|
|
126
|
+
if (toolsWrapper) {
|
|
127
|
+
toolsWrapper.remove();
|
|
170
128
|
}
|
|
171
129
|
};
|
|
172
130
|
|
|
@@ -186,6 +144,21 @@ const getElementsFromPoint = (clickX, clickY) => {
|
|
|
186
144
|
}, { elements: [], found: false }).elements;
|
|
187
145
|
};
|
|
188
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
|
+
|
|
189
162
|
const targetSameCandidates = (cache, current) => cache.length === current.length && cache.every((el, i) => el === current[i]);
|
|
190
163
|
|
|
191
164
|
let candidateCache = [];
|
|
@@ -195,7 +168,9 @@ const selectNode = (event, editableNode) => {
|
|
|
195
168
|
const clickX = event.clientX;
|
|
196
169
|
const clickY = event.clientY;
|
|
197
170
|
const clickThrough = event.metaKey || event.ctrlKey;
|
|
198
|
-
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));
|
|
199
174
|
if (editableNode && candidates.includes(editableNode)) {
|
|
200
175
|
return editableNode;
|
|
201
176
|
}
|
|
@@ -220,7 +195,7 @@ const handleNodeClick = (event, nodeProvider, editableNode, onNodeSelected) => {
|
|
|
220
195
|
event.preventDefault();
|
|
221
196
|
event.stopPropagation();
|
|
222
197
|
if (nodeProvider && !nodeProvider.contains(event.target)) {
|
|
223
|
-
clearHighlightFrame(
|
|
198
|
+
clearHighlightFrame();
|
|
224
199
|
onNodeSelected(null);
|
|
225
200
|
return;
|
|
226
201
|
}
|
|
@@ -252,48 +227,90 @@ const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, get
|
|
|
252
227
|
};
|
|
253
228
|
};
|
|
254
229
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
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();
|
|
265
263
|
return {
|
|
266
|
-
top,
|
|
267
|
-
left,
|
|
268
|
-
width,
|
|
269
|
-
height,
|
|
264
|
+
top: rect.top,
|
|
265
|
+
left: rect.left,
|
|
266
|
+
width: rect.width,
|
|
267
|
+
height: rect.height,
|
|
270
268
|
};
|
|
271
269
|
}
|
|
272
270
|
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
frame.classList.add("highlight-frame");
|
|
280
|
-
frame.style.setProperty("--frame-top", `${top}px`);
|
|
281
|
-
frame.style.setProperty("--frame-left", `${left}px`);
|
|
282
|
-
frame.style.setProperty("--frame-width", `${width}px`);
|
|
283
|
-
frame.style.setProperty("--frame-height", `${height}px`);
|
|
284
|
-
// 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
|
|
285
277
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
286
|
-
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})`);
|
|
287
298
|
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
288
299
|
rect.setAttribute("x", "0");
|
|
289
300
|
rect.setAttribute("y", "0");
|
|
290
|
-
rect.setAttribute("width",
|
|
291
|
-
rect.setAttribute("height",
|
|
301
|
+
rect.setAttribute("width", width.toString());
|
|
302
|
+
rect.setAttribute("height", height.toString());
|
|
303
|
+
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
292
304
|
rect.classList.add("highlight-frame-rect");
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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;
|
|
297
314
|
};
|
|
298
315
|
|
|
299
316
|
const createTagLabel = (node, nodeTools) => {
|
|
@@ -303,56 +320,177 @@ const createTagLabel = (node, nodeTools) => {
|
|
|
303
320
|
nodeTools.appendChild(tagLabel);
|
|
304
321
|
};
|
|
305
322
|
|
|
306
|
-
const createToolsContainer = (node, highlightFrame) => {
|
|
323
|
+
const createToolsContainer = (node, highlightFrame, isInstance = false) => {
|
|
307
324
|
const nodeTools = document.createElement("div");
|
|
308
325
|
nodeTools.className = "node-tools";
|
|
326
|
+
if (isInstance) {
|
|
327
|
+
nodeTools.classList.add("is-instance");
|
|
328
|
+
}
|
|
309
329
|
highlightFrame.appendChild(nodeTools);
|
|
310
330
|
createTagLabel(node, nodeTools);
|
|
311
331
|
};
|
|
312
332
|
|
|
313
|
-
|
|
333
|
+
function getHighlightFrameElement() {
|
|
334
|
+
return document.body.querySelector(".highlight-frame-overlay");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const highlightNode = (node) => {
|
|
314
338
|
if (!node)
|
|
315
339
|
return;
|
|
316
|
-
const existingHighlightFrame = getHighlightFrameElement(
|
|
340
|
+
const existingHighlightFrame = getHighlightFrameElement();
|
|
341
|
+
const existingToolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
|
|
317
342
|
if (existingHighlightFrame) {
|
|
318
343
|
existingHighlightFrame.remove();
|
|
319
344
|
}
|
|
320
|
-
|
|
345
|
+
if (existingToolsWrapper) {
|
|
346
|
+
existingToolsWrapper.remove();
|
|
347
|
+
}
|
|
348
|
+
const isInstance = isComponentInstance(node);
|
|
349
|
+
const highlightFrame = createHighlightFrame(node, isInstance);
|
|
321
350
|
if (node.contentEditable === "true") {
|
|
322
351
|
highlightFrame.classList.add("is-editable");
|
|
323
352
|
}
|
|
324
|
-
|
|
325
|
-
|
|
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);
|
|
326
370
|
};
|
|
327
371
|
|
|
372
|
+
const getComponentColor = () => {
|
|
373
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
374
|
+
};
|
|
328
375
|
const refreshHighlightFrame = (node, nodeProvider) => {
|
|
329
|
-
|
|
330
|
-
const
|
|
376
|
+
// Batch all DOM reads first (single layout pass)
|
|
377
|
+
const frame = getHighlightFrameElement();
|
|
331
378
|
if (!frame)
|
|
332
379
|
return;
|
|
333
|
-
|
|
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) {
|
|
334
477
|
nodeProvider.style.setProperty("--tool-opacity", `1`);
|
|
335
478
|
}
|
|
336
479
|
else {
|
|
337
480
|
nodeProvider.style.setProperty("--tool-opacity", `0`);
|
|
338
481
|
}
|
|
339
|
-
const { top, left, width, height } = getElementBounds(node, nodeProvider);
|
|
340
|
-
frame.style.setProperty("--frame-top", `${top}px`);
|
|
341
|
-
frame.style.setProperty("--frame-left", `${left}px`);
|
|
342
|
-
frame.style.setProperty("--frame-width", `${width}px`);
|
|
343
|
-
frame.style.setProperty("--frame-height", `${height}px`);
|
|
344
482
|
};
|
|
345
483
|
|
|
346
|
-
const updateHighlightFrameVisibility = (node
|
|
347
|
-
const frame = getHighlightFrameElement(
|
|
484
|
+
const updateHighlightFrameVisibility = (node) => {
|
|
485
|
+
const frame = getHighlightFrameElement();
|
|
348
486
|
if (!frame)
|
|
349
487
|
return;
|
|
350
488
|
const hasHiddenClass = node.classList.contains("hidden") || node.classList.contains("select-none");
|
|
351
489
|
const displayValue = hasHiddenClass ? "none" : "";
|
|
352
490
|
frame.style.display = displayValue;
|
|
353
|
-
const
|
|
354
|
-
if (
|
|
355
|
-
|
|
491
|
+
const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
|
|
492
|
+
if (toolsWrapper) {
|
|
493
|
+
toolsWrapper.style.display = displayValue;
|
|
356
494
|
}
|
|
357
495
|
};
|
|
358
496
|
|
|
@@ -492,21 +630,23 @@ const nodeText = () => {
|
|
|
492
630
|
const createNodeTools = (element) => {
|
|
493
631
|
const nodeProvider = element;
|
|
494
632
|
let resizeObserver = null;
|
|
495
|
-
let nodeResizeObserver = null;
|
|
496
633
|
let mutationObserver = null;
|
|
497
634
|
let selectedNode = null;
|
|
498
635
|
const text = nodeText();
|
|
499
|
-
|
|
636
|
+
// Combined throttled function for refresh + visibility update
|
|
637
|
+
const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
|
|
638
|
+
refreshHighlightFrame(node, nodeProvider);
|
|
639
|
+
updateHighlightFrameVisibility(node);
|
|
640
|
+
});
|
|
500
641
|
const handleEscape = () => {
|
|
501
642
|
if (text.isEditing()) {
|
|
502
643
|
text.blurEditMode();
|
|
503
644
|
}
|
|
504
645
|
if (selectedNode) {
|
|
505
646
|
if (nodeProvider) {
|
|
506
|
-
clearHighlightFrame(
|
|
647
|
+
clearHighlightFrame();
|
|
507
648
|
selectedNode = null;
|
|
508
649
|
resizeObserver?.disconnect();
|
|
509
|
-
nodeResizeObserver?.disconnect();
|
|
510
650
|
mutationObserver?.disconnect();
|
|
511
651
|
}
|
|
512
652
|
}
|
|
@@ -522,33 +662,31 @@ const createNodeTools = (element) => {
|
|
|
522
662
|
}
|
|
523
663
|
}
|
|
524
664
|
resizeObserver?.disconnect();
|
|
525
|
-
nodeResizeObserver?.disconnect();
|
|
526
665
|
mutationObserver?.disconnect();
|
|
527
666
|
if (node && nodeProvider) {
|
|
528
667
|
text.enableEditMode(node, nodeProvider);
|
|
529
|
-
resizeObserver = connectResizeObserver(nodeProvider, () => {
|
|
530
|
-
throttledFrameRefresh(node, nodeProvider);
|
|
531
|
-
});
|
|
532
668
|
mutationObserver = new MutationObserver(() => {
|
|
533
|
-
|
|
534
|
-
|
|
669
|
+
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
670
|
+
console.log("mutationObserver", node);
|
|
671
|
+
refreshHighlightFrame(node, nodeProvider);
|
|
672
|
+
updateHighlightFrameVisibility(node);
|
|
535
673
|
});
|
|
536
674
|
mutationObserver.observe(node, {
|
|
537
675
|
attributes: true,
|
|
538
|
-
subtree: true,
|
|
539
|
-
childList: true,
|
|
540
676
|
characterData: true,
|
|
541
677
|
});
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
678
|
+
resizeObserver = connectResizeObserver(node, () => {
|
|
679
|
+
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
680
|
+
console.log("resizeObserver", node);
|
|
681
|
+
refreshHighlightFrame(node, nodeProvider);
|
|
682
|
+
updateHighlightFrameVisibility(node);
|
|
545
683
|
});
|
|
546
684
|
}
|
|
547
685
|
selectedNode = node;
|
|
548
686
|
sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
|
|
549
|
-
highlightNode(node
|
|
687
|
+
highlightNode(node) ?? null;
|
|
550
688
|
if (node && nodeProvider) {
|
|
551
|
-
updateHighlightFrameVisibility(node
|
|
689
|
+
updateHighlightFrameVisibility(node);
|
|
552
690
|
}
|
|
553
691
|
};
|
|
554
692
|
// Setup event listener
|
|
@@ -556,22 +694,25 @@ const createNodeTools = (element) => {
|
|
|
556
694
|
const cleanup = () => {
|
|
557
695
|
removeListeners();
|
|
558
696
|
resizeObserver?.disconnect();
|
|
559
|
-
nodeResizeObserver?.disconnect();
|
|
560
697
|
mutationObserver?.disconnect();
|
|
561
698
|
text.blurEditMode();
|
|
562
|
-
|
|
699
|
+
throttledRefreshAndVisibility.cleanup();
|
|
563
700
|
};
|
|
564
701
|
const nodeTools = {
|
|
565
702
|
selectNode,
|
|
566
703
|
getSelectedNode: () => selectedNode,
|
|
567
704
|
refreshHighlightFrame: () => {
|
|
568
|
-
|
|
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
|
+
}
|
|
569
711
|
},
|
|
570
712
|
clearSelectedNode: () => {
|
|
571
|
-
clearHighlightFrame(
|
|
713
|
+
clearHighlightFrame();
|
|
572
714
|
selectedNode = null;
|
|
573
715
|
resizeObserver?.disconnect();
|
|
574
|
-
nodeResizeObserver?.disconnect();
|
|
575
716
|
mutationObserver?.disconnect();
|
|
576
717
|
},
|
|
577
718
|
getEditableNode: () => text.getEditableNode(),
|