@weasel-js/text-dom 0.0.0-stage → 1.9.4
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/LICENSE +21 -0
- package/README.md +26 -2
- package/dist/index.d.ts +345 -0
- package/dist/index.js +1089 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
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';
|
|
4
|
+
import { cssFontFamily, cssFontFamilyLoading, isCanvasFont } from '@weasel-js/font';
|
|
5
|
+
|
|
6
|
+
// src/useTextEdit.ts
|
|
7
|
+
|
|
8
|
+
// src/domOffsets.ts
|
|
9
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set(["DIV", "P"]);
|
|
10
|
+
function overlayPieces(parent) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const visit = (node) => {
|
|
13
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
14
|
+
if (node.data.length > 0) out.push({ text: node });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!(node instanceof Element)) return;
|
|
18
|
+
if (node.tagName === "BR") {
|
|
19
|
+
out.push({ br: node });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (BLOCK_TAGS.has(node.tagName) && out.length > 0) out.push({ block: node });
|
|
23
|
+
for (const child of node.childNodes) visit(child);
|
|
24
|
+
};
|
|
25
|
+
for (const child of parent.childNodes) visit(child);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
var indexIn = (el) => Array.prototype.indexOf.call(el.parentNode.childNodes, el);
|
|
29
|
+
function afterBreak(piece) {
|
|
30
|
+
return "br" in piece ? { node: piece.br.parentNode, offset: indexIn(piece.br) + 1 } : { node: piece.block, offset: 0 };
|
|
31
|
+
}
|
|
32
|
+
function charOffsetToDomPosition(parent, offset) {
|
|
33
|
+
let remaining = offset;
|
|
34
|
+
let end = { node: parent, offset: 0 };
|
|
35
|
+
let pending = null;
|
|
36
|
+
for (const piece of overlayPieces(parent)) {
|
|
37
|
+
if ("text" in piece) {
|
|
38
|
+
const len = piece.text.data.length;
|
|
39
|
+
if (remaining <= len) return { node: piece.text, offset: remaining };
|
|
40
|
+
remaining -= len;
|
|
41
|
+
end = { node: piece.text, offset: len };
|
|
42
|
+
pending = null;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (remaining === 0) {
|
|
46
|
+
return pending ?? ("br" in piece ? { node: piece.br.parentNode, offset: indexIn(piece.br) } : afterBreak(piece));
|
|
47
|
+
}
|
|
48
|
+
remaining -= 1;
|
|
49
|
+
end = afterBreak(piece);
|
|
50
|
+
pending = remaining === 0 ? end : null;
|
|
51
|
+
}
|
|
52
|
+
return end;
|
|
53
|
+
}
|
|
54
|
+
function domPositionToCharOffset(parent, node, offset) {
|
|
55
|
+
const pieces = overlayPieces(parent);
|
|
56
|
+
let total = 0;
|
|
57
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
58
|
+
for (const piece of pieces) {
|
|
59
|
+
if (!("text" in piece)) {
|
|
60
|
+
total += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (piece.text === node) return total + offset;
|
|
64
|
+
total += piece.text.data.length;
|
|
65
|
+
}
|
|
66
|
+
return total;
|
|
67
|
+
}
|
|
68
|
+
const point = document.createRange();
|
|
69
|
+
point.setStart(node, Math.max(0, Math.min(offset, node.childNodes.length)));
|
|
70
|
+
point.collapse(true);
|
|
71
|
+
for (const piece of pieces) {
|
|
72
|
+
if ("text" in piece) {
|
|
73
|
+
if (point.comparePoint(piece.text, 0) < 0) total += piece.text.data.length;
|
|
74
|
+
} else {
|
|
75
|
+
const past = afterBreak(piece);
|
|
76
|
+
if (point.comparePoint(past.node, past.offset) <= 0) total += 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return total;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/domRuns.ts
|
|
83
|
+
function solidColor(p) {
|
|
84
|
+
if (!p) return null;
|
|
85
|
+
if ("color" in p) return p.color;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
var FILL_ATTR = "data-fill";
|
|
89
|
+
var TRANSFORMS = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
|
|
90
|
+
var EMPTY_STYLE = {
|
|
91
|
+
bold: false,
|
|
92
|
+
italic: false,
|
|
93
|
+
underline: false,
|
|
94
|
+
strikethrough: false,
|
|
95
|
+
overline: false
|
|
96
|
+
};
|
|
97
|
+
function parseLetterSpacing(raw) {
|
|
98
|
+
const n = parseFloat(raw);
|
|
99
|
+
if (!Number.isFinite(n)) return void 0;
|
|
100
|
+
const unit = /^-?[\d.]+([a-z%]+)$/i.exec(raw.trim())?.[1]?.toLowerCase();
|
|
101
|
+
if (unit && unit !== "px") {
|
|
102
|
+
console.warn(
|
|
103
|
+
`weasel domRuns: letter-spacing "${raw}" uses unit "${unit}", which is not converted; treated as ${n} world units`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return n;
|
|
107
|
+
}
|
|
108
|
+
var isSmallCapsPiece = (el) => el.hasAttribute("data-small-caps");
|
|
109
|
+
function cssFontVariant(el) {
|
|
110
|
+
const v = el.style.fontVariant || el.style.getPropertyValue("font-variant-caps");
|
|
111
|
+
return v === "small-caps" || v === "normal" ? v : void 0;
|
|
112
|
+
}
|
|
113
|
+
function styleStateFromElement(el, parent) {
|
|
114
|
+
if (isSmallCapsPiece(el)) return parent;
|
|
115
|
+
const next = { ...parent };
|
|
116
|
+
const tag = el.tagName;
|
|
117
|
+
if (tag === "B" || tag === "STRONG") {
|
|
118
|
+
next.bold = true;
|
|
119
|
+
next.fontWeight = void 0;
|
|
120
|
+
}
|
|
121
|
+
if (tag === "I" || tag === "EM") next.italic = true;
|
|
122
|
+
if (tag === "U") next.underline = true;
|
|
123
|
+
if (tag === "S" || tag === "STRIKE" || tag === "DEL") next.strikethrough = true;
|
|
124
|
+
if (tag === "SUP") next.script = "super";
|
|
125
|
+
if (tag === "SUB") next.script = "sub";
|
|
126
|
+
if (el instanceof HTMLElement) {
|
|
127
|
+
const fw = el.style.fontWeight;
|
|
128
|
+
const ownWeight = Number(el.getAttribute("data-font-weight") ?? fw);
|
|
129
|
+
if (el.hasAttribute("data-font-weight") || !["", "700", "bold", "400", "normal"].includes(fw)) {
|
|
130
|
+
if (Number.isFinite(ownWeight) && ownWeight > 0) {
|
|
131
|
+
next.fontWeight = ownWeight;
|
|
132
|
+
next.bold = false;
|
|
133
|
+
}
|
|
134
|
+
} else if (fw !== "") {
|
|
135
|
+
next.bold = fw === "700" || fw === "bold";
|
|
136
|
+
next.fontWeight = void 0;
|
|
137
|
+
}
|
|
138
|
+
const fs = el.style.fontStyle;
|
|
139
|
+
if (fs === "italic") next.italic = true;
|
|
140
|
+
if (fs === "normal") next.italic = false;
|
|
141
|
+
const td = el.style.textDecoration || el.style.textDecorationLine;
|
|
142
|
+
if (td && !td.includes("none")) {
|
|
143
|
+
next.underline ||= td.includes("underline");
|
|
144
|
+
next.strikethrough ||= td.includes("line-through");
|
|
145
|
+
next.overline ||= td.includes("overline");
|
|
146
|
+
}
|
|
147
|
+
if (el.style.fontSize) {
|
|
148
|
+
const size = parseFloat(el.style.fontSize);
|
|
149
|
+
if (Number.isFinite(size)) {
|
|
150
|
+
if (el.style.fontSize.trim().endsWith("%")) next.fontScale = size / 100;
|
|
151
|
+
else next.fontSize = size;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const va = el.style.verticalAlign;
|
|
155
|
+
if (va === "super" || va === "sub") next.script = va === "super" ? "super" : "sub";
|
|
156
|
+
if (va === "baseline") next.script = void 0;
|
|
157
|
+
const rawShift = el.getAttribute("data-baseline-shift");
|
|
158
|
+
if (rawShift != null) {
|
|
159
|
+
const shift = parseFloat(rawShift);
|
|
160
|
+
if (Number.isFinite(shift)) next.baselineShift = shift;
|
|
161
|
+
}
|
|
162
|
+
const tt = el.style.textTransform;
|
|
163
|
+
if (TRANSFORMS.has(tt)) next.textTransform = tt;
|
|
164
|
+
const fv = cssFontVariant(el);
|
|
165
|
+
if (fv) next.fontVariantCaps = fv;
|
|
166
|
+
const family = el.getAttribute("data-font-family") ?? el.style.fontFamily;
|
|
167
|
+
if (family) next.fontFamily = family;
|
|
168
|
+
const fillJson = el.getAttribute(FILL_ATTR);
|
|
169
|
+
if (fillJson != null) {
|
|
170
|
+
next.fillJson = fillJson;
|
|
171
|
+
next.color = void 0;
|
|
172
|
+
} else if (el.style.color) {
|
|
173
|
+
next.color = el.style.color;
|
|
174
|
+
next.fillJson = void 0;
|
|
175
|
+
}
|
|
176
|
+
const ls = el.style.letterSpacing;
|
|
177
|
+
if (ls === "normal") {
|
|
178
|
+
next.letterSpacing = void 0;
|
|
179
|
+
} else if (ls) {
|
|
180
|
+
const px = parseLetterSpacing(ls);
|
|
181
|
+
if (px != null) next.letterSpacing = px;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return next;
|
|
185
|
+
}
|
|
186
|
+
function styleEquals(a, b) {
|
|
187
|
+
return a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && a.strikethrough === b.strikethrough && a.overline === b.overline && a.fontWeight === b.fontWeight && a.fontSize === b.fontSize && a.fontFamily === b.fontFamily && a.color === b.color && a.fillJson === b.fillJson && a.letterSpacing === b.letterSpacing && a.script === b.script && a.baselineShift === b.baselineShift && a.fontScale === b.fontScale && a.textTransform === b.textTransform && a.fontVariantCaps === b.fontVariantCaps;
|
|
188
|
+
}
|
|
189
|
+
function toRun(text, style) {
|
|
190
|
+
const run = { text };
|
|
191
|
+
if (style.bold) run.bold = true;
|
|
192
|
+
if (style.italic) run.italic = true;
|
|
193
|
+
if (style.fontWeight != null) run.fontWeight = style.fontWeight;
|
|
194
|
+
if (style.fontSize != null) run.fontSize = style.fontSize;
|
|
195
|
+
if (style.fontFamily != null) run.fontFamily = style.fontFamily;
|
|
196
|
+
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
|
+
if (style.letterSpacing != null) run.letterSpacing = style.letterSpacing;
|
|
199
|
+
if (style.underline) run.underline = true;
|
|
200
|
+
if (style.strikethrough) run.strikethrough = true;
|
|
201
|
+
if (style.overline) run.overline = true;
|
|
202
|
+
if (style.script != null) run.script = style.script;
|
|
203
|
+
if (style.baselineShift != null) run.baselineShift = style.baselineShift;
|
|
204
|
+
if (style.fontScale != null) run.fontScale = style.fontScale;
|
|
205
|
+
if (style.textTransform != null) run.textTransform = style.textTransform;
|
|
206
|
+
if (style.fontVariantCaps != null) run.fontVariantCaps = style.fontVariantCaps;
|
|
207
|
+
return run;
|
|
208
|
+
}
|
|
209
|
+
var BREAK_ATTR = "data-break";
|
|
210
|
+
function restoreBreak(el) {
|
|
211
|
+
const text = el.textContent ?? "";
|
|
212
|
+
const code = parseInt(el.getAttribute(BREAK_ATTR) ?? "", 16);
|
|
213
|
+
return text.startsWith("\n") && Number.isFinite(code) ? String.fromCharCode(code) + text.slice(1) : text;
|
|
214
|
+
}
|
|
215
|
+
function appendOverlayText(parent, text) {
|
|
216
|
+
let from = 0;
|
|
217
|
+
for (let i = 0; i < text.length; i++) {
|
|
218
|
+
const c = text.charCodeAt(i);
|
|
219
|
+
if (c === 10 || !isHardLineBreak(c) || c === 13 && text.charCodeAt(i + 1) === 10) continue;
|
|
220
|
+
if (i > from) parent.appendChild(document.createTextNode(text.slice(from, i)));
|
|
221
|
+
const span = document.createElement("span");
|
|
222
|
+
span.setAttribute(BREAK_ATTR, c.toString(16));
|
|
223
|
+
span.textContent = "\n";
|
|
224
|
+
parent.appendChild(span);
|
|
225
|
+
from = i + 1;
|
|
226
|
+
}
|
|
227
|
+
if (from < text.length) parent.appendChild(document.createTextNode(text.slice(from)));
|
|
228
|
+
}
|
|
229
|
+
function domToRuns(parent) {
|
|
230
|
+
const fragments = [];
|
|
231
|
+
function visit(node, style) {
|
|
232
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
233
|
+
const text = node.data;
|
|
234
|
+
if (text.length > 0) fragments.push({ text, style });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
238
|
+
const el = node;
|
|
239
|
+
if (el.tagName === "BR") {
|
|
240
|
+
fragments.push({ text: "\n", style });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (el.hasAttribute(BREAK_ATTR)) {
|
|
244
|
+
const restored = restoreBreak(el);
|
|
245
|
+
if (restored.length > 0) fragments.push({ text: restored, style });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (BLOCK_TAGS.has(el.tagName) && fragments.length > 0) {
|
|
249
|
+
fragments.push({ text: "\n", style });
|
|
250
|
+
}
|
|
251
|
+
const nextStyle = styleStateFromElement(el, style);
|
|
252
|
+
for (const child of Array.from(el.childNodes)) {
|
|
253
|
+
visit(child, nextStyle);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const child of Array.from(parent.childNodes)) {
|
|
257
|
+
visit(child, EMPTY_STYLE);
|
|
258
|
+
}
|
|
259
|
+
const runs = [];
|
|
260
|
+
let i = 0;
|
|
261
|
+
while (i < fragments.length) {
|
|
262
|
+
let j = i + 1;
|
|
263
|
+
while (j < fragments.length && styleEquals(fragments[i].style, fragments[j].style)) {
|
|
264
|
+
j++;
|
|
265
|
+
}
|
|
266
|
+
const merged = fragments.slice(i, j).map((f) => f.text).join("");
|
|
267
|
+
runs.push(toRun(merged, fragments[i].style));
|
|
268
|
+
i = j;
|
|
269
|
+
}
|
|
270
|
+
return runs;
|
|
271
|
+
}
|
|
272
|
+
var SMALL_CAPS_SCALE_PROPERTY = "--weasel-small-caps";
|
|
273
|
+
function smallCapsPieces(text, transform) {
|
|
274
|
+
const [shown] = transformRunTexts([text], [transform]);
|
|
275
|
+
const caps = smallCapsText(shown.text, shown.srcMap);
|
|
276
|
+
const small = new Array(text.length).fill(false);
|
|
277
|
+
caps.small?.forEach((s, i) => {
|
|
278
|
+
if (s) small[caps.srcMap ? caps.srcMap.starts[i] : i] = true;
|
|
279
|
+
});
|
|
280
|
+
const out = [];
|
|
281
|
+
let at = 0;
|
|
282
|
+
for (const ch of text) {
|
|
283
|
+
const last = out[out.length - 1];
|
|
284
|
+
if (last && last.small === small[at]) last.text += ch;
|
|
285
|
+
else out.push({ text: ch, small: small[at] });
|
|
286
|
+
at += ch.length;
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
function writeSmallCaps(span, text, transform) {
|
|
291
|
+
span.replaceChildren();
|
|
292
|
+
for (const p of smallCapsPieces(text, transform)) {
|
|
293
|
+
if (!p.small) {
|
|
294
|
+
appendOverlayText(span, p.text);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const piece = document.createElement("span");
|
|
298
|
+
piece.setAttribute("data-small-caps", "");
|
|
299
|
+
piece.style.fontSize = `calc(var(${SMALL_CAPS_SCALE_PROPERTY}, ${SMALL_CAPS_SCALE}) * 1em)`;
|
|
300
|
+
piece.style.textTransform = "uppercase";
|
|
301
|
+
piece.textContent = p.text;
|
|
302
|
+
span.appendChild(piece);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function runsToDom(runs, parent, base = DEFAULT_TEXT_STYLE) {
|
|
306
|
+
parent.replaceChildren();
|
|
307
|
+
for (const run of runs) {
|
|
308
|
+
const span = document.createElement("span");
|
|
309
|
+
span.setAttribute("data-run", "");
|
|
310
|
+
if ((run.fontVariantCaps ?? base.fontVariantCaps) === "small-caps") {
|
|
311
|
+
const scale = smallCapsScaleFor(
|
|
312
|
+
run.fontFamily ?? base.fontFamily,
|
|
313
|
+
run.fontWeight ?? (run.bold ? 700 : numericWeight(base.fontWeight)),
|
|
314
|
+
run.italic ? "italic" : base.fontStyle
|
|
315
|
+
);
|
|
316
|
+
span.style.setProperty(SMALL_CAPS_SCALE_PROPERTY, String(scale));
|
|
317
|
+
writeSmallCaps(span, run.text, run.textTransform ?? base.textTransform);
|
|
318
|
+
} else {
|
|
319
|
+
appendOverlayText(span, run.text);
|
|
320
|
+
}
|
|
321
|
+
if (run.fontVariantCaps != null) span.style.fontVariant = run.fontVariantCaps;
|
|
322
|
+
if (run.fontWeight != null) {
|
|
323
|
+
span.style.fontWeight = String(run.fontWeight);
|
|
324
|
+
span.setAttribute("data-font-weight", String(run.fontWeight));
|
|
325
|
+
} else if (run.bold) span.style.fontWeight = "700";
|
|
326
|
+
if (run.italic) span.style.fontStyle = "italic";
|
|
327
|
+
if (run.fontSize != null) span.style.fontSize = `${run.fontSize}px`;
|
|
328
|
+
else if (run.fontScale != null) span.style.fontSize = `${run.fontScale * 100}%`;
|
|
329
|
+
if (run.fontFamily != null) {
|
|
330
|
+
span.style.fontFamily = cssFontFamily(run.fontFamily, {
|
|
331
|
+
weight: run.fontWeight ?? (run.bold ? 700 : void 0),
|
|
332
|
+
style: run.italic ? "italic" : void 0
|
|
333
|
+
});
|
|
334
|
+
span.setAttribute("data-font-family", run.fontFamily);
|
|
335
|
+
}
|
|
336
|
+
const color = solidColor(run.fill);
|
|
337
|
+
if (color != null) span.style.color = color;
|
|
338
|
+
else if (run.fill !== void 0) span.setAttribute(FILL_ATTR, JSON.stringify(run.fill));
|
|
339
|
+
if (run.fill === null) span.style.color = "#000";
|
|
340
|
+
const decorations = [];
|
|
341
|
+
if (run.underline) decorations.push("underline");
|
|
342
|
+
if (run.strikethrough) decorations.push("line-through");
|
|
343
|
+
if (run.overline) decorations.push("overline");
|
|
344
|
+
if (decorations.length > 0) span.style.textDecoration = decorations.join(" ");
|
|
345
|
+
if (run.script != null) span.style.verticalAlign = run.script;
|
|
346
|
+
if (run.baselineShift != null) {
|
|
347
|
+
span.setAttribute("data-baseline-shift", String(run.baselineShift));
|
|
348
|
+
}
|
|
349
|
+
if (run.letterSpacing != null) span.style.letterSpacing = `${run.letterSpacing}px`;
|
|
350
|
+
if (run.textTransform != null) span.style.textTransform = run.textTransform;
|
|
351
|
+
parent.appendChild(span);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function sourceText(el) {
|
|
355
|
+
let out = "";
|
|
356
|
+
for (const n of el.childNodes) {
|
|
357
|
+
if (n.nodeType === Node.TEXT_NODE) out += n.data;
|
|
358
|
+
else if (n instanceof Element) out += n.hasAttribute(BREAK_ATTR) ? restoreBreak(n) : sourceText(n);
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
function piecesMatch(span, transform) {
|
|
363
|
+
const want = smallCapsPieces(sourceText(span), transform);
|
|
364
|
+
const have = [];
|
|
365
|
+
for (const n of span.childNodes) {
|
|
366
|
+
let small;
|
|
367
|
+
let text;
|
|
368
|
+
if (n.nodeType === Node.TEXT_NODE) {
|
|
369
|
+
small = false;
|
|
370
|
+
text = n.data;
|
|
371
|
+
} else if (n instanceof HTMLElement && n.hasAttribute(BREAK_ATTR)) {
|
|
372
|
+
small = false;
|
|
373
|
+
text = restoreBreak(n);
|
|
374
|
+
} else if (n instanceof HTMLElement && isSmallCapsPiece(n) && n.children.length === 0) {
|
|
375
|
+
small = true;
|
|
376
|
+
text = n.textContent ?? "";
|
|
377
|
+
} else return false;
|
|
378
|
+
if (text.length === 0) continue;
|
|
379
|
+
const last = have[have.length - 1];
|
|
380
|
+
if (last && last.small === small) last.text += text;
|
|
381
|
+
else have.push({ text, small });
|
|
382
|
+
}
|
|
383
|
+
return want.length === have.length && want.every((p, i) => p.text === have[i].text && p.small === have[i].small);
|
|
384
|
+
}
|
|
385
|
+
function normalizeSmallCaps(parent) {
|
|
386
|
+
let changed = false;
|
|
387
|
+
const visit = (el, variant, transform) => {
|
|
388
|
+
const v = cssFontVariant(el) ?? variant;
|
|
389
|
+
const tt = el.style.textTransform;
|
|
390
|
+
const t = TRANSFORMS.has(tt) ? tt : transform;
|
|
391
|
+
if (el.hasAttribute("data-run")) {
|
|
392
|
+
if (v === "small-caps") {
|
|
393
|
+
if (!piecesMatch(el, t)) {
|
|
394
|
+
writeSmallCaps(el, sourceText(el), t);
|
|
395
|
+
changed = true;
|
|
396
|
+
}
|
|
397
|
+
} else if (el.querySelector("[data-small-caps]")) {
|
|
398
|
+
const text = sourceText(el);
|
|
399
|
+
el.replaceChildren();
|
|
400
|
+
appendOverlayText(el, text);
|
|
401
|
+
changed = true;
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
for (const child of el.children) if (child instanceof HTMLElement) visit(child, v, t);
|
|
406
|
+
};
|
|
407
|
+
visit(parent, "normal", "none");
|
|
408
|
+
return changed;
|
|
409
|
+
}
|
|
410
|
+
function applyOverlayDirection(el, style) {
|
|
411
|
+
const auto = style.direction === "auto";
|
|
412
|
+
el.style.direction = auto ? "" : style.direction;
|
|
413
|
+
el.style.setProperty("unicode-bidi", auto ? "plaintext" : "");
|
|
414
|
+
el.style.setProperty("tab-size", String(style.tabSize));
|
|
415
|
+
const justify = style.align === "justify";
|
|
416
|
+
const edge = style.direction === "auto" ? justify ? "start" : style.align : resolveAlign(style.align, style.direction);
|
|
417
|
+
el.style.textAlign = justify ? "justify" : edge;
|
|
418
|
+
el.style.setProperty("text-align-last", justify ? edge : "");
|
|
419
|
+
}
|
|
420
|
+
function overlayAlignEdge(el, style) {
|
|
421
|
+
return resolveAlign(style.align, paragraphDirection(el.textContent ?? "", style.direction));
|
|
422
|
+
}
|
|
423
|
+
|
|
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;
|
|
439
|
+
}
|
|
440
|
+
function patchForToggle(current, toggle, nodeStyle) {
|
|
441
|
+
if (toggle === "super" || toggle === "sub") {
|
|
442
|
+
return { script: current.script === toggle ? void 0 : toggle };
|
|
443
|
+
}
|
|
444
|
+
const on = toggle === "bold" ? rangeWeight(current, nodeStyle).bold === true : current[toggle] === true || nodeHasFlag(nodeStyle, toggle);
|
|
445
|
+
return { [toggle]: !on };
|
|
446
|
+
}
|
|
447
|
+
function styleAtCaret(runs, at) {
|
|
448
|
+
return at > 0 ? styleAtRange(runs, at - 1, at) : styleAtRange(runs, 0, 1);
|
|
449
|
+
}
|
|
450
|
+
function spliceRunAt(runs, at, run) {
|
|
451
|
+
const out = [];
|
|
452
|
+
let pos = 0;
|
|
453
|
+
let placed = false;
|
|
454
|
+
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;
|
|
466
|
+
}
|
|
467
|
+
if (!placed) out.push(run);
|
|
468
|
+
return applyStyleToRange(out, 0, 0, {});
|
|
469
|
+
}
|
|
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);
|
|
484
|
+
}
|
|
485
|
+
function placeCaretBetweenRuns(overlay, runs, offset) {
|
|
486
|
+
let acc = 0;
|
|
487
|
+
let index = -1;
|
|
488
|
+
for (let i = 0; i < runs.length; i++) {
|
|
489
|
+
acc += runs[i].text.length;
|
|
490
|
+
if (acc === offset) {
|
|
491
|
+
index = i;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
if (acc > offset) return;
|
|
495
|
+
}
|
|
496
|
+
const span = index >= 0 ? overlay.children[index] : null;
|
|
497
|
+
const sel = window.getSelection();
|
|
498
|
+
if (!span || !sel) return;
|
|
499
|
+
const range = document.createRange();
|
|
500
|
+
range.setStartAfter(span);
|
|
501
|
+
range.collapse(true);
|
|
502
|
+
sel.removeAllRanges();
|
|
503
|
+
sel.addRange(range);
|
|
504
|
+
}
|
|
505
|
+
var NO_PENDING = Object.freeze({});
|
|
506
|
+
function mergePending(prev, patch) {
|
|
507
|
+
const next = { ...prev };
|
|
508
|
+
const superseded = supersededKey(patch);
|
|
509
|
+
if (superseded !== void 0) delete next[superseded];
|
|
510
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
511
|
+
if (value === void 0 || value === false) delete next[key];
|
|
512
|
+
else next[key] = value;
|
|
513
|
+
}
|
|
514
|
+
return next;
|
|
515
|
+
}
|
|
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
|
+
function sameSelection(a, b) {
|
|
550
|
+
if (a === null || b === null) return a === b;
|
|
551
|
+
return a.start === b.start && a.end === b.end;
|
|
552
|
+
}
|
|
553
|
+
var TEXT_EDIT_FONT_HOLD = 100;
|
|
554
|
+
function useTextEdit(opts) {
|
|
555
|
+
const optsRef = useLatest(opts);
|
|
556
|
+
const [editingId, setEditingId] = useState(null);
|
|
557
|
+
const editingIdRef = useLatest(editingId);
|
|
558
|
+
const overlayRef = useRef(null);
|
|
559
|
+
const baseRef = useRef(resolveTextStyle(void 0));
|
|
560
|
+
const [nodeStyle, setNodeStyle] = useState(null);
|
|
561
|
+
const nodeStyleRef = useRef(null);
|
|
562
|
+
const holdNodeStyle = useCallback((style) => {
|
|
563
|
+
nodeStyleRef.current = style;
|
|
564
|
+
setNodeStyle(style);
|
|
565
|
+
}, []);
|
|
566
|
+
const styleOf = useCallback(
|
|
567
|
+
(id) => nodeStyleRef.current ?? optsRef.current.getStyle(id),
|
|
568
|
+
[optsRef]
|
|
569
|
+
);
|
|
570
|
+
const tickRef = useRef(() => {
|
|
571
|
+
});
|
|
572
|
+
const frameLoop = useVisibleRaf(() => {
|
|
573
|
+
tickRef.current();
|
|
574
|
+
});
|
|
575
|
+
const initialCaretRef = useRef("all");
|
|
576
|
+
const openedRef = useRef(null);
|
|
577
|
+
const [selection, setSelection] = useState(null);
|
|
578
|
+
const [rangeStyle, setRangeStyle] = useState(null);
|
|
579
|
+
const [pendingStyle, setPendingStyle] = useState(NO_PENDING);
|
|
580
|
+
const pendingRef = useRef(NO_PENDING);
|
|
581
|
+
const pendingAtRef = useRef(null);
|
|
582
|
+
const armPending = useCallback((next, at) => {
|
|
583
|
+
const empty = Object.keys(next).length === 0;
|
|
584
|
+
pendingRef.current = empty ? NO_PENDING : next;
|
|
585
|
+
pendingAtRef.current = empty ? null : at;
|
|
586
|
+
setPendingStyle(pendingRef.current);
|
|
587
|
+
}, []);
|
|
588
|
+
const dropPending = useCallback(() => {
|
|
589
|
+
if (pendingAtRef.current === null) return;
|
|
590
|
+
pendingRef.current = NO_PENDING;
|
|
591
|
+
pendingAtRef.current = null;
|
|
592
|
+
setPendingStyle(NO_PENDING);
|
|
593
|
+
}, []);
|
|
594
|
+
const selectionRef = useRef(null);
|
|
595
|
+
const publishRange = useCallback((overlay, range) => {
|
|
596
|
+
const collapsed = range.start === range.end;
|
|
597
|
+
if (pendingAtRef.current !== null && !(collapsed && range.start === pendingAtRef.current)) {
|
|
598
|
+
dropPending();
|
|
599
|
+
}
|
|
600
|
+
selectionRef.current = range;
|
|
601
|
+
setSelection((prev) => sameSelection(prev, range) ? prev : range);
|
|
602
|
+
const runs = domToRuns(overlay);
|
|
603
|
+
setRangeStyle(collapsed ? styleAtCaret(runs, range.start) : styleAtRange(runs, range.start, range.end));
|
|
604
|
+
}, [dropPending]);
|
|
605
|
+
const syncSelection = useCallback(() => {
|
|
606
|
+
const overlay = overlayRef.current;
|
|
607
|
+
if (!overlay) return;
|
|
608
|
+
const next = readSelectionOffsets(overlay);
|
|
609
|
+
if (next === null) return;
|
|
610
|
+
publishRange(overlay, next);
|
|
611
|
+
}, [publishRange]);
|
|
612
|
+
const clearSelection = useCallback(() => {
|
|
613
|
+
selectionRef.current = null;
|
|
614
|
+
setSelection(null);
|
|
615
|
+
setRangeStyle(null);
|
|
616
|
+
dropPending();
|
|
617
|
+
}, [dropPending]);
|
|
618
|
+
const cancelEdit = useCallback(() => {
|
|
619
|
+
setEditingId(null);
|
|
620
|
+
}, []);
|
|
621
|
+
const commit = useCallback(() => {
|
|
622
|
+
const id = editingId;
|
|
623
|
+
const overlay = overlayRef.current;
|
|
624
|
+
if (id == null || !overlay) {
|
|
625
|
+
setEditingId(null);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const { getRuns, setRuns, setText, setStyle, commit: commitAll } = optsRef.current;
|
|
629
|
+
const priorRuns = getRuns?.(id);
|
|
630
|
+
const read = domToRuns(overlay);
|
|
631
|
+
const style = nodeStyleRef.current;
|
|
632
|
+
if (!style && JSON.stringify(read) === openedRef.current) {
|
|
633
|
+
setEditingId(null);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const runs = trimCaretHolder(read);
|
|
637
|
+
const hadRuns = priorRuns != null && priorRuns.length > 0;
|
|
638
|
+
const runsInPlay = hadRuns || runsCarryStyling(runs);
|
|
639
|
+
const text = runsToPlainText(runs);
|
|
640
|
+
holdNodeStyle(null);
|
|
641
|
+
if (commitAll) {
|
|
642
|
+
commitAll(id, { text, ...runsInPlay ? { runs } : {}, ...style ? { style } : {} });
|
|
643
|
+
} else {
|
|
644
|
+
if (style) setStyle?.(id, style);
|
|
645
|
+
setText?.(id, text);
|
|
646
|
+
if (setRuns && runsInPlay) setRuns(id, runs);
|
|
647
|
+
}
|
|
648
|
+
setEditingId(null);
|
|
649
|
+
}, [editingId, optsRef, holdNodeStyle]);
|
|
650
|
+
const startEdit = useCallback((id, opts2) => {
|
|
651
|
+
initialCaretRef.current = opts2?.caret ?? "all";
|
|
652
|
+
setEditingId(id);
|
|
653
|
+
}, []);
|
|
654
|
+
const isEditing = useCallback((id) => editingId === id, [editingId]);
|
|
655
|
+
const applyStyleToSelection = useCallback((patch) => {
|
|
656
|
+
const overlay = overlayRef.current;
|
|
657
|
+
if (!overlay) return;
|
|
658
|
+
if (Object.keys(patch).length === 0) return;
|
|
659
|
+
const range = readSelectionOffsets(overlay) ?? selectionRef.current;
|
|
660
|
+
if (!range) return;
|
|
661
|
+
const id = editingIdRef.current;
|
|
662
|
+
const canWriteStyle = optsRef.current.setStyle !== void 0 || optsRef.current.commit !== void 0;
|
|
663
|
+
const runs = domToRuns(overlay);
|
|
664
|
+
if (patch.bold === false && !("fontWeight" in patch) && id !== null) {
|
|
665
|
+
patch = { ...patch, ...unboldPatch(styleOf(id) ?? {}) };
|
|
666
|
+
}
|
|
667
|
+
const unsetFlag = id !== null && canWriteStyle ? Object.keys(patch).find((key) => key !== "bold" && patch[key] === false && nodeHasFlag(styleOf(id) ?? {}, key)) : void 0;
|
|
668
|
+
if (unsetFlag !== void 0 && id !== null) {
|
|
669
|
+
const r = setFlagOverRange(runs, styleOf(id) ?? {}, range.start, range.end, unsetFlag, false);
|
|
670
|
+
holdNodeStyle(r.style);
|
|
671
|
+
const resolved = resolveTextStyle(r.style, optsRef.current.getPaint?.(id));
|
|
672
|
+
baseRef.current = resolved;
|
|
673
|
+
applyOverlayStyle(overlay, resolved);
|
|
674
|
+
writeRunsPreservingSelection(overlay, r.runs, range.start, range.end, resolved);
|
|
675
|
+
publishRange(overlay, range);
|
|
676
|
+
if (range.start === range.end) {
|
|
677
|
+
armPending({ ...pendingRef.current, [unsetFlag]: false }, range.start);
|
|
678
|
+
}
|
|
679
|
+
restoreOverlayFocus(overlay, range);
|
|
680
|
+
const rest = { ...patch };
|
|
681
|
+
delete rest[unsetFlag];
|
|
682
|
+
if (Object.keys(rest).length > 0) applyStyleToSelection(rest);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (range.start === range.end) {
|
|
686
|
+
armPending(mergePending(pendingRef.current, patch), range.start);
|
|
687
|
+
restoreOverlayFocus(overlay, range);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const next = applyStyleToRange(runs, range.start, range.end, patch);
|
|
691
|
+
writeRunsPreservingSelection(overlay, next, range.start, range.end, baseRef.current);
|
|
692
|
+
restoreOverlayFocus(overlay, range);
|
|
693
|
+
publishRange(overlay, range);
|
|
694
|
+
}, [publishRange, armPending, editingIdRef, optsRef, styleOf, holdNodeStyle]);
|
|
695
|
+
const toggleStyle = useCallback((toggle) => {
|
|
696
|
+
const overlay = overlayRef.current;
|
|
697
|
+
if (!overlay) return;
|
|
698
|
+
const range = readSelectionOffsets(overlay) ?? selectionRef.current;
|
|
699
|
+
if (!range) return;
|
|
700
|
+
const runs = domToRuns(overlay);
|
|
701
|
+
const id = editingIdRef.current;
|
|
702
|
+
const nodeStyle2 = (id !== null ? styleOf(id) : void 0) ?? {};
|
|
703
|
+
const current = range.start === range.end ? patchRangeStyle(styleAtCaret(runs, range.start), pendingRef.current) : styleAtRange(runs, range.start, range.end);
|
|
704
|
+
applyStyleToSelection(patchForToggle(current, toggle, nodeStyle2));
|
|
705
|
+
}, [applyStyleToSelection, editingIdRef, styleOf]);
|
|
706
|
+
useEffect(() => {
|
|
707
|
+
if (editingId == null) return;
|
|
708
|
+
const { container, getText, getStyle, getPaint } = optsRef.current;
|
|
709
|
+
if (!container) return;
|
|
710
|
+
holdNodeStyle(null);
|
|
711
|
+
const style = resolveTextStyle(getStyle(editingId), getPaint?.(editingId));
|
|
712
|
+
const overlay = document.createElement("div");
|
|
713
|
+
const overlayClass = `weasel-text-edit-${++OVERLAY_SEQ}`;
|
|
714
|
+
overlay.classList.add(overlayClass);
|
|
715
|
+
overlay.setAttribute("contenteditable", "true");
|
|
716
|
+
overlay.spellcheck = false;
|
|
717
|
+
baseRef.current = style;
|
|
718
|
+
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
|
+
}
|
|
726
|
+
openedRef.current = JSON.stringify(domToRuns(overlay));
|
|
727
|
+
applyOverlayStyle(overlay, style);
|
|
728
|
+
const hold = holdForFaces(style, initialRuns ?? []);
|
|
729
|
+
if (hold) overlay.style.opacity = "0";
|
|
730
|
+
const styleEl = installSelectionStyle(overlayClass, style);
|
|
731
|
+
const clipBox = createClipBox();
|
|
732
|
+
clipBox.appendChild(overlay);
|
|
733
|
+
container.appendChild(clipBox);
|
|
734
|
+
overlayRef.current = overlay;
|
|
735
|
+
const place = () => {
|
|
736
|
+
const clip = optsRef.current.getClipRect?.() ?? null;
|
|
737
|
+
placeOverlay(overlay, clipBox, optsRef.current.getScreenPose(editingId), clip, style);
|
|
738
|
+
};
|
|
739
|
+
place();
|
|
740
|
+
let held = hold !== null;
|
|
741
|
+
const release = () => {
|
|
742
|
+
if (!held) return;
|
|
743
|
+
held = false;
|
|
744
|
+
clearTimeout(holdTimer);
|
|
745
|
+
place();
|
|
746
|
+
overlay.style.opacity = "";
|
|
747
|
+
};
|
|
748
|
+
const holdTimer = held ? setTimeout(release, optsRef.current.fontHold ?? TEXT_EDIT_FONT_HOLD) : void 0;
|
|
749
|
+
void hold?.then(release);
|
|
750
|
+
const range = document.createRange();
|
|
751
|
+
const initial = initialCaretRef.current;
|
|
752
|
+
if (initial === "all") {
|
|
753
|
+
range.selectNodeContents(overlay);
|
|
754
|
+
} else {
|
|
755
|
+
placeCaretAt(overlay, range, initial);
|
|
756
|
+
}
|
|
757
|
+
overlay.focus();
|
|
758
|
+
const sel = window.getSelection();
|
|
759
|
+
sel?.removeAllRanges();
|
|
760
|
+
sel?.addRange(range);
|
|
761
|
+
syncSelection();
|
|
762
|
+
const onKeyDown = (e) => {
|
|
763
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
764
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
765
|
+
e.preventDefault();
|
|
766
|
+
commit();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (e.key === "Escape") {
|
|
770
|
+
e.preventDefault();
|
|
771
|
+
if (optsRef.current.escape === "commit") commit();
|
|
772
|
+
else cancelEdit();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const toggle = toggleForKey(e);
|
|
776
|
+
if (toggle) {
|
|
777
|
+
e.preventDefault();
|
|
778
|
+
toggleStyle(toggle);
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
const onBlur = (e) => {
|
|
782
|
+
const next = e.relatedTarget;
|
|
783
|
+
const isChrome = optsRef.current.isEditorChrome;
|
|
784
|
+
if (isChrome && next instanceof Element && isChrome(next)) return;
|
|
785
|
+
commit();
|
|
786
|
+
};
|
|
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);
|
|
801
|
+
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);
|
|
818
|
+
}
|
|
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
|
+
};
|
|
839
|
+
const onPointerDownOutside = (e) => {
|
|
840
|
+
const target = e.target;
|
|
841
|
+
if (!(target instanceof Node) || overlay.contains(target)) return;
|
|
842
|
+
const isChrome = optsRef.current.isEditorChrome;
|
|
843
|
+
if (isChrome && target instanceof Element && isChrome(target)) return;
|
|
844
|
+
commit();
|
|
845
|
+
};
|
|
846
|
+
overlay.addEventListener("keydown", onKeyDown);
|
|
847
|
+
overlay.addEventListener("blur", onBlur);
|
|
848
|
+
overlay.addEventListener("input", onInput);
|
|
849
|
+
overlay.addEventListener("beforeinput", onBeforeInput);
|
|
850
|
+
document.addEventListener("pointerdown", onPointerDownOutside, true);
|
|
851
|
+
document.addEventListener("selectionchange", syncSelection);
|
|
852
|
+
tickRef.current = () => {
|
|
853
|
+
place();
|
|
854
|
+
frameLoop.request();
|
|
855
|
+
};
|
|
856
|
+
frameLoop.request();
|
|
857
|
+
return () => {
|
|
858
|
+
held = false;
|
|
859
|
+
clearTimeout(holdTimer);
|
|
860
|
+
frameLoop.cancel();
|
|
861
|
+
tickRef.current = () => {
|
|
862
|
+
};
|
|
863
|
+
overlay.removeEventListener("keydown", onKeyDown);
|
|
864
|
+
overlay.removeEventListener("blur", onBlur);
|
|
865
|
+
overlay.removeEventListener("input", onInput);
|
|
866
|
+
overlay.removeEventListener("beforeinput", onBeforeInput);
|
|
867
|
+
document.removeEventListener("pointerdown", onPointerDownOutside, true);
|
|
868
|
+
document.removeEventListener("selectionchange", syncSelection);
|
|
869
|
+
clipBox.remove();
|
|
870
|
+
styleEl?.remove();
|
|
871
|
+
overlayRef.current = null;
|
|
872
|
+
holdNodeStyle(null);
|
|
873
|
+
clearSelection();
|
|
874
|
+
};
|
|
875
|
+
}, [
|
|
876
|
+
editingId,
|
|
877
|
+
opts.container,
|
|
878
|
+
optsRef,
|
|
879
|
+
commit,
|
|
880
|
+
cancelEdit,
|
|
881
|
+
syncSelection,
|
|
882
|
+
clearSelection,
|
|
883
|
+
frameLoop,
|
|
884
|
+
toggleStyle,
|
|
885
|
+
dropPending,
|
|
886
|
+
publishRange,
|
|
887
|
+
holdNodeStyle
|
|
888
|
+
]);
|
|
889
|
+
return {
|
|
890
|
+
editingId,
|
|
891
|
+
startEdit,
|
|
892
|
+
cancelEdit,
|
|
893
|
+
commit,
|
|
894
|
+
isEditing,
|
|
895
|
+
nodeStyle,
|
|
896
|
+
selection,
|
|
897
|
+
rangeStyle,
|
|
898
|
+
pendingStyle,
|
|
899
|
+
applyStyleToSelection,
|
|
900
|
+
toggleStyle
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
var OVERLAY_SEQ = 0;
|
|
904
|
+
function holdForFaces(style, runs) {
|
|
905
|
+
const pending = [
|
|
906
|
+
cssFontFamilyLoading(style.fontFamily, {
|
|
907
|
+
weight: numericWeight(style.fontWeight),
|
|
908
|
+
style: style.fontStyle
|
|
909
|
+
}),
|
|
910
|
+
...runs.filter((r) => r.fontFamily != null).map((r) => cssFontFamilyLoading(r.fontFamily, {
|
|
911
|
+
weight: r.fontWeight ?? (r.bold ? 700 : void 0),
|
|
912
|
+
style: r.italic ? "italic" : void 0
|
|
913
|
+
}))
|
|
914
|
+
].filter((p) => p !== null);
|
|
915
|
+
return pending.length === 0 ? null : Promise.all(pending);
|
|
916
|
+
}
|
|
917
|
+
function placeCaretAt(root, range, offset) {
|
|
918
|
+
let remaining = offset;
|
|
919
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
920
|
+
let node = walker.nextNode();
|
|
921
|
+
while (node) {
|
|
922
|
+
const len = node.data.length;
|
|
923
|
+
if (remaining <= len) {
|
|
924
|
+
range.setStart(node, remaining);
|
|
925
|
+
range.setEnd(node, remaining);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
remaining -= len;
|
|
929
|
+
node = walker.nextNode();
|
|
930
|
+
}
|
|
931
|
+
range.selectNodeContents(root);
|
|
932
|
+
range.collapse(false);
|
|
933
|
+
}
|
|
934
|
+
function createClipBox() {
|
|
935
|
+
const el = document.createElement("div");
|
|
936
|
+
el.style.position = "absolute";
|
|
937
|
+
el.style.margin = "0";
|
|
938
|
+
el.style.padding = "0";
|
|
939
|
+
el.style.border = "0";
|
|
940
|
+
el.style.pointerEvents = "none";
|
|
941
|
+
return el;
|
|
942
|
+
}
|
|
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
|
+
function installSelectionStyle(overlayClass, style) {
|
|
978
|
+
if (style.selectionBackground == null && style.selectionColor == null) return null;
|
|
979
|
+
const el = document.createElement("style");
|
|
980
|
+
const decls = [];
|
|
981
|
+
if (style.selectionBackground != null) {
|
|
982
|
+
decls.push(`background: ${style.selectionBackground};`);
|
|
983
|
+
}
|
|
984
|
+
if (style.selectionColor != null) {
|
|
985
|
+
decls.push(`color: ${style.selectionColor};`);
|
|
986
|
+
}
|
|
987
|
+
el.textContent = `.${overlayClass}::selection { ${decls.join(" ")} }`;
|
|
988
|
+
document.head.appendChild(el);
|
|
989
|
+
return el;
|
|
990
|
+
}
|
|
991
|
+
var ALIGN_ANCHOR = { left: 0, center: 0.5, right: 1 };
|
|
992
|
+
function overlayTop(pose, contentHeight, drop = 0) {
|
|
993
|
+
const slack = verticalAlignOffset(pose.verticalAlign, pose.height, contentHeight);
|
|
994
|
+
return pose.y + (slack + drop) * (pose.zoom ?? 1);
|
|
995
|
+
}
|
|
996
|
+
function baselineDrop(el, style, pose, lineHeight) {
|
|
997
|
+
const canvas = layoutTextPose({
|
|
998
|
+
x: 0,
|
|
999
|
+
y: 0,
|
|
1000
|
+
width: pose.width,
|
|
1001
|
+
height: pose.height,
|
|
1002
|
+
text: " ",
|
|
1003
|
+
style: {
|
|
1004
|
+
fontFamily: style.fontFamily,
|
|
1005
|
+
fontWeight: style.fontWeight,
|
|
1006
|
+
fontStyle: style.fontStyle,
|
|
1007
|
+
fontSize: pose.fontSize,
|
|
1008
|
+
lineHeight,
|
|
1009
|
+
script: style.script
|
|
1010
|
+
}
|
|
1011
|
+
}).laid.lines[0]?.baselineY ?? 0;
|
|
1012
|
+
const css = cssBaseline(el);
|
|
1013
|
+
return canvas > 0 && css > 0 ? canvas - css : 0;
|
|
1014
|
+
}
|
|
1015
|
+
var CSS_BASELINES = /* @__PURE__ */ new Map();
|
|
1016
|
+
function cssBaseline(el) {
|
|
1017
|
+
const parent = el.parentElement;
|
|
1018
|
+
if (!parent) return 0;
|
|
1019
|
+
const { fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering } = el.style;
|
|
1020
|
+
const key = [fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering].join("|");
|
|
1021
|
+
const cached = CSS_BASELINES.get(key);
|
|
1022
|
+
if (cached !== void 0) return cached;
|
|
1023
|
+
const line = document.createElement("div");
|
|
1024
|
+
Object.assign(line.style, {
|
|
1025
|
+
position: "absolute",
|
|
1026
|
+
left: "0",
|
|
1027
|
+
top: "0",
|
|
1028
|
+
width: "100px",
|
|
1029
|
+
margin: "0",
|
|
1030
|
+
padding: "0",
|
|
1031
|
+
border: "0",
|
|
1032
|
+
visibility: "hidden",
|
|
1033
|
+
whiteSpace: "pre",
|
|
1034
|
+
fontFamily,
|
|
1035
|
+
fontWeight,
|
|
1036
|
+
fontStyle,
|
|
1037
|
+
fontSize,
|
|
1038
|
+
lineHeight,
|
|
1039
|
+
textRendering
|
|
1040
|
+
});
|
|
1041
|
+
const mark = document.createElement("span");
|
|
1042
|
+
Object.assign(mark.style, { display: "inline-block", width: "0", height: "0", verticalAlign: "baseline" });
|
|
1043
|
+
line.appendChild(mark);
|
|
1044
|
+
parent.appendChild(line);
|
|
1045
|
+
const box = line.getBoundingClientRect();
|
|
1046
|
+
const scale = box.width / 100;
|
|
1047
|
+
const baseline = scale > 0 ? (mark.getBoundingClientRect().top - box.top) / scale : 0;
|
|
1048
|
+
line.remove();
|
|
1049
|
+
if (baseline > 0 && document.fonts?.status !== "loading") {
|
|
1050
|
+
if (CSS_BASELINES.size >= 64) CSS_BASELINES.clear();
|
|
1051
|
+
CSS_BASELINES.set(key, baseline);
|
|
1052
|
+
}
|
|
1053
|
+
return baseline;
|
|
1054
|
+
}
|
|
1055
|
+
function placeOverlay(el, clipBox, pose, clip, style) {
|
|
1056
|
+
if (!pose) {
|
|
1057
|
+
el.style.display = "none";
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
clipBox.style.overflow = clip ? "clip" : "visible";
|
|
1061
|
+
clipBox.style.left = `${clip?.x ?? 0}px`;
|
|
1062
|
+
clipBox.style.top = `${clip?.y ?? 0}px`;
|
|
1063
|
+
clipBox.style.width = `${clip?.width ?? 0}px`;
|
|
1064
|
+
clipBox.style.height = `${clip?.height ?? 0}px`;
|
|
1065
|
+
el.style.display = "";
|
|
1066
|
+
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`;
|
|
1068
|
+
el.style.width = style.wrap ? `${pose.width}px` : "max-content";
|
|
1069
|
+
el.style.minWidth = style.wrap ? "" : `${pose.width}px`;
|
|
1070
|
+
const script = style.script ? scriptMetricsFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle)[style.script] : void 0;
|
|
1071
|
+
const lineHeight = pose.lineHeight ?? style.lineHeight;
|
|
1072
|
+
el.style.fontSize = `${pose.fontSize * (script?.size ?? 1)}px`;
|
|
1073
|
+
el.style.lineHeight = script ? `${pose.fontSize * lineHeight}px` : String(lineHeight);
|
|
1074
|
+
const aligned = pose.verticalAlign !== void 0 && pose.verticalAlign !== "top";
|
|
1075
|
+
el.style.minHeight = aligned ? "" : `${pose.height}px`;
|
|
1076
|
+
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`;
|
|
1078
|
+
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";
|
|
1083
|
+
const scale = pose.zoom !== void 0 ? 1 : style.fontSize > 0 ? pose.fontSize / style.fontSize : 1;
|
|
1084
|
+
el.style.letterSpacing = `${style.letterSpacing * scale}px`;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export { TEXT_EDIT_FONT_HOLD, baselineDrop, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, overlayTop, runsToDom, useTextEdit };
|
|
1088
|
+
//# sourceMappingURL=index.js.map
|
|
1089
|
+
//# sourceMappingURL=index.js.map
|