@node-edit-utils/core 2.2.6 → 2.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/node-tools/events/click/handleNodeClick.d.ts +2 -1
- package/dist/lib/node-tools/events/setupEventListener.d.ts +2 -1
- package/dist/lib/node-tools/highlight/createCornerHandles.d.ts +1 -1
- package/dist/lib/node-tools/highlight/createHighlightFrame.d.ts +1 -1
- package/dist/lib/node-tools/highlight/createToolsContainer.d.ts +1 -1
- package/dist/lib/node-tools/select/selectNode.d.ts +2 -1
- package/dist/lib/node-tools/text/helpers/enterTextEditMode.d.ts +2 -0
- package/dist/lib/node-tools/text/helpers/handleTextChange.d.ts +1 -0
- package/dist/lib/node-tools/text/helpers/shouldEnterTextEditMode.d.ts +1 -0
- package/dist/node-edit-utils.cjs.js +227 -90
- package/dist/node-edit-utils.esm.js +227 -90
- package/dist/node-edit-utils.umd.js +227 -90
- package/dist/node-edit-utils.umd.min.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -2
- package/src/index.ts +0 -2
- package/src/lib/node-tools/createNodeTools.ts +3 -16
- package/src/lib/node-tools/events/click/handleNodeClick.ts +3 -2
- package/src/lib/node-tools/events/setupEventListener.ts +3 -2
- package/src/lib/node-tools/highlight/clearHighlightFrame.ts +7 -2
- package/src/lib/node-tools/highlight/createCornerHandles.ts +12 -6
- package/src/lib/node-tools/highlight/createHighlightFrame.ts +25 -7
- package/src/lib/node-tools/highlight/createTagLabel.ts +25 -1
- package/src/lib/node-tools/highlight/createToolsContainer.ts +4 -1
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +5 -1
- package/src/lib/node-tools/highlight/highlightNode.ts +21 -11
- package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +40 -7
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +6 -1
- package/src/lib/node-tools/select/selectNode.ts +24 -5
- package/src/lib/node-tools/text/events/setupMutationObserver.ts +17 -3
- package/src/lib/node-tools/text/helpers/enterTextEditMode.ts +9 -0
- package/src/lib/node-tools/text/helpers/handleTextChange.ts +27 -0
- package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.ts +9 -0
- package/src/lib/styles/styles.css +28 -8
|
@@ -1 +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,38 +399,50 @@ 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
|
}
|
|
355
|
-
//
|
|
407
|
+
// Batch DOM reads
|
|
356
408
|
const { left, top, height } = getScreenBounds(node);
|
|
357
409
|
const bottomY = top + height;
|
|
410
|
+
// Create tools wrapper using CSS transform (GPU-accelerated)
|
|
358
411
|
const toolsWrapper = document.createElement("div");
|
|
359
412
|
toolsWrapper.classList.add("highlight-frame-tools-wrapper");
|
|
360
413
|
if (isInstance) {
|
|
361
414
|
toolsWrapper.classList.add("is-instance");
|
|
362
415
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
toolsWrapper.style.
|
|
367
|
-
toolsWrapper.style.
|
|
416
|
+
if (isTextEdit) {
|
|
417
|
+
toolsWrapper.classList.add("is-text-edit");
|
|
418
|
+
}
|
|
419
|
+
toolsWrapper.style.position = "absolute";
|
|
420
|
+
toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
|
|
421
|
+
toolsWrapper.style.transformOrigin = "left center";
|
|
368
422
|
toolsWrapper.style.pointerEvents = "none";
|
|
369
|
-
toolsWrapper.style.zIndex = "
|
|
370
|
-
createToolsContainer(node, toolsWrapper, isInstance);
|
|
371
|
-
|
|
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
|
+
}
|
|
372
431
|
};
|
|
373
432
|
|
|
374
433
|
const getComponentColor = () => {
|
|
375
434
|
return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
|
|
376
435
|
};
|
|
436
|
+
const getTextEditColor = () => {
|
|
437
|
+
return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
|
|
438
|
+
};
|
|
377
439
|
const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
378
440
|
// Batch all DOM reads first (single layout pass)
|
|
379
441
|
const frame = getHighlightFrameElement();
|
|
380
442
|
if (!frame)
|
|
381
443
|
return;
|
|
382
444
|
const isInstance = isComponentInstance(node);
|
|
445
|
+
const isTextEdit = node.contentEditable === "true";
|
|
383
446
|
// Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
|
|
384
447
|
// Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
|
|
385
448
|
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
@@ -393,6 +456,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
393
456
|
else {
|
|
394
457
|
frame.classList.remove("is-instance");
|
|
395
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
|
+
}
|
|
396
466
|
const group = frame.querySelector(".highlight-frame-group");
|
|
397
467
|
if (!group)
|
|
398
468
|
return;
|
|
@@ -403,15 +473,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
403
473
|
if (isInstance) {
|
|
404
474
|
rect.setAttribute("stroke", getComponentColor());
|
|
405
475
|
}
|
|
476
|
+
else if (isTextEdit) {
|
|
477
|
+
rect.setAttribute("stroke", getTextEditColor());
|
|
478
|
+
}
|
|
406
479
|
else {
|
|
407
480
|
rect.removeAttribute("stroke"); // Use CSS default
|
|
408
481
|
}
|
|
409
|
-
const
|
|
482
|
+
const canvasContainer = getCanvasContainer();
|
|
483
|
+
const container = canvasContainer || document.body;
|
|
484
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
410
485
|
const nodeTools = toolsWrapper?.querySelector(".node-tools");
|
|
411
486
|
const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
|
|
412
487
|
const bounds = getScreenBounds(node);
|
|
413
488
|
// Calculate all values before any DOM writes
|
|
414
489
|
const { top, left, width, height } = bounds;
|
|
490
|
+
// Ensure minimum width of 2px
|
|
491
|
+
const minWidth = Math.max(width, 3);
|
|
415
492
|
const bottomY = top + height;
|
|
416
493
|
// Update instance classes on tools wrapper and node tools
|
|
417
494
|
if (toolsWrapper) {
|
|
@@ -421,6 +498,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
421
498
|
else {
|
|
422
499
|
toolsWrapper.classList.remove("is-instance");
|
|
423
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
|
+
}
|
|
424
508
|
}
|
|
425
509
|
if (nodeTools) {
|
|
426
510
|
if (isInstance) {
|
|
@@ -429,12 +513,19 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
429
513
|
else {
|
|
430
514
|
nodeTools.classList.remove("is-instance");
|
|
431
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
|
+
}
|
|
432
523
|
}
|
|
433
524
|
// Batch all DOM writes (single paint pass)
|
|
434
525
|
// Update group transform to move entire group (rect + handles) at once
|
|
435
526
|
group.setAttribute("transform", `translate(${left}, ${top})`);
|
|
436
527
|
// Update rect dimensions (position is handled by group transform)
|
|
437
|
-
rect.setAttribute("width",
|
|
528
|
+
rect.setAttribute("width", minWidth.toString());
|
|
438
529
|
rect.setAttribute("height", height.toString());
|
|
439
530
|
// Update corner handles positions (relative to group, so only width/height matter)
|
|
440
531
|
const topLeft = group.querySelector(".handle-top-left");
|
|
@@ -449,6 +540,9 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
449
540
|
if (isInstance) {
|
|
450
541
|
handle.setAttribute("stroke", getComponentColor());
|
|
451
542
|
}
|
|
543
|
+
else if (isTextEdit) {
|
|
544
|
+
handle.setAttribute("stroke", getTextEditColor());
|
|
545
|
+
}
|
|
452
546
|
else {
|
|
453
547
|
handle.removeAttribute("stroke"); // Use CSS default
|
|
454
548
|
}
|
|
@@ -459,22 +553,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
|
|
|
459
553
|
topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
460
554
|
}
|
|
461
555
|
if (topRight) {
|
|
462
|
-
topRight.setAttribute("x", (
|
|
556
|
+
topRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
463
557
|
topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
|
|
464
558
|
}
|
|
465
559
|
if (bottomRight) {
|
|
466
|
-
bottomRight.setAttribute("x", (
|
|
560
|
+
bottomRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
|
|
467
561
|
bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
468
562
|
}
|
|
469
563
|
if (bottomLeft) {
|
|
470
564
|
bottomLeft.setAttribute("x", (-HANDLE_SIZE / 2).toString());
|
|
471
565
|
bottomLeft.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
|
|
472
566
|
}
|
|
473
|
-
// Update tools wrapper position
|
|
567
|
+
// Update tools wrapper position using CSS transform (GPU-accelerated)
|
|
474
568
|
if (toolsWrapper) {
|
|
475
|
-
toolsWrapper.style.
|
|
476
|
-
toolsWrapper.style.top = `${bottomY}px`;
|
|
569
|
+
toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
|
|
477
570
|
}
|
|
571
|
+
// Update tool opacity
|
|
478
572
|
if (zoom <= 10) {
|
|
479
573
|
nodeProvider.style.setProperty("--tool-opacity", `1`);
|
|
480
574
|
}
|
|
@@ -487,10 +581,14 @@ const updateHighlightFrameVisibility = (node) => {
|
|
|
487
581
|
const frame = getHighlightFrameElement();
|
|
488
582
|
if (!frame)
|
|
489
583
|
return;
|
|
584
|
+
// Batch DOM reads
|
|
490
585
|
const hasHiddenClass = node.classList.contains("hidden") || node.classList.contains("select-none");
|
|
491
586
|
const displayValue = hasHiddenClass ? "none" : "";
|
|
587
|
+
// Batch DOM writes
|
|
492
588
|
frame.style.display = displayValue;
|
|
493
|
-
const
|
|
589
|
+
const canvasContainer = getCanvasContainer();
|
|
590
|
+
const container = canvasContainer || document.body;
|
|
591
|
+
const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
|
|
494
592
|
if (toolsWrapper) {
|
|
495
593
|
toolsWrapper.style.display = displayValue;
|
|
496
594
|
}
|
|
@@ -547,11 +645,64 @@ const connectMutationObserver = (element, handler) => {
|
|
|
547
645
|
return mutationObserver;
|
|
548
646
|
};
|
|
549
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
|
+
|
|
550
694
|
const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
|
|
551
|
-
const
|
|
695
|
+
const throttledHandleTextChange = withRAFThrottle((mutations) => {
|
|
696
|
+
handleTextChange(node, mutations);
|
|
697
|
+
});
|
|
698
|
+
const mutationObserver = connectMutationObserver(node, (mutations) => {
|
|
699
|
+
throttledHandleTextChange(mutations);
|
|
552
700
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
553
701
|
});
|
|
554
|
-
return () =>
|
|
702
|
+
return () => {
|
|
703
|
+
mutationObserver.disconnect();
|
|
704
|
+
throttledHandleTextChange.cleanup();
|
|
705
|
+
};
|
|
555
706
|
};
|
|
556
707
|
|
|
557
708
|
const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
|
|
@@ -636,11 +787,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
636
787
|
let parentMutationObserver = null;
|
|
637
788
|
let selectedNode = null;
|
|
638
789
|
const text = nodeText(canvasName);
|
|
639
|
-
// Combined throttled function for refresh + visibility update
|
|
640
|
-
const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
|
|
641
|
-
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
642
|
-
updateHighlightFrameVisibility(node);
|
|
643
|
-
});
|
|
644
790
|
const handleEscape = () => {
|
|
645
791
|
if (text.isEditing()) {
|
|
646
792
|
text.blurEditMode();
|
|
@@ -669,7 +815,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
669
815
|
mutationObserver?.disconnect();
|
|
670
816
|
parentMutationObserver?.disconnect();
|
|
671
817
|
if (node && nodeProvider) {
|
|
672
|
-
text.enableEditMode(node, nodeProvider);
|
|
673
818
|
// Check if node is still in DOM and handle cleanup if removed
|
|
674
819
|
const checkNodeExists = () => {
|
|
675
820
|
if (!document.contains(node)) {
|
|
@@ -686,9 +831,7 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
686
831
|
mutationObserver = new MutationObserver(() => {
|
|
687
832
|
checkNodeExists();
|
|
688
833
|
if (!document.contains(node))
|
|
689
|
-
return;
|
|
690
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
691
|
-
console.log("mutationObserver", node);
|
|
834
|
+
return;
|
|
692
835
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
693
836
|
updateHighlightFrameVisibility(node);
|
|
694
837
|
});
|
|
@@ -724,8 +867,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
724
867
|
checkNodeExists();
|
|
725
868
|
if (!document.contains(node))
|
|
726
869
|
return; // Exit early if node was removed
|
|
727
|
-
// throttledRefreshAndVisibility(node, nodeProvider);
|
|
728
|
-
console.log("resizeObserver", node);
|
|
729
870
|
refreshHighlightFrame(node, nodeProvider, canvasName);
|
|
730
871
|
updateHighlightFrameVisibility(node);
|
|
731
872
|
});
|
|
@@ -735,17 +876,17 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
735
876
|
highlightNode(node) ?? null;
|
|
736
877
|
if (node && nodeProvider) {
|
|
737
878
|
updateHighlightFrameVisibility(node);
|
|
879
|
+
updateHighlightFrameVisibility(node);
|
|
738
880
|
}
|
|
739
881
|
};
|
|
740
882
|
// Setup event listener
|
|
741
|
-
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text
|
|
883
|
+
const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
|
|
742
884
|
const cleanup = () => {
|
|
743
885
|
removeListeners();
|
|
744
886
|
resizeObserver?.disconnect();
|
|
745
887
|
mutationObserver?.disconnect();
|
|
746
888
|
parentMutationObserver?.disconnect();
|
|
747
889
|
text.blurEditMode();
|
|
748
|
-
throttledRefreshAndVisibility.cleanup();
|
|
749
890
|
// Clear highlight frame and reset selected node
|
|
750
891
|
clearHighlightFrame();
|
|
751
892
|
selectedNode = null;
|
|
@@ -774,10 +915,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
|
|
|
774
915
|
return nodeTools;
|
|
775
916
|
};
|
|
776
917
|
|
|
777
|
-
const getCanvasContainer = () => {
|
|
778
|
-
return document.querySelector(".canvas-container");
|
|
779
|
-
};
|
|
780
|
-
|
|
781
918
|
const DEFAULT_WIDTH = 400;
|
|
782
919
|
const RESIZE_CONFIG = {
|
|
783
920
|
minWidth: 320,
|