@node-edit-utils/core 2.1.6 → 2.1.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.
@@ -1 +1 @@
1
- export { withRAF, withRAFThrottle } from "./withRAF";
1
+ export { withRAF, withRAFThrottle, withDoubleRAF } from "./withRAF";
@@ -2,3 +2,6 @@ export declare function withRAF(operation: () => void): () => void;
2
2
  export declare function withRAFThrottle<T extends (...args: any[]) => void>(func: T): T & {
3
3
  cleanup: () => void;
4
4
  };
5
+ export declare function withDoubleRAF<T extends (...args: any[]) => void>(func: T): T & {
6
+ cleanup: () => void;
7
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Markup Canvas
3
3
  * High-performance markup canvas with zoom and pan capabilities
4
- * @version 2.1.6
4
+ * @version 2.1.8
5
5
  */
6
6
  'use strict';
7
7
 
@@ -30,6 +30,42 @@ function withRAFThrottle(func) {
30
30
  };
31
31
  return throttled;
32
32
  }
33
+ // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
34
+ function withDoubleRAF(func) {
35
+ let rafId1 = null;
36
+ let rafId2 = null;
37
+ let lastArgs = null;
38
+ const throttled = (...args) => {
39
+ lastArgs = args;
40
+ if (rafId1 === null) {
41
+ rafId1 = requestAnimationFrame(() => {
42
+ // First RAF: let browser complete layout
43
+ rafId2 = requestAnimationFrame(() => {
44
+ // Second RAF: read bounds after layout is complete
45
+ if (lastArgs) {
46
+ const currentArgs = lastArgs;
47
+ rafId1 = null;
48
+ rafId2 = null;
49
+ lastArgs = null;
50
+ func(...currentArgs);
51
+ }
52
+ });
53
+ });
54
+ }
55
+ };
56
+ throttled.cleanup = () => {
57
+ if (rafId1 !== null) {
58
+ cancelAnimationFrame(rafId1);
59
+ rafId1 = null;
60
+ }
61
+ if (rafId2 !== null) {
62
+ cancelAnimationFrame(rafId2);
63
+ rafId2 = null;
64
+ }
65
+ lastArgs = null;
66
+ };
67
+ return throttled;
68
+ }
33
69
 
34
70
  const getCanvasWindowValue = (path) => {
35
71
  // biome-ignore lint/suspicious/noExplicitAny: global window extension
@@ -458,11 +494,11 @@ const nodeText = () => {
458
494
  const createNodeTools = (element) => {
459
495
  const nodeProvider = element;
460
496
  let resizeObserver = null;
497
+ let nodeResizeObserver = null;
461
498
  let mutationObserver = null;
462
- let parentMutationObserver = null;
463
499
  let selectedNode = null;
464
500
  const text = nodeText();
465
- const throttledFrameRefresh = withRAFThrottle(refreshHighlightFrame);
501
+ const throttledFrameRefresh = withDoubleRAF(refreshHighlightFrame);
466
502
  const handleEscape = () => {
467
503
  if (text.isEditing()) {
468
504
  text.blurEditMode();
@@ -472,8 +508,8 @@ const createNodeTools = (element) => {
472
508
  clearHighlightFrame(nodeProvider);
473
509
  selectedNode = null;
474
510
  resizeObserver?.disconnect();
511
+ nodeResizeObserver?.disconnect();
475
512
  mutationObserver?.disconnect();
476
- parentMutationObserver?.disconnect();
477
513
  }
478
514
  }
479
515
  };
@@ -488,8 +524,8 @@ const createNodeTools = (element) => {
488
524
  }
489
525
  }
490
526
  resizeObserver?.disconnect();
527
+ nodeResizeObserver?.disconnect();
491
528
  mutationObserver?.disconnect();
492
- parentMutationObserver?.disconnect();
493
529
  if (node && nodeProvider) {
494
530
  text.enableEditMode(node, nodeProvider);
495
531
  resizeObserver = connectResizeObserver(nodeProvider, () => {
@@ -505,17 +541,10 @@ const createNodeTools = (element) => {
505
541
  childList: true,
506
542
  characterData: true,
507
543
  });
508
- const parent = node.parentElement;
509
- if (parent) {
510
- parentMutationObserver = new MutationObserver(() => {
511
- throttledFrameRefresh(node, nodeProvider);
512
- updateHighlightFrameVisibility(node, nodeProvider);
513
- });
514
- parentMutationObserver.observe(parent, {
515
- childList: true,
516
- subtree: true,
517
- });
518
- }
544
+ nodeResizeObserver = connectResizeObserver(node, () => {
545
+ throttledFrameRefresh(node, nodeProvider);
546
+ updateHighlightFrameVisibility(node, nodeProvider);
547
+ });
519
548
  }
520
549
  selectedNode = node;
521
550
  sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
@@ -529,8 +558,8 @@ const createNodeTools = (element) => {
529
558
  const cleanup = () => {
530
559
  removeListeners();
531
560
  resizeObserver?.disconnect();
561
+ nodeResizeObserver?.disconnect();
532
562
  mutationObserver?.disconnect();
533
- parentMutationObserver?.disconnect();
534
563
  text.blurEditMode();
535
564
  throttledFrameRefresh.cleanup();
536
565
  };
@@ -544,8 +573,8 @@ const createNodeTools = (element) => {
544
573
  clearHighlightFrame(nodeProvider);
545
574
  selectedNode = null;
546
575
  resizeObserver?.disconnect();
576
+ nodeResizeObserver?.disconnect();
547
577
  mutationObserver?.disconnect();
548
- parentMutationObserver?.disconnect();
549
578
  },
550
579
  getEditableNode: () => text.getEditableNode(),
551
580
  cleanup,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Markup Canvas
3
3
  * High-performance markup canvas with zoom and pan capabilities
4
- * @version 2.1.6
4
+ * @version 2.1.8
5
5
  */
6
6
  // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
7
7
  function withRAFThrottle(func) {
@@ -28,6 +28,42 @@ function withRAFThrottle(func) {
28
28
  };
29
29
  return throttled;
30
30
  }
31
+ // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
32
+ function withDoubleRAF(func) {
33
+ let rafId1 = null;
34
+ let rafId2 = null;
35
+ let lastArgs = null;
36
+ const throttled = (...args) => {
37
+ lastArgs = args;
38
+ if (rafId1 === null) {
39
+ rafId1 = requestAnimationFrame(() => {
40
+ // First RAF: let browser complete layout
41
+ rafId2 = requestAnimationFrame(() => {
42
+ // Second RAF: read bounds after layout is complete
43
+ if (lastArgs) {
44
+ const currentArgs = lastArgs;
45
+ rafId1 = null;
46
+ rafId2 = null;
47
+ lastArgs = null;
48
+ func(...currentArgs);
49
+ }
50
+ });
51
+ });
52
+ }
53
+ };
54
+ throttled.cleanup = () => {
55
+ if (rafId1 !== null) {
56
+ cancelAnimationFrame(rafId1);
57
+ rafId1 = null;
58
+ }
59
+ if (rafId2 !== null) {
60
+ cancelAnimationFrame(rafId2);
61
+ rafId2 = null;
62
+ }
63
+ lastArgs = null;
64
+ };
65
+ return throttled;
66
+ }
31
67
 
32
68
  const getCanvasWindowValue = (path) => {
33
69
  // biome-ignore lint/suspicious/noExplicitAny: global window extension
@@ -456,11 +492,11 @@ const nodeText = () => {
456
492
  const createNodeTools = (element) => {
457
493
  const nodeProvider = element;
458
494
  let resizeObserver = null;
495
+ let nodeResizeObserver = null;
459
496
  let mutationObserver = null;
460
- let parentMutationObserver = null;
461
497
  let selectedNode = null;
462
498
  const text = nodeText();
463
- const throttledFrameRefresh = withRAFThrottle(refreshHighlightFrame);
499
+ const throttledFrameRefresh = withDoubleRAF(refreshHighlightFrame);
464
500
  const handleEscape = () => {
465
501
  if (text.isEditing()) {
466
502
  text.blurEditMode();
@@ -470,8 +506,8 @@ const createNodeTools = (element) => {
470
506
  clearHighlightFrame(nodeProvider);
471
507
  selectedNode = null;
472
508
  resizeObserver?.disconnect();
509
+ nodeResizeObserver?.disconnect();
473
510
  mutationObserver?.disconnect();
474
- parentMutationObserver?.disconnect();
475
511
  }
476
512
  }
477
513
  };
@@ -486,8 +522,8 @@ const createNodeTools = (element) => {
486
522
  }
487
523
  }
488
524
  resizeObserver?.disconnect();
525
+ nodeResizeObserver?.disconnect();
489
526
  mutationObserver?.disconnect();
490
- parentMutationObserver?.disconnect();
491
527
  if (node && nodeProvider) {
492
528
  text.enableEditMode(node, nodeProvider);
493
529
  resizeObserver = connectResizeObserver(nodeProvider, () => {
@@ -503,17 +539,10 @@ const createNodeTools = (element) => {
503
539
  childList: true,
504
540
  characterData: true,
505
541
  });
506
- const parent = node.parentElement;
507
- if (parent) {
508
- parentMutationObserver = new MutationObserver(() => {
509
- throttledFrameRefresh(node, nodeProvider);
510
- updateHighlightFrameVisibility(node, nodeProvider);
511
- });
512
- parentMutationObserver.observe(parent, {
513
- childList: true,
514
- subtree: true,
515
- });
516
- }
542
+ nodeResizeObserver = connectResizeObserver(node, () => {
543
+ throttledFrameRefresh(node, nodeProvider);
544
+ updateHighlightFrameVisibility(node, nodeProvider);
545
+ });
517
546
  }
518
547
  selectedNode = node;
519
548
  sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
@@ -527,8 +556,8 @@ const createNodeTools = (element) => {
527
556
  const cleanup = () => {
528
557
  removeListeners();
529
558
  resizeObserver?.disconnect();
559
+ nodeResizeObserver?.disconnect();
530
560
  mutationObserver?.disconnect();
531
- parentMutationObserver?.disconnect();
532
561
  text.blurEditMode();
533
562
  throttledFrameRefresh.cleanup();
534
563
  };
@@ -542,8 +571,8 @@ const createNodeTools = (element) => {
542
571
  clearHighlightFrame(nodeProvider);
543
572
  selectedNode = null;
544
573
  resizeObserver?.disconnect();
574
+ nodeResizeObserver?.disconnect();
545
575
  mutationObserver?.disconnect();
546
- parentMutationObserver?.disconnect();
547
576
  },
548
577
  getEditableNode: () => text.getEditableNode(),
549
578
  cleanup,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Markup Canvas
3
3
  * High-performance markup canvas with zoom and pan capabilities
4
- * @version 2.1.6
4
+ * @version 2.1.8
5
5
  */
6
6
  (function (global, factory) {
7
7
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
@@ -34,6 +34,42 @@
34
34
  };
35
35
  return throttled;
36
36
  }
37
+ // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
38
+ function withDoubleRAF(func) {
39
+ let rafId1 = null;
40
+ let rafId2 = null;
41
+ let lastArgs = null;
42
+ const throttled = (...args) => {
43
+ lastArgs = args;
44
+ if (rafId1 === null) {
45
+ rafId1 = requestAnimationFrame(() => {
46
+ // First RAF: let browser complete layout
47
+ rafId2 = requestAnimationFrame(() => {
48
+ // Second RAF: read bounds after layout is complete
49
+ if (lastArgs) {
50
+ const currentArgs = lastArgs;
51
+ rafId1 = null;
52
+ rafId2 = null;
53
+ lastArgs = null;
54
+ func(...currentArgs);
55
+ }
56
+ });
57
+ });
58
+ }
59
+ };
60
+ throttled.cleanup = () => {
61
+ if (rafId1 !== null) {
62
+ cancelAnimationFrame(rafId1);
63
+ rafId1 = null;
64
+ }
65
+ if (rafId2 !== null) {
66
+ cancelAnimationFrame(rafId2);
67
+ rafId2 = null;
68
+ }
69
+ lastArgs = null;
70
+ };
71
+ return throttled;
72
+ }
37
73
 
38
74
  const getCanvasWindowValue = (path) => {
39
75
  // biome-ignore lint/suspicious/noExplicitAny: global window extension
@@ -462,11 +498,11 @@
462
498
  const createNodeTools = (element) => {
463
499
  const nodeProvider = element;
464
500
  let resizeObserver = null;
501
+ let nodeResizeObserver = null;
465
502
  let mutationObserver = null;
466
- let parentMutationObserver = null;
467
503
  let selectedNode = null;
468
504
  const text = nodeText();
469
- const throttledFrameRefresh = withRAFThrottle(refreshHighlightFrame);
505
+ const throttledFrameRefresh = withDoubleRAF(refreshHighlightFrame);
470
506
  const handleEscape = () => {
471
507
  if (text.isEditing()) {
472
508
  text.blurEditMode();
@@ -476,8 +512,8 @@
476
512
  clearHighlightFrame(nodeProvider);
477
513
  selectedNode = null;
478
514
  resizeObserver?.disconnect();
515
+ nodeResizeObserver?.disconnect();
479
516
  mutationObserver?.disconnect();
480
- parentMutationObserver?.disconnect();
481
517
  }
482
518
  }
483
519
  };
@@ -492,8 +528,8 @@
492
528
  }
493
529
  }
494
530
  resizeObserver?.disconnect();
531
+ nodeResizeObserver?.disconnect();
495
532
  mutationObserver?.disconnect();
496
- parentMutationObserver?.disconnect();
497
533
  if (node && nodeProvider) {
498
534
  text.enableEditMode(node, nodeProvider);
499
535
  resizeObserver = connectResizeObserver(nodeProvider, () => {
@@ -509,17 +545,10 @@
509
545
  childList: true,
510
546
  characterData: true,
511
547
  });
512
- const parent = node.parentElement;
513
- if (parent) {
514
- parentMutationObserver = new MutationObserver(() => {
515
- throttledFrameRefresh(node, nodeProvider);
516
- updateHighlightFrameVisibility(node, nodeProvider);
517
- });
518
- parentMutationObserver.observe(parent, {
519
- childList: true,
520
- subtree: true,
521
- });
522
- }
548
+ nodeResizeObserver = connectResizeObserver(node, () => {
549
+ throttledFrameRefresh(node, nodeProvider);
550
+ updateHighlightFrameVisibility(node, nodeProvider);
551
+ });
523
552
  }
524
553
  selectedNode = node;
525
554
  sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
@@ -533,8 +562,8 @@
533
562
  const cleanup = () => {
534
563
  removeListeners();
535
564
  resizeObserver?.disconnect();
565
+ nodeResizeObserver?.disconnect();
536
566
  mutationObserver?.disconnect();
537
- parentMutationObserver?.disconnect();
538
567
  text.blurEditMode();
539
568
  throttledFrameRefresh.cleanup();
540
569
  };
@@ -548,8 +577,8 @@
548
577
  clearHighlightFrame(nodeProvider);
549
578
  selectedNode = null;
550
579
  resizeObserver?.disconnect();
580
+ nodeResizeObserver?.disconnect();
551
581
  mutationObserver?.disconnect();
552
- parentMutationObserver?.disconnect();
553
582
  },
554
583
  getEditableNode: () => text.getEditableNode(),
555
584
  cleanup,
@@ -1 +1 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).MarkupCanvas={})}(this,function(e){"use strict";function t(e){let t=null,n=null;const o=(...o)=>{n=o,null===t&&(t=requestAnimationFrame(()=>{n&&e(...n),t=null,n=null}))};return o.cleanup=()=>{null!==t&&(cancelAnimationFrame(t),t=null,n=null)},o}const n=e=>{const t=window.canvas;return e.reduce((e,t)=>e?.[t],t)};function o(e){return e.querySelector(".highlight-frame")}const r=e=>{if(!e)return;const t=o(e);t&&t.remove()},s=["path","rect","circle","ellipse","polygon","line","polyline","text","text-noci"];let i=[],a=0;const l=(e,t)=>{let n=null;const o=e.clientX,r=e.clientY,l=e.metaKey||e.ctrlKey,d=((e,t)=>{const n=document.elementsFromPoint(e,t);return Array.from(n).reduce((e,t)=>e.found?e:"node-provider"===t.getAttribute("data-role")?(e.found=!0,e):(e.elements.push(t),e),{elements:[],found:!1}).elements})(o,r).filter(e=>!s.includes(e.tagName.toLowerCase())&&!e.classList.contains("select-none"));if(t&&d.includes(t))return t;if(l)return i=[],n=d[0],n;var c,u;u=d,(c=i).length===u.length&&c.every((e,t)=>e===u[t])?a<=d.length&&a++:a=0;return n=d[d.length-1-a],i=d,n},d=(e,t,n,o)=>{const s=e=>{((e,t)=>{if("application"===e.data.source&&"selectedNodeChanged"===e.data.action){const o=e.data.data,r=document.querySelector(`[data-node-id="${o}"]`);if(n=r,n?.classList.contains("select-none"))return void t?.(null);r&&t?.(r)}var n})(e,t)},i=n=>{((e,t,n,o)=>{if(e.preventDefault(),e.stopPropagation(),t&&!t.contains(e.target))return r(t),void o(null);o(l(e,n))})(n,e,o(),t)},a=e=>{"Escape"===e.key&&(e.preventDefault(),e.stopPropagation(),n?.())};return window.addEventListener("message",s),document.addEventListener("click",i),document.addEventListener("keydown",a),()=>{window.removeEventListener("message",s),document.removeEventListener("click",i),document.removeEventListener("keydown",a)}};function c(e,t){const o=e.getBoundingClientRect(),r=t.getBoundingClientRect(),s=o.top-r.top,i=o.left-r.left,a=n(["zoom","current"])??1;return{top:parseFloat((s/a).toFixed(5)),left:parseFloat((i/a).toFixed(5)),width:Math.max(4,parseFloat((o.width/a).toFixed(5))),height:parseFloat((o.height/a).toFixed(5))}}const u=(e,t)=>{const n=document.createElement("div");n.className="node-tools",t.appendChild(n),((e,t)=>{const n=document.createElement("div");n.className="tag-label",n.textContent=e.tagName.toLowerCase(),t.appendChild(n)})(e,n)},m=(e,t)=>{if(!e)return;const r=o(t);r&&r.remove();const s=((e,t)=>{const{top:o,left:r,width:s,height:i}=c(e,t),a=n(["zoom","current"])??1;document.body.style.setProperty("--zoom",a.toString()),document.body.style.setProperty("--stroke-width",(2/a).toFixed(3));const l=document.createElement("div");l.classList.add("highlight-frame"),l.style.setProperty("--frame-top",`${o}px`),l.style.setProperty("--frame-left",`${r}px`),l.style.setProperty("--frame-width",`${s}px`),l.style.setProperty("--frame-height",`${i}px`);const d=document.createElementNS("http://www.w3.org/2000/svg","svg");d.classList.add("highlight-frame-svg");const u=document.createElementNS("http://www.w3.org/2000/svg","rect");return u.setAttribute("x","0"),u.setAttribute("y","0"),u.setAttribute("width","100%"),u.setAttribute("height","100%"),u.classList.add("highlight-frame-rect"),d.appendChild(u),l.appendChild(d),l})(e,t);"true"===e.contentEditable&&s.classList.add("is-editable"),u(e,s),t.appendChild(s)},p=(e,t)=>{const r=o(t),s=n(["zoom","current"])??1;if(!r)return;s>=.3?t.style.setProperty("--tool-opacity","1"):t.style.setProperty("--tool-opacity","0");const{top:i,left:a,width:l,height:d}=c(e,t);r.style.setProperty("--frame-top",`${i}px`),r.style.setProperty("--frame-left",`${a}px`),r.style.setProperty("--frame-width",`${l}px`),r.style.setProperty("--frame-height",`${d}px`)},v=(e,t)=>{const n=o(t);if(!n)return;const r=e.classList.contains("hidden")||e.classList.contains("select-none")?"none":"";n.style.display=r;const s=n.querySelector(".tag-label");s&&(s.style.display=r)},h=e=>{const t=e=>{"Enter"===e.key&&(e.preventDefault(),e.stopPropagation(),(()=>{const e=window.getSelection();if(e&&e.rangeCount>0){const t=e.getRangeAt(0);t.deleteContents();const n=document.createElement("br");t.insertNode(n),t.setStartAfter(n),t.setEndAfter(n),e.removeAllRanges(),e.addRange(t)}})())};return e.addEventListener("keydown",t),()=>{e.removeEventListener("keydown",t)}},y=(e,t)=>{const n=((e,t)=>{const n=new MutationObserver(e=>{t(e)});return n.observe(e,{subtree:!0,childList:!0,characterData:!0}),n})(e,()=>{p(e,t)});return()=>n.disconnect()},f=()=>{let e=null,t=!1,o=null;const r=()=>{if(t||!e)return;t=!0;var r;(r=e).contentEditable="false",r.classList.remove("is-editable"),r.style.outline="none",(()=>{const e=n(["keyboard","disableTextEditMode"]);e?.()})(),o?.(),e=null,t=!1};return{enableEditMode:(t,s)=>{if(e===t)return;e&&e!==t&&r();const i=(e=>Array.from(e.childNodes).some(e=>e.nodeType===Node.TEXT_NODE&&e.textContent?.trim()))(t);i&&(e=t,(e=>{e.contentEditable="true",e.classList.add("is-editable"),e.style.outline="none"})(t),(()=>{const e=n(["keyboard","enableTextEditMode"]);e?.()})(),o=((e,t,n)=>{if(!t)return()=>{};e.addEventListener("blur",n);const o=h(e),r=y(e,t);return()=>{e.removeEventListener("blur",n),o(),r?.()}})(t,s,r))},blurEditMode:r,getEditableNode:()=>e,isEditing:()=>null!==e}},b=320,g=1680,w=[{name:"Mobile",rawValue:390,value:"320px"},{name:"Tablet Portrait",rawValue:768,value:"768px"},{name:"Tablet Landscape",rawValue:1024,value:"1024px"},{name:"Notebook",rawValue:1280,value:"1280px"},{name:"Desktop",rawValue:1680,value:"1680px"}],E=(e,t,n)=>{const o=parseFloat(document.body.dataset.zoom||"1"),r=((e,t)=>{const n=e+Math.round(t);return Math.max(b,Math.min(g,n))})(n,(e.clientX-t)/o);return r},L=(e,t)=>{e.style.setProperty("--container-width",`${t}px`),((e,t)=>{e.querySelectorAll(".resize-preset-button").forEach((e,n)=>{n<w.length&&(w[n].rawValue===t?e.classList.add("is-active"):e.classList.remove("is-active"))})})(e,t)};e.createCanvasObserver=function(){const e=document.querySelector(".transform-layer");if(!e)return{disconnect:()=>{}};const o=t(()=>{(()=>{const e=n(["zoom","current"]);document.body.style.setProperty("--zoom",e.toFixed(5)),document.body.style.setProperty("--stroke-width",(2/e).toFixed(3)),document.body.dataset.zoom=e.toFixed(5),document.body.dataset.strokeWidth=(2/e).toFixed(3)})()}),r=new MutationObserver(()=>{o()});return r.observe(e,{attributes:!0,attributeOldValue:!0,subtree:!0,childList:!0}),{disconnect:function(){o.cleanup(),r.disconnect()}}},e.createNodeTools=e=>{const n=e;let o=null,s=null,i=null,a=null;const l=f(),c=t(p),u=e=>{if(a!==e){if(l.isEditing()){const t=l.getEditableNode();t&&t!==e&&l.blurEditMode()}if(o?.disconnect(),s?.disconnect(),i?.disconnect(),e&&n){l.enableEditMode(e,n),o=((e,t)=>{const n=new ResizeObserver(e=>{t(e)});return n.observe(e),n})(n,()=>{c(e,n)}),s=new MutationObserver(()=>{c(e,n),v(e,n)}),s.observe(e,{attributes:!0,subtree:!0,childList:!0,characterData:!0});const t=e.parentElement;t&&(i=new MutationObserver(()=>{c(e,n),v(e,n)}),i.observe(t,{childList:!0,subtree:!0}))}var t,r;a=e,t="selectedNodeChanged",r=e?.getAttribute("data-node-id")??null,window.parent.postMessage({source:"node-edit-utils",action:t,data:r,timestamp:Date.now()},"*"),m(e,n),e&&n&&v(e,n)}},h=d(n,u,()=>{l.isEditing()&&l.blurEditMode(),a&&n&&(r(n),a=null,o?.disconnect(),s?.disconnect(),i?.disconnect())},l.getEditableNode),y={selectNode:u,getSelectedNode:()=>a,refreshHighlightFrame:()=>{c(a,n)},clearSelectedNode:()=>{r(n),a=null,o?.disconnect(),s?.disconnect(),i?.disconnect()},getEditableNode:()=>l.getEditableNode(),cleanup:()=>{h(),o?.disconnect(),s?.disconnect(),i?.disconnect(),l.blurEditMode(),c.cleanup()}};var b,g;return b="nodeTools",g=y,"undefined"!=typeof window&&(window[b]=g),y},e.createViewport=e=>{const n=document.querySelector(".canvas-container"),o=e.querySelector(".resize-handle");o&&o.remove();const r=(e=>{const t=document.createElement("div");return t.className="resize-handle",e.appendChild(t),t})(e);e.style.setProperty("--container-width","400px"),((e,t,n)=>{const o=document.createElement("div");o.className="resize-presets",w.forEach(e=>{const r=document.createElement("button");r.textContent=e.name,r.className="resize-preset-button",r.addEventListener("click",()=>{n(t,e.rawValue)}),o.appendChild(r)}),e.appendChild(o)})(r,e,L);let s=!1,i=0,a=0;const l=t(t=>{if(!s)return;n&&(n.style.cursor="ew-resize");const o=E(t,i,a);L(e,o)}),d=((e,t,n,o,r)=>(e.addEventListener("mousedown",t),document.addEventListener("mousemove",n),document.addEventListener("mouseup",o),window.addEventListener("blur",r),()=>{e.removeEventListener("mousedown",t),document.removeEventListener("mousemove",n),document.removeEventListener("mouseup",o),window.removeEventListener("blur",r)}))(r,t=>{t.preventDefault(),t.stopPropagation(),s=!0,i=t.clientX,a=e.offsetWidth},l,e=>{e.preventDefault(),e.stopPropagation(),n&&(n.style.cursor="default"),s=!1},()=>{s=!1});return{setWidth:t=>{L(e,t)},cleanup:()=>{s=!1,l?.cleanup(),d(),r.remove()}}}});
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).MarkupCanvas={})}(this,function(e){"use strict";function t(e){let t=null,n=null;const o=(...o)=>{n=o,null===t&&(t=requestAnimationFrame(()=>{n&&e(...n),t=null,n=null}))};return o.cleanup=()=>{null!==t&&(cancelAnimationFrame(t),t=null,n=null)},o}const n=e=>{const t=window.canvas;return e.reduce((e,t)=>e?.[t],t)};const o=(e,t)=>{const n=new ResizeObserver(e=>{t(e)});return n.observe(e),n};function r(e){return e.querySelector(".highlight-frame")}const s=e=>{if(!e)return;const t=r(e);t&&t.remove()},l=["path","rect","circle","ellipse","polygon","line","polyline","text","text-noci"];let a=[],i=0;const d=(e,t)=>{let n=null;const o=e.clientX,r=e.clientY,s=e.metaKey||e.ctrlKey,d=((e,t)=>{const n=document.elementsFromPoint(e,t);return Array.from(n).reduce((e,t)=>e.found?e:"node-provider"===t.getAttribute("data-role")?(e.found=!0,e):(e.elements.push(t),e),{elements:[],found:!1}).elements})(o,r).filter(e=>!l.includes(e.tagName.toLowerCase())&&!e.classList.contains("select-none"));if(t&&d.includes(t))return t;if(s)return a=[],n=d[0],n;var c,u;u=d,(c=a).length===u.length&&c.every((e,t)=>e===u[t])?i<=d.length&&i++:i=0;return n=d[d.length-1-i],a=d,n},c=(e,t,n,o)=>{const r=e=>{((e,t)=>{if("application"===e.data.source&&"selectedNodeChanged"===e.data.action){const o=e.data.data,r=document.querySelector(`[data-node-id="${o}"]`);if(n=r,n?.classList.contains("select-none"))return void t?.(null);r&&t?.(r)}var n})(e,t)},l=n=>{((e,t,n,o)=>{if(e.preventDefault(),e.stopPropagation(),t&&!t.contains(e.target))return s(t),void o(null);o(d(e,n))})(n,e,o(),t)},a=e=>{"Escape"===e.key&&(e.preventDefault(),e.stopPropagation(),n?.())};return window.addEventListener("message",r),document.addEventListener("click",l),document.addEventListener("keydown",a),()=>{window.removeEventListener("message",r),document.removeEventListener("click",l),document.removeEventListener("keydown",a)}};function u(e,t){const o=e.getBoundingClientRect(),r=t.getBoundingClientRect(),s=o.top-r.top,l=o.left-r.left,a=n(["zoom","current"])??1;return{top:parseFloat((s/a).toFixed(5)),left:parseFloat((l/a).toFixed(5)),width:Math.max(4,parseFloat((o.width/a).toFixed(5))),height:parseFloat((o.height/a).toFixed(5))}}const m=(e,t)=>{const n=document.createElement("div");n.className="node-tools",t.appendChild(n),((e,t)=>{const n=document.createElement("div");n.className="tag-label",n.textContent=e.tagName.toLowerCase(),t.appendChild(n)})(e,n)},p=(e,t)=>{if(!e)return;const o=r(t);o&&o.remove();const s=((e,t)=>{const{top:o,left:r,width:s,height:l}=u(e,t),a=n(["zoom","current"])??1;document.body.style.setProperty("--zoom",a.toString()),document.body.style.setProperty("--stroke-width",(2/a).toFixed(3));const i=document.createElement("div");i.classList.add("highlight-frame"),i.style.setProperty("--frame-top",`${o}px`),i.style.setProperty("--frame-left",`${r}px`),i.style.setProperty("--frame-width",`${s}px`),i.style.setProperty("--frame-height",`${l}px`);const d=document.createElementNS("http://www.w3.org/2000/svg","svg");d.classList.add("highlight-frame-svg");const c=document.createElementNS("http://www.w3.org/2000/svg","rect");return c.setAttribute("x","0"),c.setAttribute("y","0"),c.setAttribute("width","100%"),c.setAttribute("height","100%"),c.classList.add("highlight-frame-rect"),d.appendChild(c),i.appendChild(d),i})(e,t);"true"===e.contentEditable&&s.classList.add("is-editable"),m(e,s),t.appendChild(s)},h=(e,t)=>{const o=r(t),s=n(["zoom","current"])??1;if(!o)return;s>=.3?t.style.setProperty("--tool-opacity","1"):t.style.setProperty("--tool-opacity","0");const{top:l,left:a,width:i,height:d}=u(e,t);o.style.setProperty("--frame-top",`${l}px`),o.style.setProperty("--frame-left",`${a}px`),o.style.setProperty("--frame-width",`${i}px`),o.style.setProperty("--frame-height",`${d}px`)},v=(e,t)=>{const n=r(t);if(!n)return;const o=e.classList.contains("hidden")||e.classList.contains("select-none")?"none":"";n.style.display=o;const s=n.querySelector(".tag-label");s&&(s.style.display=o)},f=e=>{const t=e=>{"Enter"===e.key&&(e.preventDefault(),e.stopPropagation(),(()=>{const e=window.getSelection();if(e&&e.rangeCount>0){const t=e.getRangeAt(0);t.deleteContents();const n=document.createElement("br");t.insertNode(n),t.setStartAfter(n),t.setEndAfter(n),e.removeAllRanges(),e.addRange(t)}})())};return e.addEventListener("keydown",t),()=>{e.removeEventListener("keydown",t)}},y=(e,t)=>{const n=((e,t)=>{const n=new MutationObserver(e=>{t(e)});return n.observe(e,{subtree:!0,childList:!0,characterData:!0}),n})(e,()=>{h(e,t)});return()=>n.disconnect()},g=()=>{let e=null,t=!1,o=null;const r=()=>{if(t||!e)return;t=!0;var r;(r=e).contentEditable="false",r.classList.remove("is-editable"),r.style.outline="none",(()=>{const e=n(["keyboard","disableTextEditMode"]);e?.()})(),o?.(),e=null,t=!1};return{enableEditMode:(t,s)=>{if(e===t)return;e&&e!==t&&r();const l=(e=>Array.from(e.childNodes).some(e=>e.nodeType===Node.TEXT_NODE&&e.textContent?.trim()))(t);l&&(e=t,(e=>{e.contentEditable="true",e.classList.add("is-editable"),e.style.outline="none"})(t),(()=>{const e=n(["keyboard","enableTextEditMode"]);e?.()})(),o=((e,t,n)=>{if(!t)return()=>{};e.addEventListener("blur",n);const o=f(e),r=y(e,t);return()=>{e.removeEventListener("blur",n),o(),r?.()}})(t,s,r))},blurEditMode:r,getEditableNode:()=>e,isEditing:()=>null!==e}},b=320,w=1680,E=[{name:"Mobile",rawValue:390,value:"320px"},{name:"Tablet Portrait",rawValue:768,value:"768px"},{name:"Tablet Landscape",rawValue:1024,value:"1024px"},{name:"Notebook",rawValue:1280,value:"1280px"},{name:"Desktop",rawValue:1680,value:"1680px"}],x=(e,t,n)=>{const o=parseFloat(document.body.dataset.zoom||"1"),r=((e,t)=>{const n=e+Math.round(t);return Math.max(b,Math.min(w,n))})(n,(e.clientX-t)/o);return r},L=(e,t)=>{e.style.setProperty("--container-width",`${t}px`),((e,t)=>{e.querySelectorAll(".resize-preset-button").forEach((e,n)=>{n<E.length&&(E[n].rawValue===t?e.classList.add("is-active"):e.classList.remove("is-active"))})})(e,t)};e.createCanvasObserver=function(){const e=document.querySelector(".transform-layer");if(!e)return{disconnect:()=>{}};const o=t(()=>{(()=>{const e=n(["zoom","current"]);document.body.style.setProperty("--zoom",e.toFixed(5)),document.body.style.setProperty("--stroke-width",(2/e).toFixed(3)),document.body.dataset.zoom=e.toFixed(5),document.body.dataset.strokeWidth=(2/e).toFixed(3)})()}),r=new MutationObserver(()=>{o()});return r.observe(e,{attributes:!0,attributeOldValue:!0,subtree:!0,childList:!0}),{disconnect:function(){o.cleanup(),r.disconnect()}}},e.createNodeTools=e=>{const t=e;let n=null,r=null,l=null,a=null;const i=g(),d=function(e){let t=null,n=null,o=null;const r=(...r)=>{o=r,null===t&&(t=requestAnimationFrame(()=>{n=requestAnimationFrame(()=>{if(o){const r=o;t=null,n=null,o=null,e(...r)}})}))};return r.cleanup=()=>{null!==t&&(cancelAnimationFrame(t),t=null),null!==n&&(cancelAnimationFrame(n),n=null),o=null},r}(h),u=e=>{if(a!==e){if(i.isEditing()){const t=i.getEditableNode();t&&t!==e&&i.blurEditMode()}var s,c;n?.disconnect(),r?.disconnect(),l?.disconnect(),e&&t&&(i.enableEditMode(e,t),n=o(t,()=>{d(e,t)}),l=new MutationObserver(()=>{d(e,t),v(e,t)}),l.observe(e,{attributes:!0,subtree:!0,childList:!0,characterData:!0}),r=o(e,()=>{d(e,t),v(e,t)})),a=e,s="selectedNodeChanged",c=e?.getAttribute("data-node-id")??null,window.parent.postMessage({source:"node-edit-utils",action:s,data:c,timestamp:Date.now()},"*"),p(e,t),e&&t&&v(e,t)}},m=c(t,u,()=>{i.isEditing()&&i.blurEditMode(),a&&t&&(s(t),a=null,n?.disconnect(),r?.disconnect(),l?.disconnect())},i.getEditableNode),f={selectNode:u,getSelectedNode:()=>a,refreshHighlightFrame:()=>{d(a,t)},clearSelectedNode:()=>{s(t),a=null,n?.disconnect(),r?.disconnect(),l?.disconnect()},getEditableNode:()=>i.getEditableNode(),cleanup:()=>{m(),n?.disconnect(),r?.disconnect(),l?.disconnect(),i.blurEditMode(),d.cleanup()}};var y,b;return y="nodeTools",b=f,"undefined"!=typeof window&&(window[y]=b),f},e.createViewport=e=>{const n=document.querySelector(".canvas-container"),o=e.querySelector(".resize-handle");o&&o.remove();const r=(e=>{const t=document.createElement("div");return t.className="resize-handle",e.appendChild(t),t})(e);e.style.setProperty("--container-width","400px"),((e,t,n)=>{const o=document.createElement("div");o.className="resize-presets",E.forEach(e=>{const r=document.createElement("button");r.textContent=e.name,r.className="resize-preset-button",r.addEventListener("click",()=>{n(t,e.rawValue)}),o.appendChild(r)}),e.appendChild(o)})(r,e,L);let s=!1,l=0,a=0;const i=t(t=>{if(!s)return;n&&(n.style.cursor="ew-resize");const o=x(t,l,a);L(e,o)}),d=((e,t,n,o,r)=>(e.addEventListener("mousedown",t),document.addEventListener("mousemove",n),document.addEventListener("mouseup",o),window.addEventListener("blur",r),()=>{e.removeEventListener("mousedown",t),document.removeEventListener("mousemove",n),document.removeEventListener("mouseup",o),window.removeEventListener("blur",r)}))(r,t=>{t.preventDefault(),t.stopPropagation(),s=!0,l=t.clientX,a=e.offsetWidth},i,e=>{e.preventDefault(),e.stopPropagation(),n&&(n.style.cursor="default"),s=!1},()=>{s=!1});return{setWidth:t=>{L(e,t)},cleanup:()=>{s=!1,i?.cleanup(),d(),r.remove()}}}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-edit-utils/core",
3
- "version": "2.1.6",
3
+ "version": "2.1.8",
4
4
  "description": "Utilities for editing nodes in a dom tree.",
5
5
  "type": "module",
6
6
  "main": "dist/node-edit-utils.cjs.js",
@@ -1 +1 @@
1
- export { withRAF, withRAFThrottle } from "./withRAF";
1
+ export { withRAF, withRAFThrottle, withDoubleRAF } from "./withRAF";
@@ -37,3 +37,44 @@ export function withRAFThrottle<T extends (...args: any[]) => void>(func: T): T
37
37
 
38
38
  return throttled as T & { cleanup: () => void };
39
39
  }
40
+
41
+ // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
42
+ export function withDoubleRAF<T extends (...args: any[]) => void>(func: T): T & { cleanup: () => void } {
43
+ let rafId1: number | null = null;
44
+ let rafId2: number | null = null;
45
+ let lastArgs: Parameters<T> | null = null;
46
+
47
+ const throttled = (...args: Parameters<T>) => {
48
+ lastArgs = args;
49
+
50
+ if (rafId1 === null) {
51
+ rafId1 = requestAnimationFrame(() => {
52
+ // First RAF: let browser complete layout
53
+ rafId2 = requestAnimationFrame(() => {
54
+ // Second RAF: read bounds after layout is complete
55
+ if (lastArgs) {
56
+ const currentArgs = lastArgs;
57
+ rafId1 = null;
58
+ rafId2 = null;
59
+ lastArgs = null;
60
+ func(...currentArgs);
61
+ }
62
+ });
63
+ });
64
+ }
65
+ };
66
+
67
+ throttled.cleanup = () => {
68
+ if (rafId1 !== null) {
69
+ cancelAnimationFrame(rafId1);
70
+ rafId1 = null;
71
+ }
72
+ if (rafId2 !== null) {
73
+ cancelAnimationFrame(rafId2);
74
+ rafId2 = null;
75
+ }
76
+ lastArgs = null;
77
+ };
78
+
79
+ return throttled as T & { cleanup: () => void };
80
+ }
@@ -1,4 +1,4 @@
1
- import { withRAFThrottle } from "../helpers";
1
+ import { withDoubleRAF } from "../helpers";
2
2
  import { connectResizeObserver } from "../helpers/observer/connectResizeObserver";
3
3
  import { sendPostMessage } from "../post-message/sendPostMessage";
4
4
  import { bindToWindow } from "../window/bindToWindow";
@@ -14,12 +14,12 @@ export const createNodeTools = (element: HTMLElement | null): NodeTools => {
14
14
  const nodeProvider = element;
15
15
 
16
16
  let resizeObserver: ResizeObserver | null = null;
17
+ let nodeResizeObserver: ResizeObserver | null = null;
17
18
  let mutationObserver: MutationObserver | null = null;
18
- let parentMutationObserver: MutationObserver | null = null;
19
19
  let selectedNode: HTMLElement | null = null;
20
20
 
21
21
  const text = nodeText();
22
- const throttledFrameRefresh = withRAFThrottle(refreshHighlightFrame);
22
+ const throttledFrameRefresh = withDoubleRAF(refreshHighlightFrame);
23
23
 
24
24
  const handleEscape = (): void => {
25
25
  if (text.isEditing()) {
@@ -32,8 +32,8 @@ export const createNodeTools = (element: HTMLElement | null): NodeTools => {
32
32
  selectedNode = null;
33
33
 
34
34
  resizeObserver?.disconnect();
35
+ nodeResizeObserver?.disconnect();
35
36
  mutationObserver?.disconnect();
36
- parentMutationObserver?.disconnect();
37
37
  }
38
38
  }
39
39
  };
@@ -51,8 +51,8 @@ export const createNodeTools = (element: HTMLElement | null): NodeTools => {
51
51
  }
52
52
 
53
53
  resizeObserver?.disconnect();
54
+ nodeResizeObserver?.disconnect();
54
55
  mutationObserver?.disconnect();
55
- parentMutationObserver?.disconnect();
56
56
 
57
57
  if (node && nodeProvider) {
58
58
  text.enableEditMode(node, nodeProvider);
@@ -73,19 +73,10 @@ export const createNodeTools = (element: HTMLElement | null): NodeTools => {
73
73
  characterData: true,
74
74
  });
75
75
 
76
- const parent = node.parentElement;
77
-
78
- if (parent) {
79
- parentMutationObserver = new MutationObserver(() => {
80
- throttledFrameRefresh(node, nodeProvider);
81
- updateHighlightFrameVisibility(node, nodeProvider);
82
- });
83
-
84
- parentMutationObserver.observe(parent, {
85
- childList: true,
86
- subtree: true,
87
- });
88
- }
76
+ nodeResizeObserver = connectResizeObserver(node, () => {
77
+ throttledFrameRefresh(node, nodeProvider);
78
+ updateHighlightFrameVisibility(node, nodeProvider);
79
+ });
89
80
  }
90
81
 
91
82
  selectedNode = node;
@@ -103,8 +94,8 @@ export const createNodeTools = (element: HTMLElement | null): NodeTools => {
103
94
  const cleanup = (): void => {
104
95
  removeListeners();
105
96
  resizeObserver?.disconnect();
97
+ nodeResizeObserver?.disconnect();
106
98
  mutationObserver?.disconnect();
107
- parentMutationObserver?.disconnect();
108
99
 
109
100
  text.blurEditMode();
110
101
  throttledFrameRefresh.cleanup();
@@ -120,8 +111,8 @@ export const createNodeTools = (element: HTMLElement | null): NodeTools => {
120
111
  clearHighlightFrame(nodeProvider);
121
112
  selectedNode = null;
122
113
  resizeObserver?.disconnect();
114
+ nodeResizeObserver?.disconnect();
123
115
  mutationObserver?.disconnect();
124
- parentMutationObserver?.disconnect();
125
116
  },
126
117
  getEditableNode: () => text.getEditableNode(),
127
118
  cleanup,