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