@weasel-js/text-dom 1.9.4 → 1.10.1

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
- import { useState, useRef, useCallback, useEffect } from 'react';
1
+ import { useRef, useState, 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;
589
+ }
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 }] : [];
449
666
  }
450
- function spliceRunAt(runs, at, run) {
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,141 @@ 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/styleShortcuts.ts
806
+ var STYLE_SHORTCUTS = [
807
+ { key: "b", shift: false, toggle: "bold" },
808
+ { key: "i", shift: false, toggle: "italic" },
809
+ { key: "u", shift: false, toggle: "underline" },
810
+ { key: "x", shift: true, toggle: "strikethrough" },
811
+ { key: "=", shift: true, toggle: "super" },
812
+ { key: "-", shift: true, toggle: "sub" }
813
+ ];
814
+ var UNSHIFT = { "+": "=", _: "-" };
815
+ function toggleForKey(e) {
816
+ if (!e.metaKey && !e.ctrlKey) return null;
817
+ const raw = e.key.toLowerCase();
818
+ const key = UNSHIFT[raw] ?? raw;
819
+ return STYLE_SHORTCUTS.find((s) => s.key === key && s.shift === e.shiftKey)?.toggle ?? null;
820
+ }
821
+ function useEditingId(opts) {
822
+ const [editingId, setState] = useState(null);
823
+ const live = useRef(null);
824
+ const setEditingId = useCallback((id) => {
825
+ const prev = live.current;
826
+ if (prev === id) return;
827
+ live.current = id;
828
+ setState(id);
829
+ if (prev !== null) opts.current.onEnd?.(prev);
830
+ if (id !== null) opts.current.onStart?.(id);
831
+ }, [opts]);
832
+ return [editingId, setEditingId];
833
+ }
834
+
835
+ // src/textEditTypes.ts
836
+ var TEXT_EDIT_FONT_HOLD = 100;
837
+
838
+ // src/useTextEdit.ts
839
+ function patchForToggle(current, toggle, nodeStyle) {
840
+ if (toggle === "super" || toggle === "sub") {
841
+ return { script: current.script === toggle ? void 0 : toggle };
842
+ }
843
+ const on = toggle === "bold" ? rangeWeight(current, nodeStyle).bold === true : current[toggle] === true || nodeHasFlag(nodeStyle, toggle);
844
+ return { [toggle]: !on };
845
+ }
846
+ function styleAtCaret(runs, at) {
847
+ return at > 0 ? styleAtRange(runs, at - 1, at) : styleAtRange(runs, 0, 1);
848
+ }
849
+ function restoreOverlayFocus(overlay, range) {
850
+ const active = document.activeElement;
851
+ if (active === overlay || overlay.contains(active)) return;
852
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLElement && active.isContentEditable || active instanceof Element && active.closest('[role="dialog"]') !== null) return;
853
+ overlay.focus();
854
+ selectOffsets(overlay, range.start, range.end);
855
+ }
505
856
  var NO_PENDING = Object.freeze({});
506
857
  function mergePending(prev, patch) {
507
858
  const next = { ...prev };
@@ -513,47 +864,13 @@ function mergePending(prev, patch) {
513
864
  }
514
865
  return next;
515
866
  }
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
867
  function sameSelection(a, b) {
550
868
  if (a === null || b === null) return a === b;
551
869
  return a.start === b.start && a.end === b.end;
552
870
  }
553
- var TEXT_EDIT_FONT_HOLD = 100;
554
871
  function useTextEdit(opts) {
555
872
  const optsRef = useLatest(opts);
556
- const [editingId, setEditingId] = useState(null);
873
+ const [editingId, setEditingId] = useEditingId(optsRef);
557
874
  const editingIdRef = useLatest(editingId);
558
875
  const overlayRef = useRef(null);
559
876
  const baseRef = useRef(resolveTextStyle(void 0));
@@ -574,6 +891,7 @@ function useTextEdit(opts) {
574
891
  });
575
892
  const initialCaretRef = useRef("all");
576
893
  const openedRef = useRef(null);
894
+ const holderRef = useRef(null);
577
895
  const [selection, setSelection] = useState(null);
578
896
  const [rangeStyle, setRangeStyle] = useState(null);
579
897
  const [pendingStyle, setPendingStyle] = useState(NO_PENDING);
@@ -617,7 +935,7 @@ function useTextEdit(opts) {
617
935
  }, [dropPending]);
618
936
  const cancelEdit = useCallback(() => {
619
937
  setEditingId(null);
620
- }, []);
938
+ }, [setEditingId]);
621
939
  const commit = useCallback(() => {
622
940
  const id = editingId;
623
941
  const overlay = overlayRef.current;
@@ -633,7 +951,7 @@ function useTextEdit(opts) {
633
951
  setEditingId(null);
634
952
  return;
635
953
  }
636
- const runs = trimCaretHolder(read);
954
+ const runs = holderRef.current?.trim(read) ?? read;
637
955
  const hadRuns = priorRuns != null && priorRuns.length > 0;
638
956
  const runsInPlay = hadRuns || runsCarryStyling(runs);
639
957
  const text = runsToPlainText(runs);
@@ -646,13 +964,13 @@ function useTextEdit(opts) {
646
964
  if (setRuns && runsInPlay) setRuns(id, runs);
647
965
  }
648
966
  setEditingId(null);
649
- }, [editingId, optsRef, holdNodeStyle]);
967
+ }, [editingId, optsRef, holdNodeStyle, setEditingId]);
650
968
  const startEdit = useCallback((id, opts2) => {
651
969
  initialCaretRef.current = opts2?.caret ?? "all";
652
970
  setEditingId(id);
653
- }, []);
971
+ }, [setEditingId]);
654
972
  const isEditing = useCallback((id) => editingId === id, [editingId]);
655
- const applyStyleToSelection = useCallback((patch) => {
973
+ const applyStyleToSelection = useCallback(function applyStyle(patch) {
656
974
  const overlay = overlayRef.current;
657
975
  if (!overlay) return;
658
976
  if (Object.keys(patch).length === 0) return;
@@ -679,7 +997,7 @@ function useTextEdit(opts) {
679
997
  restoreOverlayFocus(overlay, range);
680
998
  const rest = { ...patch };
681
999
  delete rest[unsetFlag];
682
- if (Object.keys(rest).length > 0) applyStyleToSelection(rest);
1000
+ if (Object.keys(rest).length > 0) applyStyle(rest);
683
1001
  return;
684
1002
  }
685
1003
  if (range.start === range.end) {
@@ -716,13 +1034,7 @@ function useTextEdit(opts) {
716
1034
  overlay.spellcheck = false;
717
1035
  baseRef.current = style;
718
1036
  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
- }
1037
+ holderRef.current = seedOverlay(overlay, getText(editingId), initialRuns, style);
726
1038
  openedRef.current = JSON.stringify(domToRuns(overlay));
727
1039
  applyOverlayStyle(overlay, style);
728
1040
  const hold = holdForFaces(style, initialRuns ?? []);
@@ -780,67 +1092,26 @@ function useTextEdit(opts) {
780
1092
  };
781
1093
  const onBlur = (e) => {
782
1094
  const next = e.relatedTarget;
783
- const isChrome = optsRef.current.isEditorChrome;
784
- if (isChrome && next instanceof Element && isChrome(next)) return;
1095
+ if (isChrome(optsRef.current, next)) return;
785
1096
  commit();
786
1097
  };
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);
1098
+ const { onBeforeInput, onInput } = typingHandlers(overlay, {
1099
+ pending: () => pendingRef.current,
1100
+ base: () => baseRef.current,
1101
+ lastRange: () => selectionRef.current,
1102
+ onWritten: (caret) => {
801
1103
  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);
1104
+ publishRange(overlay, { start: caret, end: caret });
1105
+ },
1106
+ onEdited: () => {
1107
+ holderRef.current?.edited(overlay);
1108
+ place();
818
1109
  }
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
- };
1110
+ });
839
1111
  const onPointerDownOutside = (e) => {
840
1112
  const target = e.target;
841
1113
  if (!(target instanceof Node) || overlay.contains(target)) return;
842
- const isChrome = optsRef.current.isEditorChrome;
843
- if (isChrome && target instanceof Element && isChrome(target)) return;
1114
+ if (isChrome(optsRef.current, target)) return;
844
1115
  commit();
845
1116
  };
846
1117
  overlay.addEventListener("keydown", onKeyDown);
@@ -897,9 +1168,21 @@ function useTextEdit(opts) {
897
1168
  rangeStyle,
898
1169
  pendingStyle,
899
1170
  applyStyleToSelection,
900
- toggleStyle
1171
+ toggleStyle,
1172
+ effectiveStyle: shownStyle(opts, editingId, nodeStyle, rangeStyle, pendingStyle)
901
1173
  };
902
1174
  }
1175
+ function isChrome(opts, el) {
1176
+ return el instanceof Element && (opts.isEditorChrome?.(el) ?? false);
1177
+ }
1178
+ function shownStyle(opts, editingId, nodeStyle, rangeStyle, pendingStyle) {
1179
+ if (editingId === null) return null;
1180
+ return effectiveRangeStyle(
1181
+ patchRangeStyle(rangeStyle ?? {}, pendingStyle),
1182
+ nodeStyle ?? opts.getStyle(editingId),
1183
+ opts.getPaint?.(editingId)
1184
+ );
1185
+ }
903
1186
  var OVERLAY_SEQ = 0;
904
1187
  function holdForFaces(style, runs) {
905
1188
  const pending = [
@@ -940,40 +1223,6 @@ function createClipBox() {
940
1223
  el.style.pointerEvents = "none";
941
1224
  return el;
942
1225
  }
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
1226
  function installSelectionStyle(overlayClass, style) {
978
1227
  if (style.selectionBackground == null && style.selectionColor == null) return null;
979
1228
  const el = document.createElement("style");
@@ -1064,7 +1313,8 @@ function placeOverlay(el, clipBox, pose, clip, style) {
1064
1313
  clipBox.style.height = `${clip?.height ?? 0}px`;
1065
1314
  el.style.display = "";
1066
1315
  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`;
1316
+ const left = pose.x + pose.width * (pose.zoom ?? 1) * anchor;
1317
+ el.style.left = `${left - (clip?.x ?? 0)}px`;
1068
1318
  el.style.width = style.wrap ? `${pose.width}px` : "max-content";
1069
1319
  el.style.minWidth = style.wrap ? "" : `${pose.width}px`;
1070
1320
  const script = style.script ? scriptMetricsFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle)[style.script] : void 0;
@@ -1074,16 +1324,14 @@ function placeOverlay(el, clipBox, pose, clip, style) {
1074
1324
  const aligned = pose.verticalAlign !== void 0 && pose.verticalAlign !== "top";
1075
1325
  el.style.minHeight = aligned ? "" : `${pose.height}px`;
1076
1326
  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`;
1327
+ const top = overlayTop(pose, aligned ? el.offsetHeight : pose.height, drop);
1328
+ el.style.top = `${top - (clip?.y ?? 0)}px`;
1078
1329
  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";
1330
+ el.style.transform = overlayTransform(pose, left, top, anchor);
1083
1331
  const scale = pose.zoom !== void 0 ? 1 : style.fontSize > 0 ? pose.fontSize / style.fontSize : 1;
1084
1332
  el.style.letterSpacing = `${style.letterSpacing * scale}px`;
1085
1333
  }
1086
1334
 
1087
- export { TEXT_EDIT_FONT_HOLD, baselineDrop, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, overlayTop, runsToDom, useTextEdit };
1335
+ export { TEXT_EDIT_FONT_HOLD, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, runsToDom, useTextEdit };
1088
1336
  //# sourceMappingURL=index.js.map
1089
1337
  //# sourceMappingURL=index.js.map