@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 +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.9
|
|
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,73 @@ const connectMutationObserver = (element, handler) => {
|
|
|
548
645
|
return mutationObserver;
|
|
549
646
|
};
|
|
550
647
|
|
|
648
|
+
const handleTextChange = (node, mutations) => {
|
|
649
|
+
// Check if any mutation is a text content change
|
|
650
|
+
const hasTextChange = mutations.some((mutation) => {
|
|
651
|
+
return (mutation.type === "characterData" ||
|
|
652
|
+
(mutation.type === "childList" && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)));
|
|
653
|
+
});
|
|
654
|
+
if (!hasTextChange) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
// Get the text content of the node
|
|
658
|
+
const textContent = node.textContent ?? "";
|
|
659
|
+
// Get the node ID
|
|
660
|
+
const nodeId = node.getAttribute("data-node-id");
|
|
661
|
+
console.log("textContentChanged", nodeId, textContent);
|
|
662
|
+
// Send postMessage with the text change
|
|
663
|
+
sendPostMessage("textContentChanged", {
|
|
664
|
+
nodeId,
|
|
665
|
+
textContent,
|
|
666
|
+
});
|
|
667
|
+
};
|
|
668
|
+
|
|
551
669
|
const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
|
|
552
|
-
|
|
670
|
+
// Accumulate mutations instead of replacing them
|
|
671
|
+
let pendingMutations = [];
|
|
672
|
+
let rafId1 = null;
|
|
673
|
+
let rafId2 = null;
|
|
674
|
+
const processMutations = () => {
|
|
675
|
+
if (pendingMutations.length > 0) {
|
|
676
|
+
const mutationsToProcess = [...pendingMutations];
|
|
677
|
+
pendingMutations = [];
|
|
678
|
+
handleTextChange(node, mutationsToProcess);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const scheduleProcess = () => {
|
|
682
|
+
if (rafId1 === null) {
|
|
683
|
+
rafId1 = requestAnimationFrame(() => {
|
|
684
|
+
// First RAF: let browser complete layout
|
|
685
|
+
rafId2 = requestAnimationFrame(() => {
|
|
686
|
+
// Second RAF: read textContent after layout is complete
|
|
687
|
+
processMutations();
|
|
688
|
+
rafId1 = null;
|
|
689
|
+
rafId2 = null;
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
const cleanup = () => {
|
|
695
|
+
if (rafId1 !== null) {
|
|
696
|
+
cancelAnimationFrame(rafId1);
|
|
697
|
+
rafId1 = null;
|
|
698
|
+
}
|
|
699
|
+
if (rafId2 !== null) {
|
|
700
|
+
cancelAnimationFrame(rafId2);
|
|
701
|
+
rafId2 = null;
|
|
702
|
+
}
|
|
703
|
+
pendingMutations = [];
|
|
704
|
+
};
|
|
705
|
+
const mutationObserver = connectMutationObserver(node, (mutations) => {
|
|
706
|
+
// Accumulate mutations instead of replacing
|
|
707
|
+
pendingMutations.push(...mutations);
|
|
708
|
+
scheduleProcess();
|
|
553
709
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
554
710
|
});
|
|
555
|
-
return () =>
|
|
711
|
+
return () => {
|
|
712
|
+
mutationObserver.disconnect();
|
|
713
|
+
cleanup();
|
|
714
|
+
};
|
|
556
715
|
};
|
|
557
716
|
|
|
558
717
|
const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
|
|
@@ -637,11 +796,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
637
796
|
let parentMutationObserver = null;
|
|
638
797
|
let selectedNode = null;
|
|
639
798
|
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
799
|
const handleEscape = () => {
|
|
646
800
|
if (text.isEditing()) {
|
|
647
801
|
text.blurEditMode();
|
|
@@ -670,7 +824,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
670
824
|
mutationObserver?.disconnect();
|
|
671
825
|
parentMutationObserver?.disconnect();
|
|
672
826
|
if (node && nodeProvider) {
|
|
673
|
-
text.enableEditMode(node, nodeProvider);
|
|
674
827
|
// Check if node is still in DOM and handle cleanup if removed
|
|
675
828
|
const checkNodeExists = () => {
|
|
676
829
|
if (!document.contains(node)) {
|
|
@@ -687,9 +840,7 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
687
840
|
mutationObserver = new MutationObserver(() => {
|
|
688
841
|
checkNodeExists();
|
|
689
842
|
if (!document.contains(node))
|
|
690
|
-
return;
|
|
691
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
692
|
-
console.log("mutationObserver", node);
|
|
843
|
+
return;
|
|
693
844
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
694
845
|
updateHighlightFrameVisibility(node);
|
|
695
846
|
});
|
|
@@ -725,8 +876,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
725
876
|
checkNodeExists();
|
|
726
877
|
if (!document.contains(node))
|
|
727
878
|
return; // Exit early if node was removed
|
|
728
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
729
|
-
console.log("resizeObserver", node);
|
|
730
879
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
731
880
|
updateHighlightFrameVisibility(node);
|
|
732
881
|
});
|
|
@@ -740,14 +889,13 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
740
889
|
}
|
|
741
890
|
};
|
|
742
891
|
// Setup event listener
|
|
743
|
-
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text
|
|
892
|
+
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
|
|
744
893
|
const cleanup = () => {
|
|
745
894
|
removeListeners();
|
|
746
895
|
resizeObserver?.disconnect();
|
|
747
896
|
mutationObserver?.disconnect();
|
|
748
897
|
parentMutationObserver?.disconnect();
|
|
749
898
|
text.blurEditMode();
|
|
750
|
-
throttledRefreshAndVisibility.cleanup();
|
|
751
899
|
// Clear highlight frame and reset selected node
|
|
752
900
|
clearHighlightFrame();
|
|
753
901
|
selectedNode = null;
|
|
@@ -776,9 +924,31 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
776
924
|
return nodeTools;
|
|
777
925
|
};
|
|
778
926
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
927
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
|
|
928
|
+
function withRAFThrottle(func) {
|
|
929
|
+
let rafId = null;
|
|
930
|
+
let lastArgs = null;
|
|
931
|
+
const throttled = (...args) => {
|
|
932
|
+
lastArgs = args;
|
|
933
|
+
if (rafId === null) {
|
|
934
|
+
rafId = requestAnimationFrame(() => {
|
|
935
|
+
if (lastArgs) {
|
|
936
|
+
func(...lastArgs);
|
|
937
|
+
}
|
|
938
|
+
rafId = null;
|
|
939
|
+
lastArgs = null;
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
throttled.cleanup = () => {
|
|
944
|
+
if (rafId !== null) {
|
|
945
|
+
cancelAnimationFrame(rafId);
|
|
946
|
+
rafId = null;
|
|
947
|
+
lastArgs = null;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
return throttled;
|
|
951
|
+
}
|
|
782
952
|
|
|
783
953
|
const DEFAULT_WIDTH = 400;
|
|
784
954
|
const RESIZE_CONFIG = {
|