@weasel-js/text-dom 0.0.0-stage → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1322 @@
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, effectiveRangeStyle, SMALL_CAPS_SCALE, isHardLineBreak, fontString, supersededKey, rangeWeight, scriptMetricsFor, transformRunTexts, smallCapsText, resolveAlign, paragraphDirection, layoutTextPose, verticalAlignOffset } from '@weasel-js/text';
4
+ import { cssFontFamily, cssFontFamilyLoading, isCanvasFont } from '@weasel-js/font';
5
+ import { isSolidPaint } from '@weasel-js/paint';
6
+
7
+ // src/useTextEdit.ts
8
+
9
+ // src/domOffsets.ts
10
+ var BLOCK_TAGS = /* @__PURE__ */ new Set(["DIV", "P"]);
11
+ function overlayPieces(parent) {
12
+ const out = [];
13
+ const visit = (node) => {
14
+ if (node.nodeType === Node.TEXT_NODE) {
15
+ if (node.data.length > 0) out.push({ text: node });
16
+ return;
17
+ }
18
+ if (!(node instanceof Element)) return;
19
+ if (node.tagName === "BR") {
20
+ out.push({ br: node });
21
+ return;
22
+ }
23
+ if (BLOCK_TAGS.has(node.tagName) && out.length > 0) out.push({ block: node });
24
+ for (const child of node.childNodes) visit(child);
25
+ };
26
+ for (const child of parent.childNodes) visit(child);
27
+ return out;
28
+ }
29
+ var indexIn = (el) => Array.prototype.indexOf.call(el.parentNode.childNodes, el);
30
+ function afterBreak(piece) {
31
+ return "br" in piece ? { node: piece.br.parentNode, offset: indexIn(piece.br) + 1 } : { node: piece.block, offset: 0 };
32
+ }
33
+ function charOffsetToDomPosition(parent, offset) {
34
+ let remaining = offset;
35
+ let end = { node: parent, offset: 0 };
36
+ let pending = null;
37
+ for (const piece of overlayPieces(parent)) {
38
+ if ("text" in piece) {
39
+ const len = piece.text.data.length;
40
+ if (remaining <= len) return { node: piece.text, offset: remaining };
41
+ remaining -= len;
42
+ end = { node: piece.text, offset: len };
43
+ pending = null;
44
+ continue;
45
+ }
46
+ if (remaining === 0) {
47
+ return pending ?? ("br" in piece ? { node: piece.br.parentNode, offset: indexIn(piece.br) } : afterBreak(piece));
48
+ }
49
+ remaining -= 1;
50
+ end = afterBreak(piece);
51
+ pending = remaining === 0 ? end : null;
52
+ }
53
+ return end;
54
+ }
55
+ function domPositionToCharOffset(parent, node, offset) {
56
+ const pieces = overlayPieces(parent);
57
+ let total = 0;
58
+ if (node.nodeType === Node.TEXT_NODE) {
59
+ for (const piece of pieces) {
60
+ if (!("text" in piece)) {
61
+ total += 1;
62
+ continue;
63
+ }
64
+ if (piece.text === node) return total + offset;
65
+ total += piece.text.data.length;
66
+ }
67
+ return total;
68
+ }
69
+ const point = document.createRange();
70
+ point.setStart(node, Math.max(0, Math.min(offset, node.childNodes.length)));
71
+ point.collapse(true);
72
+ for (const piece of pieces) {
73
+ if ("text" in piece) {
74
+ if (point.comparePoint(piece.text, 0) < 0) total += piece.text.data.length;
75
+ } else {
76
+ const past = afterBreak(piece);
77
+ if (point.comparePoint(past.node, past.offset) <= 0) total += 1;
78
+ }
79
+ }
80
+ return total;
81
+ }
82
+
83
+ // src/domRuns.ts
84
+ function solidColor(p) {
85
+ if (!p) return null;
86
+ if (isSolidPaint(p)) return p.color;
87
+ return null;
88
+ }
89
+ var FILL_ATTR = "data-fill";
90
+ var TRANSFORMS = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
91
+ var EMPTY_STYLE = {
92
+ bold: false,
93
+ italic: false,
94
+ underline: false,
95
+ strikethrough: false,
96
+ overline: false
97
+ };
98
+ function parseLetterSpacing(raw) {
99
+ const n = parseFloat(raw);
100
+ if (!Number.isFinite(n)) return void 0;
101
+ const unit = /^-?[\d.]+([a-z%]+)$/i.exec(raw.trim())?.[1]?.toLowerCase();
102
+ if (unit && unit !== "px") {
103
+ console.warn(
104
+ `weasel domRuns: letter-spacing "${raw}" uses unit "${unit}", which is not converted; treated as ${n} world units`
105
+ );
106
+ }
107
+ return n;
108
+ }
109
+ var isSmallCapsPiece = (el) => el.hasAttribute("data-small-caps");
110
+ function cssFontVariant(el) {
111
+ const v = el.style.fontVariant || el.style.getPropertyValue("font-variant-caps");
112
+ return v === "small-caps" || v === "normal" ? v : void 0;
113
+ }
114
+ function styleStateFromElement(el, parent) {
115
+ if (isSmallCapsPiece(el)) return parent;
116
+ const next = { ...parent };
117
+ const tag = el.tagName;
118
+ if (tag === "B" || tag === "STRONG") {
119
+ next.bold = true;
120
+ next.fontWeight = void 0;
121
+ }
122
+ if (tag === "I" || tag === "EM") next.italic = true;
123
+ if (tag === "U") next.underline = true;
124
+ if (tag === "S" || tag === "STRIKE" || tag === "DEL") next.strikethrough = true;
125
+ if (tag === "SUP") next.script = "super";
126
+ if (tag === "SUB") next.script = "sub";
127
+ if (el instanceof HTMLElement) {
128
+ const fw = el.style.fontWeight;
129
+ const ownWeight = Number(el.getAttribute("data-font-weight") ?? fw);
130
+ if (el.hasAttribute("data-font-weight") || !["", "700", "bold", "400", "normal"].includes(fw)) {
131
+ if (Number.isFinite(ownWeight) && ownWeight > 0) {
132
+ next.fontWeight = ownWeight;
133
+ next.bold = false;
134
+ }
135
+ } else if (fw !== "") {
136
+ next.bold = fw === "700" || fw === "bold";
137
+ next.fontWeight = void 0;
138
+ }
139
+ const fs = el.style.fontStyle;
140
+ if (fs === "italic") next.italic = true;
141
+ if (fs === "normal") next.italic = false;
142
+ const td = el.style.textDecoration || el.style.textDecorationLine;
143
+ if (td && !td.includes("none")) {
144
+ next.underline ||= td.includes("underline");
145
+ next.strikethrough ||= td.includes("line-through");
146
+ next.overline ||= td.includes("overline");
147
+ }
148
+ if (el.style.fontSize) {
149
+ const size = parseFloat(el.style.fontSize);
150
+ if (Number.isFinite(size)) {
151
+ if (el.style.fontSize.trim().endsWith("%")) next.fontScale = size / 100;
152
+ else next.fontSize = size;
153
+ }
154
+ }
155
+ const va = el.style.verticalAlign;
156
+ if (va === "super" || va === "sub") next.script = va === "super" ? "super" : "sub";
157
+ if (va === "baseline") next.script = void 0;
158
+ const rawShift = el.getAttribute("data-baseline-shift");
159
+ if (rawShift != null) {
160
+ const shift = parseFloat(rawShift);
161
+ if (Number.isFinite(shift)) next.baselineShift = shift;
162
+ }
163
+ const tt = el.style.textTransform;
164
+ if (TRANSFORMS.has(tt)) next.textTransform = tt;
165
+ const fv = cssFontVariant(el);
166
+ if (fv) next.fontVariantCaps = fv;
167
+ const family = el.getAttribute("data-font-family") ?? el.style.fontFamily;
168
+ if (family) next.fontFamily = family;
169
+ const fillJson = el.getAttribute(FILL_ATTR);
170
+ if (fillJson != null) {
171
+ next.fillJson = fillJson;
172
+ next.color = void 0;
173
+ } else if (el.style.color) {
174
+ next.color = el.style.color;
175
+ next.fillJson = void 0;
176
+ }
177
+ const ls = el.style.letterSpacing;
178
+ if (ls === "normal") {
179
+ next.letterSpacing = void 0;
180
+ } else if (ls) {
181
+ const px = parseLetterSpacing(ls);
182
+ if (px != null) next.letterSpacing = px;
183
+ }
184
+ }
185
+ return next;
186
+ }
187
+ function styleEquals(a, b) {
188
+ 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;
189
+ }
190
+ function toRun(text, style) {
191
+ const run = { text };
192
+ if (style.bold) run.bold = true;
193
+ if (style.italic) run.italic = true;
194
+ if (style.fontWeight != null) run.fontWeight = style.fontWeight;
195
+ if (style.fontSize != null) run.fontSize = style.fontSize;
196
+ if (style.fontFamily != null) run.fontFamily = style.fontFamily;
197
+ if (style.fillJson !== void 0) run.fill = JSON.parse(style.fillJson);
198
+ else if (style.color != null) run.fill = { paint: { kind: "solid", color: style.color } };
199
+ if (style.letterSpacing != null) run.letterSpacing = style.letterSpacing;
200
+ if (style.underline) run.underline = true;
201
+ if (style.strikethrough) run.strikethrough = true;
202
+ if (style.overline) run.overline = true;
203
+ if (style.script != null) run.script = style.script;
204
+ if (style.baselineShift != null) run.baselineShift = style.baselineShift;
205
+ if (style.fontScale != null) run.fontScale = style.fontScale;
206
+ if (style.textTransform != null) run.textTransform = style.textTransform;
207
+ if (style.fontVariantCaps != null) run.fontVariantCaps = style.fontVariantCaps;
208
+ return run;
209
+ }
210
+ var BREAK_ATTR = "data-break";
211
+ function restoreBreak(el) {
212
+ const text = el.textContent ?? "";
213
+ const code = parseInt(el.getAttribute(BREAK_ATTR) ?? "", 16);
214
+ return text.startsWith("\n") && Number.isFinite(code) ? String.fromCharCode(code) + text.slice(1) : text;
215
+ }
216
+ function appendOverlayText(parent, text) {
217
+ let from = 0;
218
+ for (let i = 0; i < text.length; i++) {
219
+ const c = text.charCodeAt(i);
220
+ if (c === 10 || !isHardLineBreak(c) || c === 13 && text.charCodeAt(i + 1) === 10) continue;
221
+ if (i > from) parent.appendChild(document.createTextNode(text.slice(from, i)));
222
+ const span = document.createElement("span");
223
+ span.setAttribute(BREAK_ATTR, c.toString(16));
224
+ span.textContent = "\n";
225
+ parent.appendChild(span);
226
+ from = i + 1;
227
+ }
228
+ if (from < text.length) parent.appendChild(document.createTextNode(text.slice(from)));
229
+ }
230
+ function domToRuns(parent) {
231
+ const fragments = [];
232
+ function visit(node, style) {
233
+ if (node.nodeType === Node.TEXT_NODE) {
234
+ const text = node.data;
235
+ if (text.length > 0) fragments.push({ text, style });
236
+ return;
237
+ }
238
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
239
+ const el = node;
240
+ if (el.tagName === "BR") {
241
+ fragments.push({ text: "\n", style });
242
+ return;
243
+ }
244
+ if (el.hasAttribute(BREAK_ATTR)) {
245
+ const restored = restoreBreak(el);
246
+ if (restored.length > 0) fragments.push({ text: restored, style });
247
+ return;
248
+ }
249
+ if (BLOCK_TAGS.has(el.tagName) && fragments.length > 0) {
250
+ fragments.push({ text: "\n", style });
251
+ }
252
+ const nextStyle = styleStateFromElement(el, style);
253
+ for (const child of Array.from(el.childNodes)) {
254
+ visit(child, nextStyle);
255
+ }
256
+ }
257
+ for (const child of Array.from(parent.childNodes)) {
258
+ visit(child, EMPTY_STYLE);
259
+ }
260
+ const runs = [];
261
+ let i = 0;
262
+ while (i < fragments.length) {
263
+ let j = i + 1;
264
+ while (j < fragments.length && styleEquals(fragments[i].style, fragments[j].style)) {
265
+ j++;
266
+ }
267
+ const merged = fragments.slice(i, j).map((f) => f.text).join("");
268
+ runs.push(toRun(merged, fragments[i].style));
269
+ i = j;
270
+ }
271
+ return runs;
272
+ }
273
+ var SMALL_CAPS_SCALE_PROPERTY = "--weasel-small-caps";
274
+ function smallCapsPieces(text, transform) {
275
+ const [shown] = transformRunTexts([text], [transform]);
276
+ const caps = smallCapsText(shown.text, shown.srcMap);
277
+ const small = new Array(text.length).fill(false);
278
+ caps.small?.forEach((s, i) => {
279
+ if (s) small[caps.srcMap ? caps.srcMap.starts[i] : i] = true;
280
+ });
281
+ const out = [];
282
+ let at = 0;
283
+ for (const ch of text) {
284
+ const last = out[out.length - 1];
285
+ if (last && last.small === small[at]) last.text += ch;
286
+ else out.push({ text: ch, small: small[at] });
287
+ at += ch.length;
288
+ }
289
+ return out;
290
+ }
291
+ function writeSmallCaps(span, text, transform) {
292
+ span.replaceChildren();
293
+ for (const p of smallCapsPieces(text, transform)) {
294
+ if (!p.small) {
295
+ appendOverlayText(span, p.text);
296
+ continue;
297
+ }
298
+ const piece = document.createElement("span");
299
+ piece.setAttribute("data-small-caps", "");
300
+ piece.style.fontSize = `calc(var(${SMALL_CAPS_SCALE_PROPERTY}, ${SMALL_CAPS_SCALE}) * 1em)`;
301
+ piece.style.textTransform = "uppercase";
302
+ piece.textContent = p.text;
303
+ span.appendChild(piece);
304
+ }
305
+ }
306
+ function runsToDom(runs, parent, base = DEFAULT_TEXT_STYLE) {
307
+ parent.replaceChildren();
308
+ for (const run of runs) {
309
+ const span = document.createElement("span");
310
+ span.setAttribute("data-run", "");
311
+ if ((run.fontVariantCaps ?? base.fontVariantCaps) === "small-caps") {
312
+ const scale = smallCapsScaleFor(
313
+ run.fontFamily ?? base.fontFamily,
314
+ run.fontWeight ?? (run.bold ? 700 : numericWeight(base.fontWeight)),
315
+ run.italic ? "italic" : base.fontStyle
316
+ );
317
+ span.style.setProperty(SMALL_CAPS_SCALE_PROPERTY, String(scale));
318
+ writeSmallCaps(span, run.text, run.textTransform ?? base.textTransform);
319
+ } else {
320
+ appendOverlayText(span, run.text);
321
+ }
322
+ if (run.fontVariantCaps != null) span.style.fontVariant = run.fontVariantCaps;
323
+ if (run.fontWeight != null) {
324
+ span.style.fontWeight = String(run.fontWeight);
325
+ span.setAttribute("data-font-weight", String(run.fontWeight));
326
+ } else if (run.bold) span.style.fontWeight = "700";
327
+ if (run.italic) span.style.fontStyle = "italic";
328
+ if (run.fontSize != null) span.style.fontSize = `${run.fontSize}px`;
329
+ else if (run.fontScale != null) span.style.fontSize = `${run.fontScale * 100}%`;
330
+ if (run.fontFamily != null) {
331
+ span.style.fontFamily = cssFontFamily(run.fontFamily, {
332
+ weight: run.fontWeight ?? (run.bold ? 700 : void 0),
333
+ style: run.italic ? "italic" : void 0
334
+ });
335
+ span.setAttribute("data-font-family", run.fontFamily);
336
+ }
337
+ const color = solidColor(run.fill?.paint);
338
+ if (color != null) span.style.color = color;
339
+ else if (run.fill !== void 0) span.setAttribute(FILL_ATTR, JSON.stringify(run.fill));
340
+ if (run.fill === null) span.style.color = "#000";
341
+ const decorations = [];
342
+ if (run.underline) decorations.push("underline");
343
+ if (run.strikethrough) decorations.push("line-through");
344
+ if (run.overline) decorations.push("overline");
345
+ if (decorations.length > 0) span.style.textDecoration = decorations.join(" ");
346
+ if (run.script != null) span.style.verticalAlign = run.script;
347
+ if (run.baselineShift != null) {
348
+ span.setAttribute("data-baseline-shift", String(run.baselineShift));
349
+ }
350
+ if (run.letterSpacing != null) span.style.letterSpacing = `${run.letterSpacing}px`;
351
+ if (run.textTransform != null) span.style.textTransform = run.textTransform;
352
+ parent.appendChild(span);
353
+ }
354
+ }
355
+ function sourceText(el) {
356
+ let out = "";
357
+ for (const n of el.childNodes) {
358
+ if (n.nodeType === Node.TEXT_NODE) out += n.data;
359
+ else if (n instanceof Element) out += n.hasAttribute(BREAK_ATTR) ? restoreBreak(n) : sourceText(n);
360
+ }
361
+ return out;
362
+ }
363
+ function piecesMatch(span, transform) {
364
+ const want = smallCapsPieces(sourceText(span), transform);
365
+ const have = [];
366
+ for (const n of span.childNodes) {
367
+ let small;
368
+ let text;
369
+ if (n.nodeType === Node.TEXT_NODE) {
370
+ small = false;
371
+ text = n.data;
372
+ } else if (n instanceof HTMLElement && n.hasAttribute(BREAK_ATTR)) {
373
+ small = false;
374
+ text = restoreBreak(n);
375
+ } else if (n instanceof HTMLElement && isSmallCapsPiece(n) && n.children.length === 0) {
376
+ small = true;
377
+ text = n.textContent ?? "";
378
+ } else return false;
379
+ if (text.length === 0) continue;
380
+ const last = have[have.length - 1];
381
+ if (last && last.small === small) last.text += text;
382
+ else have.push({ text, small });
383
+ }
384
+ return want.length === have.length && want.every((p, i) => p.text === have[i].text && p.small === have[i].small);
385
+ }
386
+ function normalizeSmallCaps(parent) {
387
+ let changed = false;
388
+ const visit = (el, variant, transform) => {
389
+ const v = cssFontVariant(el) ?? variant;
390
+ const tt = el.style.textTransform;
391
+ const t = TRANSFORMS.has(tt) ? tt : transform;
392
+ if (el.hasAttribute("data-run")) {
393
+ if (v === "small-caps") {
394
+ if (!piecesMatch(el, t)) {
395
+ writeSmallCaps(el, sourceText(el), t);
396
+ changed = true;
397
+ }
398
+ } else if (el.querySelector("[data-small-caps]")) {
399
+ const text = sourceText(el);
400
+ el.replaceChildren();
401
+ appendOverlayText(el, text);
402
+ changed = true;
403
+ }
404
+ return;
405
+ }
406
+ for (const child of el.children) if (child instanceof HTMLElement) visit(child, v, t);
407
+ };
408
+ visit(parent, "normal", "none");
409
+ return changed;
410
+ }
411
+ function applyOverlayDirection(el, style) {
412
+ const auto = style.direction === "auto";
413
+ el.style.direction = auto ? "" : style.direction;
414
+ el.style.setProperty("unicode-bidi", auto ? "plaintext" : "");
415
+ el.style.setProperty("tab-size", String(style.tabSize));
416
+ const justify = style.align === "justify";
417
+ const edge = style.direction === "auto" ? justify ? "start" : style.align : resolveAlign(style.align, style.direction);
418
+ el.style.textAlign = justify ? "justify" : edge;
419
+ el.style.setProperty("text-align-last", justify ? edge : "");
420
+ }
421
+ function overlayAlignEdge(el, style) {
422
+ return resolveAlign(style.align, paragraphDirection(el.textContent ?? "", style.direction));
423
+ }
424
+
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";
440
+ }
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;
505
+ }
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 };
509
+ }
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 }] : [];
666
+ }
667
+ function replaceRunRange(runs, start, end, insert) {
668
+ const out = [];
669
+ let pos = 0;
670
+ let placed = false;
671
+ const place = () => {
672
+ if (!placed) out.push(...insert.map((r) => ({ ...r })));
673
+ placed = true;
674
+ };
675
+ for (const r of runs) {
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)) });
681
+ }
682
+ place();
683
+ return applyStyleToRange(out, 0, 0, {});
684
+ }
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;
698
+ }
699
+
700
+ // src/overlayTyping.ts
701
+ function placeCaretBetweenRuns(overlay, runs, offset) {
702
+ let acc = 0;
703
+ let index = -1;
704
+ for (let i = 0; i < runs.length; i++) {
705
+ acc += runs[i].text.length;
706
+ if (acc === offset) {
707
+ index = i;
708
+ break;
709
+ }
710
+ if (acc > offset) return;
711
+ }
712
+ const span = index >= 0 ? overlay.children[index] : null;
713
+ const sel = window.getSelection();
714
+ if (!span || !sel) return;
715
+ const range = document.createRange();
716
+ range.setStartAfter(span);
717
+ range.collapse(true);
718
+ sel.removeAllRanges();
719
+ sel.addRange(range);
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
+ }
841
+ var NO_PENDING = Object.freeze({});
842
+ function mergePending(prev, patch) {
843
+ const next = { ...prev };
844
+ const superseded = supersededKey(patch);
845
+ if (superseded !== void 0) delete next[superseded];
846
+ for (const [key, value] of Object.entries(patch)) {
847
+ if (value === void 0 || value === false) delete next[key];
848
+ else next[key] = value;
849
+ }
850
+ return next;
851
+ }
852
+ function sameSelection(a, b) {
853
+ if (a === null || b === null) return a === b;
854
+ return a.start === b.start && a.end === b.end;
855
+ }
856
+ function useTextEdit(opts) {
857
+ const optsRef = useLatest(opts);
858
+ const [editingId, setEditingId] = useState(null);
859
+ const editingIdRef = useLatest(editingId);
860
+ const overlayRef = useRef(null);
861
+ const baseRef = useRef(resolveTextStyle(void 0));
862
+ const [nodeStyle, setNodeStyle] = useState(null);
863
+ const nodeStyleRef = useRef(null);
864
+ const holdNodeStyle = useCallback((style) => {
865
+ nodeStyleRef.current = style;
866
+ setNodeStyle(style);
867
+ }, []);
868
+ const styleOf = useCallback(
869
+ (id) => nodeStyleRef.current ?? optsRef.current.getStyle(id),
870
+ [optsRef]
871
+ );
872
+ const tickRef = useRef(() => {
873
+ });
874
+ const frameLoop = useVisibleRaf(() => {
875
+ tickRef.current();
876
+ });
877
+ const initialCaretRef = useRef("all");
878
+ const openedRef = useRef(null);
879
+ const holderRef = useRef(null);
880
+ const [selection, setSelection] = useState(null);
881
+ const [rangeStyle, setRangeStyle] = useState(null);
882
+ const [pendingStyle, setPendingStyle] = useState(NO_PENDING);
883
+ const pendingRef = useRef(NO_PENDING);
884
+ const pendingAtRef = useRef(null);
885
+ const armPending = useCallback((next, at) => {
886
+ const empty = Object.keys(next).length === 0;
887
+ pendingRef.current = empty ? NO_PENDING : next;
888
+ pendingAtRef.current = empty ? null : at;
889
+ setPendingStyle(pendingRef.current);
890
+ }, []);
891
+ const dropPending = useCallback(() => {
892
+ if (pendingAtRef.current === null) return;
893
+ pendingRef.current = NO_PENDING;
894
+ pendingAtRef.current = null;
895
+ setPendingStyle(NO_PENDING);
896
+ }, []);
897
+ const selectionRef = useRef(null);
898
+ const publishRange = useCallback((overlay, range) => {
899
+ const collapsed = range.start === range.end;
900
+ if (pendingAtRef.current !== null && !(collapsed && range.start === pendingAtRef.current)) {
901
+ dropPending();
902
+ }
903
+ selectionRef.current = range;
904
+ setSelection((prev) => sameSelection(prev, range) ? prev : range);
905
+ const runs = domToRuns(overlay);
906
+ setRangeStyle(collapsed ? styleAtCaret(runs, range.start) : styleAtRange(runs, range.start, range.end));
907
+ }, [dropPending]);
908
+ const syncSelection = useCallback(() => {
909
+ const overlay = overlayRef.current;
910
+ if (!overlay) return;
911
+ const next = readSelectionOffsets(overlay);
912
+ if (next === null) return;
913
+ publishRange(overlay, next);
914
+ }, [publishRange]);
915
+ const clearSelection = useCallback(() => {
916
+ selectionRef.current = null;
917
+ setSelection(null);
918
+ setRangeStyle(null);
919
+ dropPending();
920
+ }, [dropPending]);
921
+ const cancelEdit = useCallback(() => {
922
+ setEditingId(null);
923
+ }, []);
924
+ const commit = useCallback(() => {
925
+ const id = editingId;
926
+ const overlay = overlayRef.current;
927
+ if (id == null || !overlay) {
928
+ setEditingId(null);
929
+ return;
930
+ }
931
+ const { getRuns, setRuns, setText, setStyle, commit: commitAll } = optsRef.current;
932
+ const priorRuns = getRuns?.(id);
933
+ const read = domToRuns(overlay);
934
+ const style = nodeStyleRef.current;
935
+ if (!style && JSON.stringify(read) === openedRef.current) {
936
+ setEditingId(null);
937
+ return;
938
+ }
939
+ const runs = holderRef.current?.trim(read) ?? read;
940
+ const hadRuns = priorRuns != null && priorRuns.length > 0;
941
+ const runsInPlay = hadRuns || runsCarryStyling(runs);
942
+ const text = runsToPlainText(runs);
943
+ holdNodeStyle(null);
944
+ if (commitAll) {
945
+ commitAll(id, { text, ...runsInPlay ? { runs } : {}, ...style ? { style } : {} });
946
+ } else {
947
+ if (style) setStyle?.(id, style);
948
+ setText?.(id, text);
949
+ if (setRuns && runsInPlay) setRuns(id, runs);
950
+ }
951
+ setEditingId(null);
952
+ }, [editingId, optsRef, holdNodeStyle]);
953
+ const startEdit = useCallback((id, opts2) => {
954
+ initialCaretRef.current = opts2?.caret ?? "all";
955
+ setEditingId(id);
956
+ }, []);
957
+ const isEditing = useCallback((id) => editingId === id, [editingId]);
958
+ const applyStyleToSelection = useCallback((patch) => {
959
+ const overlay = overlayRef.current;
960
+ if (!overlay) return;
961
+ if (Object.keys(patch).length === 0) return;
962
+ const range = readSelectionOffsets(overlay) ?? selectionRef.current;
963
+ if (!range) return;
964
+ const id = editingIdRef.current;
965
+ const canWriteStyle = optsRef.current.setStyle !== void 0 || optsRef.current.commit !== void 0;
966
+ const runs = domToRuns(overlay);
967
+ if (patch.bold === false && !("fontWeight" in patch) && id !== null) {
968
+ patch = { ...patch, ...unboldPatch(styleOf(id) ?? {}) };
969
+ }
970
+ const unsetFlag = id !== null && canWriteStyle ? Object.keys(patch).find((key) => key !== "bold" && patch[key] === false && nodeHasFlag(styleOf(id) ?? {}, key)) : void 0;
971
+ if (unsetFlag !== void 0 && id !== null) {
972
+ const r = setFlagOverRange(runs, styleOf(id) ?? {}, range.start, range.end, unsetFlag, false);
973
+ holdNodeStyle(r.style);
974
+ const resolved = resolveTextStyle(r.style, optsRef.current.getPaint?.(id));
975
+ baseRef.current = resolved;
976
+ applyOverlayStyle(overlay, resolved);
977
+ writeRunsPreservingSelection(overlay, r.runs, range.start, range.end, resolved);
978
+ publishRange(overlay, range);
979
+ if (range.start === range.end) {
980
+ armPending({ ...pendingRef.current, [unsetFlag]: false }, range.start);
981
+ }
982
+ restoreOverlayFocus(overlay, range);
983
+ const rest = { ...patch };
984
+ delete rest[unsetFlag];
985
+ if (Object.keys(rest).length > 0) applyStyleToSelection(rest);
986
+ return;
987
+ }
988
+ if (range.start === range.end) {
989
+ armPending(mergePending(pendingRef.current, patch), range.start);
990
+ restoreOverlayFocus(overlay, range);
991
+ return;
992
+ }
993
+ const next = applyStyleToRange(runs, range.start, range.end, patch);
994
+ writeRunsPreservingSelection(overlay, next, range.start, range.end, baseRef.current);
995
+ restoreOverlayFocus(overlay, range);
996
+ publishRange(overlay, range);
997
+ }, [publishRange, armPending, editingIdRef, optsRef, styleOf, holdNodeStyle]);
998
+ const toggleStyle = useCallback((toggle) => {
999
+ const overlay = overlayRef.current;
1000
+ if (!overlay) return;
1001
+ const range = readSelectionOffsets(overlay) ?? selectionRef.current;
1002
+ if (!range) return;
1003
+ const runs = domToRuns(overlay);
1004
+ const id = editingIdRef.current;
1005
+ const nodeStyle2 = (id !== null ? styleOf(id) : void 0) ?? {};
1006
+ const current = range.start === range.end ? patchRangeStyle(styleAtCaret(runs, range.start), pendingRef.current) : styleAtRange(runs, range.start, range.end);
1007
+ applyStyleToSelection(patchForToggle(current, toggle, nodeStyle2));
1008
+ }, [applyStyleToSelection, editingIdRef, styleOf]);
1009
+ useEffect(() => {
1010
+ if (editingId == null) return;
1011
+ const { container, getText, getStyle, getPaint } = optsRef.current;
1012
+ if (!container) return;
1013
+ holdNodeStyle(null);
1014
+ const style = resolveTextStyle(getStyle(editingId), getPaint?.(editingId));
1015
+ const overlay = document.createElement("div");
1016
+ const overlayClass = `weasel-text-edit-${++OVERLAY_SEQ}`;
1017
+ overlay.classList.add(overlayClass);
1018
+ overlay.setAttribute("contenteditable", "true");
1019
+ overlay.spellcheck = false;
1020
+ baseRef.current = style;
1021
+ const initialRuns = optsRef.current.getRuns?.(editingId);
1022
+ holderRef.current = seedOverlay(overlay, getText(editingId), initialRuns, style);
1023
+ openedRef.current = JSON.stringify(domToRuns(overlay));
1024
+ applyOverlayStyle(overlay, style);
1025
+ const hold = holdForFaces(style, initialRuns ?? []);
1026
+ if (hold) overlay.style.opacity = "0";
1027
+ const styleEl = installSelectionStyle(overlayClass, style);
1028
+ const clipBox = createClipBox();
1029
+ clipBox.appendChild(overlay);
1030
+ container.appendChild(clipBox);
1031
+ overlayRef.current = overlay;
1032
+ const place = () => {
1033
+ const clip = optsRef.current.getClipRect?.() ?? null;
1034
+ placeOverlay(overlay, clipBox, optsRef.current.getScreenPose(editingId), clip, style);
1035
+ };
1036
+ place();
1037
+ let held = hold !== null;
1038
+ const release = () => {
1039
+ if (!held) return;
1040
+ held = false;
1041
+ clearTimeout(holdTimer);
1042
+ place();
1043
+ overlay.style.opacity = "";
1044
+ };
1045
+ const holdTimer = held ? setTimeout(release, optsRef.current.fontHold ?? TEXT_EDIT_FONT_HOLD) : void 0;
1046
+ void hold?.then(release);
1047
+ const range = document.createRange();
1048
+ const initial = initialCaretRef.current;
1049
+ if (initial === "all") {
1050
+ range.selectNodeContents(overlay);
1051
+ } else {
1052
+ placeCaretAt(overlay, range, initial);
1053
+ }
1054
+ overlay.focus();
1055
+ const sel = window.getSelection();
1056
+ sel?.removeAllRanges();
1057
+ sel?.addRange(range);
1058
+ syncSelection();
1059
+ const onKeyDown = (e) => {
1060
+ if (e.isComposing || e.keyCode === 229) return;
1061
+ if (e.key === "Enter" && !e.shiftKey) {
1062
+ e.preventDefault();
1063
+ commit();
1064
+ return;
1065
+ }
1066
+ if (e.key === "Escape") {
1067
+ e.preventDefault();
1068
+ if (optsRef.current.escape === "commit") commit();
1069
+ else cancelEdit();
1070
+ return;
1071
+ }
1072
+ const toggle = toggleForKey(e);
1073
+ if (toggle) {
1074
+ e.preventDefault();
1075
+ toggleStyle(toggle);
1076
+ }
1077
+ };
1078
+ const onBlur = (e) => {
1079
+ const next = e.relatedTarget;
1080
+ if (isChrome(optsRef.current, next)) return;
1081
+ commit();
1082
+ };
1083
+ const { onBeforeInput, onInput } = typingHandlers(overlay, {
1084
+ pending: () => pendingRef.current,
1085
+ base: () => baseRef.current,
1086
+ lastRange: () => selectionRef.current,
1087
+ onWritten: (caret) => {
1088
+ dropPending();
1089
+ publishRange(overlay, { start: caret, end: caret });
1090
+ },
1091
+ onEdited: () => {
1092
+ holderRef.current?.edited(overlay);
1093
+ place();
1094
+ }
1095
+ });
1096
+ const onPointerDownOutside = (e) => {
1097
+ const target = e.target;
1098
+ if (!(target instanceof Node) || overlay.contains(target)) return;
1099
+ if (isChrome(optsRef.current, target)) return;
1100
+ commit();
1101
+ };
1102
+ overlay.addEventListener("keydown", onKeyDown);
1103
+ overlay.addEventListener("blur", onBlur);
1104
+ overlay.addEventListener("input", onInput);
1105
+ overlay.addEventListener("beforeinput", onBeforeInput);
1106
+ document.addEventListener("pointerdown", onPointerDownOutside, true);
1107
+ document.addEventListener("selectionchange", syncSelection);
1108
+ tickRef.current = () => {
1109
+ place();
1110
+ frameLoop.request();
1111
+ };
1112
+ frameLoop.request();
1113
+ return () => {
1114
+ held = false;
1115
+ clearTimeout(holdTimer);
1116
+ frameLoop.cancel();
1117
+ tickRef.current = () => {
1118
+ };
1119
+ overlay.removeEventListener("keydown", onKeyDown);
1120
+ overlay.removeEventListener("blur", onBlur);
1121
+ overlay.removeEventListener("input", onInput);
1122
+ overlay.removeEventListener("beforeinput", onBeforeInput);
1123
+ document.removeEventListener("pointerdown", onPointerDownOutside, true);
1124
+ document.removeEventListener("selectionchange", syncSelection);
1125
+ clipBox.remove();
1126
+ styleEl?.remove();
1127
+ overlayRef.current = null;
1128
+ holdNodeStyle(null);
1129
+ clearSelection();
1130
+ };
1131
+ }, [
1132
+ editingId,
1133
+ opts.container,
1134
+ optsRef,
1135
+ commit,
1136
+ cancelEdit,
1137
+ syncSelection,
1138
+ clearSelection,
1139
+ frameLoop,
1140
+ toggleStyle,
1141
+ dropPending,
1142
+ publishRange,
1143
+ holdNodeStyle
1144
+ ]);
1145
+ return {
1146
+ editingId,
1147
+ startEdit,
1148
+ cancelEdit,
1149
+ commit,
1150
+ isEditing,
1151
+ nodeStyle,
1152
+ selection,
1153
+ rangeStyle,
1154
+ pendingStyle,
1155
+ applyStyleToSelection,
1156
+ toggleStyle,
1157
+ effectiveStyle: shownStyle(opts, editingId, nodeStyle, rangeStyle, pendingStyle)
1158
+ };
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
+ }
1171
+ var OVERLAY_SEQ = 0;
1172
+ function holdForFaces(style, runs) {
1173
+ const pending = [
1174
+ cssFontFamilyLoading(style.fontFamily, {
1175
+ weight: numericWeight(style.fontWeight),
1176
+ style: style.fontStyle
1177
+ }),
1178
+ ...runs.filter((r) => r.fontFamily != null).map((r) => cssFontFamilyLoading(r.fontFamily, {
1179
+ weight: r.fontWeight ?? (r.bold ? 700 : void 0),
1180
+ style: r.italic ? "italic" : void 0
1181
+ }))
1182
+ ].filter((p) => p !== null);
1183
+ return pending.length === 0 ? null : Promise.all(pending);
1184
+ }
1185
+ function placeCaretAt(root, range, offset) {
1186
+ let remaining = offset;
1187
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1188
+ let node = walker.nextNode();
1189
+ while (node) {
1190
+ const len = node.data.length;
1191
+ if (remaining <= len) {
1192
+ range.setStart(node, remaining);
1193
+ range.setEnd(node, remaining);
1194
+ return;
1195
+ }
1196
+ remaining -= len;
1197
+ node = walker.nextNode();
1198
+ }
1199
+ range.selectNodeContents(root);
1200
+ range.collapse(false);
1201
+ }
1202
+ function createClipBox() {
1203
+ const el = document.createElement("div");
1204
+ el.style.position = "absolute";
1205
+ el.style.margin = "0";
1206
+ el.style.padding = "0";
1207
+ el.style.border = "0";
1208
+ el.style.pointerEvents = "none";
1209
+ return el;
1210
+ }
1211
+ function installSelectionStyle(overlayClass, style) {
1212
+ if (style.selectionBackground == null && style.selectionColor == null) return null;
1213
+ const el = document.createElement("style");
1214
+ const decls = [];
1215
+ if (style.selectionBackground != null) {
1216
+ decls.push(`background: ${style.selectionBackground};`);
1217
+ }
1218
+ if (style.selectionColor != null) {
1219
+ decls.push(`color: ${style.selectionColor};`);
1220
+ }
1221
+ el.textContent = `.${overlayClass}::selection { ${decls.join(" ")} }`;
1222
+ document.head.appendChild(el);
1223
+ return el;
1224
+ }
1225
+ var ALIGN_ANCHOR = { left: 0, center: 0.5, right: 1 };
1226
+ function overlayTop(pose, contentHeight, drop = 0) {
1227
+ const slack = verticalAlignOffset(pose.verticalAlign, pose.height, contentHeight);
1228
+ return pose.y + (slack + drop) * (pose.zoom ?? 1);
1229
+ }
1230
+ function baselineDrop(el, style, pose, lineHeight) {
1231
+ const canvas = layoutTextPose({
1232
+ x: 0,
1233
+ y: 0,
1234
+ width: pose.width,
1235
+ height: pose.height,
1236
+ text: " ",
1237
+ style: {
1238
+ fontFamily: style.fontFamily,
1239
+ fontWeight: style.fontWeight,
1240
+ fontStyle: style.fontStyle,
1241
+ fontSize: pose.fontSize,
1242
+ lineHeight,
1243
+ script: style.script
1244
+ }
1245
+ }).laid.lines[0]?.baselineY ?? 0;
1246
+ const css = cssBaseline(el);
1247
+ return canvas > 0 && css > 0 ? canvas - css : 0;
1248
+ }
1249
+ var CSS_BASELINES = /* @__PURE__ */ new Map();
1250
+ function cssBaseline(el) {
1251
+ const parent = el.parentElement;
1252
+ if (!parent) return 0;
1253
+ const { fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering } = el.style;
1254
+ const key = [fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering].join("|");
1255
+ const cached = CSS_BASELINES.get(key);
1256
+ if (cached !== void 0) return cached;
1257
+ const line = document.createElement("div");
1258
+ Object.assign(line.style, {
1259
+ position: "absolute",
1260
+ left: "0",
1261
+ top: "0",
1262
+ width: "100px",
1263
+ margin: "0",
1264
+ padding: "0",
1265
+ border: "0",
1266
+ visibility: "hidden",
1267
+ whiteSpace: "pre",
1268
+ fontFamily,
1269
+ fontWeight,
1270
+ fontStyle,
1271
+ fontSize,
1272
+ lineHeight,
1273
+ textRendering
1274
+ });
1275
+ const mark = document.createElement("span");
1276
+ Object.assign(mark.style, { display: "inline-block", width: "0", height: "0", verticalAlign: "baseline" });
1277
+ line.appendChild(mark);
1278
+ parent.appendChild(line);
1279
+ const box = line.getBoundingClientRect();
1280
+ const scale = box.width / 100;
1281
+ const baseline = scale > 0 ? (mark.getBoundingClientRect().top - box.top) / scale : 0;
1282
+ line.remove();
1283
+ if (baseline > 0 && document.fonts?.status !== "loading") {
1284
+ if (CSS_BASELINES.size >= 64) CSS_BASELINES.clear();
1285
+ CSS_BASELINES.set(key, baseline);
1286
+ }
1287
+ return baseline;
1288
+ }
1289
+ function placeOverlay(el, clipBox, pose, clip, style) {
1290
+ if (!pose) {
1291
+ el.style.display = "none";
1292
+ return;
1293
+ }
1294
+ clipBox.style.overflow = clip ? "clip" : "visible";
1295
+ clipBox.style.left = `${clip?.x ?? 0}px`;
1296
+ clipBox.style.top = `${clip?.y ?? 0}px`;
1297
+ clipBox.style.width = `${clip?.width ?? 0}px`;
1298
+ clipBox.style.height = `${clip?.height ?? 0}px`;
1299
+ el.style.display = "";
1300
+ const anchor = style.wrap ? 0 : ALIGN_ANCHOR[overlayAlignEdge(el, style)];
1301
+ const left = pose.x + pose.width * (pose.zoom ?? 1) * anchor;
1302
+ el.style.left = `${left - (clip?.x ?? 0)}px`;
1303
+ el.style.width = style.wrap ? `${pose.width}px` : "max-content";
1304
+ el.style.minWidth = style.wrap ? "" : `${pose.width}px`;
1305
+ const script = style.script ? scriptMetricsFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle)[style.script] : void 0;
1306
+ const lineHeight = pose.lineHeight ?? style.lineHeight;
1307
+ el.style.fontSize = `${pose.fontSize * (script?.size ?? 1)}px`;
1308
+ el.style.lineHeight = script ? `${pose.fontSize * lineHeight}px` : String(lineHeight);
1309
+ const aligned = pose.verticalAlign !== void 0 && pose.verticalAlign !== "top";
1310
+ el.style.minHeight = aligned ? "" : `${pose.height}px`;
1311
+ const drop = baselineDrop(el, style, pose, lineHeight) - (script?.shift ?? 0) * pose.fontSize;
1312
+ const top = overlayTop(pose, aligned ? el.offsetHeight : pose.height, drop);
1313
+ el.style.top = `${top - (clip?.y ?? 0)}px`;
1314
+ el.style.transformOrigin = "0 0";
1315
+ el.style.transform = overlayTransform(pose, left, top, anchor);
1316
+ const scale = pose.zoom !== void 0 ? 1 : style.fontSize > 0 ? pose.fontSize / style.fontSize : 1;
1317
+ el.style.letterSpacing = `${style.letterSpacing * scale}px`;
1318
+ }
1319
+
1320
+ export { TEXT_EDIT_FONT_HOLD, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, runsToDom, useTextEdit };
1321
+ //# sourceMappingURL=index.js.map
1322
+ //# sourceMappingURL=index.js.map