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