@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/README.md +5 -0
- package/dist/index.d.ts +44 -74
- package/dist/index.js +424 -191
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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,
|
|
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 (
|
|
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 = {
|
|
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/
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
];
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return
|
|
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
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
445
|
-
|
|
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
|
|
448
|
-
|
|
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
|
|
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
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
682
|
+
place();
|
|
468
683
|
return applyStyleToRange(out, 0, 0, {});
|
|
469
684
|
}
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
784
|
-
if (isChrome && next instanceof Element && isChrome(next)) return;
|
|
1080
|
+
if (isChrome(optsRef.current, next)) return;
|
|
785
1081
|
commit();
|
|
786
1082
|
};
|
|
787
|
-
const onBeforeInput = (
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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:
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1320
|
+
export { TEXT_EDIT_FONT_HOLD, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, runsToDom, useTextEdit };
|
|
1088
1321
|
//# sourceMappingURL=index.js.map
|
|
1089
1322
|
//# sourceMappingURL=index.js.map
|