@aznabee/freehand-ui 0.1.6 → 0.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.
@@ -312,6 +312,10 @@ var DEFAULT_OPTIONS = {
312
312
  */
313
313
  zIndex: null
314
314
  };
315
+ var OVERLAY_CLASS = "doodle-ui-overlay";
316
+ function isOverlayNode(node) {
317
+ return node?.classList?.contains(OVERLAY_CLASS) ?? false;
318
+ }
315
319
  function resolveElement(target) {
316
320
  if (!target) return null;
317
321
  if (typeof target === "string") {
@@ -399,20 +403,6 @@ function isValidPosition(position) {
399
403
  "center"
400
404
  ].includes(position);
401
405
  }
402
- function getScrollableAncestors(element) {
403
- const ancestors = [];
404
- let node = element.parentElement;
405
- while (node && node !== document.documentElement) {
406
- const style = getComputedStyle(node);
407
- const overflow = style.overflow + style.overflowY + style.overflowX;
408
- if (/(auto|scroll|overlay)/.test(overflow)) {
409
- ancestors.push(node);
410
- }
411
- node = node.parentElement;
412
- }
413
- ancestors.push(window);
414
- return ancestors;
415
- }
416
406
  function resolveOverlayZIndex(element, override) {
417
407
  if (override != null) return override;
418
408
  const { zIndex, position } = getComputedStyle(element);
@@ -432,12 +422,20 @@ function resolveOverlayZIndex(element, override) {
432
422
  return null;
433
423
  }
434
424
  function insertOverlaySvg(svg, element) {
435
- const parent = element.parentNode;
436
- if (!parent) {
437
- document.body.appendChild(svg);
438
- return;
425
+ const setPosition = getComputedStyle(element).position === "static";
426
+ if (setPosition) {
427
+ element.style.position = "relative";
428
+ }
429
+ element.appendChild(svg);
430
+ return setPosition;
431
+ }
432
+ function removeOverlaySvg(svg, element, clearPosition) {
433
+ if (svg.parentNode) {
434
+ svg.parentNode.removeChild(svg);
435
+ }
436
+ if (clearPosition) {
437
+ element.style.position = "";
439
438
  }
440
- parent.insertBefore(svg, element.nextSibling);
441
439
  }
442
440
  function syncOverlayStacking(svg, element, zIndexOverride) {
443
441
  const zIndex = resolveOverlayZIndex(element, zIndexOverride);
@@ -464,7 +462,10 @@ function averageCornerRadius(element) {
464
462
  function detectBorderRadius(element) {
465
463
  const own = averageCornerRadius(element);
466
464
  if (own > 0) return own;
467
- const child = element.children.length === 1 ? element.firstElementChild : null;
465
+ const content = Array.from(element.children).filter(
466
+ (node) => !isOverlayNode(node)
467
+ );
468
+ const child = content.length === 1 ? content[0] : null;
468
469
  if (!child) return 0;
469
470
  const outer = element.getBoundingClientRect();
470
471
  const inner = child.getBoundingClientRect();
@@ -495,22 +496,6 @@ function relativeRect(outer, inner) {
495
496
  radius: inner.radius
496
497
  };
497
498
  }
498
- function unionRects(rects, margin = 0) {
499
- if (rects.length === 0) {
500
- return { x: 0, y: 0, width: 0, height: 0, radius: 0 };
501
- }
502
- const minX = Math.min(...rects.map((r) => r.x)) - margin;
503
- const minY = Math.min(...rects.map((r) => r.y)) - margin;
504
- const maxX = Math.max(...rects.map((r) => r.x + r.width)) + margin;
505
- const maxY = Math.max(...rects.map((r) => r.y + r.height)) + margin;
506
- return {
507
- x: minX,
508
- y: minY,
509
- width: maxX - minX,
510
- height: maxY - minY,
511
- radius: 0
512
- };
513
- }
514
499
  function inflateRect(rect, amount) {
515
500
  return {
516
501
  x: rect.x - amount,
@@ -1320,19 +1305,18 @@ function placeNoteText(note, box, style, seed, options = {}) {
1320
1305
  }
1321
1306
  function createOverlaySvg(left, top, width, height) {
1322
1307
  const svg = document.createElementNS(SVG_NS, "svg");
1323
- svg.setAttribute("class", "doodle-ui-overlay");
1308
+ svg.setAttribute("class", OVERLAY_CLASS);
1324
1309
  svg.setAttribute("xmlns", SVG_NS);
1325
1310
  svg.setAttribute("width", String(width));
1326
1311
  svg.setAttribute("height", String(height));
1327
1312
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1328
- svg.style.position = "fixed";
1313
+ svg.style.position = "absolute";
1329
1314
  svg.style.left = `${left}px`;
1330
1315
  svg.style.top = `${top}px`;
1331
1316
  svg.style.width = `${width}px`;
1332
1317
  svg.style.height = `${height}px`;
1333
1318
  svg.style.pointerEvents = "none";
1334
1319
  svg.style.overflow = "visible";
1335
- svg.style.willChange = "transform";
1336
1320
  return svg;
1337
1321
  }
1338
1322
 
@@ -1863,13 +1847,27 @@ var instances = /* @__PURE__ */ new WeakMap();
1863
1847
  function now() {
1864
1848
  return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1865
1849
  }
1866
- function localBand(overlayRect, inset = 8) {
1850
+ function localBand(originLeft, inset = 8) {
1867
1851
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1868
1852
  return {
1869
- x: -overlayRect.x + inset,
1853
+ x: -originLeft + inset,
1870
1854
  width: Math.max(0, width - inset * 2)
1871
1855
  };
1872
1856
  }
1857
+ function paddingBox(element) {
1858
+ const rect = element.getBoundingClientRect();
1859
+ const style = getComputedStyle(element);
1860
+ const left = parseFloat(style.borderLeftWidth) || 0;
1861
+ const top = parseFloat(style.borderTopWidth) || 0;
1862
+ const right = parseFloat(style.borderRightWidth) || 0;
1863
+ const bottom = parseFloat(style.borderBottomWidth) || 0;
1864
+ return {
1865
+ left: rect.left + left,
1866
+ top: rect.top + top,
1867
+ width: Math.max(0, rect.width - left - right),
1868
+ height: Math.max(0, rect.height - top - bottom)
1869
+ };
1870
+ }
1873
1871
  var DoodleOverlay = class {
1874
1872
  /**
1875
1873
  * @param {Element} element
@@ -1881,13 +1879,11 @@ var DoodleOverlay = class {
1881
1879
  this.seed = Math.floor(Math.random() * 1e9);
1882
1880
  this.startedAt = now();
1883
1881
  this.svg = null;
1882
+ this.setPosition = false;
1884
1883
  this.resizeObserver = null;
1885
1884
  this.mutationObserver = null;
1886
1885
  this.pendingFrame = null;
1887
- this.pendingRepositionFrame = null;
1888
- this.basePosition = { x: 0, y: 0 };
1889
1886
  this.listeners = [];
1890
- this.onScroll = this.scheduleReposition.bind(this);
1891
1887
  this.onResize = this.scheduleUpdate.bind(this);
1892
1888
  this.onLanguageChange = this.scheduleUpdate.bind(this);
1893
1889
  this.childElements = [];
@@ -1895,23 +1891,22 @@ var DoodleOverlay = class {
1895
1891
  }
1896
1892
  mount() {
1897
1893
  this.svg = createOverlaySvg(0, 0, 0, 0);
1898
- insertOverlaySvg(this.svg, this.element);
1894
+ this.setPosition = insertOverlaySvg(this.svg, this.element);
1899
1895
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1900
1896
  this.resizeObserver = new ResizeObserver(this.onResize);
1901
1897
  this.resizeObserver.observe(this.element);
1902
1898
  if (this.options.children) {
1903
1899
  this.observeChildren();
1904
1900
  }
1905
- this.mutationObserver = new MutationObserver(this.scheduleUpdate.bind(this));
1901
+ this.mutationObserver = new MutationObserver((records) => {
1902
+ const relevant = records.some((record) => !this.svg.contains(record.target));
1903
+ if (relevant) this.scheduleUpdate();
1904
+ });
1906
1905
  this.mutationObserver.observe(this.element, {
1907
1906
  childList: true,
1908
1907
  subtree: true,
1909
1908
  attributes: true
1910
1909
  });
1911
- for (const target of getScrollableAncestors(this.element)) {
1912
- target.addEventListener("scroll", this.onScroll, { passive: true });
1913
- this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1914
- }
1915
1910
  window.addEventListener("resize", this.onResize, { passive: true });
1916
1911
  this.listeners.push({
1917
1912
  target: window,
@@ -1947,46 +1942,6 @@ var DoodleOverlay = class {
1947
1942
  this.update();
1948
1943
  });
1949
1944
  }
1950
- scheduleReposition() {
1951
- if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1952
- return;
1953
- }
1954
- this.pendingRepositionFrame = scheduleFrame(() => {
1955
- this.pendingRepositionFrame = null;
1956
- this.reposition();
1957
- });
1958
- }
1959
- /**
1960
- * Cheap scroll-driven path: slide the overlay to the element's new
1961
- * viewport position without touching its contents. Scrolling can't change
1962
- * an element's size, so the rects, viewBox and every hand-drawn stroke
1963
- * inside them are still valid - only the overlay's position needs to move.
1964
- *
1965
- * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1966
- * same box-geometry pass as layout, so the browser re-checks layout on
1967
- * every write; `transform` is composited, so the scroll path never
1968
- * touches layout at all.
1969
- */
1970
- reposition() {
1971
- if (!this.svg || !this.element.isConnected) return;
1972
- const { padding, radius } = this.options;
1973
- const targets = this.getTargets();
1974
- const margin = this.overlayMargin();
1975
- const absoluteRects = targets.map(
1976
- (target) => getElementBounds(target, padding, radius)
1977
- );
1978
- const overlayRect = unionRects(absoluteRects, margin);
1979
- const currentWidth = parseFloat(this.svg.style.width) || 0;
1980
- const currentHeight = parseFloat(this.svg.style.height) || 0;
1981
- const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1982
- if (resized) {
1983
- this.update();
1984
- return;
1985
- }
1986
- const dx = overlayRect.x - this.basePosition.x;
1987
- const dy = overlayRect.y - this.basePosition.y;
1988
- this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1989
- }
1990
1945
  getTargets() {
1991
1946
  if (this.options.children) {
1992
1947
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -1998,23 +1953,23 @@ var DoodleOverlay = class {
1998
1953
  if (!this.svg || !this.element.isConnected) return;
1999
1954
  const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
2000
1955
  const targets = this.getTargets();
2001
- const margin = this.overlayMargin();
2002
1956
  const elapsed = now() - this.startedAt;
2003
1957
  const absoluteRects = targets.map(
2004
1958
  (target) => getElementBounds(target, padding, radius)
2005
1959
  );
2006
- const overlayRect = unionRects(absoluteRects, margin);
2007
1960
  clearSvg(this.svg);
2008
1961
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
2009
- this.svg.setAttribute("width", String(overlayRect.width));
2010
- this.svg.setAttribute("height", String(overlayRect.height));
2011
- this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
2012
- this.svg.style.left = `${overlayRect.x}px`;
2013
- this.svg.style.top = `${overlayRect.y}px`;
2014
- this.svg.style.width = `${overlayRect.width}px`;
2015
- this.svg.style.height = `${overlayRect.height}px`;
2016
- this.svg.style.transform = "";
2017
- this.basePosition = { x: overlayRect.x, y: overlayRect.y };
1962
+ const box = paddingBox(this.element);
1963
+ const width = Math.max(1, box.width);
1964
+ const height = Math.max(1, box.height);
1965
+ const origin = { x: box.left, y: box.top };
1966
+ this.svg.setAttribute("width", String(width));
1967
+ this.svg.setAttribute("height", String(height));
1968
+ this.svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1969
+ this.svg.style.left = "0px";
1970
+ this.svg.style.top = "0px";
1971
+ this.svg.style.width = `${width}px`;
1972
+ this.svg.style.height = `${height}px`;
2018
1973
  const strokeStyle = { color, strokeWidth, roughness, opacity };
2019
1974
  const noteStyle = {
2020
1975
  color,
@@ -2025,12 +1980,12 @@ var DoodleOverlay = class {
2025
1980
  };
2026
1981
  const noteGroups = [];
2027
1982
  const noteLayout = {
2028
- band: localBand(overlayRect),
1983
+ band: localBand(origin.x),
2029
1984
  gap: this.options.arrow ? 40 : 14
2030
1985
  };
2031
1986
  targets.forEach((target, index) => {
2032
1987
  const absolute = absoluteRects[index];
2033
- const local = relativeRect(overlayRect, absolute);
1988
+ const local = relativeRect(origin, absolute);
2034
1989
  const seed = this.seed + index * 31;
2035
1990
  const note = this.options.note && index === 0 ? renderAnnotation(
2036
1991
  this.svg,
@@ -2076,24 +2031,11 @@ var DoodleOverlay = class {
2076
2031
  this.svg.appendChild(group);
2077
2032
  }
2078
2033
  }
2079
- /**
2080
- * Room the overlay needs beyond the element for notes, arrows and decorations.
2081
- */
2082
- overlayMargin() {
2083
- const { note, arrow, decorations } = this.options;
2084
- if (!note && !arrow && !decorations) return 48;
2085
- const { text } = resolveLocalizedText(note?.text, note?.locale);
2086
- return Math.max(96, Math.min(240, 80 + text.length * 8));
2087
- }
2088
2034
  destroy() {
2089
2035
  if (this.pendingFrame != null) {
2090
2036
  cancelFrame(this.pendingFrame);
2091
2037
  this.pendingFrame = null;
2092
2038
  }
2093
- if (this.pendingRepositionFrame != null) {
2094
- cancelFrame(this.pendingRepositionFrame);
2095
- this.pendingRepositionFrame = null;
2096
- }
2097
2039
  if (this.resizeObserver) {
2098
2040
  this.resizeObserver.disconnect();
2099
2041
  this.resizeObserver = null;
@@ -2110,8 +2052,8 @@ var DoodleOverlay = class {
2110
2052
  target.removeEventListener(type, handler);
2111
2053
  }
2112
2054
  this.listeners = [];
2113
- if (this.svg?.parentNode) {
2114
- this.svg.parentNode.removeChild(this.svg);
2055
+ if (this.svg) {
2056
+ removeOverlaySvg(this.svg, this.element, this.setPosition);
2115
2057
  }
2116
2058
  this.svg = null;
2117
2059
  instances.delete(this.element);
@@ -287,6 +287,10 @@ var DEFAULT_OPTIONS = {
287
287
  */
288
288
  zIndex: null
289
289
  };
290
+ var OVERLAY_CLASS = "doodle-ui-overlay";
291
+ function isOverlayNode(node) {
292
+ return node?.classList?.contains(OVERLAY_CLASS) ?? false;
293
+ }
290
294
  function resolveElement(target) {
291
295
  if (!target) return null;
292
296
  if (typeof target === "string") {
@@ -374,20 +378,6 @@ function isValidPosition(position) {
374
378
  "center"
375
379
  ].includes(position);
376
380
  }
377
- function getScrollableAncestors(element) {
378
- const ancestors = [];
379
- let node = element.parentElement;
380
- while (node && node !== document.documentElement) {
381
- const style = getComputedStyle(node);
382
- const overflow = style.overflow + style.overflowY + style.overflowX;
383
- if (/(auto|scroll|overlay)/.test(overflow)) {
384
- ancestors.push(node);
385
- }
386
- node = node.parentElement;
387
- }
388
- ancestors.push(window);
389
- return ancestors;
390
- }
391
381
  function resolveOverlayZIndex(element, override) {
392
382
  if (override != null) return override;
393
383
  const { zIndex, position } = getComputedStyle(element);
@@ -407,12 +397,20 @@ function resolveOverlayZIndex(element, override) {
407
397
  return null;
408
398
  }
409
399
  function insertOverlaySvg(svg, element) {
410
- const parent = element.parentNode;
411
- if (!parent) {
412
- document.body.appendChild(svg);
413
- return;
400
+ const setPosition = getComputedStyle(element).position === "static";
401
+ if (setPosition) {
402
+ element.style.position = "relative";
403
+ }
404
+ element.appendChild(svg);
405
+ return setPosition;
406
+ }
407
+ function removeOverlaySvg(svg, element, clearPosition) {
408
+ if (svg.parentNode) {
409
+ svg.parentNode.removeChild(svg);
410
+ }
411
+ if (clearPosition) {
412
+ element.style.position = "";
414
413
  }
415
- parent.insertBefore(svg, element.nextSibling);
416
414
  }
417
415
  function syncOverlayStacking(svg, element, zIndexOverride) {
418
416
  const zIndex = resolveOverlayZIndex(element, zIndexOverride);
@@ -439,7 +437,10 @@ function averageCornerRadius(element) {
439
437
  function detectBorderRadius(element) {
440
438
  const own = averageCornerRadius(element);
441
439
  if (own > 0) return own;
442
- const child = element.children.length === 1 ? element.firstElementChild : null;
440
+ const content = Array.from(element.children).filter(
441
+ (node) => !isOverlayNode(node)
442
+ );
443
+ const child = content.length === 1 ? content[0] : null;
443
444
  if (!child) return 0;
444
445
  const outer = element.getBoundingClientRect();
445
446
  const inner = child.getBoundingClientRect();
@@ -470,22 +471,6 @@ function relativeRect(outer, inner) {
470
471
  radius: inner.radius
471
472
  };
472
473
  }
473
- function unionRects(rects, margin = 0) {
474
- if (rects.length === 0) {
475
- return { x: 0, y: 0, width: 0, height: 0, radius: 0 };
476
- }
477
- const minX = Math.min(...rects.map((r) => r.x)) - margin;
478
- const minY = Math.min(...rects.map((r) => r.y)) - margin;
479
- const maxX = Math.max(...rects.map((r) => r.x + r.width)) + margin;
480
- const maxY = Math.max(...rects.map((r) => r.y + r.height)) + margin;
481
- return {
482
- x: minX,
483
- y: minY,
484
- width: maxX - minX,
485
- height: maxY - minY,
486
- radius: 0
487
- };
488
- }
489
474
  function inflateRect(rect, amount) {
490
475
  return {
491
476
  x: rect.x - amount,
@@ -1295,19 +1280,18 @@ function placeNoteText(note, box, style, seed, options = {}) {
1295
1280
  }
1296
1281
  function createOverlaySvg(left, top, width, height) {
1297
1282
  const svg = document.createElementNS(SVG_NS, "svg");
1298
- svg.setAttribute("class", "doodle-ui-overlay");
1283
+ svg.setAttribute("class", OVERLAY_CLASS);
1299
1284
  svg.setAttribute("xmlns", SVG_NS);
1300
1285
  svg.setAttribute("width", String(width));
1301
1286
  svg.setAttribute("height", String(height));
1302
1287
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1303
- svg.style.position = "fixed";
1288
+ svg.style.position = "absolute";
1304
1289
  svg.style.left = `${left}px`;
1305
1290
  svg.style.top = `${top}px`;
1306
1291
  svg.style.width = `${width}px`;
1307
1292
  svg.style.height = `${height}px`;
1308
1293
  svg.style.pointerEvents = "none";
1309
1294
  svg.style.overflow = "visible";
1310
- svg.style.willChange = "transform";
1311
1295
  return svg;
1312
1296
  }
1313
1297
 
@@ -1838,13 +1822,27 @@ var instances = /* @__PURE__ */ new WeakMap();
1838
1822
  function now() {
1839
1823
  return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1840
1824
  }
1841
- function localBand(overlayRect, inset = 8) {
1825
+ function localBand(originLeft, inset = 8) {
1842
1826
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1843
1827
  return {
1844
- x: -overlayRect.x + inset,
1828
+ x: -originLeft + inset,
1845
1829
  width: Math.max(0, width - inset * 2)
1846
1830
  };
1847
1831
  }
1832
+ function paddingBox(element) {
1833
+ const rect = element.getBoundingClientRect();
1834
+ const style = getComputedStyle(element);
1835
+ const left = parseFloat(style.borderLeftWidth) || 0;
1836
+ const top = parseFloat(style.borderTopWidth) || 0;
1837
+ const right = parseFloat(style.borderRightWidth) || 0;
1838
+ const bottom = parseFloat(style.borderBottomWidth) || 0;
1839
+ return {
1840
+ left: rect.left + left,
1841
+ top: rect.top + top,
1842
+ width: Math.max(0, rect.width - left - right),
1843
+ height: Math.max(0, rect.height - top - bottom)
1844
+ };
1845
+ }
1848
1846
  var DoodleOverlay = class {
1849
1847
  /**
1850
1848
  * @param {Element} element
@@ -1856,13 +1854,11 @@ var DoodleOverlay = class {
1856
1854
  this.seed = Math.floor(Math.random() * 1e9);
1857
1855
  this.startedAt = now();
1858
1856
  this.svg = null;
1857
+ this.setPosition = false;
1859
1858
  this.resizeObserver = null;
1860
1859
  this.mutationObserver = null;
1861
1860
  this.pendingFrame = null;
1862
- this.pendingRepositionFrame = null;
1863
- this.basePosition = { x: 0, y: 0 };
1864
1861
  this.listeners = [];
1865
- this.onScroll = this.scheduleReposition.bind(this);
1866
1862
  this.onResize = this.scheduleUpdate.bind(this);
1867
1863
  this.onLanguageChange = this.scheduleUpdate.bind(this);
1868
1864
  this.childElements = [];
@@ -1870,23 +1866,22 @@ var DoodleOverlay = class {
1870
1866
  }
1871
1867
  mount() {
1872
1868
  this.svg = createOverlaySvg(0, 0, 0, 0);
1873
- insertOverlaySvg(this.svg, this.element);
1869
+ this.setPosition = insertOverlaySvg(this.svg, this.element);
1874
1870
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1875
1871
  this.resizeObserver = new ResizeObserver(this.onResize);
1876
1872
  this.resizeObserver.observe(this.element);
1877
1873
  if (this.options.children) {
1878
1874
  this.observeChildren();
1879
1875
  }
1880
- this.mutationObserver = new MutationObserver(this.scheduleUpdate.bind(this));
1876
+ this.mutationObserver = new MutationObserver((records) => {
1877
+ const relevant = records.some((record) => !this.svg.contains(record.target));
1878
+ if (relevant) this.scheduleUpdate();
1879
+ });
1881
1880
  this.mutationObserver.observe(this.element, {
1882
1881
  childList: true,
1883
1882
  subtree: true,
1884
1883
  attributes: true
1885
1884
  });
1886
- for (const target of getScrollableAncestors(this.element)) {
1887
- target.addEventListener("scroll", this.onScroll, { passive: true });
1888
- this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1889
- }
1890
1885
  window.addEventListener("resize", this.onResize, { passive: true });
1891
1886
  this.listeners.push({
1892
1887
  target: window,
@@ -1922,46 +1917,6 @@ var DoodleOverlay = class {
1922
1917
  this.update();
1923
1918
  });
1924
1919
  }
1925
- scheduleReposition() {
1926
- if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1927
- return;
1928
- }
1929
- this.pendingRepositionFrame = scheduleFrame(() => {
1930
- this.pendingRepositionFrame = null;
1931
- this.reposition();
1932
- });
1933
- }
1934
- /**
1935
- * Cheap scroll-driven path: slide the overlay to the element's new
1936
- * viewport position without touching its contents. Scrolling can't change
1937
- * an element's size, so the rects, viewBox and every hand-drawn stroke
1938
- * inside them are still valid - only the overlay's position needs to move.
1939
- *
1940
- * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1941
- * same box-geometry pass as layout, so the browser re-checks layout on
1942
- * every write; `transform` is composited, so the scroll path never
1943
- * touches layout at all.
1944
- */
1945
- reposition() {
1946
- if (!this.svg || !this.element.isConnected) return;
1947
- const { padding, radius } = this.options;
1948
- const targets = this.getTargets();
1949
- const margin = this.overlayMargin();
1950
- const absoluteRects = targets.map(
1951
- (target) => getElementBounds(target, padding, radius)
1952
- );
1953
- const overlayRect = unionRects(absoluteRects, margin);
1954
- const currentWidth = parseFloat(this.svg.style.width) || 0;
1955
- const currentHeight = parseFloat(this.svg.style.height) || 0;
1956
- const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1957
- if (resized) {
1958
- this.update();
1959
- return;
1960
- }
1961
- const dx = overlayRect.x - this.basePosition.x;
1962
- const dy = overlayRect.y - this.basePosition.y;
1963
- this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1964
- }
1965
1920
  getTargets() {
1966
1921
  if (this.options.children) {
1967
1922
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -1973,23 +1928,23 @@ var DoodleOverlay = class {
1973
1928
  if (!this.svg || !this.element.isConnected) return;
1974
1929
  const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
1975
1930
  const targets = this.getTargets();
1976
- const margin = this.overlayMargin();
1977
1931
  const elapsed = now() - this.startedAt;
1978
1932
  const absoluteRects = targets.map(
1979
1933
  (target) => getElementBounds(target, padding, radius)
1980
1934
  );
1981
- const overlayRect = unionRects(absoluteRects, margin);
1982
1935
  clearSvg(this.svg);
1983
1936
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1984
- this.svg.setAttribute("width", String(overlayRect.width));
1985
- this.svg.setAttribute("height", String(overlayRect.height));
1986
- this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
1987
- this.svg.style.left = `${overlayRect.x}px`;
1988
- this.svg.style.top = `${overlayRect.y}px`;
1989
- this.svg.style.width = `${overlayRect.width}px`;
1990
- this.svg.style.height = `${overlayRect.height}px`;
1991
- this.svg.style.transform = "";
1992
- this.basePosition = { x: overlayRect.x, y: overlayRect.y };
1937
+ const box = paddingBox(this.element);
1938
+ const width = Math.max(1, box.width);
1939
+ const height = Math.max(1, box.height);
1940
+ const origin = { x: box.left, y: box.top };
1941
+ this.svg.setAttribute("width", String(width));
1942
+ this.svg.setAttribute("height", String(height));
1943
+ this.svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1944
+ this.svg.style.left = "0px";
1945
+ this.svg.style.top = "0px";
1946
+ this.svg.style.width = `${width}px`;
1947
+ this.svg.style.height = `${height}px`;
1993
1948
  const strokeStyle = { color, strokeWidth, roughness, opacity };
1994
1949
  const noteStyle = {
1995
1950
  color,
@@ -2000,12 +1955,12 @@ var DoodleOverlay = class {
2000
1955
  };
2001
1956
  const noteGroups = [];
2002
1957
  const noteLayout = {
2003
- band: localBand(overlayRect),
1958
+ band: localBand(origin.x),
2004
1959
  gap: this.options.arrow ? 40 : 14
2005
1960
  };
2006
1961
  targets.forEach((target, index) => {
2007
1962
  const absolute = absoluteRects[index];
2008
- const local = relativeRect(overlayRect, absolute);
1963
+ const local = relativeRect(origin, absolute);
2009
1964
  const seed = this.seed + index * 31;
2010
1965
  const note = this.options.note && index === 0 ? renderAnnotation(
2011
1966
  this.svg,
@@ -2051,24 +2006,11 @@ var DoodleOverlay = class {
2051
2006
  this.svg.appendChild(group);
2052
2007
  }
2053
2008
  }
2054
- /**
2055
- * Room the overlay needs beyond the element for notes, arrows and decorations.
2056
- */
2057
- overlayMargin() {
2058
- const { note, arrow, decorations } = this.options;
2059
- if (!note && !arrow && !decorations) return 48;
2060
- const { text } = resolveLocalizedText(note?.text, note?.locale);
2061
- return Math.max(96, Math.min(240, 80 + text.length * 8));
2062
- }
2063
2009
  destroy() {
2064
2010
  if (this.pendingFrame != null) {
2065
2011
  cancelFrame(this.pendingFrame);
2066
2012
  this.pendingFrame = null;
2067
2013
  }
2068
- if (this.pendingRepositionFrame != null) {
2069
- cancelFrame(this.pendingRepositionFrame);
2070
- this.pendingRepositionFrame = null;
2071
- }
2072
2014
  if (this.resizeObserver) {
2073
2015
  this.resizeObserver.disconnect();
2074
2016
  this.resizeObserver = null;
@@ -2085,8 +2027,8 @@ var DoodleOverlay = class {
2085
2027
  target.removeEventListener(type, handler);
2086
2028
  }
2087
2029
  this.listeners = [];
2088
- if (this.svg?.parentNode) {
2089
- this.svg.parentNode.removeChild(this.svg);
2030
+ if (this.svg) {
2031
+ removeOverlaySvg(this.svg, this.element, this.setPosition);
2090
2032
  }
2091
2033
  this.svg = null;
2092
2034
  instances.delete(this.element);
package/dist/react.cjs CHANGED
@@ -317,6 +317,10 @@ var DEFAULT_OPTIONS = {
317
317
  */
318
318
  zIndex: null
319
319
  };
320
+ var OVERLAY_CLASS = "doodle-ui-overlay";
321
+ function isOverlayNode(node) {
322
+ return node?.classList?.contains(OVERLAY_CLASS) ?? false;
323
+ }
320
324
  function resolveElement(target) {
321
325
  if (!target) return null;
322
326
  if (typeof target === "string") {
@@ -404,20 +408,6 @@ function isValidPosition(position) {
404
408
  "center"
405
409
  ].includes(position);
406
410
  }
407
- function getScrollableAncestors(element) {
408
- const ancestors = [];
409
- let node = element.parentElement;
410
- while (node && node !== document.documentElement) {
411
- const style = getComputedStyle(node);
412
- const overflow = style.overflow + style.overflowY + style.overflowX;
413
- if (/(auto|scroll|overlay)/.test(overflow)) {
414
- ancestors.push(node);
415
- }
416
- node = node.parentElement;
417
- }
418
- ancestors.push(window);
419
- return ancestors;
420
- }
421
411
  function resolveOverlayZIndex(element, override) {
422
412
  if (override != null) return override;
423
413
  const { zIndex, position } = getComputedStyle(element);
@@ -437,12 +427,20 @@ function resolveOverlayZIndex(element, override) {
437
427
  return null;
438
428
  }
439
429
  function insertOverlaySvg(svg, element) {
440
- const parent = element.parentNode;
441
- if (!parent) {
442
- document.body.appendChild(svg);
443
- return;
430
+ const setPosition = getComputedStyle(element).position === "static";
431
+ if (setPosition) {
432
+ element.style.position = "relative";
433
+ }
434
+ element.appendChild(svg);
435
+ return setPosition;
436
+ }
437
+ function removeOverlaySvg(svg, element, clearPosition) {
438
+ if (svg.parentNode) {
439
+ svg.parentNode.removeChild(svg);
440
+ }
441
+ if (clearPosition) {
442
+ element.style.position = "";
444
443
  }
445
- parent.insertBefore(svg, element.nextSibling);
446
444
  }
447
445
  function syncOverlayStacking(svg, element, zIndexOverride) {
448
446
  const zIndex = resolveOverlayZIndex(element, zIndexOverride);
@@ -469,7 +467,10 @@ function averageCornerRadius(element) {
469
467
  function detectBorderRadius(element) {
470
468
  const own = averageCornerRadius(element);
471
469
  if (own > 0) return own;
472
- const child = element.children.length === 1 ? element.firstElementChild : null;
470
+ const content = Array.from(element.children).filter(
471
+ (node) => !isOverlayNode(node)
472
+ );
473
+ const child = content.length === 1 ? content[0] : null;
473
474
  if (!child) return 0;
474
475
  const outer = element.getBoundingClientRect();
475
476
  const inner = child.getBoundingClientRect();
@@ -500,22 +501,6 @@ function relativeRect(outer, inner) {
500
501
  radius: inner.radius
501
502
  };
502
503
  }
503
- function unionRects(rects, margin = 0) {
504
- if (rects.length === 0) {
505
- return { x: 0, y: 0, width: 0, height: 0, radius: 0 };
506
- }
507
- const minX = Math.min(...rects.map((r) => r.x)) - margin;
508
- const minY = Math.min(...rects.map((r) => r.y)) - margin;
509
- const maxX = Math.max(...rects.map((r) => r.x + r.width)) + margin;
510
- const maxY = Math.max(...rects.map((r) => r.y + r.height)) + margin;
511
- return {
512
- x: minX,
513
- y: minY,
514
- width: maxX - minX,
515
- height: maxY - minY,
516
- radius: 0
517
- };
518
- }
519
504
  function inflateRect(rect, amount) {
520
505
  return {
521
506
  x: rect.x - amount,
@@ -1325,19 +1310,18 @@ function placeNoteText(note, box, style, seed, options = {}) {
1325
1310
  }
1326
1311
  function createOverlaySvg(left, top, width, height) {
1327
1312
  const svg = document.createElementNS(SVG_NS, "svg");
1328
- svg.setAttribute("class", "doodle-ui-overlay");
1313
+ svg.setAttribute("class", OVERLAY_CLASS);
1329
1314
  svg.setAttribute("xmlns", SVG_NS);
1330
1315
  svg.setAttribute("width", String(width));
1331
1316
  svg.setAttribute("height", String(height));
1332
1317
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1333
- svg.style.position = "fixed";
1318
+ svg.style.position = "absolute";
1334
1319
  svg.style.left = `${left}px`;
1335
1320
  svg.style.top = `${top}px`;
1336
1321
  svg.style.width = `${width}px`;
1337
1322
  svg.style.height = `${height}px`;
1338
1323
  svg.style.pointerEvents = "none";
1339
1324
  svg.style.overflow = "visible";
1340
- svg.style.willChange = "transform";
1341
1325
  return svg;
1342
1326
  }
1343
1327
 
@@ -1868,13 +1852,27 @@ var instances = /* @__PURE__ */ new WeakMap();
1868
1852
  function now() {
1869
1853
  return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1870
1854
  }
1871
- function localBand(overlayRect, inset = 8) {
1855
+ function localBand(originLeft, inset = 8) {
1872
1856
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1873
1857
  return {
1874
- x: -overlayRect.x + inset,
1858
+ x: -originLeft + inset,
1875
1859
  width: Math.max(0, width - inset * 2)
1876
1860
  };
1877
1861
  }
1862
+ function paddingBox(element) {
1863
+ const rect = element.getBoundingClientRect();
1864
+ const style = getComputedStyle(element);
1865
+ const left = parseFloat(style.borderLeftWidth) || 0;
1866
+ const top = parseFloat(style.borderTopWidth) || 0;
1867
+ const right = parseFloat(style.borderRightWidth) || 0;
1868
+ const bottom = parseFloat(style.borderBottomWidth) || 0;
1869
+ return {
1870
+ left: rect.left + left,
1871
+ top: rect.top + top,
1872
+ width: Math.max(0, rect.width - left - right),
1873
+ height: Math.max(0, rect.height - top - bottom)
1874
+ };
1875
+ }
1878
1876
  var DoodleOverlay = class {
1879
1877
  /**
1880
1878
  * @param {Element} element
@@ -1886,13 +1884,11 @@ var DoodleOverlay = class {
1886
1884
  this.seed = Math.floor(Math.random() * 1e9);
1887
1885
  this.startedAt = now();
1888
1886
  this.svg = null;
1887
+ this.setPosition = false;
1889
1888
  this.resizeObserver = null;
1890
1889
  this.mutationObserver = null;
1891
1890
  this.pendingFrame = null;
1892
- this.pendingRepositionFrame = null;
1893
- this.basePosition = { x: 0, y: 0 };
1894
1891
  this.listeners = [];
1895
- this.onScroll = this.scheduleReposition.bind(this);
1896
1892
  this.onResize = this.scheduleUpdate.bind(this);
1897
1893
  this.onLanguageChange = this.scheduleUpdate.bind(this);
1898
1894
  this.childElements = [];
@@ -1900,23 +1896,22 @@ var DoodleOverlay = class {
1900
1896
  }
1901
1897
  mount() {
1902
1898
  this.svg = createOverlaySvg(0, 0, 0, 0);
1903
- insertOverlaySvg(this.svg, this.element);
1899
+ this.setPosition = insertOverlaySvg(this.svg, this.element);
1904
1900
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1905
1901
  this.resizeObserver = new ResizeObserver(this.onResize);
1906
1902
  this.resizeObserver.observe(this.element);
1907
1903
  if (this.options.children) {
1908
1904
  this.observeChildren();
1909
1905
  }
1910
- this.mutationObserver = new MutationObserver(this.scheduleUpdate.bind(this));
1906
+ this.mutationObserver = new MutationObserver((records) => {
1907
+ const relevant = records.some((record) => !this.svg.contains(record.target));
1908
+ if (relevant) this.scheduleUpdate();
1909
+ });
1911
1910
  this.mutationObserver.observe(this.element, {
1912
1911
  childList: true,
1913
1912
  subtree: true,
1914
1913
  attributes: true
1915
1914
  });
1916
- for (const target of getScrollableAncestors(this.element)) {
1917
- target.addEventListener("scroll", this.onScroll, { passive: true });
1918
- this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1919
- }
1920
1915
  window.addEventListener("resize", this.onResize, { passive: true });
1921
1916
  this.listeners.push({
1922
1917
  target: window,
@@ -1952,46 +1947,6 @@ var DoodleOverlay = class {
1952
1947
  this.update();
1953
1948
  });
1954
1949
  }
1955
- scheduleReposition() {
1956
- if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1957
- return;
1958
- }
1959
- this.pendingRepositionFrame = scheduleFrame(() => {
1960
- this.pendingRepositionFrame = null;
1961
- this.reposition();
1962
- });
1963
- }
1964
- /**
1965
- * Cheap scroll-driven path: slide the overlay to the element's new
1966
- * viewport position without touching its contents. Scrolling can't change
1967
- * an element's size, so the rects, viewBox and every hand-drawn stroke
1968
- * inside them are still valid - only the overlay's position needs to move.
1969
- *
1970
- * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1971
- * same box-geometry pass as layout, so the browser re-checks layout on
1972
- * every write; `transform` is composited, so the scroll path never
1973
- * touches layout at all.
1974
- */
1975
- reposition() {
1976
- if (!this.svg || !this.element.isConnected) return;
1977
- const { padding, radius } = this.options;
1978
- const targets = this.getTargets();
1979
- const margin = this.overlayMargin();
1980
- const absoluteRects = targets.map(
1981
- (target) => getElementBounds(target, padding, radius)
1982
- );
1983
- const overlayRect = unionRects(absoluteRects, margin);
1984
- const currentWidth = parseFloat(this.svg.style.width) || 0;
1985
- const currentHeight = parseFloat(this.svg.style.height) || 0;
1986
- const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1987
- if (resized) {
1988
- this.update();
1989
- return;
1990
- }
1991
- const dx = overlayRect.x - this.basePosition.x;
1992
- const dy = overlayRect.y - this.basePosition.y;
1993
- this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1994
- }
1995
1950
  getTargets() {
1996
1951
  if (this.options.children) {
1997
1952
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -2003,23 +1958,23 @@ var DoodleOverlay = class {
2003
1958
  if (!this.svg || !this.element.isConnected) return;
2004
1959
  const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
2005
1960
  const targets = this.getTargets();
2006
- const margin = this.overlayMargin();
2007
1961
  const elapsed = now() - this.startedAt;
2008
1962
  const absoluteRects = targets.map(
2009
1963
  (target) => getElementBounds(target, padding, radius)
2010
1964
  );
2011
- const overlayRect = unionRects(absoluteRects, margin);
2012
1965
  clearSvg(this.svg);
2013
1966
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
2014
- this.svg.setAttribute("width", String(overlayRect.width));
2015
- this.svg.setAttribute("height", String(overlayRect.height));
2016
- this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
2017
- this.svg.style.left = `${overlayRect.x}px`;
2018
- this.svg.style.top = `${overlayRect.y}px`;
2019
- this.svg.style.width = `${overlayRect.width}px`;
2020
- this.svg.style.height = `${overlayRect.height}px`;
2021
- this.svg.style.transform = "";
2022
- this.basePosition = { x: overlayRect.x, y: overlayRect.y };
1967
+ const box = paddingBox(this.element);
1968
+ const width = Math.max(1, box.width);
1969
+ const height = Math.max(1, box.height);
1970
+ const origin = { x: box.left, y: box.top };
1971
+ this.svg.setAttribute("width", String(width));
1972
+ this.svg.setAttribute("height", String(height));
1973
+ this.svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1974
+ this.svg.style.left = "0px";
1975
+ this.svg.style.top = "0px";
1976
+ this.svg.style.width = `${width}px`;
1977
+ this.svg.style.height = `${height}px`;
2023
1978
  const strokeStyle = { color, strokeWidth, roughness, opacity };
2024
1979
  const noteStyle = {
2025
1980
  color,
@@ -2030,12 +1985,12 @@ var DoodleOverlay = class {
2030
1985
  };
2031
1986
  const noteGroups = [];
2032
1987
  const noteLayout = {
2033
- band: localBand(overlayRect),
1988
+ band: localBand(origin.x),
2034
1989
  gap: this.options.arrow ? 40 : 14
2035
1990
  };
2036
1991
  targets.forEach((target, index) => {
2037
1992
  const absolute = absoluteRects[index];
2038
- const local = relativeRect(overlayRect, absolute);
1993
+ const local = relativeRect(origin, absolute);
2039
1994
  const seed = this.seed + index * 31;
2040
1995
  const note = this.options.note && index === 0 ? renderAnnotation(
2041
1996
  this.svg,
@@ -2081,24 +2036,11 @@ var DoodleOverlay = class {
2081
2036
  this.svg.appendChild(group);
2082
2037
  }
2083
2038
  }
2084
- /**
2085
- * Room the overlay needs beyond the element for notes, arrows and decorations.
2086
- */
2087
- overlayMargin() {
2088
- const { note, arrow, decorations } = this.options;
2089
- if (!note && !arrow && !decorations) return 48;
2090
- const { text } = resolveLocalizedText(note?.text, note?.locale);
2091
- return Math.max(96, Math.min(240, 80 + text.length * 8));
2092
- }
2093
2039
  destroy() {
2094
2040
  if (this.pendingFrame != null) {
2095
2041
  cancelFrame(this.pendingFrame);
2096
2042
  this.pendingFrame = null;
2097
2043
  }
2098
- if (this.pendingRepositionFrame != null) {
2099
- cancelFrame(this.pendingRepositionFrame);
2100
- this.pendingRepositionFrame = null;
2101
- }
2102
2044
  if (this.resizeObserver) {
2103
2045
  this.resizeObserver.disconnect();
2104
2046
  this.resizeObserver = null;
@@ -2115,8 +2057,8 @@ var DoodleOverlay = class {
2115
2057
  target.removeEventListener(type, handler);
2116
2058
  }
2117
2059
  this.listeners = [];
2118
- if (this.svg?.parentNode) {
2119
- this.svg.parentNode.removeChild(this.svg);
2060
+ if (this.svg) {
2061
+ removeOverlaySvg(this.svg, this.element, this.setPosition);
2120
2062
  }
2121
2063
  this.svg = null;
2122
2064
  instances.delete(this.element);
package/dist/react.js CHANGED
@@ -300,6 +300,10 @@ var DEFAULT_OPTIONS = {
300
300
  */
301
301
  zIndex: null
302
302
  };
303
+ var OVERLAY_CLASS = "doodle-ui-overlay";
304
+ function isOverlayNode(node) {
305
+ return node?.classList?.contains(OVERLAY_CLASS) ?? false;
306
+ }
303
307
  function resolveElement(target) {
304
308
  if (!target) return null;
305
309
  if (typeof target === "string") {
@@ -387,20 +391,6 @@ function isValidPosition(position) {
387
391
  "center"
388
392
  ].includes(position);
389
393
  }
390
- function getScrollableAncestors(element) {
391
- const ancestors = [];
392
- let node = element.parentElement;
393
- while (node && node !== document.documentElement) {
394
- const style = getComputedStyle(node);
395
- const overflow = style.overflow + style.overflowY + style.overflowX;
396
- if (/(auto|scroll|overlay)/.test(overflow)) {
397
- ancestors.push(node);
398
- }
399
- node = node.parentElement;
400
- }
401
- ancestors.push(window);
402
- return ancestors;
403
- }
404
394
  function resolveOverlayZIndex(element, override) {
405
395
  if (override != null) return override;
406
396
  const { zIndex, position } = getComputedStyle(element);
@@ -420,12 +410,20 @@ function resolveOverlayZIndex(element, override) {
420
410
  return null;
421
411
  }
422
412
  function insertOverlaySvg(svg, element) {
423
- const parent = element.parentNode;
424
- if (!parent) {
425
- document.body.appendChild(svg);
426
- return;
413
+ const setPosition = getComputedStyle(element).position === "static";
414
+ if (setPosition) {
415
+ element.style.position = "relative";
416
+ }
417
+ element.appendChild(svg);
418
+ return setPosition;
419
+ }
420
+ function removeOverlaySvg(svg, element, clearPosition) {
421
+ if (svg.parentNode) {
422
+ svg.parentNode.removeChild(svg);
423
+ }
424
+ if (clearPosition) {
425
+ element.style.position = "";
427
426
  }
428
- parent.insertBefore(svg, element.nextSibling);
429
427
  }
430
428
  function syncOverlayStacking(svg, element, zIndexOverride) {
431
429
  const zIndex = resolveOverlayZIndex(element, zIndexOverride);
@@ -452,7 +450,10 @@ function averageCornerRadius(element) {
452
450
  function detectBorderRadius(element) {
453
451
  const own = averageCornerRadius(element);
454
452
  if (own > 0) return own;
455
- const child = element.children.length === 1 ? element.firstElementChild : null;
453
+ const content = Array.from(element.children).filter(
454
+ (node) => !isOverlayNode(node)
455
+ );
456
+ const child = content.length === 1 ? content[0] : null;
456
457
  if (!child) return 0;
457
458
  const outer = element.getBoundingClientRect();
458
459
  const inner = child.getBoundingClientRect();
@@ -483,22 +484,6 @@ function relativeRect(outer, inner) {
483
484
  radius: inner.radius
484
485
  };
485
486
  }
486
- function unionRects(rects, margin = 0) {
487
- if (rects.length === 0) {
488
- return { x: 0, y: 0, width: 0, height: 0, radius: 0 };
489
- }
490
- const minX = Math.min(...rects.map((r) => r.x)) - margin;
491
- const minY = Math.min(...rects.map((r) => r.y)) - margin;
492
- const maxX = Math.max(...rects.map((r) => r.x + r.width)) + margin;
493
- const maxY = Math.max(...rects.map((r) => r.y + r.height)) + margin;
494
- return {
495
- x: minX,
496
- y: minY,
497
- width: maxX - minX,
498
- height: maxY - minY,
499
- radius: 0
500
- };
501
- }
502
487
  function inflateRect(rect, amount) {
503
488
  return {
504
489
  x: rect.x - amount,
@@ -1308,19 +1293,18 @@ function placeNoteText(note, box, style, seed, options = {}) {
1308
1293
  }
1309
1294
  function createOverlaySvg(left, top, width, height) {
1310
1295
  const svg = document.createElementNS(SVG_NS, "svg");
1311
- svg.setAttribute("class", "doodle-ui-overlay");
1296
+ svg.setAttribute("class", OVERLAY_CLASS);
1312
1297
  svg.setAttribute("xmlns", SVG_NS);
1313
1298
  svg.setAttribute("width", String(width));
1314
1299
  svg.setAttribute("height", String(height));
1315
1300
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1316
- svg.style.position = "fixed";
1301
+ svg.style.position = "absolute";
1317
1302
  svg.style.left = `${left}px`;
1318
1303
  svg.style.top = `${top}px`;
1319
1304
  svg.style.width = `${width}px`;
1320
1305
  svg.style.height = `${height}px`;
1321
1306
  svg.style.pointerEvents = "none";
1322
1307
  svg.style.overflow = "visible";
1323
- svg.style.willChange = "transform";
1324
1308
  return svg;
1325
1309
  }
1326
1310
 
@@ -1851,13 +1835,27 @@ var instances = /* @__PURE__ */ new WeakMap();
1851
1835
  function now() {
1852
1836
  return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1853
1837
  }
1854
- function localBand(overlayRect, inset = 8) {
1838
+ function localBand(originLeft, inset = 8) {
1855
1839
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1856
1840
  return {
1857
- x: -overlayRect.x + inset,
1841
+ x: -originLeft + inset,
1858
1842
  width: Math.max(0, width - inset * 2)
1859
1843
  };
1860
1844
  }
1845
+ function paddingBox(element) {
1846
+ const rect = element.getBoundingClientRect();
1847
+ const style = getComputedStyle(element);
1848
+ const left = parseFloat(style.borderLeftWidth) || 0;
1849
+ const top = parseFloat(style.borderTopWidth) || 0;
1850
+ const right = parseFloat(style.borderRightWidth) || 0;
1851
+ const bottom = parseFloat(style.borderBottomWidth) || 0;
1852
+ return {
1853
+ left: rect.left + left,
1854
+ top: rect.top + top,
1855
+ width: Math.max(0, rect.width - left - right),
1856
+ height: Math.max(0, rect.height - top - bottom)
1857
+ };
1858
+ }
1861
1859
  var DoodleOverlay = class {
1862
1860
  /**
1863
1861
  * @param {Element} element
@@ -1869,13 +1867,11 @@ var DoodleOverlay = class {
1869
1867
  this.seed = Math.floor(Math.random() * 1e9);
1870
1868
  this.startedAt = now();
1871
1869
  this.svg = null;
1870
+ this.setPosition = false;
1872
1871
  this.resizeObserver = null;
1873
1872
  this.mutationObserver = null;
1874
1873
  this.pendingFrame = null;
1875
- this.pendingRepositionFrame = null;
1876
- this.basePosition = { x: 0, y: 0 };
1877
1874
  this.listeners = [];
1878
- this.onScroll = this.scheduleReposition.bind(this);
1879
1875
  this.onResize = this.scheduleUpdate.bind(this);
1880
1876
  this.onLanguageChange = this.scheduleUpdate.bind(this);
1881
1877
  this.childElements = [];
@@ -1883,23 +1879,22 @@ var DoodleOverlay = class {
1883
1879
  }
1884
1880
  mount() {
1885
1881
  this.svg = createOverlaySvg(0, 0, 0, 0);
1886
- insertOverlaySvg(this.svg, this.element);
1882
+ this.setPosition = insertOverlaySvg(this.svg, this.element);
1887
1883
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1888
1884
  this.resizeObserver = new ResizeObserver(this.onResize);
1889
1885
  this.resizeObserver.observe(this.element);
1890
1886
  if (this.options.children) {
1891
1887
  this.observeChildren();
1892
1888
  }
1893
- this.mutationObserver = new MutationObserver(this.scheduleUpdate.bind(this));
1889
+ this.mutationObserver = new MutationObserver((records) => {
1890
+ const relevant = records.some((record) => !this.svg.contains(record.target));
1891
+ if (relevant) this.scheduleUpdate();
1892
+ });
1894
1893
  this.mutationObserver.observe(this.element, {
1895
1894
  childList: true,
1896
1895
  subtree: true,
1897
1896
  attributes: true
1898
1897
  });
1899
- for (const target of getScrollableAncestors(this.element)) {
1900
- target.addEventListener("scroll", this.onScroll, { passive: true });
1901
- this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1902
- }
1903
1898
  window.addEventListener("resize", this.onResize, { passive: true });
1904
1899
  this.listeners.push({
1905
1900
  target: window,
@@ -1935,46 +1930,6 @@ var DoodleOverlay = class {
1935
1930
  this.update();
1936
1931
  });
1937
1932
  }
1938
- scheduleReposition() {
1939
- if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1940
- return;
1941
- }
1942
- this.pendingRepositionFrame = scheduleFrame(() => {
1943
- this.pendingRepositionFrame = null;
1944
- this.reposition();
1945
- });
1946
- }
1947
- /**
1948
- * Cheap scroll-driven path: slide the overlay to the element's new
1949
- * viewport position without touching its contents. Scrolling can't change
1950
- * an element's size, so the rects, viewBox and every hand-drawn stroke
1951
- * inside them are still valid - only the overlay's position needs to move.
1952
- *
1953
- * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1954
- * same box-geometry pass as layout, so the browser re-checks layout on
1955
- * every write; `transform` is composited, so the scroll path never
1956
- * touches layout at all.
1957
- */
1958
- reposition() {
1959
- if (!this.svg || !this.element.isConnected) return;
1960
- const { padding, radius } = this.options;
1961
- const targets = this.getTargets();
1962
- const margin = this.overlayMargin();
1963
- const absoluteRects = targets.map(
1964
- (target) => getElementBounds(target, padding, radius)
1965
- );
1966
- const overlayRect = unionRects(absoluteRects, margin);
1967
- const currentWidth = parseFloat(this.svg.style.width) || 0;
1968
- const currentHeight = parseFloat(this.svg.style.height) || 0;
1969
- const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1970
- if (resized) {
1971
- this.update();
1972
- return;
1973
- }
1974
- const dx = overlayRect.x - this.basePosition.x;
1975
- const dy = overlayRect.y - this.basePosition.y;
1976
- this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1977
- }
1978
1933
  getTargets() {
1979
1934
  if (this.options.children) {
1980
1935
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -1986,23 +1941,23 @@ var DoodleOverlay = class {
1986
1941
  if (!this.svg || !this.element.isConnected) return;
1987
1942
  const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
1988
1943
  const targets = this.getTargets();
1989
- const margin = this.overlayMargin();
1990
1944
  const elapsed = now() - this.startedAt;
1991
1945
  const absoluteRects = targets.map(
1992
1946
  (target) => getElementBounds(target, padding, radius)
1993
1947
  );
1994
- const overlayRect = unionRects(absoluteRects, margin);
1995
1948
  clearSvg(this.svg);
1996
1949
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1997
- this.svg.setAttribute("width", String(overlayRect.width));
1998
- this.svg.setAttribute("height", String(overlayRect.height));
1999
- this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
2000
- this.svg.style.left = `${overlayRect.x}px`;
2001
- this.svg.style.top = `${overlayRect.y}px`;
2002
- this.svg.style.width = `${overlayRect.width}px`;
2003
- this.svg.style.height = `${overlayRect.height}px`;
2004
- this.svg.style.transform = "";
2005
- this.basePosition = { x: overlayRect.x, y: overlayRect.y };
1950
+ const box = paddingBox(this.element);
1951
+ const width = Math.max(1, box.width);
1952
+ const height = Math.max(1, box.height);
1953
+ const origin = { x: box.left, y: box.top };
1954
+ this.svg.setAttribute("width", String(width));
1955
+ this.svg.setAttribute("height", String(height));
1956
+ this.svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1957
+ this.svg.style.left = "0px";
1958
+ this.svg.style.top = "0px";
1959
+ this.svg.style.width = `${width}px`;
1960
+ this.svg.style.height = `${height}px`;
2006
1961
  const strokeStyle = { color, strokeWidth, roughness, opacity };
2007
1962
  const noteStyle = {
2008
1963
  color,
@@ -2013,12 +1968,12 @@ var DoodleOverlay = class {
2013
1968
  };
2014
1969
  const noteGroups = [];
2015
1970
  const noteLayout = {
2016
- band: localBand(overlayRect),
1971
+ band: localBand(origin.x),
2017
1972
  gap: this.options.arrow ? 40 : 14
2018
1973
  };
2019
1974
  targets.forEach((target, index) => {
2020
1975
  const absolute = absoluteRects[index];
2021
- const local = relativeRect(overlayRect, absolute);
1976
+ const local = relativeRect(origin, absolute);
2022
1977
  const seed = this.seed + index * 31;
2023
1978
  const note = this.options.note && index === 0 ? renderAnnotation(
2024
1979
  this.svg,
@@ -2064,24 +2019,11 @@ var DoodleOverlay = class {
2064
2019
  this.svg.appendChild(group);
2065
2020
  }
2066
2021
  }
2067
- /**
2068
- * Room the overlay needs beyond the element for notes, arrows and decorations.
2069
- */
2070
- overlayMargin() {
2071
- const { note, arrow, decorations } = this.options;
2072
- if (!note && !arrow && !decorations) return 48;
2073
- const { text } = resolveLocalizedText(note?.text, note?.locale);
2074
- return Math.max(96, Math.min(240, 80 + text.length * 8));
2075
- }
2076
2022
  destroy() {
2077
2023
  if (this.pendingFrame != null) {
2078
2024
  cancelFrame(this.pendingFrame);
2079
2025
  this.pendingFrame = null;
2080
2026
  }
2081
- if (this.pendingRepositionFrame != null) {
2082
- cancelFrame(this.pendingRepositionFrame);
2083
- this.pendingRepositionFrame = null;
2084
- }
2085
2027
  if (this.resizeObserver) {
2086
2028
  this.resizeObserver.disconnect();
2087
2029
  this.resizeObserver = null;
@@ -2098,8 +2040,8 @@ var DoodleOverlay = class {
2098
2040
  target.removeEventListener(type, handler);
2099
2041
  }
2100
2042
  this.listeners = [];
2101
- if (this.svg?.parentNode) {
2102
- this.svg.parentNode.removeChild(this.svg);
2043
+ if (this.svg) {
2044
+ removeOverlaySvg(this.svg, this.element, this.setPosition);
2103
2045
  }
2104
2046
  this.svg = null;
2105
2047
  instances.delete(this.element);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aznabee/freehand-ui",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Wrap any existing web element with a beautiful, responsive, hand-drawn doodle layer",
5
5
  "type": "module",
6
6
  "main": "./dist/freehand-ui.cjs",