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