@node-edit-utils/core 2.2.7 → 2.2.9
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 +251 -81
- package/dist/node-edit-utils.esm.js +251 -81
- package/dist/node-edit-utils.umd.js +251 -81
- package/dist/node-edit-utils.umd.min.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -2
- package/src/lib/node-tools/createNodeTools.ts +2 -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 +17 -6
- package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +37 -4
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +4 -1
- package/src/lib/node-tools/select/selectNode.ts +24 -5
- package/src/lib/node-tools/text/events/setupMutationObserver.ts +53 -3
- package/src/lib/node-tools/text/helpers/enterTextEditMode.ts +9 -0
- package/src/lib/node-tools/text/helpers/handleTextChange.ts +29 -0
- package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.ts +9 -0
- package/src/lib/styles/styles.css +23 -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.9
|
|
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,7 +403,8 @@
|
|
|
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
|
}
|
|
@@ -365,24 +417,36 @@
|
|
|
365
417
|
if (isInstance) {
|
|
366
418
|
toolsWrapper.classList.add("is-instance");
|
|
367
419
|
}
|
|
368
|
-
|
|
420
|
+
if (isTextEdit) {
|
|
421
|
+
toolsWrapper.classList.add("is-text-edit");
|
|
422
|
+
}
|
|
423
|
+
toolsWrapper.style.position = "absolute";
|
|
369
424
|
toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
|
|
370
425
|
toolsWrapper.style.transformOrigin = "left center";
|
|
371
426
|
toolsWrapper.style.pointerEvents = "none";
|
|
372
|
-
toolsWrapper.style.zIndex = "
|
|
373
|
-
createToolsContainer(node, toolsWrapper, isInstance);
|
|
374
|
-
|
|
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
|
+
}
|
|
375
435
|
};
|
|
376
436
|
|
|
377
437
|
const getComponentColor = () => {
|
|
378
438
|
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
379
439
|
};
|
|
440
|
+
const getTextEditColor = () => {
|
|
441
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
|
|
442
|
+
};
|
|
380
443
|
const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
381
444
|
// Batch all DOM reads first (single layout pass)
|
|
382
445
|
const frame = getHighlightFrameElement();
|
|
383
446
|
if (!frame)
|
|
384
447
|
return;
|
|
385
448
|
const isInstance = isComponentInstance(node);
|
|
449
|
+
const isTextEdit = node.contentEditable === "true";
|
|
386
450
|
// Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
|
|
387
451
|
// Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
|
|
388
452
|
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
@@ -396,6 +460,13 @@
|
|
|
396
460
|
else {
|
|
397
461
|
frame.classList.remove("is-instance");
|
|
398
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
|
+
}
|
|
399
470
|
const group = frame.querySelector(".highlight-frame-group");
|
|
400
471
|
if (!group)
|
|
401
472
|
return;
|
|
@@ -406,15 +477,22 @@
|
|
|
406
477
|
if (isInstance) {
|
|
407
478
|
rect.setAttribute("stroke", getComponentColor());
|
|
408
479
|
}
|
|
480
|
+
else if (isTextEdit) {
|
|
481
|
+
rect.setAttribute("stroke", getTextEditColor());
|
|
482
|
+
}
|
|
409
483
|
else {
|
|
410
484
|
rect.removeAttribute("stroke"); // Use CSS default
|
|
411
485
|
}
|
|
412
|
-
const
|
|
486
|
+
const canvasContainer = getCanvasContainer();
|
|
487
|
+
const container = canvasContainer || document.body;
|
|
488
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
413
489
|
const nodeTools = toolsWrapper?.querySelector(".node-tools");
|
|
414
490
|
const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
|
|
415
491
|
const bounds = getScreenBounds(node);
|
|
416
492
|
// Calculate all values before any DOM writes
|
|
417
493
|
const { top, left, width, height } = bounds;
|
|
494
|
+
// Ensure minimum width of 2px
|
|
495
|
+
const minWidth = Math.max(width, 3);
|
|
418
496
|
const bottomY = top + height;
|
|
419
497
|
// Update instance classes on tools wrapper and node tools
|
|
420
498
|
if (toolsWrapper) {
|
|
@@ -424,6 +502,13 @@
|
|
|
424
502
|
else {
|
|
425
503
|
toolsWrapper.classList.remove("is-instance");
|
|
426
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
|
+
}
|
|
427
512
|
}
|
|
428
513
|
if (nodeTools) {
|
|
429
514
|
if (isInstance) {
|
|
@@ -432,12 +517,19 @@
|
|
|
432
517
|
else {
|
|
433
518
|
nodeTools.classList.remove("is-instance");
|
|
434
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
|
+
}
|
|
435
527
|
}
|
|
436
528
|
// Batch all DOM writes (single paint pass)
|
|
437
529
|
// Update group transform to move entire group (rect + handles) at once
|
|
438
530
|
group.setAttribute("transform", `translate(${left}, ${top})`);
|
|
439
531
|
// Update rect dimensions (position is handled by group transform)
|
|
440
|
-
rect.setAttribute("width",
|
|
532
|
+
rect.setAttribute("width", minWidth.toString());
|
|
441
533
|
rect.setAttribute("height", height.toString());
|
|
442
534
|
// Update corner handles positions (relative to group, so only width/height matter)
|
|
443
535
|
const topLeft = group.querySelector(".handle-top-left");
|
|
@@ -452,6 +544,9 @@
|
|
|
452
544
|
if (isInstance) {
|
|
453
545
|
handle.setAttribute("stroke", getComponentColor());
|
|
454
546
|
}
|
|
547
|
+
else if (isTextEdit) {
|
|
548
|
+
handle.setAttribute("stroke", getTextEditColor());
|
|
549
|
+
}
|
|
455
550
|
else {
|
|
456
551
|
handle.removeAttribute("stroke"); // Use CSS default
|
|
457
552
|
}
|
|
@@ -462,11 +557,11 @@
|
|
|
462
557
|
topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
463
558
|
}
|
|
464
559
|
if (topRight) {
|
|
465
|
-
topRight.setAttribute("x", (
|
|
560
|
+
topRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
466
561
|
topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
467
562
|
}
|
|
468
563
|
if (bottomRight) {
|
|
469
|
-
bottomRight.setAttribute("x", (
|
|
564
|
+
bottomRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
470
565
|
bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
471
566
|
}
|
|
472
567
|
if (bottomLeft) {
|
|
@@ -495,7 +590,9 @@
|
|
|
495
590
|
const displayValue = hasHiddenClass ? "none" : "";
|
|
496
591
|
// Batch DOM writes
|
|
497
592
|
frame.style.display = displayValue;
|
|
498
|
-
const
|
|
593
|
+
const canvasContainer = getCanvasContainer();
|
|
594
|
+
const container = canvasContainer || document.body;
|
|
595
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
499
596
|
if (toolsWrapper) {
|
|
500
597
|
toolsWrapper.style.display = displayValue;
|
|
501
598
|
}
|
|
@@ -552,11 +649,73 @@
|
|
|
552
649
|
return mutationObserver;
|
|
553
650
|
};
|
|
554
651
|
|
|
652
|
+
const handleTextChange = (node, mutations) => {
|
|
653
|
+
// Check if any mutation is a text content change
|
|
654
|
+
const hasTextChange = mutations.some((mutation) => {
|
|
655
|
+
return (mutation.type === "characterData" ||
|
|
656
|
+
(mutation.type === "childList" && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)));
|
|
657
|
+
});
|
|
658
|
+
if (!hasTextChange) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
// Get the text content of the node
|
|
662
|
+
const textContent = node.textContent ?? "";
|
|
663
|
+
// Get the node ID
|
|
664
|
+
const nodeId = node.getAttribute("data-node-id");
|
|
665
|
+
console.log("textContentChanged", nodeId, textContent);
|
|
666
|
+
// Send postMessage with the text change
|
|
667
|
+
sendPostMessage("textContentChanged", {
|
|
668
|
+
nodeId,
|
|
669
|
+
textContent,
|
|
670
|
+
});
|
|
671
|
+
};
|
|
672
|
+
|
|
555
673
|
const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
|
|
556
|
-
|
|
674
|
+
// Accumulate mutations instead of replacing them
|
|
675
|
+
let pendingMutations = [];
|
|
676
|
+
let rafId1 = null;
|
|
677
|
+
let rafId2 = null;
|
|
678
|
+
const processMutations = () => {
|
|
679
|
+
if (pendingMutations.length > 0) {
|
|
680
|
+
const mutationsToProcess = [...pendingMutations];
|
|
681
|
+
pendingMutations = [];
|
|
682
|
+
handleTextChange(node, mutationsToProcess);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
const scheduleProcess = () => {
|
|
686
|
+
if (rafId1 === null) {
|
|
687
|
+
rafId1 = requestAnimationFrame(() => {
|
|
688
|
+
// First RAF: let browser complete layout
|
|
689
|
+
rafId2 = requestAnimationFrame(() => {
|
|
690
|
+
// Second RAF: read textContent after layout is complete
|
|
691
|
+
processMutations();
|
|
692
|
+
rafId1 = null;
|
|
693
|
+
rafId2 = null;
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
const cleanup = () => {
|
|
699
|
+
if (rafId1 !== null) {
|
|
700
|
+
cancelAnimationFrame(rafId1);
|
|
701
|
+
rafId1 = null;
|
|
702
|
+
}
|
|
703
|
+
if (rafId2 !== null) {
|
|
704
|
+
cancelAnimationFrame(rafId2);
|
|
705
|
+
rafId2 = null;
|
|
706
|
+
}
|
|
707
|
+
pendingMutations = [];
|
|
708
|
+
};
|
|
709
|
+
const mutationObserver = connectMutationObserver(node, (mutations) => {
|
|
710
|
+
// Accumulate mutations instead of replacing
|
|
711
|
+
pendingMutations.push(...mutations);
|
|
712
|
+
scheduleProcess();
|
|
557
713
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
558
714
|
});
|
|
559
|
-
return () =>
|
|
715
|
+
return () => {
|
|
716
|
+
mutationObserver.disconnect();
|
|
717
|
+
cleanup();
|
|
718
|
+
};
|
|
560
719
|
};
|
|
561
720
|
|
|
562
721
|
const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
|
|
@@ -641,11 +800,6 @@
|
|
|
641
800
|
let parentMutationObserver = null;
|
|
642
801
|
let selectedNode = null;
|
|
643
802
|
const text = nodeText(canvasName);
|
|
644
|
-
// Combined throttled function for refresh + visibility update
|
|
645
|
-
const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
|
|
646
|
-
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
647
|
-
updateHighlightFrameVisibility(node);
|
|
648
|
-
});
|
|
649
803
|
const handleEscape = () => {
|
|
650
804
|
if (text.isEditing()) {
|
|
651
805
|
text.blurEditMode();
|
|
@@ -674,7 +828,6 @@
|
|
|
674
828
|
mutationObserver?.disconnect();
|
|
675
829
|
parentMutationObserver?.disconnect();
|
|
676
830
|
if (node && nodeProvider) {
|
|
677
|
-
text.enableEditMode(node, nodeProvider);
|
|
678
831
|
// Check if node is still in DOM and handle cleanup if removed
|
|
679
832
|
const checkNodeExists = () => {
|
|
680
833
|
if (!document.contains(node)) {
|
|
@@ -691,9 +844,7 @@
|
|
|
691
844
|
mutationObserver = new MutationObserver(() => {
|
|
692
845
|
checkNodeExists();
|
|
693
846
|
if (!document.contains(node))
|
|
694
|
-
return;
|
|
695
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
696
|
-
console.log("mutationObserver", node);
|
|
847
|
+
return;
|
|
697
848
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
698
849
|
updateHighlightFrameVisibility(node);
|
|
699
850
|
});
|
|
@@ -729,8 +880,6 @@
|
|
|
729
880
|
checkNodeExists();
|
|
730
881
|
if (!document.contains(node))
|
|
731
882
|
return; // Exit early if node was removed
|
|
732
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
733
|
-
console.log("resizeObserver", node);
|
|
734
883
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
735
884
|
updateHighlightFrameVisibility(node);
|
|
736
885
|
});
|
|
@@ -744,14 +893,13 @@
|
|
|
744
893
|
}
|
|
745
894
|
};
|
|
746
895
|
// Setup event listener
|
|
747
|
-
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text
|
|
896
|
+
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
|
|
748
897
|
const cleanup = () => {
|
|
749
898
|
removeListeners();
|
|
750
899
|
resizeObserver?.disconnect();
|
|
751
900
|
mutationObserver?.disconnect();
|
|
752
901
|
parentMutationObserver?.disconnect();
|
|
753
902
|
text.blurEditMode();
|
|
754
|
-
throttledRefreshAndVisibility.cleanup();
|
|
755
903
|
// Clear highlight frame and reset selected node
|
|
756
904
|
clearHighlightFrame();
|
|
757
905
|
selectedNode = null;
|
|
@@ -780,9 +928,31 @@
|
|
|
780
928
|
return nodeTools;
|
|
781
929
|
};
|
|
782
930
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
931
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
|
|
932
|
+
function withRAFThrottle(func) {
|
|
933
|
+
let rafId = null;
|
|
934
|
+
let lastArgs = null;
|
|
935
|
+
const throttled = (...args) => {
|
|
936
|
+
lastArgs = args;
|
|
937
|
+
if (rafId === null) {
|
|
938
|
+
rafId = requestAnimationFrame(() => {
|
|
939
|
+
if (lastArgs) {
|
|
940
|
+
func(...lastArgs);
|
|
941
|
+
}
|
|
942
|
+
rafId = null;
|
|
943
|
+
lastArgs = null;
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
throttled.cleanup = () => {
|
|
948
|
+
if (rafId !== null) {
|
|
949
|
+
cancelAnimationFrame(rafId);
|
|
950
|
+
rafId = null;
|
|
951
|
+
lastArgs = null;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
return throttled;
|
|
955
|
+
}
|
|
786
956
|
|
|
787
957
|
const DEFAULT_WIDTH = 400;
|
|
788
958
|
const RESIZE_CONFIG = {
|