@node-edit-utils/core 2.2.7 → 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 +217 -82
- package/dist/node-edit-utils.esm.js +217 -82
- package/dist/node-edit-utils.umd.js +217 -82
- 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 +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 +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.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,7 +397,8 @@ 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
|
}
|
|
@@ -359,24 +411,36 @@ const highlightNode = (node) => {
|
|
|
359
411
|
if (isInstance) {
|
|
360
412
|
toolsWrapper.classList.add("is-instance");
|
|
361
413
|
}
|
|
362
|
-
|
|
414
|
+
if (isTextEdit) {
|
|
415
|
+
toolsWrapper.classList.add("is-text-edit");
|
|
416
|
+
}
|
|
417
|
+
toolsWrapper.style.position = "absolute";
|
|
363
418
|
toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
|
|
364
419
|
toolsWrapper.style.transformOrigin = "left center";
|
|
365
420
|
toolsWrapper.style.pointerEvents = "none";
|
|
366
|
-
toolsWrapper.style.zIndex = "
|
|
367
|
-
createToolsContainer(node, toolsWrapper, isInstance);
|
|
368
|
-
|
|
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
|
+
}
|
|
369
429
|
};
|
|
370
430
|
|
|
371
431
|
const getComponentColor = () => {
|
|
372
432
|
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
373
433
|
};
|
|
434
|
+
const getTextEditColor = () => {
|
|
435
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
|
|
436
|
+
};
|
|
374
437
|
const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
375
438
|
// Batch all DOM reads first (single layout pass)
|
|
376
439
|
const frame = getHighlightFrameElement();
|
|
377
440
|
if (!frame)
|
|
378
441
|
return;
|
|
379
442
|
const isInstance = isComponentInstance(node);
|
|
443
|
+
const isTextEdit = node.contentEditable === "true";
|
|
380
444
|
// Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
|
|
381
445
|
// Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
|
|
382
446
|
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
@@ -390,6 +454,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
390
454
|
else {
|
|
391
455
|
frame.classList.remove("is-instance");
|
|
392
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
|
+
}
|
|
393
464
|
const group = frame.querySelector(".highlight-frame-group");
|
|
394
465
|
if (!group)
|
|
395
466
|
return;
|
|
@@ -400,15 +471,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
400
471
|
if (isInstance) {
|
|
401
472
|
rect.setAttribute("stroke", getComponentColor());
|
|
402
473
|
}
|
|
474
|
+
else if (isTextEdit) {
|
|
475
|
+
rect.setAttribute("stroke", getTextEditColor());
|
|
476
|
+
}
|
|
403
477
|
else {
|
|
404
478
|
rect.removeAttribute("stroke"); // Use CSS default
|
|
405
479
|
}
|
|
406
|
-
const
|
|
480
|
+
const canvasContainer = getCanvasContainer();
|
|
481
|
+
const container = canvasContainer || document.body;
|
|
482
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
407
483
|
const nodeTools = toolsWrapper?.querySelector(".node-tools");
|
|
408
484
|
const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
|
|
409
485
|
const bounds = getScreenBounds(node);
|
|
410
486
|
// Calculate all values before any DOM writes
|
|
411
487
|
const { top, left, width, height } = bounds;
|
|
488
|
+
// Ensure minimum width of 2px
|
|
489
|
+
const minWidth = Math.max(width, 3);
|
|
412
490
|
const bottomY = top + height;
|
|
413
491
|
// Update instance classes on tools wrapper and node tools
|
|
414
492
|
if (toolsWrapper) {
|
|
@@ -418,6 +496,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
418
496
|
else {
|
|
419
497
|
toolsWrapper.classList.remove("is-instance");
|
|
420
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
|
+
}
|
|
421
506
|
}
|
|
422
507
|
if (nodeTools) {
|
|
423
508
|
if (isInstance) {
|
|
@@ -426,12 +511,19 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
426
511
|
else {
|
|
427
512
|
nodeTools.classList.remove("is-instance");
|
|
428
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
|
+
}
|
|
429
521
|
}
|
|
430
522
|
// Batch all DOM writes (single paint pass)
|
|
431
523
|
// Update group transform to move entire group (rect + handles) at once
|
|
432
524
|
group.setAttribute("transform", `translate(${left}, ${top})`);
|
|
433
525
|
// Update rect dimensions (position is handled by group transform)
|
|
434
|
-
rect.setAttribute("width",
|
|
526
|
+
rect.setAttribute("width", minWidth.toString());
|
|
435
527
|
rect.setAttribute("height", height.toString());
|
|
436
528
|
// Update corner handles positions (relative to group, so only width/height matter)
|
|
437
529
|
const topLeft = group.querySelector(".handle-top-left");
|
|
@@ -446,6 +538,9 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
446
538
|
if (isInstance) {
|
|
447
539
|
handle.setAttribute("stroke", getComponentColor());
|
|
448
540
|
}
|
|
541
|
+
else if (isTextEdit) {
|
|
542
|
+
handle.setAttribute("stroke", getTextEditColor());
|
|
543
|
+
}
|
|
449
544
|
else {
|
|
450
545
|
handle.removeAttribute("stroke"); // Use CSS default
|
|
451
546
|
}
|
|
@@ -456,11 +551,11 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
456
551
|
topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
457
552
|
}
|
|
458
553
|
if (topRight) {
|
|
459
|
-
topRight.setAttribute("x", (
|
|
554
|
+
topRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
460
555
|
topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
461
556
|
}
|
|
462
557
|
if (bottomRight) {
|
|
463
|
-
bottomRight.setAttribute("x", (
|
|
558
|
+
bottomRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
464
559
|
bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
465
560
|
}
|
|
466
561
|
if (bottomLeft) {
|
|
@@ -489,7 +584,9 @@ const updateHighlightFrameVisibility = (node) => {
|
|
|
489
584
|
const displayValue = hasHiddenClass ? "none" : "";
|
|
490
585
|
// Batch DOM writes
|
|
491
586
|
frame.style.display = displayValue;
|
|
492
|
-
const
|
|
587
|
+
const canvasContainer = getCanvasContainer();
|
|
588
|
+
const container = canvasContainer || document.body;
|
|
589
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
493
590
|
if (toolsWrapper) {
|
|
494
591
|
toolsWrapper.style.display = displayValue;
|
|
495
592
|
}
|
|
@@ -546,11 +643,64 @@ const connectMutationObserver = (element, handler) => {
|
|
|
546
643
|
return mutationObserver;
|
|
547
644
|
};
|
|
548
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
|
+
|
|
549
692
|
const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
|
|
550
|
-
const
|
|
693
|
+
const throttledHandleTextChange = withRAFThrottle((mutations) => {
|
|
694
|
+
handleTextChange(node, mutations);
|
|
695
|
+
});
|
|
696
|
+
const mutationObserver = connectMutationObserver(node, (mutations) => {
|
|
697
|
+
throttledHandleTextChange(mutations);
|
|
551
698
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
552
699
|
});
|
|
553
|
-
return () =>
|
|
700
|
+
return () => {
|
|
701
|
+
mutationObserver.disconnect();
|
|
702
|
+
throttledHandleTextChange.cleanup();
|
|
703
|
+
};
|
|
554
704
|
};
|
|
555
705
|
|
|
556
706
|
const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
|
|
@@ -635,11 +785,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
635
785
|
let parentMutationObserver = null;
|
|
636
786
|
let selectedNode = null;
|
|
637
787
|
const text = nodeText(canvasName);
|
|
638
|
-
// Combined throttled function for refresh + visibility update
|
|
639
|
-
const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
|
|
640
|
-
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
641
|
-
updateHighlightFrameVisibility(node);
|
|
642
|
-
});
|
|
643
788
|
const handleEscape = () => {
|
|
644
789
|
if (text.isEditing()) {
|
|
645
790
|
text.blurEditMode();
|
|
@@ -668,7 +813,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
668
813
|
mutationObserver?.disconnect();
|
|
669
814
|
parentMutationObserver?.disconnect();
|
|
670
815
|
if (node && nodeProvider) {
|
|
671
|
-
text.enableEditMode(node, nodeProvider);
|
|
672
816
|
// Check if node is still in DOM and handle cleanup if removed
|
|
673
817
|
const checkNodeExists = () => {
|
|
674
818
|
if (!document.contains(node)) {
|
|
@@ -685,9 +829,7 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
685
829
|
mutationObserver = new MutationObserver(() => {
|
|
686
830
|
checkNodeExists();
|
|
687
831
|
if (!document.contains(node))
|
|
688
|
-
return;
|
|
689
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
690
|
-
console.log("mutationObserver", node);
|
|
832
|
+
return;
|
|
691
833
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
692
834
|
updateHighlightFrameVisibility(node);
|
|
693
835
|
});
|
|
@@ -723,8 +865,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
723
865
|
checkNodeExists();
|
|
724
866
|
if (!document.contains(node))
|
|
725
867
|
return; // Exit early if node was removed
|
|
726
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
727
|
-
console.log("resizeObserver", node);
|
|
728
868
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
729
869
|
updateHighlightFrameVisibility(node);
|
|
730
870
|
});
|
|
@@ -738,14 +878,13 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
738
878
|
}
|
|
739
879
|
};
|
|
740
880
|
// Setup event listener
|
|
741
|
-
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text
|
|
881
|
+
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
|
|
742
882
|
const cleanup = () => {
|
|
743
883
|
removeListeners();
|
|
744
884
|
resizeObserver?.disconnect();
|
|
745
885
|
mutationObserver?.disconnect();
|
|
746
886
|
parentMutationObserver?.disconnect();
|
|
747
887
|
text.blurEditMode();
|
|
748
|
-
throttledRefreshAndVisibility.cleanup();
|
|
749
888
|
// Clear highlight frame and reset selected node
|
|
750
889
|
clearHighlightFrame();
|
|
751
890
|
selectedNode = null;
|
|
@@ -774,10 +913,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
774
913
|
return nodeTools;
|
|
775
914
|
};
|
|
776
915
|
|
|
777
|
-
const getCanvasContainer = () => {
|
|
778
|
-
return document.querySelector(".canvas-container");
|
|
779
|
-
};
|
|
780
|
-
|
|
781
916
|
const DEFAULT_WIDTH = 400;
|
|
782
917
|
const RESIZE_CONFIG = {
|
|
783
918
|
minWidth: 320,
|