@weasel-js/text-dom 1.9.4 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
2
  import { useLatest, useVisibleRaf } from '@weasel-js/react';
3
- import { DEFAULT_TEXT_STYLE, smallCapsScaleFor, numericWeight, resolveTextStyle, styleAtRange, runsCarryStyling, runsToPlainText, unboldPatch, nodeHasFlag, setFlagOverRange, applyStyleToRange, patchRangeStyle, fontString, verticalAlignOffset, layoutTextPose, SMALL_CAPS_SCALE, isHardLineBreak, supersededKey, rangeWeight, resolveAlign, scriptMetricsFor, transformRunTexts, smallCapsText, paragraphDirection } from '@weasel-js/text';
3
+ import { DEFAULT_TEXT_STYLE, smallCapsScaleFor, numericWeight, resolveTextStyle, styleAtRange, runsCarryStyling, runsToPlainText, unboldPatch, nodeHasFlag, setFlagOverRange, applyStyleToRange, patchRangeStyle, effectiveRangeStyle, SMALL_CAPS_SCALE, isHardLineBreak, fontString, supersededKey, rangeWeight, scriptMetricsFor, transformRunTexts, smallCapsText, resolveAlign, paragraphDirection, layoutTextPose, verticalAlignOffset } from '@weasel-js/text';
4
4
  import { cssFontFamily, cssFontFamilyLoading, isCanvasFont } from '@weasel-js/font';
5
+ import { isSolidPaint } from '@weasel-js/paint';
5
6
 
6
7
  // src/useTextEdit.ts
7
8
 
@@ -82,7 +83,7 @@ function domPositionToCharOffset(parent, node, offset) {
82
83
  // src/domRuns.ts
83
84
  function solidColor(p) {
84
85
  if (!p) return null;
85
- if ("color" in p) return p.color;
86
+ if (isSolidPaint(p)) return p.color;
86
87
  return null;
87
88
  }
88
89
  var FILL_ATTR = "data-fill";
@@ -194,7 +195,7 @@ function toRun(text, style) {
194
195
  if (style.fontSize != null) run.fontSize = style.fontSize;
195
196
  if (style.fontFamily != null) run.fontFamily = style.fontFamily;
196
197
  if (style.fillJson !== void 0) run.fill = JSON.parse(style.fillJson);
197
- else if (style.color != null) run.fill = { fill: "solid", color: style.color };
198
+ else if (style.color != null) run.fill = { paint: { kind: "solid", color: style.color } };
198
199
  if (style.letterSpacing != null) run.letterSpacing = style.letterSpacing;
199
200
  if (style.underline) run.underline = true;
200
201
  if (style.strikethrough) run.strikethrough = true;
@@ -333,7 +334,7 @@ function runsToDom(runs, parent, base = DEFAULT_TEXT_STYLE) {
333
334
  });
334
335
  span.setAttribute("data-font-family", run.fontFamily);
335
336
  }
336
- const color = solidColor(run.fill);
337
+ const color = solidColor(run.fill?.paint);
337
338
  if (color != null) span.style.color = color;
338
339
  else if (run.fill !== void 0) span.setAttribute(FILL_ATTR, JSON.stringify(run.fill));
339
340
  if (run.fill === null) span.style.color = "#000";
@@ -421,67 +422,282 @@ function overlayAlignEdge(el, style) {
421
422
  return resolveAlign(style.align, paragraphDirection(el.textContent ?? "", style.direction));
422
423
  }
423
424
 
424
- // src/useTextEdit.ts
425
- var STYLE_SHORTCUTS = [
426
- { key: "b", shift: false, toggle: "bold" },
427
- { key: "i", shift: false, toggle: "italic" },
428
- { key: "u", shift: false, toggle: "underline" },
429
- { key: "x", shift: true, toggle: "strikethrough" },
430
- { key: "=", shift: true, toggle: "super" },
431
- { key: "-", shift: true, toggle: "sub" }
432
- ];
433
- var UNSHIFT = { "+": "=", _: "-" };
434
- function toggleForKey(e) {
435
- if (!e.metaKey && !e.ctrlKey) return null;
436
- const raw = e.key.toLowerCase();
437
- const key = UNSHIFT[raw] ?? raw;
438
- return STYLE_SHORTCUTS.find((s) => s.key === key && s.shift === e.shiftKey)?.toggle ?? null;
425
+ // src/overlayFrame.ts
426
+ function overlayTransform(pose, left, top, anchor) {
427
+ const zoom = pose.zoom ?? 1;
428
+ const parts = [];
429
+ if (pose.frame) {
430
+ const [a, b, c, d] = pose.frame;
431
+ const dx = left - (pose.x + pose.width * zoom / 2);
432
+ const dy = top - (pose.y + pose.height * zoom / 2);
433
+ const m = [a * zoom, b * zoom, c * zoom, d * zoom, (a - 1) * dx + c * dy, b * dx + (d - 1) * dy];
434
+ parts.push(`matrix(${m.join(", ")})`);
435
+ } else if (zoom !== 1) {
436
+ parts.push(`scale(${zoom})`);
437
+ }
438
+ if (anchor !== 0) parts.push(`translateX(${-anchor * 100}%)`);
439
+ return parts.length > 0 ? parts.join(" ") : "none";
439
440
  }
440
- function patchForToggle(current, toggle, nodeStyle) {
441
- if (toggle === "super" || toggle === "sub") {
442
- return { script: current.script === toggle ? void 0 : toggle };
441
+ function applyOverlayStyle(el, style) {
442
+ el.style.position = "absolute";
443
+ el.style.pointerEvents = "auto";
444
+ el.style.boxSizing = "border-box";
445
+ el.style.margin = "0";
446
+ el.style.padding = "0";
447
+ el.style.border = "0";
448
+ el.style.outline = "none";
449
+ el.style.background = "transparent";
450
+ const paint = style.fill?.paint;
451
+ el.style.color = isSolidPaint(paint) ? paint.color : "#000";
452
+ el.style.caretColor = style.caretColor;
453
+ const fontFamily = cssFontFamily(style.fontFamily, {
454
+ weight: numericWeight(style.fontWeight),
455
+ style: style.fontStyle
456
+ });
457
+ el.style.font = fontString({ ...style, fontFamily });
458
+ el.style.textRendering = isCanvasFont(style.fontFamily) ? "" : "geometricPrecision";
459
+ el.style.lineHeight = String(style.lineHeight);
460
+ applyOverlayDirection(el, style);
461
+ const decorations = [];
462
+ if (style.underline) decorations.push("underline");
463
+ if (style.strikethrough) decorations.push("line-through");
464
+ if (style.overline) decorations.push("overline");
465
+ el.style.textDecoration = decorations.length > 0 ? decorations.join(" ") : "none";
466
+ el.style.textTransform = style.textTransform;
467
+ el.style.fontVariant = style.fontVariantCaps;
468
+ el.style.setProperty(
469
+ SMALL_CAPS_SCALE_PROPERTY,
470
+ style.fontVariantCaps === "small-caps" ? String(smallCapsScaleFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle)) : ""
471
+ );
472
+ el.style.whiteSpace = style.wrap ? "pre-wrap" : "pre";
473
+ el.style.overflowWrap = "normal";
474
+ el.style.wordBreak = "normal";
475
+ }
476
+ var endsInBreak = (text) => text.length > 0 && isHardLineBreak(text.charCodeAt(text.length - 1));
477
+ function seedOverlay(overlay, text, runs, base) {
478
+ const styled = runs !== void 0 && runs.length > 0;
479
+ if (styled) runsToDom(runs, overlay, base);
480
+ else if (base.fontVariantCaps === "small-caps") runsToDom([{ text }], overlay, base);
481
+ else appendOverlayText(overlay, text);
482
+ let ownBreak = endsInBreak(styled ? runsToPlainText(runs) : text);
483
+ return {
484
+ edited(el) {
485
+ if (ownBreak && !endsInBreak(runsToPlainText(domToRuns(el)))) ownBreak = false;
486
+ },
487
+ trim: (read) => ownBreak ? read : trimCaretHolder(read)
488
+ };
489
+ }
490
+ function trimCaretHolder(runs) {
491
+ const last = runs[runs.length - 1];
492
+ if (last === void 0 || !last.text.endsWith("\n")) return runs;
493
+ const trimmed = { ...last, text: last.text.slice(0, -1) };
494
+ const head = runs.slice(0, -1);
495
+ return trimmed.text === "" ? head : [...head, trimmed];
496
+ }
497
+
498
+ // src/overlaySelection.ts
499
+ function readSelectionOffsets(overlay) {
500
+ const sel = window.getSelection();
501
+ if (!sel || sel.rangeCount === 0) return null;
502
+ const range = sel.getRangeAt(0);
503
+ if (!overlay.contains(range.startContainer) || !overlay.contains(range.endContainer)) {
504
+ return null;
443
505
  }
444
- const on = toggle === "bold" ? rangeWeight(current, nodeStyle).bold === true : current[toggle] === true || nodeHasFlag(nodeStyle, toggle);
445
- return { [toggle]: !on };
506
+ const start = domPositionToCharOffset(overlay, range.startContainer, range.startOffset);
507
+ const end = domPositionToCharOffset(overlay, range.endContainer, range.endOffset);
508
+ return start <= end ? { start, end } : { start: end, end: start };
446
509
  }
447
- function styleAtCaret(runs, at) {
448
- return at > 0 ? styleAtRange(runs, at - 1, at) : styleAtRange(runs, 0, 1);
510
+ function selectOffsets(overlay, start, end) {
511
+ const a = charOffsetToDomPosition(overlay, start);
512
+ const b = charOffsetToDomPosition(overlay, end);
513
+ const sel = window.getSelection();
514
+ if (!a || !b || !sel) return;
515
+ const range = document.createRange();
516
+ range.setStart(a.node, a.offset);
517
+ range.setEnd(b.node, b.offset);
518
+ sel.removeAllRanges();
519
+ sel.addRange(range);
520
+ }
521
+ function writeRunsPreservingSelection(overlay, runs, start, end, base) {
522
+ const active = document.activeElement;
523
+ const inTextEntry = active !== overlay && !overlay.contains(active) && (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLElement && active.isContentEditable);
524
+ runsToDom(runs, overlay, base);
525
+ if (!inTextEntry) selectOffsets(overlay, start, end);
526
+ }
527
+
528
+ // src/pasteRuns.ts
529
+ var UNSEEN = /* @__PURE__ */ new Set([
530
+ "HEAD",
531
+ "IFRAME",
532
+ "NOSCRIPT",
533
+ "OBJECT",
534
+ "SCRIPT",
535
+ "STYLE",
536
+ "TEMPLATE",
537
+ "TITLE"
538
+ ]);
539
+ var BLOCKS = /* @__PURE__ */ new Set([
540
+ "ADDRESS",
541
+ "ARTICLE",
542
+ "ASIDE",
543
+ "BLOCKQUOTE",
544
+ "CAPTION",
545
+ "DD",
546
+ "DETAILS",
547
+ "DIV",
548
+ "DL",
549
+ "DT",
550
+ "FIELDSET",
551
+ "FIGCAPTION",
552
+ "FIGURE",
553
+ "FOOTER",
554
+ "FORM",
555
+ "H1",
556
+ "H2",
557
+ "H3",
558
+ "H4",
559
+ "H5",
560
+ "H6",
561
+ "HEADER",
562
+ "HR",
563
+ "LI",
564
+ "MAIN",
565
+ "NAV",
566
+ "OL",
567
+ "P",
568
+ "PRE",
569
+ "SECTION",
570
+ "SUMMARY",
571
+ "TABLE",
572
+ "TR",
573
+ "UL"
574
+ ]);
575
+ var CELLS = /* @__PURE__ */ new Set(["TD", "TH"]);
576
+ function keepsSpace(el, inherited) {
577
+ const collapse = el.style.getPropertyValue("white-space-collapse");
578
+ const ws = collapse || el.style.getPropertyValue("white-space");
579
+ if (ws === "") return el.tagName === "PRE" || inherited;
580
+ return /^(pre|pre-wrap|break-spaces|preserve)\b/.test(ws);
581
+ }
582
+ function emphasis(text, style) {
583
+ const read = toRun(text, style);
584
+ const run = { text };
585
+ if (read.bold || (read.fontWeight ?? 0) >= 600) run.bold = true;
586
+ for (const key of ["italic", "underline", "strikethrough", "overline"]) if (read[key]) run[key] = true;
587
+ if (read.script) run.script = read.script;
588
+ return run;
449
589
  }
450
- function spliceRunAt(runs, at, run) {
590
+ function htmlToRuns(html) {
591
+ const body = new DOMParser().parseFromString(html, "text/html").body;
592
+ const runs = [];
593
+ let gap = { line: false, tab: false };
594
+ let onLine = false;
595
+ let spaced = false;
596
+ const write = (text, style) => {
597
+ const lead = (gap.line && runs.length > 0 ? "\n" : "") + (gap.tab ? " " : "");
598
+ runs.push(emphasis(lead + text, style));
599
+ gap = { line: false, tab: false };
600
+ onLine = true;
601
+ spaced = false;
602
+ };
603
+ const trimSpace = () => {
604
+ const last = runs[runs.length - 1];
605
+ if (spaced && last) last.text = last.text.slice(0, -1);
606
+ spaced = false;
607
+ };
608
+ const boundary = () => {
609
+ trimSpace();
610
+ gap = { line: gap.line || onLine, tab: false };
611
+ onLine = false;
612
+ };
613
+ const visit = (node, style, keep) => {
614
+ if (node.nodeType === Node.TEXT_NODE) {
615
+ const data = node.data.replace(/\r\n?/g, "\n");
616
+ if (keep) {
617
+ if (data.length > 0) write(data, style);
618
+ return;
619
+ }
620
+ data.split(/[ \t\n\f]+/).forEach((word, i) => {
621
+ if (i > 0 && onLine && !spaced) {
622
+ write(" ", style);
623
+ spaced = true;
624
+ }
625
+ if (word.length > 0) write(word, style);
626
+ });
627
+ return;
628
+ }
629
+ if (!(node instanceof HTMLElement) || UNSEEN.has(node.tagName)) return;
630
+ if (node.tagName === "BR") {
631
+ if (node.classList.contains("Apple-interchange-newline")) return;
632
+ trimSpace();
633
+ write("\n", style);
634
+ onLine = false;
635
+ return;
636
+ }
637
+ const block = BLOCKS.has(node.tagName);
638
+ if (block) boundary();
639
+ if (CELLS.has(node.tagName) && node.previousElementSibling !== null) {
640
+ trimSpace();
641
+ gap.tab = true;
642
+ }
643
+ const next = styleStateFromElement(node, style);
644
+ const keeps = keepsSpace(node, keep);
645
+ for (const child of node.childNodes) visit(child, next, keeps);
646
+ if (block) boundary();
647
+ };
648
+ visit(body, EMPTY_STYLE, false);
649
+ trimSpace();
650
+ const kept = (run) => JSON.stringify({ ...run, text: "" });
651
+ const merged = [];
652
+ for (const run of runs) {
653
+ const last = merged[merged.length - 1];
654
+ if (run.text.length === 0) continue;
655
+ if (last && kept(last) === kept(run)) last.text += run.text;
656
+ else merged.push(run);
657
+ }
658
+ return merged;
659
+ }
660
+ function clipboardRuns(data) {
661
+ const html = data.getData("text/html");
662
+ const fromHtml = html ? htmlToRuns(html) : [];
663
+ if (fromHtml.length > 0) return fromHtml;
664
+ const plain = data.getData("text/plain").replace(/\r\n/g, "\n");
665
+ return plain.length > 0 ? [{ text: plain }] : [];
666
+ }
667
+ function replaceRunRange(runs, start, end, insert) {
451
668
  const out = [];
452
669
  let pos = 0;
453
670
  let placed = false;
671
+ const place = () => {
672
+ if (!placed) out.push(...insert.map((r) => ({ ...r })));
673
+ placed = true;
674
+ };
454
675
  for (const r of runs) {
455
- const end = pos + r.text.length;
456
- if (!placed && at <= end) {
457
- const k = at - pos;
458
- if (k > 0) out.push({ ...r, text: r.text.slice(0, k) });
459
- out.push(run);
460
- if (k < r.text.length) out.push({ ...r, text: r.text.slice(k) });
461
- placed = true;
462
- } else {
463
- out.push({ ...r });
464
- }
465
- pos = end;
676
+ const from = pos;
677
+ pos += r.text.length;
678
+ if (from < start) out.push({ ...r, text: r.text.slice(0, start - from) });
679
+ if (pos >= start) place();
680
+ if (pos > end) out.push({ ...r, text: r.text.slice(Math.max(0, end - from)) });
466
681
  }
467
- if (!placed) out.push(run);
682
+ place();
468
683
  return applyStyleToRange(out, 0, 0, {});
469
684
  }
470
- function restoreOverlayFocus(overlay, range) {
471
- const active = document.activeElement;
472
- if (active === overlay || overlay.contains(active)) return;
473
- if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLElement && active.isContentEditable || active instanceof Element && active.closest('[role="dialog"]') !== null) return;
474
- overlay.focus();
475
- const a = charOffsetToDomPosition(overlay, range.start);
476
- const b = charOffsetToDomPosition(overlay, range.end);
477
- const sel = window.getSelection();
478
- if (!a || !b || !sel) return;
479
- const r = document.createRange();
480
- r.setStart(a.node, a.offset);
481
- r.setEnd(b.node, b.offset);
482
- sel.removeAllRanges();
483
- sel.addRange(r);
685
+ function runStyleAt(runs, at) {
686
+ let pos = 0;
687
+ let found = runs[0];
688
+ for (const r of runs) {
689
+ if (r.text.length === 0) continue;
690
+ if (pos >= at && pos > 0) break;
691
+ found = r;
692
+ pos += r.text.length;
693
+ }
694
+ if (!found) return {};
695
+ const style = { ...found };
696
+ delete style.text;
697
+ return style;
484
698
  }
699
+
700
+ // src/overlayTyping.ts
485
701
  function placeCaretBetweenRuns(overlay, runs, offset) {
486
702
  let acc = 0;
487
703
  let index = -1;
@@ -502,6 +718,126 @@ function placeCaretBetweenRuns(overlay, runs, offset) {
502
718
  sel.removeAllRanges();
503
719
  sel.addRange(range);
504
720
  }
721
+ function resplitSmallCaps(overlay, e) {
722
+ if (e instanceof InputEvent && e.isComposing) return;
723
+ const at = readSelectionOffsets(overlay);
724
+ if (normalizeSmallCaps(overlay) && at) selectOffsets(overlay, at.start, at.end);
725
+ }
726
+ function namesItsRange(ie, caret) {
727
+ const target = ie.getTargetRanges?.()[0];
728
+ return target !== void 0 && caret.collapsed && !target.collapsed;
729
+ }
730
+ function replacedOffsets(overlay, ie, host) {
731
+ const target = ie.getTargetRanges?.()[0];
732
+ if (target && overlay.contains(target.startContainer) && overlay.contains(target.endContainer)) {
733
+ const a = domPositionToCharOffset(overlay, target.startContainer, target.startOffset);
734
+ const b = domPositionToCharOffset(overlay, target.endContainer, target.endOffset);
735
+ return { start: Math.min(a, b), end: Math.max(a, b) };
736
+ }
737
+ return readSelectionOffsets(overlay) ?? host.lastRange();
738
+ }
739
+ function styledForRange(pasted, runs, at, pending) {
740
+ const base = { ...runStyleAt(runs, at.start === at.end ? at.start : at.start + 1) };
741
+ for (const [key, value] of Object.entries(pending)) {
742
+ if (value === void 0 || value === false) delete base[key];
743
+ else base[key] = value;
744
+ }
745
+ return pasted.map((run) => {
746
+ const styled = { ...base, ...run };
747
+ const superseded = supersededKey(run);
748
+ if (superseded !== void 0) delete styled[superseded];
749
+ return styled;
750
+ });
751
+ }
752
+ var PASTES = /* @__PURE__ */ new Set(["insertFromPaste", "insertFromDrop"]);
753
+ function typingHandlers(overlay, host) {
754
+ const edited = (e) => {
755
+ resplitSmallCaps(overlay, e);
756
+ host.onEdited();
757
+ };
758
+ const onPaste = (ie) => {
759
+ const at = ie.dataTransfer ? replacedOffsets(overlay, ie, host) : null;
760
+ if (!ie.dataTransfer || !at) return;
761
+ ie.preventDefault();
762
+ const pasted = clipboardRuns(ie.dataTransfer);
763
+ if (pasted.length === 0) return;
764
+ const runs = domToRuns(overlay);
765
+ const next = replaceRunRange(runs, at.start, at.end, styledForRange(pasted, runs, at, host.pending()));
766
+ const after = at.start + runsToPlainText(pasted).length;
767
+ writeRunsPreservingSelection(overlay, next, after, after, host.base());
768
+ host.onWritten(after);
769
+ host.onEdited();
770
+ };
771
+ const onBeforeInput = (ie) => {
772
+ if (PASTES.has(ie.inputType)) {
773
+ onPaste(ie);
774
+ return;
775
+ }
776
+ if (ie.inputType !== "insertText" || !ie.data) return;
777
+ const sel = window.getSelection();
778
+ const caret = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
779
+ if (caret && namesItsRange(ie, caret)) return;
780
+ const pending = host.pending();
781
+ if (Object.keys(pending).length > 0) {
782
+ const at = readSelectionOffsets(overlay) ?? host.lastRange();
783
+ if (!at || at.start !== at.end) return;
784
+ ie.preventDefault();
785
+ const typed = { ...pending, text: ie.data };
786
+ const next = replaceRunRange(domToRuns(overlay), at.start, at.start, [typed]);
787
+ const after = at.start + ie.data.length;
788
+ writeRunsPreservingSelection(overlay, next, after, after, host.base());
789
+ placeCaretBetweenRuns(overlay, next, after);
790
+ host.onWritten(after);
791
+ host.onEdited();
792
+ return;
793
+ }
794
+ if (!sel || !caret || !caret.collapsed || caret.startContainer.nodeType === Node.TEXT_NODE) return;
795
+ if (!overlay.contains(caret.startContainer)) return;
796
+ ie.preventDefault();
797
+ const textNode = document.createTextNode(ie.data);
798
+ caret.insertNode(textNode);
799
+ sel.collapse(textNode, ie.data.length);
800
+ edited();
801
+ };
802
+ return { onBeforeInput, onInput: edited };
803
+ }
804
+
805
+ // src/textEditTypes.ts
806
+ var TEXT_EDIT_FONT_HOLD = 100;
807
+
808
+ // src/useTextEdit.ts
809
+ var STYLE_SHORTCUTS = [
810
+ { key: "b", shift: false, toggle: "bold" },
811
+ { key: "i", shift: false, toggle: "italic" },
812
+ { key: "u", shift: false, toggle: "underline" },
813
+ { key: "x", shift: true, toggle: "strikethrough" },
814
+ { key: "=", shift: true, toggle: "super" },
815
+ { key: "-", shift: true, toggle: "sub" }
816
+ ];
817
+ var UNSHIFT = { "+": "=", _: "-" };
818
+ function toggleForKey(e) {
819
+ if (!e.metaKey && !e.ctrlKey) return null;
820
+ const raw = e.key.toLowerCase();
821
+ const key = UNSHIFT[raw] ?? raw;
822
+ return STYLE_SHORTCUTS.find((s) => s.key === key && s.shift === e.shiftKey)?.toggle ?? null;
823
+ }
824
+ function patchForToggle(current, toggle, nodeStyle) {
825
+ if (toggle === "super" || toggle === "sub") {
826
+ return { script: current.script === toggle ? void 0 : toggle };
827
+ }
828
+ const on = toggle === "bold" ? rangeWeight(current, nodeStyle).bold === true : current[toggle] === true || nodeHasFlag(nodeStyle, toggle);
829
+ return { [toggle]: !on };
830
+ }
831
+ function styleAtCaret(runs, at) {
832
+ return at > 0 ? styleAtRange(runs, at - 1, at) : styleAtRange(runs, 0, 1);
833
+ }
834
+ function restoreOverlayFocus(overlay, range) {
835
+ const active = document.activeElement;
836
+ if (active === overlay || overlay.contains(active)) return;
837
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLElement && active.isContentEditable || active instanceof Element && active.closest('[role="dialog"]') !== null) return;
838
+ overlay.focus();
839
+ selectOffsets(overlay, range.start, range.end);
840
+ }
505
841
  var NO_PENDING = Object.freeze({});
506
842
  function mergePending(prev, patch) {
507
843
  const next = { ...prev };
@@ -513,44 +849,10 @@ function mergePending(prev, patch) {
513
849
  }
514
850
  return next;
515
851
  }
516
- function readSelectionOffsets(overlay) {
517
- const sel = window.getSelection();
518
- if (!sel || sel.rangeCount === 0) return null;
519
- const range = sel.getRangeAt(0);
520
- if (!overlay.contains(range.startContainer) || !overlay.contains(range.endContainer)) {
521
- return null;
522
- }
523
- const start = domPositionToCharOffset(overlay, range.startContainer, range.startOffset);
524
- const end = domPositionToCharOffset(overlay, range.endContainer, range.endOffset);
525
- return start <= end ? { start, end } : { start: end, end: start };
526
- }
527
- function writeRunsPreservingSelection(overlay, runs, start, end, base) {
528
- const active = document.activeElement;
529
- const inTextEntry = active !== overlay && !overlay.contains(active) && (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLElement && active.isContentEditable);
530
- runsToDom(runs, overlay, base);
531
- if (inTextEntry) return;
532
- const a = charOffsetToDomPosition(overlay, start);
533
- const b = charOffsetToDomPosition(overlay, end);
534
- const sel = window.getSelection();
535
- if (!a || !b || !sel) return;
536
- const range = document.createRange();
537
- range.setStart(a.node, a.offset);
538
- range.setEnd(b.node, b.offset);
539
- sel.removeAllRanges();
540
- sel.addRange(range);
541
- }
542
- function trimCaretHolder(runs) {
543
- const last = runs[runs.length - 1];
544
- if (last === void 0 || !last.text.endsWith("\n")) return runs;
545
- const trimmed = { ...last, text: last.text.slice(0, -1) };
546
- const head = runs.slice(0, -1);
547
- return trimmed.text === "" ? head : [...head, trimmed];
548
- }
549
852
  function sameSelection(a, b) {
550
853
  if (a === null || b === null) return a === b;
551
854
  return a.start === b.start && a.end === b.end;
552
855
  }
553
- var TEXT_EDIT_FONT_HOLD = 100;
554
856
  function useTextEdit(opts) {
555
857
  const optsRef = useLatest(opts);
556
858
  const [editingId, setEditingId] = useState(null);
@@ -574,6 +876,7 @@ function useTextEdit(opts) {
574
876
  });
575
877
  const initialCaretRef = useRef("all");
576
878
  const openedRef = useRef(null);
879
+ const holderRef = useRef(null);
577
880
  const [selection, setSelection] = useState(null);
578
881
  const [rangeStyle, setRangeStyle] = useState(null);
579
882
  const [pendingStyle, setPendingStyle] = useState(NO_PENDING);
@@ -633,7 +936,7 @@ function useTextEdit(opts) {
633
936
  setEditingId(null);
634
937
  return;
635
938
  }
636
- const runs = trimCaretHolder(read);
939
+ const runs = holderRef.current?.trim(read) ?? read;
637
940
  const hadRuns = priorRuns != null && priorRuns.length > 0;
638
941
  const runsInPlay = hadRuns || runsCarryStyling(runs);
639
942
  const text = runsToPlainText(runs);
@@ -716,13 +1019,7 @@ function useTextEdit(opts) {
716
1019
  overlay.spellcheck = false;
717
1020
  baseRef.current = style;
718
1021
  const initialRuns = optsRef.current.getRuns?.(editingId);
719
- if (initialRuns && initialRuns.length > 0) {
720
- runsToDom(initialRuns, overlay, style);
721
- } else if (style.fontVariantCaps === "small-caps") {
722
- runsToDom([{ text: getText(editingId) }], overlay, style);
723
- } else {
724
- appendOverlayText(overlay, getText(editingId));
725
- }
1022
+ holderRef.current = seedOverlay(overlay, getText(editingId), initialRuns, style);
726
1023
  openedRef.current = JSON.stringify(domToRuns(overlay));
727
1024
  applyOverlayStyle(overlay, style);
728
1025
  const hold = holdForFaces(style, initialRuns ?? []);
@@ -780,67 +1077,26 @@ function useTextEdit(opts) {
780
1077
  };
781
1078
  const onBlur = (e) => {
782
1079
  const next = e.relatedTarget;
783
- const isChrome = optsRef.current.isEditorChrome;
784
- if (isChrome && next instanceof Element && isChrome(next)) return;
1080
+ if (isChrome(optsRef.current, next)) return;
785
1081
  commit();
786
1082
  };
787
- const onBeforeInput = (ie) => {
788
- if (ie.inputType !== "insertText" || !ie.data) return;
789
- const pending = pendingRef.current;
790
- if (Object.keys(pending).length > 0) {
791
- const at = readSelectionOffsets(overlay) ?? selectionRef.current;
792
- if (!at || at.start !== at.end) return;
793
- ie.preventDefault();
794
- const next = spliceRunAt(domToRuns(overlay), at.start, {
795
- ...pending,
796
- text: ie.data
797
- });
798
- const after = at.start + ie.data.length;
799
- writeRunsPreservingSelection(overlay, next, after, after, baseRef.current);
800
- placeCaretBetweenRuns(overlay, next, after);
1083
+ const { onBeforeInput, onInput } = typingHandlers(overlay, {
1084
+ pending: () => pendingRef.current,
1085
+ base: () => baseRef.current,
1086
+ lastRange: () => selectionRef.current,
1087
+ onWritten: (caret) => {
801
1088
  dropPending();
802
- publishRange(overlay, { start: after, end: after });
803
- return;
804
- }
805
- ie.preventDefault();
806
- const sel2 = window.getSelection();
807
- if (!sel2 || sel2.rangeCount === 0) return;
808
- const range2 = sel2.getRangeAt(0);
809
- range2.deleteContents();
810
- {
811
- const textNode = document.createTextNode(ie.data);
812
- range2.insertNode(textNode);
813
- const after = document.createRange();
814
- after.setStartAfter(textNode);
815
- after.collapse(true);
816
- sel2.removeAllRanges();
817
- sel2.addRange(after);
1089
+ publishRange(overlay, { start: caret, end: caret });
1090
+ },
1091
+ onEdited: () => {
1092
+ holderRef.current?.edited(overlay);
1093
+ place();
818
1094
  }
819
- resplitSmallCaps();
820
- };
821
- const resplitSmallCaps = (e) => {
822
- if (e instanceof InputEvent && e.isComposing) return;
823
- const at = readSelectionOffsets(overlay);
824
- if (!normalizeSmallCaps(overlay) || !at) return;
825
- const a = charOffsetToDomPosition(overlay, at.start);
826
- const b = charOffsetToDomPosition(overlay, at.end);
827
- const sel2 = window.getSelection();
828
- if (!a || !b || !sel2) return;
829
- const r = document.createRange();
830
- r.setStart(a.node, a.offset);
831
- r.setEnd(b.node, b.offset);
832
- sel2.removeAllRanges();
833
- sel2.addRange(r);
834
- };
835
- const onInput = (e) => {
836
- resplitSmallCaps(e);
837
- place();
838
- };
1095
+ });
839
1096
  const onPointerDownOutside = (e) => {
840
1097
  const target = e.target;
841
1098
  if (!(target instanceof Node) || overlay.contains(target)) return;
842
- const isChrome = optsRef.current.isEditorChrome;
843
- if (isChrome && target instanceof Element && isChrome(target)) return;
1099
+ if (isChrome(optsRef.current, target)) return;
844
1100
  commit();
845
1101
  };
846
1102
  overlay.addEventListener("keydown", onKeyDown);
@@ -897,9 +1153,21 @@ function useTextEdit(opts) {
897
1153
  rangeStyle,
898
1154
  pendingStyle,
899
1155
  applyStyleToSelection,
900
- toggleStyle
1156
+ toggleStyle,
1157
+ effectiveStyle: shownStyle(opts, editingId, nodeStyle, rangeStyle, pendingStyle)
901
1158
  };
902
1159
  }
1160
+ function isChrome(opts, el) {
1161
+ return el instanceof Element && (opts.isEditorChrome?.(el) ?? false);
1162
+ }
1163
+ function shownStyle(opts, editingId, nodeStyle, rangeStyle, pendingStyle) {
1164
+ if (editingId === null) return null;
1165
+ return effectiveRangeStyle(
1166
+ patchRangeStyle(rangeStyle ?? {}, pendingStyle),
1167
+ nodeStyle ?? opts.getStyle(editingId),
1168
+ opts.getPaint?.(editingId)
1169
+ );
1170
+ }
903
1171
  var OVERLAY_SEQ = 0;
904
1172
  function holdForFaces(style, runs) {
905
1173
  const pending = [
@@ -940,40 +1208,6 @@ function createClipBox() {
940
1208
  el.style.pointerEvents = "none";
941
1209
  return el;
942
1210
  }
943
- function applyOverlayStyle(el, style) {
944
- el.style.position = "absolute";
945
- el.style.pointerEvents = "auto";
946
- el.style.boxSizing = "border-box";
947
- el.style.margin = "0";
948
- el.style.padding = "0";
949
- el.style.border = "0";
950
- el.style.outline = "none";
951
- el.style.background = "transparent";
952
- el.style.color = style.fill !== null && "color" in style.fill ? style.fill.color : "#000";
953
- el.style.caretColor = style.caretColor;
954
- const fontFamily = cssFontFamily(style.fontFamily, {
955
- weight: numericWeight(style.fontWeight),
956
- style: style.fontStyle
957
- });
958
- el.style.font = fontString({ ...style, fontFamily });
959
- el.style.textRendering = isCanvasFont(style.fontFamily) ? "" : "geometricPrecision";
960
- el.style.lineHeight = String(style.lineHeight);
961
- applyOverlayDirection(el, style);
962
- const decorations = [];
963
- if (style.underline) decorations.push("underline");
964
- if (style.strikethrough) decorations.push("line-through");
965
- if (style.overline) decorations.push("overline");
966
- el.style.textDecoration = decorations.length > 0 ? decorations.join(" ") : "none";
967
- el.style.textTransform = style.textTransform;
968
- el.style.fontVariant = style.fontVariantCaps;
969
- el.style.setProperty(
970
- SMALL_CAPS_SCALE_PROPERTY,
971
- style.fontVariantCaps === "small-caps" ? String(smallCapsScaleFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle)) : ""
972
- );
973
- el.style.whiteSpace = style.wrap ? "pre-wrap" : "pre";
974
- el.style.overflowWrap = "normal";
975
- el.style.wordBreak = "normal";
976
- }
977
1211
  function installSelectionStyle(overlayClass, style) {
978
1212
  if (style.selectionBackground == null && style.selectionColor == null) return null;
979
1213
  const el = document.createElement("style");
@@ -1064,7 +1298,8 @@ function placeOverlay(el, clipBox, pose, clip, style) {
1064
1298
  clipBox.style.height = `${clip?.height ?? 0}px`;
1065
1299
  el.style.display = "";
1066
1300
  const anchor = style.wrap ? 0 : ALIGN_ANCHOR[overlayAlignEdge(el, style)];
1067
- el.style.left = `${pose.x + pose.width * (pose.zoom ?? 1) * anchor - (clip?.x ?? 0)}px`;
1301
+ const left = pose.x + pose.width * (pose.zoom ?? 1) * anchor;
1302
+ el.style.left = `${left - (clip?.x ?? 0)}px`;
1068
1303
  el.style.width = style.wrap ? `${pose.width}px` : "max-content";
1069
1304
  el.style.minWidth = style.wrap ? "" : `${pose.width}px`;
1070
1305
  const script = style.script ? scriptMetricsFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle)[style.script] : void 0;
@@ -1074,16 +1309,14 @@ function placeOverlay(el, clipBox, pose, clip, style) {
1074
1309
  const aligned = pose.verticalAlign !== void 0 && pose.verticalAlign !== "top";
1075
1310
  el.style.minHeight = aligned ? "" : `${pose.height}px`;
1076
1311
  const drop = baselineDrop(el, style, pose, lineHeight) - (script?.shift ?? 0) * pose.fontSize;
1077
- el.style.top = `${overlayTop(pose, aligned ? el.offsetHeight : pose.height, drop) - (clip?.y ?? 0)}px`;
1312
+ const top = overlayTop(pose, aligned ? el.offsetHeight : pose.height, drop);
1313
+ el.style.top = `${top - (clip?.y ?? 0)}px`;
1078
1314
  el.style.transformOrigin = "0 0";
1079
- const transforms = [];
1080
- if (pose.zoom !== void 0 && pose.zoom !== 1) transforms.push(`scale(${pose.zoom})`);
1081
- if (anchor !== 0) transforms.push(`translateX(${-anchor * 100}%)`);
1082
- el.style.transform = transforms.length > 0 ? transforms.join(" ") : "none";
1315
+ el.style.transform = overlayTransform(pose, left, top, anchor);
1083
1316
  const scale = pose.zoom !== void 0 ? 1 : style.fontSize > 0 ? pose.fontSize / style.fontSize : 1;
1084
1317
  el.style.letterSpacing = `${style.letterSpacing * scale}px`;
1085
1318
  }
1086
1319
 
1087
- export { TEXT_EDIT_FONT_HOLD, baselineDrop, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, overlayTop, runsToDom, useTextEdit };
1320
+ export { TEXT_EDIT_FONT_HOLD, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, runsToDom, useTextEdit };
1088
1321
  //# sourceMappingURL=index.js.map
1089
1322
  //# sourceMappingURL=index.js.map