@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
|
(function (global, factory) {
|
|
7
7
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
@@ -61,32 +61,6 @@
|
|
|
61
61
|
return resizeObserver;
|
|
62
62
|
};
|
|
63
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
|
-
|
|
90
64
|
function sendPostMessage(action, data) {
|
|
91
65
|
window.parent.postMessage({
|
|
92
66
|
source: "node-edit-utils",
|
|
@@ -123,17 +97,30 @@
|
|
|
123
97
|
}
|
|
124
98
|
};
|
|
125
99
|
|
|
100
|
+
const getCanvasContainer = () => {
|
|
101
|
+
return document.querySelector(".canvas-container");
|
|
102
|
+
};
|
|
103
|
+
|
|
126
104
|
const clearHighlightFrame = () => {
|
|
127
|
-
const
|
|
105
|
+
const canvasContainer = getCanvasContainer();
|
|
106
|
+
const container = canvasContainer || document.body;
|
|
107
|
+
const frame = container.querySelector(".highlight-frame-overlay");
|
|
128
108
|
if (frame) {
|
|
129
109
|
frame.remove();
|
|
130
110
|
}
|
|
131
|
-
const toolsWrapper =
|
|
111
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
132
112
|
if (toolsWrapper) {
|
|
133
113
|
toolsWrapper.remove();
|
|
134
114
|
}
|
|
135
115
|
};
|
|
136
116
|
|
|
117
|
+
const enterTextEditMode = (node, nodeProvider, text) => {
|
|
118
|
+
if (!node || !nodeProvider) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
text.enableEditMode(node, nodeProvider);
|
|
122
|
+
};
|
|
123
|
+
|
|
137
124
|
const IGNORED_DOM_ELEMENTS = ["path", "rect", "circle", "ellipse", "polygon", "line", "polyline", "text", "text-noci"];
|
|
138
125
|
|
|
139
126
|
const getElementsFromPoint = (clickX, clickY) => {
|
|
@@ -169,7 +156,8 @@
|
|
|
169
156
|
|
|
170
157
|
let candidateCache = [];
|
|
171
158
|
let attempt = 0;
|
|
172
|
-
|
|
159
|
+
let lastSelectedNode = null;
|
|
160
|
+
const selectNode = (event, nodeProvider, text) => {
|
|
173
161
|
let selectedNode = null;
|
|
174
162
|
const clickX = event.clientX;
|
|
175
163
|
const clickY = event.clientY;
|
|
@@ -177,16 +165,23 @@
|
|
|
177
165
|
const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
|
|
178
166
|
!element.classList.contains("select-none") &&
|
|
179
167
|
!isInsideComponent(element));
|
|
168
|
+
const editableNode = text.getEditableNode();
|
|
180
169
|
if (editableNode && candidates.includes(editableNode)) {
|
|
181
|
-
|
|
170
|
+
selectedNode = editableNode;
|
|
171
|
+
lastSelectedNode = selectedNode;
|
|
172
|
+
return selectedNode;
|
|
182
173
|
}
|
|
183
174
|
if (clickThrough) {
|
|
184
175
|
candidateCache = [];
|
|
185
176
|
selectedNode = candidates[0];
|
|
177
|
+
if (lastSelectedNode && lastSelectedNode === selectedNode) {
|
|
178
|
+
enterTextEditMode(selectedNode, nodeProvider, text);
|
|
179
|
+
}
|
|
180
|
+
lastSelectedNode = selectedNode;
|
|
186
181
|
return selectedNode;
|
|
187
182
|
}
|
|
188
183
|
if (targetSameCandidates(candidateCache, candidates)) {
|
|
189
|
-
attempt <= candidates.length && attempt++;
|
|
184
|
+
attempt <= candidates.length - 2 && attempt++;
|
|
190
185
|
}
|
|
191
186
|
else {
|
|
192
187
|
attempt = 0;
|
|
@@ -194,10 +189,14 @@
|
|
|
194
189
|
const nodeIndex = candidates.length - 1 - attempt;
|
|
195
190
|
selectedNode = candidates[nodeIndex];
|
|
196
191
|
candidateCache = candidates;
|
|
192
|
+
if (lastSelectedNode && lastSelectedNode === selectedNode) {
|
|
193
|
+
enterTextEditMode(selectedNode, nodeProvider, text);
|
|
194
|
+
}
|
|
195
|
+
lastSelectedNode = selectedNode;
|
|
197
196
|
return selectedNode;
|
|
198
197
|
};
|
|
199
198
|
|
|
200
|
-
const handleNodeClick = (event, nodeProvider,
|
|
199
|
+
const handleNodeClick = (event, nodeProvider, text, onNodeSelected) => {
|
|
201
200
|
event.preventDefault();
|
|
202
201
|
event.stopPropagation();
|
|
203
202
|
if (nodeProvider && !nodeProvider.contains(event.target)) {
|
|
@@ -205,16 +204,16 @@
|
|
|
205
204
|
onNodeSelected(null);
|
|
206
205
|
return;
|
|
207
206
|
}
|
|
208
|
-
const selectedNode = selectNode(event,
|
|
207
|
+
const selectedNode = selectNode(event, nodeProvider, text);
|
|
209
208
|
onNodeSelected(selectedNode);
|
|
210
209
|
};
|
|
211
210
|
|
|
212
|
-
const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed,
|
|
211
|
+
const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
|
|
213
212
|
const messageHandler = (event) => {
|
|
214
213
|
processPostMessage(event, onNodeSelected);
|
|
215
214
|
};
|
|
216
215
|
const documentClickHandler = (event) => {
|
|
217
|
-
handleNodeClick(event, nodeProvider,
|
|
216
|
+
handleNodeClick(event, nodeProvider, text, onNodeSelected);
|
|
218
217
|
};
|
|
219
218
|
const documentKeydownHandler = (event) => {
|
|
220
219
|
if (event.key === "Escape") {
|
|
@@ -241,7 +240,10 @@
|
|
|
241
240
|
const getComponentColor$2 = () => {
|
|
242
241
|
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
243
242
|
};
|
|
244
|
-
const
|
|
243
|
+
const getTextEditColor$2 = () => {
|
|
244
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
|
|
245
|
+
};
|
|
246
|
+
const createCornerHandle = (group, x, y, className, isInstance = false, isTextEdit = false) => {
|
|
245
247
|
const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
246
248
|
// Position relative to group (offset by half handle size to center on corner)
|
|
247
249
|
handle.setAttribute("x", (x - HANDLE_SIZE / 2).toString());
|
|
@@ -253,15 +255,18 @@
|
|
|
253
255
|
if (isInstance) {
|
|
254
256
|
handle.setAttribute("stroke", getComponentColor$2());
|
|
255
257
|
}
|
|
258
|
+
else if (isTextEdit) {
|
|
259
|
+
handle.setAttribute("stroke", getTextEditColor$2());
|
|
260
|
+
}
|
|
256
261
|
group.appendChild(handle);
|
|
257
262
|
return handle;
|
|
258
263
|
};
|
|
259
|
-
const createCornerHandles = (group, width, height, isInstance = false) => {
|
|
264
|
+
const createCornerHandles = (group, width, height, isInstance = false, isTextEdit = false) => {
|
|
260
265
|
// 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);
|
|
266
|
+
createCornerHandle(group, 0, 0, "handle-top-left", isInstance, isTextEdit);
|
|
267
|
+
createCornerHandle(group, width, 0, "handle-top-right", isInstance, isTextEdit);
|
|
268
|
+
createCornerHandle(group, width, height, "handle-bottom-right", isInstance, isTextEdit);
|
|
269
|
+
createCornerHandle(group, 0, height, "handle-bottom-left", isInstance, isTextEdit);
|
|
265
270
|
};
|
|
266
271
|
|
|
267
272
|
function getScreenBounds(element) {
|
|
@@ -277,23 +282,31 @@
|
|
|
277
282
|
const getComponentColor$1 = () => {
|
|
278
283
|
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
279
284
|
};
|
|
280
|
-
const
|
|
285
|
+
const getTextEditColor$1 = () => {
|
|
286
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
|
|
287
|
+
};
|
|
288
|
+
const createHighlightFrame = (node, isInstance = false, isTextEdit = false) => {
|
|
281
289
|
const { top, left, width, height } = getScreenBounds(node);
|
|
290
|
+
// Ensure minimum width of 2px
|
|
291
|
+
const minWidth = Math.max(width, 3);
|
|
282
292
|
// Create fixed SVG overlay
|
|
283
293
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
284
294
|
svg.classList.add("highlight-frame-overlay");
|
|
285
295
|
if (isInstance) {
|
|
286
296
|
svg.classList.add("is-instance");
|
|
287
297
|
}
|
|
298
|
+
if (isTextEdit) {
|
|
299
|
+
svg.classList.add("is-text-edit");
|
|
300
|
+
}
|
|
288
301
|
svg.setAttribute("data-node-id", node.getAttribute("data-node-id") || "");
|
|
289
302
|
// Set fixed positioning
|
|
290
|
-
svg.style.position = "
|
|
303
|
+
svg.style.position = "absolute";
|
|
291
304
|
svg.style.top = "0";
|
|
292
305
|
svg.style.left = "0";
|
|
293
306
|
svg.style.width = "100vw";
|
|
294
307
|
svg.style.height = "100vh";
|
|
295
308
|
svg.style.pointerEvents = "none";
|
|
296
|
-
svg.style.zIndex = "
|
|
309
|
+
svg.style.zIndex = "500";
|
|
297
310
|
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
298
311
|
const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
|
299
312
|
svg.setAttribute("width", viewportWidth.toString());
|
|
@@ -304,47 +317,85 @@
|
|
|
304
317
|
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
305
318
|
rect.setAttribute("x", "0");
|
|
306
319
|
rect.setAttribute("y", "0");
|
|
307
|
-
rect.setAttribute("width",
|
|
320
|
+
rect.setAttribute("width", minWidth.toString());
|
|
308
321
|
rect.setAttribute("height", height.toString());
|
|
309
322
|
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
310
323
|
rect.classList.add("highlight-frame-rect");
|
|
311
|
-
// Apply instance color if it's an instance
|
|
324
|
+
// Apply instance color if it's an instance, otherwise text edit color if in text edit mode
|
|
312
325
|
if (isInstance) {
|
|
313
326
|
rect.setAttribute("stroke", getComponentColor$1());
|
|
314
327
|
}
|
|
328
|
+
else if (isTextEdit) {
|
|
329
|
+
rect.setAttribute("stroke", getTextEditColor$1());
|
|
330
|
+
}
|
|
315
331
|
group.appendChild(rect);
|
|
316
|
-
createCornerHandles(group,
|
|
332
|
+
createCornerHandles(group, minWidth, height, isInstance, isTextEdit);
|
|
317
333
|
svg.appendChild(group);
|
|
318
|
-
|
|
334
|
+
const canvasContainer = getCanvasContainer();
|
|
335
|
+
if (canvasContainer) {
|
|
336
|
+
canvasContainer.appendChild(svg);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
document.body.appendChild(svg);
|
|
340
|
+
}
|
|
319
341
|
return svg;
|
|
320
342
|
};
|
|
321
343
|
|
|
344
|
+
const TAG_NAME_MAP = {
|
|
345
|
+
div: "Container",
|
|
346
|
+
h1: "Heading 1",
|
|
347
|
+
h2: "Heading 2",
|
|
348
|
+
h3: "Heading 3",
|
|
349
|
+
h4: "Heading 4",
|
|
350
|
+
h5: "Heading 5",
|
|
351
|
+
h6: "Heading 6",
|
|
352
|
+
p: "Text",
|
|
353
|
+
li: "List Item",
|
|
354
|
+
ul: "Unordered List",
|
|
355
|
+
ol: "Ordered List",
|
|
356
|
+
img: "Image",
|
|
357
|
+
a: "Link",
|
|
358
|
+
};
|
|
359
|
+
const capitalize = (str) => {
|
|
360
|
+
if (!str)
|
|
361
|
+
return str;
|
|
362
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
363
|
+
};
|
|
322
364
|
const createTagLabel = (node, nodeTools) => {
|
|
323
365
|
const tagLabel = document.createElement("div");
|
|
324
366
|
tagLabel.className = "tag-label";
|
|
325
|
-
|
|
367
|
+
const instanceName = node.getAttribute("data-instance-name");
|
|
368
|
+
const tagName = node.tagName.toLowerCase();
|
|
369
|
+
const labelText = instanceName || TAG_NAME_MAP[tagName] || tagName;
|
|
370
|
+
tagLabel.textContent = capitalize(labelText);
|
|
326
371
|
nodeTools.appendChild(tagLabel);
|
|
327
372
|
};
|
|
328
373
|
|
|
329
|
-
const createToolsContainer = (node, highlightFrame, isInstance = false) => {
|
|
374
|
+
const createToolsContainer = (node, highlightFrame, isInstance = false, isTextEdit = false) => {
|
|
330
375
|
const nodeTools = document.createElement("div");
|
|
331
376
|
nodeTools.className = "node-tools";
|
|
332
377
|
if (isInstance) {
|
|
333
378
|
nodeTools.classList.add("is-instance");
|
|
334
379
|
}
|
|
380
|
+
if (isTextEdit) {
|
|
381
|
+
nodeTools.classList.add("is-text-edit");
|
|
382
|
+
}
|
|
335
383
|
highlightFrame.appendChild(nodeTools);
|
|
336
384
|
createTagLabel(node, nodeTools);
|
|
337
385
|
};
|
|
338
386
|
|
|
339
387
|
function getHighlightFrameElement() {
|
|
340
|
-
|
|
388
|
+
const canvasContainer = getCanvasContainer();
|
|
389
|
+
const container = canvasContainer || document.body;
|
|
390
|
+
return container.querySelector(".highlight-frame-overlay");
|
|
341
391
|
}
|
|
342
392
|
|
|
343
393
|
const highlightNode = (node) => {
|
|
344
394
|
if (!node)
|
|
345
395
|
return;
|
|
346
396
|
const existingHighlightFrame = getHighlightFrameElement();
|
|
347
|
-
const
|
|
397
|
+
const canvasContainer = getCanvasContainer();
|
|
398
|
+
const existingToolsWrapper = canvasContainer?.querySelector(".highlight-frame-tools-wrapper") || document.body.querySelector(".highlight-frame-tools-wrapper");
|
|
348
399
|
if (existingHighlightFrame) {
|
|
349
400
|
existingHighlightFrame.remove();
|
|
350
401
|
}
|
|
@@ -352,38 +403,50 @@
|
|
|
352
403
|
existingToolsWrapper.remove();
|
|
353
404
|
}
|
|
354
405
|
const isInstance = isComponentInstance(node);
|
|
355
|
-
const
|
|
406
|
+
const isTextEdit = node.contentEditable === "true";
|
|
407
|
+
const highlightFrame = createHighlightFrame(node, isInstance, isTextEdit);
|
|
356
408
|
if (node.contentEditable === "true") {
|
|
357
409
|
highlightFrame.classList.add("is-editable");
|
|
358
410
|
}
|
|
359
|
-
//
|
|
411
|
+
// Batch DOM reads
|
|
360
412
|
const { left, top, height } = getScreenBounds(node);
|
|
361
413
|
const bottomY = top + height;
|
|
414
|
+
// Create tools wrapper using CSS transform (GPU-accelerated)
|
|
362
415
|
const toolsWrapper = document.createElement("div");
|
|
363
416
|
toolsWrapper.classList.add("highlight-frame-tools-wrapper");
|
|
364
417
|
if (isInstance) {
|
|
365
418
|
toolsWrapper.classList.add("is-instance");
|
|
366
419
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
toolsWrapper.style.
|
|
371
|
-
toolsWrapper.style.
|
|
420
|
+
if (isTextEdit) {
|
|
421
|
+
toolsWrapper.classList.add("is-text-edit");
|
|
422
|
+
}
|
|
423
|
+
toolsWrapper.style.position = "absolute";
|
|
424
|
+
toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
|
|
425
|
+
toolsWrapper.style.transformOrigin = "left center";
|
|
372
426
|
toolsWrapper.style.pointerEvents = "none";
|
|
373
|
-
toolsWrapper.style.zIndex = "
|
|
374
|
-
createToolsContainer(node, toolsWrapper, isInstance);
|
|
375
|
-
|
|
427
|
+
toolsWrapper.style.zIndex = "500";
|
|
428
|
+
createToolsContainer(node, toolsWrapper, isInstance, isTextEdit);
|
|
429
|
+
if (canvasContainer) {
|
|
430
|
+
canvasContainer.appendChild(toolsWrapper);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
document.body.appendChild(toolsWrapper);
|
|
434
|
+
}
|
|
376
435
|
};
|
|
377
436
|
|
|
378
437
|
const getComponentColor = () => {
|
|
379
438
|
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
380
439
|
};
|
|
440
|
+
const getTextEditColor = () => {
|
|
441
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
|
|
442
|
+
};
|
|
381
443
|
const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
382
444
|
// Batch all DOM reads first (single layout pass)
|
|
383
445
|
const frame = getHighlightFrameElement();
|
|
384
446
|
if (!frame)
|
|
385
447
|
return;
|
|
386
448
|
const isInstance = isComponentInstance(node);
|
|
449
|
+
const isTextEdit = node.contentEditable === "true";
|
|
387
450
|
// Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
|
|
388
451
|
// Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
|
|
389
452
|
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
@@ -397,6 +460,13 @@
|
|
|
397
460
|
else {
|
|
398
461
|
frame.classList.remove("is-instance");
|
|
399
462
|
}
|
|
463
|
+
// Update text edit class
|
|
464
|
+
if (isTextEdit) {
|
|
465
|
+
frame.classList.add("is-text-edit");
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
frame.classList.remove("is-text-edit");
|
|
469
|
+
}
|
|
400
470
|
const group = frame.querySelector(".highlight-frame-group");
|
|
401
471
|
if (!group)
|
|
402
472
|
return;
|
|
@@ -407,15 +477,22 @@
|
|
|
407
477
|
if (isInstance) {
|
|
408
478
|
rect.setAttribute("stroke", getComponentColor());
|
|
409
479
|
}
|
|
480
|
+
else if (isTextEdit) {
|
|
481
|
+
rect.setAttribute("stroke", getTextEditColor());
|
|
482
|
+
}
|
|
410
483
|
else {
|
|
411
484
|
rect.removeAttribute("stroke"); // Use CSS default
|
|
412
485
|
}
|
|
413
|
-
const
|
|
486
|
+
const canvasContainer = getCanvasContainer();
|
|
487
|
+
const container = canvasContainer || document.body;
|
|
488
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
414
489
|
const nodeTools = toolsWrapper?.querySelector(".node-tools");
|
|
415
490
|
const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
|
|
416
491
|
const bounds = getScreenBounds(node);
|
|
417
492
|
// Calculate all values before any DOM writes
|
|
418
493
|
const { top, left, width, height } = bounds;
|
|
494
|
+
// Ensure minimum width of 2px
|
|
495
|
+
const minWidth = Math.max(width, 3);
|
|
419
496
|
const bottomY = top + height;
|
|
420
497
|
// Update instance classes on tools wrapper and node tools
|
|
421
498
|
if (toolsWrapper) {
|
|
@@ -425,6 +502,13 @@
|
|
|
425
502
|
else {
|
|
426
503
|
toolsWrapper.classList.remove("is-instance");
|
|
427
504
|
}
|
|
505
|
+
// Update text edit class
|
|
506
|
+
if (isTextEdit) {
|
|
507
|
+
toolsWrapper.classList.add("is-text-edit");
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
toolsWrapper.classList.remove("is-text-edit");
|
|
511
|
+
}
|
|
428
512
|
}
|
|
429
513
|
if (nodeTools) {
|
|
430
514
|
if (isInstance) {
|
|
@@ -433,12 +517,19 @@
|
|
|
433
517
|
else {
|
|
434
518
|
nodeTools.classList.remove("is-instance");
|
|
435
519
|
}
|
|
520
|
+
// Update text edit class
|
|
521
|
+
if (isTextEdit) {
|
|
522
|
+
nodeTools.classList.add("is-text-edit");
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
nodeTools.classList.remove("is-text-edit");
|
|
526
|
+
}
|
|
436
527
|
}
|
|
437
528
|
// Batch all DOM writes (single paint pass)
|
|
438
529
|
// Update group transform to move entire group (rect + handles) at once
|
|
439
530
|
group.setAttribute("transform", `translate(${left}, ${top})`);
|
|
440
531
|
// Update rect dimensions (position is handled by group transform)
|
|
441
|
-
rect.setAttribute("width",
|
|
532
|
+
rect.setAttribute("width", minWidth.toString());
|
|
442
533
|
rect.setAttribute("height", height.toString());
|
|
443
534
|
// Update corner handles positions (relative to group, so only width/height matter)
|
|
444
535
|
const topLeft = group.querySelector(".handle-top-left");
|
|
@@ -453,6 +544,9 @@
|
|
|
453
544
|
if (isInstance) {
|
|
454
545
|
handle.setAttribute("stroke", getComponentColor());
|
|
455
546
|
}
|
|
547
|
+
else if (isTextEdit) {
|
|
548
|
+
handle.setAttribute("stroke", getTextEditColor());
|
|
549
|
+
}
|
|
456
550
|
else {
|
|
457
551
|
handle.removeAttribute("stroke"); // Use CSS default
|
|
458
552
|
}
|
|
@@ -463,22 +557,22 @@
|
|
|
463
557
|
topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
464
558
|
}
|
|
465
559
|
if (topRight) {
|
|
466
|
-
topRight.setAttribute("x", (
|
|
560
|
+
topRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
467
561
|
topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
468
562
|
}
|
|
469
563
|
if (bottomRight) {
|
|
470
|
-
bottomRight.setAttribute("x", (
|
|
564
|
+
bottomRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
471
565
|
bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
472
566
|
}
|
|
473
567
|
if (bottomLeft) {
|
|
474
568
|
bottomLeft.setAttribute("x", (-HANDLE_SIZE / 2).toString());
|
|
475
569
|
bottomLeft.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
476
570
|
}
|
|
477
|
-
// Update tools wrapper position
|
|
571
|
+
// Update tools wrapper position using CSS transform (GPU-accelerated)
|
|
478
572
|
if (toolsWrapper) {
|
|
479
|
-
toolsWrapper.style.
|
|
480
|
-
toolsWrapper.style.top = `${bottomY}px`;
|
|
573
|
+
toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
|
|
481
574
|
}
|
|
575
|
+
// Update tool opacity
|
|
482
576
|
if (zoom <= 10) {
|
|
483
577
|
nodeProvider.style.setProperty("--tool-opacity", `1`);
|
|
484
578
|
}
|
|
@@ -491,10 +585,14 @@
|
|
|
491
585
|
const frame = getHighlightFrameElement();
|
|
492
586
|
if (!frame)
|
|
493
587
|
return;
|
|
588
|
+
// Batch DOM reads
|
|
494
589
|
const hasHiddenClass = node.classList.contains("hidden") || node.classList.contains("select-none");
|
|
495
590
|
const displayValue = hasHiddenClass ? "none" : "";
|
|
591
|
+
// Batch DOM writes
|
|
496
592
|
frame.style.display = displayValue;
|
|
497
|
-
const
|
|
593
|
+
const canvasContainer = getCanvasContainer();
|
|
594
|
+
const container = canvasContainer || document.body;
|
|
595
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
498
596
|
if (toolsWrapper) {
|
|
499
597
|
toolsWrapper.style.display = displayValue;
|
|
500
598
|
}
|
|
@@ -551,11 +649,64 @@
|
|
|
551
649
|
return mutationObserver;
|
|
552
650
|
};
|
|
553
651
|
|
|
652
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
|
|
653
|
+
function withRAFThrottle(func) {
|
|
654
|
+
let rafId = null;
|
|
655
|
+
let lastArgs = null;
|
|
656
|
+
const throttled = (...args) => {
|
|
657
|
+
lastArgs = args;
|
|
658
|
+
if (rafId === null) {
|
|
659
|
+
rafId = requestAnimationFrame(() => {
|
|
660
|
+
if (lastArgs) {
|
|
661
|
+
func(...lastArgs);
|
|
662
|
+
}
|
|
663
|
+
rafId = null;
|
|
664
|
+
lastArgs = null;
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
throttled.cleanup = () => {
|
|
669
|
+
if (rafId !== null) {
|
|
670
|
+
cancelAnimationFrame(rafId);
|
|
671
|
+
rafId = null;
|
|
672
|
+
lastArgs = null;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
return throttled;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const handleTextChange = (node, mutations) => {
|
|
679
|
+
// Check if any mutation is a text content change
|
|
680
|
+
const hasTextChange = mutations.some((mutation) => {
|
|
681
|
+
return (mutation.type === "characterData" ||
|
|
682
|
+
(mutation.type === "childList" && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)));
|
|
683
|
+
});
|
|
684
|
+
if (!hasTextChange) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
// Get the text content of the node
|
|
688
|
+
const textContent = node.textContent ?? "";
|
|
689
|
+
// Get the node ID
|
|
690
|
+
const nodeId = node.getAttribute("data-node-id");
|
|
691
|
+
// Send postMessage with the text change
|
|
692
|
+
sendPostMessage("textContentChanged", {
|
|
693
|
+
nodeId,
|
|
694
|
+
textContent,
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
|
|
554
698
|
const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
|
|
555
|
-
const
|
|
699
|
+
const throttledHandleTextChange = withRAFThrottle((mutations) => {
|
|
700
|
+
handleTextChange(node, mutations);
|
|
701
|
+
});
|
|
702
|
+
const mutationObserver = connectMutationObserver(node, (mutations) => {
|
|
703
|
+
throttledHandleTextChange(mutations);
|
|
556
704
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
557
705
|
});
|
|
558
|
-
return () =>
|
|
706
|
+
return () => {
|
|
707
|
+
mutationObserver.disconnect();
|
|
708
|
+
throttledHandleTextChange.cleanup();
|
|
709
|
+
};
|
|
559
710
|
};
|
|
560
711
|
|
|
561
712
|
const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
|
|
@@ -640,11 +791,6 @@
|
|
|
640
791
|
let parentMutationObserver = null;
|
|
641
792
|
let selectedNode = null;
|
|
642
793
|
const text = nodeText(canvasName);
|
|
643
|
-
// Combined throttled function for refresh + visibility update
|
|
644
|
-
const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
|
|
645
|
-
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
646
|
-
updateHighlightFrameVisibility(node);
|
|
647
|
-
});
|
|
648
794
|
const handleEscape = () => {
|
|
649
795
|
if (text.isEditing()) {
|
|
650
796
|
text.blurEditMode();
|
|
@@ -673,7 +819,6 @@
|
|
|
673
819
|
mutationObserver?.disconnect();
|
|
674
820
|
parentMutationObserver?.disconnect();
|
|
675
821
|
if (node && nodeProvider) {
|
|
676
|
-
text.enableEditMode(node, nodeProvider);
|
|
677
822
|
// Check if node is still in DOM and handle cleanup if removed
|
|
678
823
|
const checkNodeExists = () => {
|
|
679
824
|
if (!document.contains(node)) {
|
|
@@ -690,9 +835,7 @@
|
|
|
690
835
|
mutationObserver = new MutationObserver(() => {
|
|
691
836
|
checkNodeExists();
|
|
692
837
|
if (!document.contains(node))
|
|
693
|
-
return;
|
|
694
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
695
|
-
console.log("mutationObserver", node);
|
|
838
|
+
return;
|
|
696
839
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
697
840
|
updateHighlightFrameVisibility(node);
|
|
698
841
|
});
|
|
@@ -728,8 +871,6 @@
|
|
|
728
871
|
checkNodeExists();
|
|
729
872
|
if (!document.contains(node))
|
|
730
873
|
return; // Exit early if node was removed
|
|
731
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
732
|
-
console.log("resizeObserver", node);
|
|
733
874
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
734
875
|
updateHighlightFrameVisibility(node);
|
|
735
876
|
});
|
|
@@ -739,17 +880,17 @@
|
|
|
739
880
|
highlightNode(node) ?? null;
|
|
740
881
|
if (node && nodeProvider) {
|
|
741
882
|
updateHighlightFrameVisibility(node);
|
|
883
|
+
updateHighlightFrameVisibility(node);
|
|
742
884
|
}
|
|
743
885
|
};
|
|
744
886
|
// Setup event listener
|
|
745
|
-
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text
|
|
887
|
+
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
|
|
746
888
|
const cleanup = () => {
|
|
747
889
|
removeListeners();
|
|
748
890
|
resizeObserver?.disconnect();
|
|
749
891
|
mutationObserver?.disconnect();
|
|
750
892
|
parentMutationObserver?.disconnect();
|
|
751
893
|
text.blurEditMode();
|
|
752
|
-
throttledRefreshAndVisibility.cleanup();
|
|
753
894
|
// Clear highlight frame and reset selected node
|
|
754
895
|
clearHighlightFrame();
|
|
755
896
|
selectedNode = null;
|
|
@@ -778,10 +919,6 @@
|
|
|
778
919
|
return nodeTools;
|
|
779
920
|
};
|
|
780
921
|
|
|
781
|
-
const getCanvasContainer = () => {
|
|
782
|
-
return document.querySelector(".canvas-container");
|
|
783
|
-
};
|
|
784
|
-
|
|
785
922
|
const DEFAULT_WIDTH = 400;
|
|
786
923
|
const RESIZE_CONFIG = {
|
|
787
924
|
minWidth: 320,
|