@node-edit-utils/core 2.2.6 → 2.2.8
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/events/click/handleNodeClick.d.ts +2 -1
- package/dist/lib/node-tools/events/setupEventListener.d.ts +2 -1
- package/dist/lib/node-tools/highlight/createCornerHandles.d.ts +1 -1
- 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/select/selectNode.d.ts +2 -1
- package/dist/lib/node-tools/text/helpers/enterTextEditMode.d.ts +2 -0
- package/dist/lib/node-tools/text/helpers/handleTextChange.d.ts +1 -0
- package/dist/lib/node-tools/text/helpers/shouldEnterTextEditMode.d.ts +1 -0
- package/dist/node-edit-utils.cjs.js +227 -90
- package/dist/node-edit-utils.esm.js +227 -90
- package/dist/node-edit-utils.umd.js +227 -90
- package/dist/node-edit-utils.umd.min.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -2
- package/src/index.ts +0 -2
- package/src/lib/node-tools/createNodeTools.ts +3 -16
- package/src/lib/node-tools/events/click/handleNodeClick.ts +3 -2
- package/src/lib/node-tools/events/setupEventListener.ts +3 -2
- package/src/lib/node-tools/highlight/clearHighlightFrame.ts +7 -2
- package/src/lib/node-tools/highlight/createCornerHandles.ts +12 -6
- package/src/lib/node-tools/highlight/createHighlightFrame.ts +25 -7
- package/src/lib/node-tools/highlight/createTagLabel.ts +25 -1
- package/src/lib/node-tools/highlight/createToolsContainer.ts +4 -1
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +5 -1
- package/src/lib/node-tools/highlight/highlightNode.ts +21 -11
- package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +40 -7
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +6 -1
- package/src/lib/node-tools/select/selectNode.ts +24 -5
- package/src/lib/node-tools/text/events/setupMutationObserver.ts +17 -3
- package/src/lib/node-tools/text/helpers/enterTextEditMode.ts +9 -0
- package/src/lib/node-tools/text/helpers/handleTextChange.ts +27 -0
- package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.ts +9 -0
- package/src/lib/styles/styles.css +28 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Markup Canvas
|
|
3
3
|
* High-performance markup canvas with zoom and pan capabilities
|
|
4
|
-
* @version 2.2.
|
|
4
|
+
* @version 2.2.8
|
|
5
5
|
*/
|
|
6
6
|
const getCanvasWindowValue = (path, canvasName = "canvas") => {
|
|
7
7
|
// biome-ignore lint/suspicious/noExplicitAny: global window extension
|
|
@@ -55,32 +55,6 @@ const connectResizeObserver = (element, handler) => {
|
|
|
55
55
|
return resizeObserver;
|
|
56
56
|
};
|
|
57
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
|
-
|
|
84
58
|
function sendPostMessage(action, data) {
|
|
85
59
|
window.parent.postMessage({
|
|
86
60
|
source: "node-edit-utils",
|
|
@@ -117,17 +91,30 @@ const processPostMessage = (event, onNodeSelected) => {
|
|
|
117
91
|
}
|
|
118
92
|
};
|
|
119
93
|
|
|
94
|
+
const getCanvasContainer = () => {
|
|
95
|
+
return document.querySelector(".canvas-container");
|
|
96
|
+
};
|
|
97
|
+
|
|
120
98
|
const clearHighlightFrame = () => {
|
|
121
|
-
const
|
|
99
|
+
const canvasContainer = getCanvasContainer();
|
|
100
|
+
const container = canvasContainer || document.body;
|
|
101
|
+
const frame = container.querySelector(".highlight-frame-overlay");
|
|
122
102
|
if (frame) {
|
|
123
103
|
frame.remove();
|
|
124
104
|
}
|
|
125
|
-
const toolsWrapper =
|
|
105
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
126
106
|
if (toolsWrapper) {
|
|
127
107
|
toolsWrapper.remove();
|
|
128
108
|
}
|
|
129
109
|
};
|
|
130
110
|
|
|
111
|
+
const enterTextEditMode = (node, nodeProvider, text) => {
|
|
112
|
+
if (!node || !nodeProvider) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
text.enableEditMode(node, nodeProvider);
|
|
116
|
+
};
|
|
117
|
+
|
|
131
118
|
const IGNORED_DOM_ELEMENTS = ["path", "rect", "circle", "ellipse", "polygon", "line", "polyline", "text", "text-noci"];
|
|
132
119
|
|
|
133
120
|
const getElementsFromPoint = (clickX, clickY) => {
|
|
@@ -163,7 +150,8 @@ const targetSameCandidates = (cache, current) => cache.length === current.length
|
|
|
163
150
|
|
|
164
151
|
let candidateCache = [];
|
|
165
152
|
let attempt = 0;
|
|
166
|
-
|
|
153
|
+
let lastSelectedNode = null;
|
|
154
|
+
const selectNode = (event, nodeProvider, text) => {
|
|
167
155
|
let selectedNode = null;
|
|
168
156
|
const clickX = event.clientX;
|
|
169
157
|
const clickY = event.clientY;
|
|
@@ -171,16 +159,23 @@ const selectNode = (event, editableNode) => {
|
|
|
171
159
|
const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
|
|
172
160
|
!element.classList.contains("select-none") &&
|
|
173
161
|
!isInsideComponent(element));
|
|
162
|
+
const editableNode = text.getEditableNode();
|
|
174
163
|
if (editableNode && candidates.includes(editableNode)) {
|
|
175
|
-
|
|
164
|
+
selectedNode = editableNode;
|
|
165
|
+
lastSelectedNode = selectedNode;
|
|
166
|
+
return selectedNode;
|
|
176
167
|
}
|
|
177
168
|
if (clickThrough) {
|
|
178
169
|
candidateCache = [];
|
|
179
170
|
selectedNode = candidates[0];
|
|
171
|
+
if (lastSelectedNode && lastSelectedNode === selectedNode) {
|
|
172
|
+
enterTextEditMode(selectedNode, nodeProvider, text);
|
|
173
|
+
}
|
|
174
|
+
lastSelectedNode = selectedNode;
|
|
180
175
|
return selectedNode;
|
|
181
176
|
}
|
|
182
177
|
if (targetSameCandidates(candidateCache, candidates)) {
|
|
183
|
-
attempt <= candidates.length && attempt++;
|
|
178
|
+
attempt <= candidates.length - 2 && attempt++;
|
|
184
179
|
}
|
|
185
180
|
else {
|
|
186
181
|
attempt = 0;
|
|
@@ -188,10 +183,14 @@ const selectNode = (event, editableNode) => {
|
|
|
188
183
|
const nodeIndex = candidates.length - 1 - attempt;
|
|
189
184
|
selectedNode = candidates[nodeIndex];
|
|
190
185
|
candidateCache = candidates;
|
|
186
|
+
if (lastSelectedNode && lastSelectedNode === selectedNode) {
|
|
187
|
+
enterTextEditMode(selectedNode, nodeProvider, text);
|
|
188
|
+
}
|
|
189
|
+
lastSelectedNode = selectedNode;
|
|
191
190
|
return selectedNode;
|
|
192
191
|
};
|
|
193
192
|
|
|
194
|
-
const handleNodeClick = (event, nodeProvider,
|
|
193
|
+
const handleNodeClick = (event, nodeProvider, text, onNodeSelected) => {
|
|
195
194
|
event.preventDefault();
|
|
196
195
|
event.stopPropagation();
|
|
197
196
|
if (nodeProvider && !nodeProvider.contains(event.target)) {
|
|
@@ -199,16 +198,16 @@ const handleNodeClick = (event, nodeProvider, editableNode, onNodeSelected) => {
|
|
|
199
198
|
onNodeSelected(null);
|
|
200
199
|
return;
|
|
201
200
|
}
|
|
202
|
-
const selectedNode = selectNode(event,
|
|
201
|
+
const selectedNode = selectNode(event, nodeProvider, text);
|
|
203
202
|
onNodeSelected(selectedNode);
|
|
204
203
|
};
|
|
205
204
|
|
|
206
|
-
const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed,
|
|
205
|
+
const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
|
|
207
206
|
const messageHandler = (event) => {
|
|
208
207
|
processPostMessage(event, onNodeSelected);
|
|
209
208
|
};
|
|
210
209
|
const documentClickHandler = (event) => {
|
|
211
|
-
handleNodeClick(event, nodeProvider,
|
|
210
|
+
handleNodeClick(event, nodeProvider, text, onNodeSelected);
|
|
212
211
|
};
|
|
213
212
|
const documentKeydownHandler = (event) => {
|
|
214
213
|
if (event.key === "Escape") {
|
|
@@ -235,7 +234,10 @@ const HANDLE_SIZE = 6;
|
|
|
235
234
|
const getComponentColor$2 = () => {
|
|
236
235
|
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
237
236
|
};
|
|
238
|
-
const
|
|
237
|
+
const getTextEditColor$2 = () => {
|
|
238
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
|
|
239
|
+
};
|
|
240
|
+
const createCornerHandle = (group, x, y, className, isInstance = false, isTextEdit = false) => {
|
|
239
241
|
const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
240
242
|
// Position relative to group (offset by half handle size to center on corner)
|
|
241
243
|
handle.setAttribute("x", (x - HANDLE_SIZE / 2).toString());
|
|
@@ -247,15 +249,18 @@ const createCornerHandle = (group, x, y, className, isInstance = false) => {
|
|
|
247
249
|
if (isInstance) {
|
|
248
250
|
handle.setAttribute("stroke", getComponentColor$2());
|
|
249
251
|
}
|
|
252
|
+
else if (isTextEdit) {
|
|
253
|
+
handle.setAttribute("stroke", getTextEditColor$2());
|
|
254
|
+
}
|
|
250
255
|
group.appendChild(handle);
|
|
251
256
|
return handle;
|
|
252
257
|
};
|
|
253
|
-
const createCornerHandles = (group, width, height, isInstance = false) => {
|
|
258
|
+
const createCornerHandles = (group, width, height, isInstance = false, isTextEdit = false) => {
|
|
254
259
|
// 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);
|
|
260
|
+
createCornerHandle(group, 0, 0, "handle-top-left", isInstance, isTextEdit);
|
|
261
|
+
createCornerHandle(group, width, 0, "handle-top-right", isInstance, isTextEdit);
|
|
262
|
+
createCornerHandle(group, width, height, "handle-bottom-right", isInstance, isTextEdit);
|
|
263
|
+
createCornerHandle(group, 0, height, "handle-bottom-left", isInstance, isTextEdit);
|
|
259
264
|
};
|
|
260
265
|
|
|
261
266
|
function getScreenBounds(element) {
|
|
@@ -271,23 +276,31 @@ function getScreenBounds(element) {
|
|
|
271
276
|
const getComponentColor$1 = () => {
|
|
272
277
|
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
273
278
|
};
|
|
274
|
-
const
|
|
279
|
+
const getTextEditColor$1 = () => {
|
|
280
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
|
|
281
|
+
};
|
|
282
|
+
const createHighlightFrame = (node, isInstance = false, isTextEdit = false) => {
|
|
275
283
|
const { top, left, width, height } = getScreenBounds(node);
|
|
284
|
+
// Ensure minimum width of 2px
|
|
285
|
+
const minWidth = Math.max(width, 3);
|
|
276
286
|
// Create fixed SVG overlay
|
|
277
287
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
278
288
|
svg.classList.add("highlight-frame-overlay");
|
|
279
289
|
if (isInstance) {
|
|
280
290
|
svg.classList.add("is-instance");
|
|
281
291
|
}
|
|
292
|
+
if (isTextEdit) {
|
|
293
|
+
svg.classList.add("is-text-edit");
|
|
294
|
+
}
|
|
282
295
|
svg.setAttribute("data-node-id", node.getAttribute("data-node-id") || "");
|
|
283
296
|
// Set fixed positioning
|
|
284
|
-
svg.style.position = "
|
|
297
|
+
svg.style.position = "absolute";
|
|
285
298
|
svg.style.top = "0";
|
|
286
299
|
svg.style.left = "0";
|
|
287
300
|
svg.style.width = "100vw";
|
|
288
301
|
svg.style.height = "100vh";
|
|
289
302
|
svg.style.pointerEvents = "none";
|
|
290
|
-
svg.style.zIndex = "
|
|
303
|
+
svg.style.zIndex = "500";
|
|
291
304
|
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
292
305
|
const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
|
293
306
|
svg.setAttribute("width", viewportWidth.toString());
|
|
@@ -298,47 +311,85 @@ const createHighlightFrame = (node, isInstance = false) => {
|
|
|
298
311
|
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
299
312
|
rect.setAttribute("x", "0");
|
|
300
313
|
rect.setAttribute("y", "0");
|
|
301
|
-
rect.setAttribute("width",
|
|
314
|
+
rect.setAttribute("width", minWidth.toString());
|
|
302
315
|
rect.setAttribute("height", height.toString());
|
|
303
316
|
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
304
317
|
rect.classList.add("highlight-frame-rect");
|
|
305
|
-
// Apply instance color if it's an instance
|
|
318
|
+
// Apply instance color if it's an instance, otherwise text edit color if in text edit mode
|
|
306
319
|
if (isInstance) {
|
|
307
320
|
rect.setAttribute("stroke", getComponentColor$1());
|
|
308
321
|
}
|
|
322
|
+
else if (isTextEdit) {
|
|
323
|
+
rect.setAttribute("stroke", getTextEditColor$1());
|
|
324
|
+
}
|
|
309
325
|
group.appendChild(rect);
|
|
310
|
-
createCornerHandles(group,
|
|
326
|
+
createCornerHandles(group, minWidth, height, isInstance, isTextEdit);
|
|
311
327
|
svg.appendChild(group);
|
|
312
|
-
|
|
328
|
+
const canvasContainer = getCanvasContainer();
|
|
329
|
+
if (canvasContainer) {
|
|
330
|
+
canvasContainer.appendChild(svg);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
document.body.appendChild(svg);
|
|
334
|
+
}
|
|
313
335
|
return svg;
|
|
314
336
|
};
|
|
315
337
|
|
|
338
|
+
const TAG_NAME_MAP = {
|
|
339
|
+
div: "Container",
|
|
340
|
+
h1: "Heading 1",
|
|
341
|
+
h2: "Heading 2",
|
|
342
|
+
h3: "Heading 3",
|
|
343
|
+
h4: "Heading 4",
|
|
344
|
+
h5: "Heading 5",
|
|
345
|
+
h6: "Heading 6",
|
|
346
|
+
p: "Text",
|
|
347
|
+
li: "List Item",
|
|
348
|
+
ul: "Unordered List",
|
|
349
|
+
ol: "Ordered List",
|
|
350
|
+
img: "Image",
|
|
351
|
+
a: "Link",
|
|
352
|
+
};
|
|
353
|
+
const capitalize = (str) => {
|
|
354
|
+
if (!str)
|
|
355
|
+
return str;
|
|
356
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
357
|
+
};
|
|
316
358
|
const createTagLabel = (node, nodeTools) => {
|
|
317
359
|
const tagLabel = document.createElement("div");
|
|
318
360
|
tagLabel.className = "tag-label";
|
|
319
|
-
|
|
361
|
+
const instanceName = node.getAttribute("data-instance-name");
|
|
362
|
+
const tagName = node.tagName.toLowerCase();
|
|
363
|
+
const labelText = instanceName || TAG_NAME_MAP[tagName] || tagName;
|
|
364
|
+
tagLabel.textContent = capitalize(labelText);
|
|
320
365
|
nodeTools.appendChild(tagLabel);
|
|
321
366
|
};
|
|
322
367
|
|
|
323
|
-
const createToolsContainer = (node, highlightFrame, isInstance = false) => {
|
|
368
|
+
const createToolsContainer = (node, highlightFrame, isInstance = false, isTextEdit = false) => {
|
|
324
369
|
const nodeTools = document.createElement("div");
|
|
325
370
|
nodeTools.className = "node-tools";
|
|
326
371
|
if (isInstance) {
|
|
327
372
|
nodeTools.classList.add("is-instance");
|
|
328
373
|
}
|
|
374
|
+
if (isTextEdit) {
|
|
375
|
+
nodeTools.classList.add("is-text-edit");
|
|
376
|
+
}
|
|
329
377
|
highlightFrame.appendChild(nodeTools);
|
|
330
378
|
createTagLabel(node, nodeTools);
|
|
331
379
|
};
|
|
332
380
|
|
|
333
381
|
function getHighlightFrameElement() {
|
|
334
|
-
|
|
382
|
+
const canvasContainer = getCanvasContainer();
|
|
383
|
+
const container = canvasContainer || document.body;
|
|
384
|
+
return container.querySelector(".highlight-frame-overlay");
|
|
335
385
|
}
|
|
336
386
|
|
|
337
387
|
const highlightNode = (node) => {
|
|
338
388
|
if (!node)
|
|
339
389
|
return;
|
|
340
390
|
const existingHighlightFrame = getHighlightFrameElement();
|
|
341
|
-
const
|
|
391
|
+
const canvasContainer = getCanvasContainer();
|
|
392
|
+
const existingToolsWrapper = canvasContainer?.querySelector(".highlight-frame-tools-wrapper") || document.body.querySelector(".highlight-frame-tools-wrapper");
|
|
342
393
|
if (existingHighlightFrame) {
|
|
343
394
|
existingHighlightFrame.remove();
|
|
344
395
|
}
|
|
@@ -346,38 +397,50 @@ const highlightNode = (node) => {
|
|
|
346
397
|
existingToolsWrapper.remove();
|
|
347
398
|
}
|
|
348
399
|
const isInstance = isComponentInstance(node);
|
|
349
|
-
const
|
|
400
|
+
const isTextEdit = node.contentEditable === "true";
|
|
401
|
+
const highlightFrame = createHighlightFrame(node, isInstance, isTextEdit);
|
|
350
402
|
if (node.contentEditable === "true") {
|
|
351
403
|
highlightFrame.classList.add("is-editable");
|
|
352
404
|
}
|
|
353
|
-
//
|
|
405
|
+
// Batch DOM reads
|
|
354
406
|
const { left, top, height } = getScreenBounds(node);
|
|
355
407
|
const bottomY = top + height;
|
|
408
|
+
// Create tools wrapper using CSS transform (GPU-accelerated)
|
|
356
409
|
const toolsWrapper = document.createElement("div");
|
|
357
410
|
toolsWrapper.classList.add("highlight-frame-tools-wrapper");
|
|
358
411
|
if (isInstance) {
|
|
359
412
|
toolsWrapper.classList.add("is-instance");
|
|
360
413
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
toolsWrapper.style.
|
|
365
|
-
toolsWrapper.style.
|
|
414
|
+
if (isTextEdit) {
|
|
415
|
+
toolsWrapper.classList.add("is-text-edit");
|
|
416
|
+
}
|
|
417
|
+
toolsWrapper.style.position = "absolute";
|
|
418
|
+
toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
|
|
419
|
+
toolsWrapper.style.transformOrigin = "left center";
|
|
366
420
|
toolsWrapper.style.pointerEvents = "none";
|
|
367
|
-
toolsWrapper.style.zIndex = "
|
|
368
|
-
createToolsContainer(node, toolsWrapper, isInstance);
|
|
369
|
-
|
|
421
|
+
toolsWrapper.style.zIndex = "500";
|
|
422
|
+
createToolsContainer(node, toolsWrapper, isInstance, isTextEdit);
|
|
423
|
+
if (canvasContainer) {
|
|
424
|
+
canvasContainer.appendChild(toolsWrapper);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
document.body.appendChild(toolsWrapper);
|
|
428
|
+
}
|
|
370
429
|
};
|
|
371
430
|
|
|
372
431
|
const getComponentColor = () => {
|
|
373
432
|
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
374
433
|
};
|
|
434
|
+
const getTextEditColor = () => {
|
|
435
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
|
|
436
|
+
};
|
|
375
437
|
const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
376
438
|
// Batch all DOM reads first (single layout pass)
|
|
377
439
|
const frame = getHighlightFrameElement();
|
|
378
440
|
if (!frame)
|
|
379
441
|
return;
|
|
380
442
|
const isInstance = isComponentInstance(node);
|
|
443
|
+
const isTextEdit = node.contentEditable === "true";
|
|
381
444
|
// Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
|
|
382
445
|
// Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
|
|
383
446
|
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
@@ -391,6 +454,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
391
454
|
else {
|
|
392
455
|
frame.classList.remove("is-instance");
|
|
393
456
|
}
|
|
457
|
+
// Update text edit class
|
|
458
|
+
if (isTextEdit) {
|
|
459
|
+
frame.classList.add("is-text-edit");
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
frame.classList.remove("is-text-edit");
|
|
463
|
+
}
|
|
394
464
|
const group = frame.querySelector(".highlight-frame-group");
|
|
395
465
|
if (!group)
|
|
396
466
|
return;
|
|
@@ -401,15 +471,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
401
471
|
if (isInstance) {
|
|
402
472
|
rect.setAttribute("stroke", getComponentColor());
|
|
403
473
|
}
|
|
474
|
+
else if (isTextEdit) {
|
|
475
|
+
rect.setAttribute("stroke", getTextEditColor());
|
|
476
|
+
}
|
|
404
477
|
else {
|
|
405
478
|
rect.removeAttribute("stroke"); // Use CSS default
|
|
406
479
|
}
|
|
407
|
-
const
|
|
480
|
+
const canvasContainer = getCanvasContainer();
|
|
481
|
+
const container = canvasContainer || document.body;
|
|
482
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
408
483
|
const nodeTools = toolsWrapper?.querySelector(".node-tools");
|
|
409
484
|
const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
|
|
410
485
|
const bounds = getScreenBounds(node);
|
|
411
486
|
// Calculate all values before any DOM writes
|
|
412
487
|
const { top, left, width, height } = bounds;
|
|
488
|
+
// Ensure minimum width of 2px
|
|
489
|
+
const minWidth = Math.max(width, 3);
|
|
413
490
|
const bottomY = top + height;
|
|
414
491
|
// Update instance classes on tools wrapper and node tools
|
|
415
492
|
if (toolsWrapper) {
|
|
@@ -419,6 +496,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
419
496
|
else {
|
|
420
497
|
toolsWrapper.classList.remove("is-instance");
|
|
421
498
|
}
|
|
499
|
+
// Update text edit class
|
|
500
|
+
if (isTextEdit) {
|
|
501
|
+
toolsWrapper.classList.add("is-text-edit");
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
toolsWrapper.classList.remove("is-text-edit");
|
|
505
|
+
}
|
|
422
506
|
}
|
|
423
507
|
if (nodeTools) {
|
|
424
508
|
if (isInstance) {
|
|
@@ -427,12 +511,19 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
427
511
|
else {
|
|
428
512
|
nodeTools.classList.remove("is-instance");
|
|
429
513
|
}
|
|
514
|
+
// Update text edit class
|
|
515
|
+
if (isTextEdit) {
|
|
516
|
+
nodeTools.classList.add("is-text-edit");
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
nodeTools.classList.remove("is-text-edit");
|
|
520
|
+
}
|
|
430
521
|
}
|
|
431
522
|
// Batch all DOM writes (single paint pass)
|
|
432
523
|
// Update group transform to move entire group (rect + handles) at once
|
|
433
524
|
group.setAttribute("transform", `translate(${left}, ${top})`);
|
|
434
525
|
// Update rect dimensions (position is handled by group transform)
|
|
435
|
-
rect.setAttribute("width",
|
|
526
|
+
rect.setAttribute("width", minWidth.toString());
|
|
436
527
|
rect.setAttribute("height", height.toString());
|
|
437
528
|
// Update corner handles positions (relative to group, so only width/height matter)
|
|
438
529
|
const topLeft = group.querySelector(".handle-top-left");
|
|
@@ -447,6 +538,9 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
447
538
|
if (isInstance) {
|
|
448
539
|
handle.setAttribute("stroke", getComponentColor());
|
|
449
540
|
}
|
|
541
|
+
else if (isTextEdit) {
|
|
542
|
+
handle.setAttribute("stroke", getTextEditColor());
|
|
543
|
+
}
|
|
450
544
|
else {
|
|
451
545
|
handle.removeAttribute("stroke"); // Use CSS default
|
|
452
546
|
}
|
|
@@ -457,22 +551,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
457
551
|
topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
458
552
|
}
|
|
459
553
|
if (topRight) {
|
|
460
|
-
topRight.setAttribute("x", (
|
|
554
|
+
topRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
461
555
|
topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
462
556
|
}
|
|
463
557
|
if (bottomRight) {
|
|
464
|
-
bottomRight.setAttribute("x", (
|
|
558
|
+
bottomRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
465
559
|
bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
466
560
|
}
|
|
467
561
|
if (bottomLeft) {
|
|
468
562
|
bottomLeft.setAttribute("x", (-HANDLE_SIZE / 2).toString());
|
|
469
563
|
bottomLeft.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
470
564
|
}
|
|
471
|
-
// Update tools wrapper position
|
|
565
|
+
// Update tools wrapper position using CSS transform (GPU-accelerated)
|
|
472
566
|
if (toolsWrapper) {
|
|
473
|
-
toolsWrapper.style.
|
|
474
|
-
toolsWrapper.style.top = `${bottomY}px`;
|
|
567
|
+
toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
|
|
475
568
|
}
|
|
569
|
+
// Update tool opacity
|
|
476
570
|
if (zoom <= 10) {
|
|
477
571
|
nodeProvider.style.setProperty("--tool-opacity", `1`);
|
|
478
572
|
}
|
|
@@ -485,10 +579,14 @@ const updateHighlightFrameVisibility = (node) => {
|
|
|
485
579
|
const frame = getHighlightFrameElement();
|
|
486
580
|
if (!frame)
|
|
487
581
|
return;
|
|
582
|
+
// Batch DOM reads
|
|
488
583
|
const hasHiddenClass = node.classList.contains("hidden") || node.classList.contains("select-none");
|
|
489
584
|
const displayValue = hasHiddenClass ? "none" : "";
|
|
585
|
+
// Batch DOM writes
|
|
490
586
|
frame.style.display = displayValue;
|
|
491
|
-
const
|
|
587
|
+
const canvasContainer = getCanvasContainer();
|
|
588
|
+
const container = canvasContainer || document.body;
|
|
589
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
492
590
|
if (toolsWrapper) {
|
|
493
591
|
toolsWrapper.style.display = displayValue;
|
|
494
592
|
}
|
|
@@ -545,11 +643,64 @@ const connectMutationObserver = (element, handler) => {
|
|
|
545
643
|
return mutationObserver;
|
|
546
644
|
};
|
|
547
645
|
|
|
646
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
|
|
647
|
+
function withRAFThrottle(func) {
|
|
648
|
+
let rafId = null;
|
|
649
|
+
let lastArgs = null;
|
|
650
|
+
const throttled = (...args) => {
|
|
651
|
+
lastArgs = args;
|
|
652
|
+
if (rafId === null) {
|
|
653
|
+
rafId = requestAnimationFrame(() => {
|
|
654
|
+
if (lastArgs) {
|
|
655
|
+
func(...lastArgs);
|
|
656
|
+
}
|
|
657
|
+
rafId = null;
|
|
658
|
+
lastArgs = null;
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
throttled.cleanup = () => {
|
|
663
|
+
if (rafId !== null) {
|
|
664
|
+
cancelAnimationFrame(rafId);
|
|
665
|
+
rafId = null;
|
|
666
|
+
lastArgs = null;
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
return throttled;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const handleTextChange = (node, mutations) => {
|
|
673
|
+
// Check if any mutation is a text content change
|
|
674
|
+
const hasTextChange = mutations.some((mutation) => {
|
|
675
|
+
return (mutation.type === "characterData" ||
|
|
676
|
+
(mutation.type === "childList" && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)));
|
|
677
|
+
});
|
|
678
|
+
if (!hasTextChange) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
// Get the text content of the node
|
|
682
|
+
const textContent = node.textContent ?? "";
|
|
683
|
+
// Get the node ID
|
|
684
|
+
const nodeId = node.getAttribute("data-node-id");
|
|
685
|
+
// Send postMessage with the text change
|
|
686
|
+
sendPostMessage("textContentChanged", {
|
|
687
|
+
nodeId,
|
|
688
|
+
textContent,
|
|
689
|
+
});
|
|
690
|
+
};
|
|
691
|
+
|
|
548
692
|
const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
|
|
549
|
-
const
|
|
693
|
+
const throttledHandleTextChange = withRAFThrottle((mutations) => {
|
|
694
|
+
handleTextChange(node, mutations);
|
|
695
|
+
});
|
|
696
|
+
const mutationObserver = connectMutationObserver(node, (mutations) => {
|
|
697
|
+
throttledHandleTextChange(mutations);
|
|
550
698
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
551
699
|
});
|
|
552
|
-
return () =>
|
|
700
|
+
return () => {
|
|
701
|
+
mutationObserver.disconnect();
|
|
702
|
+
throttledHandleTextChange.cleanup();
|
|
703
|
+
};
|
|
553
704
|
};
|
|
554
705
|
|
|
555
706
|
const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
|
|
@@ -634,11 +785,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
634
785
|
let parentMutationObserver = null;
|
|
635
786
|
let selectedNode = null;
|
|
636
787
|
const text = nodeText(canvasName);
|
|
637
|
-
// Combined throttled function for refresh + visibility update
|
|
638
|
-
const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
|
|
639
|
-
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
640
|
-
updateHighlightFrameVisibility(node);
|
|
641
|
-
});
|
|
642
788
|
const handleEscape = () => {
|
|
643
789
|
if (text.isEditing()) {
|
|
644
790
|
text.blurEditMode();
|
|
@@ -667,7 +813,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
667
813
|
mutationObserver?.disconnect();
|
|
668
814
|
parentMutationObserver?.disconnect();
|
|
669
815
|
if (node && nodeProvider) {
|
|
670
|
-
text.enableEditMode(node, nodeProvider);
|
|
671
816
|
// Check if node is still in DOM and handle cleanup if removed
|
|
672
817
|
const checkNodeExists = () => {
|
|
673
818
|
if (!document.contains(node)) {
|
|
@@ -684,9 +829,7 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
684
829
|
mutationObserver = new MutationObserver(() => {
|
|
685
830
|
checkNodeExists();
|
|
686
831
|
if (!document.contains(node))
|
|
687
|
-
return;
|
|
688
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
689
|
-
console.log("mutationObserver", node);
|
|
832
|
+
return;
|
|
690
833
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
691
834
|
updateHighlightFrameVisibility(node);
|
|
692
835
|
});
|
|
@@ -722,8 +865,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
722
865
|
checkNodeExists();
|
|
723
866
|
if (!document.contains(node))
|
|
724
867
|
return; // Exit early if node was removed
|
|
725
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
726
|
-
console.log("resizeObserver", node);
|
|
727
868
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
728
869
|
updateHighlightFrameVisibility(node);
|
|
729
870
|
});
|
|
@@ -733,17 +874,17 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
733
874
|
highlightNode(node) ?? null;
|
|
734
875
|
if (node && nodeProvider) {
|
|
735
876
|
updateHighlightFrameVisibility(node);
|
|
877
|
+
updateHighlightFrameVisibility(node);
|
|
736
878
|
}
|
|
737
879
|
};
|
|
738
880
|
// Setup event listener
|
|
739
|
-
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text
|
|
881
|
+
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
|
|
740
882
|
const cleanup = () => {
|
|
741
883
|
removeListeners();
|
|
742
884
|
resizeObserver?.disconnect();
|
|
743
885
|
mutationObserver?.disconnect();
|
|
744
886
|
parentMutationObserver?.disconnect();
|
|
745
887
|
text.blurEditMode();
|
|
746
|
-
throttledRefreshAndVisibility.cleanup();
|
|
747
888
|
// Clear highlight frame and reset selected node
|
|
748
889
|
clearHighlightFrame();
|
|
749
890
|
selectedNode = null;
|
|
@@ -772,10 +913,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
772
913
|
return nodeTools;
|
|
773
914
|
};
|
|
774
915
|
|
|
775
|
-
const getCanvasContainer = () => {
|
|
776
|
-
return document.querySelector(".canvas-container");
|
|
777
|
-
};
|
|
778
|
-
|
|
779
916
|
const DEFAULT_WIDTH = 400;
|
|
780
917
|
const RESIZE_CONFIG = {
|
|
781
918
|
minWidth: 320,
|