@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/text-dom
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
In-place text editing over a canvas: a `contenteditable` overlay that reads and
|
|
4
|
+
writes styled runs.
|
|
5
|
+
|
|
6
|
+
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
7
|
+
scene-graph canvas kit for React. See the
|
|
8
|
+
[API reference](https://orochi235.github.io/weasel/api/).
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @weasel-js/text-dom
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`useTextEdit` mounts an editable element over a box on screen, set in the same
|
|
15
|
+
face, size, and alignment the canvas draws the text in, and hands back the
|
|
16
|
+
edited text as `StyledRun`s from `@weasel-js/text` when the edit commits. It
|
|
17
|
+
takes a screen pose and a `TextStyle`; it does not know about a scene graph or
|
|
18
|
+
a renderer. `useSceneTextEdit` in `@weasel-js/core` is this hook wired to a
|
|
19
|
+
scene node.
|
|
20
|
+
|
|
21
|
+
While an edit is open the hook reports the selection's shared style
|
|
22
|
+
(`MIXED` where the selected runs disagree) and applies a style or a flag such
|
|
23
|
+
as bold to the selected range.
|
|
24
|
+
|
|
25
|
+
`runsToDom` and `domToRuns` are the conversion between runs and the overlay's
|
|
26
|
+
DOM, and `charOffsetToDomPosition` / `domPositionToCharOffset` map a character
|
|
27
|
+
offset in the text to a DOM position and back.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { FlagKey, StyledRun, TextStyle, TextVerticalAlign, TextPaint, RangeStyle, RunStylePatch, ResolvedTextStyle } from '@weasel-js/text';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-place text editing via a contenteditable overlay positioned over the
|
|
5
|
+
* text node's screen-space pose. Enter / blur commit; Shift+Enter inserts a
|
|
6
|
+
* newline; Escape cancels. The hook owns the DOM lifecycle of the overlay
|
|
7
|
+
* element — caller just provides a container, the lookup hooks, and a
|
|
8
|
+
* `setText` callback (which is responsible for op-batching/undo).
|
|
9
|
+
*
|
|
10
|
+
* The text layer should hide the node it's currently editing
|
|
11
|
+
* (`isHidden: (n) => n.id === editingId`) so the overlay isn't drawn twice.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A styling a shortcut or a control toggles. The additive booleans, plus the
|
|
16
|
+
* two `script` values — which are exclusive rather than additive, so they
|
|
17
|
+
* toggle against one enum rather than each owning a boolean.
|
|
18
|
+
*/
|
|
19
|
+
type StyleToggle = FlagKey | 'super' | 'sub';
|
|
20
|
+
/**
|
|
21
|
+
* The caret's character range within the text being edited. Half-open
|
|
22
|
+
* `[start, end)` over the concatenated run text, normalized so `start <= end`
|
|
23
|
+
* regardless of which way the user dragged. `start === end` is a collapsed
|
|
24
|
+
* caret — a real position, not the absence of one, which is why the hook
|
|
25
|
+
* reports `null` rather than a zero-width range when there is no caret.
|
|
26
|
+
*/
|
|
27
|
+
interface TextEditSelection {
|
|
28
|
+
start: number;
|
|
29
|
+
end: number;
|
|
30
|
+
}
|
|
31
|
+
/** Screen-space pose passed to `useTextEdit` so the overlay can be placed and sized in CSS pixels. */
|
|
32
|
+
interface TextEditScreenPose {
|
|
33
|
+
/** Top-left in CSS pixels relative to `container`. Always screen pixels,
|
|
34
|
+
* including when `zoom` is set — the scale is anchored at this point, not
|
|
35
|
+
* translated by it. */
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
/** Pre-scale — CSS pixels, or world units when `zoom` is set. */
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
/** Pre-scale font size: `style.fontSize * zoom` when `zoom` is omitted, the
|
|
42
|
+
* world-unit `style.fontSize` when it is set. */
|
|
43
|
+
fontSize: number;
|
|
44
|
+
/** Effective on-screen line height multiplier (defaults to style.lineHeight). */
|
|
45
|
+
lineHeight?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Where the text sits within `height`, as the canvas places it. `'center'`
|
|
48
|
+
* and `'bottom'` size the overlay to its own content and offset it by the
|
|
49
|
+
* slack, re-measured as typing changes the line count. Default `'top'`.
|
|
50
|
+
*/
|
|
51
|
+
verticalAlign?: TextVerticalAlign;
|
|
52
|
+
/**
|
|
53
|
+
* CSS scale applied to the overlay (`transform: scale(zoom)`, anchored at
|
|
54
|
+
* its top-left). Every other size on this pose, and every typographic
|
|
55
|
+
* metric the hook writes, is then **pre-scale** — pass world units and the
|
|
56
|
+
* transform does the world→screen conversion.
|
|
57
|
+
*
|
|
58
|
+
* This is the only way run-level typography can be correct at a zoom other
|
|
59
|
+
* than 1. `runsToDom` emits run `fontSize` / `letterSpacing` in world units
|
|
60
|
+
* and `domToRuns` reads them straight back; threading a scale through the
|
|
61
|
+
* writer and its inverse through the reader would go lossy on fractional
|
|
62
|
+
* zooms. Scaling the whole overlay instead leaves that serializer pure and
|
|
63
|
+
* scales node-level and run-level values by the same factor for free.
|
|
64
|
+
*
|
|
65
|
+
* Omit it (the default, `1`) and the pose is plain screen pixels — the hook
|
|
66
|
+
* then infers the world→screen factor from `fontSize / style.fontSize` to
|
|
67
|
+
* scale node-level `letterSpacing`, and run-level overrides are left
|
|
68
|
+
* unscaled. Correct only at zoom 1.
|
|
69
|
+
*/
|
|
70
|
+
zoom?: number;
|
|
71
|
+
}
|
|
72
|
+
/** A box in CSS pixels relative to `useTextEdit`'s `container`. */
|
|
73
|
+
interface TextEditClipRect {
|
|
74
|
+
x: number;
|
|
75
|
+
y: number;
|
|
76
|
+
width: number;
|
|
77
|
+
height: number;
|
|
78
|
+
}
|
|
79
|
+
/** What one edit session changed, as `UseTextEditOptions.commit` receives it. */
|
|
80
|
+
interface TextEditCommit {
|
|
81
|
+
text: string;
|
|
82
|
+
runs?: StyledRun[];
|
|
83
|
+
style?: TextStyle;
|
|
84
|
+
}
|
|
85
|
+
/** Options for `useTextEdit`. */
|
|
86
|
+
interface UseTextEditOptions {
|
|
87
|
+
/** Element the overlay is appended to. Must be `position: relative`/absolute. */
|
|
88
|
+
container: HTMLElement | null;
|
|
89
|
+
/** Read the current text for `id`. */
|
|
90
|
+
getText: (id: string) => string;
|
|
91
|
+
/** Read style for `id` (used for font setup on the overlay). */
|
|
92
|
+
getStyle: (id: string) => TextStyle | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Read the node's paint for `id` — its `data.fill` / `data.stroke`. The
|
|
95
|
+
* overlay paints its text and caret from the fill, so omitting this
|
|
96
|
+
* renders every node's editor in the default black however the node
|
|
97
|
+
* itself is painted.
|
|
98
|
+
*/
|
|
99
|
+
getPaint?: (id: string) => TextPaint | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Write style back for `id`. Optional; needed only to turn a flag **off**
|
|
102
|
+
* inside a node whose own `TextStyle` sets it. Run flags are additive, so
|
|
103
|
+
* that edit is expressed by lowering the node flag and raising it on the
|
|
104
|
+
* runs outside the selection — which needs somewhere to put the new node
|
|
105
|
+
* style. Without this or `commit`, such a toggle is declined and nothing
|
|
106
|
+
* changes. See `flagRange` in `@weasel-js/text`.
|
|
107
|
+
*
|
|
108
|
+
* Called when the edit commits, never during it: the session holds the
|
|
109
|
+
* rewritten style until then (see `UseTextEditReturn.nodeStyle`), so a
|
|
110
|
+
* canceled edit writes nothing.
|
|
111
|
+
*/
|
|
112
|
+
setStyle?: (id: string, style: TextStyle) => void;
|
|
113
|
+
/** Read screen-space pose for `id`. Called per frame while editing. */
|
|
114
|
+
getScreenPose: (id: string) => TextEditScreenPose | null;
|
|
115
|
+
/**
|
|
116
|
+
* The box, in CSS pixels relative to `container`, the overlay is visible
|
|
117
|
+
* within — where the canvas clips the glyphs the overlay stands in for.
|
|
118
|
+
* Re-read every frame. Omit it, or return `null`, to clip nothing.
|
|
119
|
+
*
|
|
120
|
+
* The clip cannot scroll, so a caret moving past its edge leaves the page
|
|
121
|
+
* where it is; a scrollable container would drag the canvas after the caret.
|
|
122
|
+
*/
|
|
123
|
+
getClipRect?: () => TextEditClipRect | null;
|
|
124
|
+
/** Commit text. Caller wraps in op/undo. Required unless `commit` is given. */
|
|
125
|
+
setText?: (id: string, text: string) => void;
|
|
126
|
+
/**
|
|
127
|
+
* Take the whole edit in one call, so the caller can make it one write and
|
|
128
|
+
* one undo step. When given, it replaces `setText`, `setRuns`, and
|
|
129
|
+
* `setStyle`, none of which is called. `runs` is present when runs are in
|
|
130
|
+
* play (see `setRuns`), `style` when the edit rewrote the node's own style
|
|
131
|
+
* (see `setStyle`).
|
|
132
|
+
*/
|
|
133
|
+
commit?: (id: string, edit: TextEditCommit) => void;
|
|
134
|
+
/**
|
|
135
|
+
* Optional: read the current rich-text runs for `id`. When provided AND
|
|
136
|
+
* returns a non-empty array, the overlay renders the runs as styled
|
|
137
|
+
* `<span data-run>` elements; otherwise the overlay is seeded with plain
|
|
138
|
+
* text via `getText`.
|
|
139
|
+
*/
|
|
140
|
+
getRuns?: (id: string) => readonly StyledRun[] | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* Optional: commit rich-text runs back to the node. When omitted, only
|
|
143
|
+
* `setText` is called with the plain-text form on commit. When provided,
|
|
144
|
+
* commit calls both `setText` (with `runsToPlainText(runs)`) and
|
|
145
|
+
* `setRuns(id, runs)` — but only when runs are actually in play: the node
|
|
146
|
+
* already had some, or the edit produced styling. A plain-text edit of a
|
|
147
|
+
* plain-text node still calls `setText` alone, so a node that has never
|
|
148
|
+
* been styled doesn't grow a single-run `runs` array just for being edited.
|
|
149
|
+
*/
|
|
150
|
+
setRuns?: (id: string, runs: StyledRun[]) => void;
|
|
151
|
+
/**
|
|
152
|
+
* Is `el` part of the editor's own chrome — a character-options bar, a
|
|
153
|
+
* color popover, anything whose whole purpose is to style the text being
|
|
154
|
+
* edited? Focus moving into one does not end the edit.
|
|
155
|
+
*
|
|
156
|
+
* Without this, the controls the feature exists for are exactly what
|
|
157
|
+
* destroys it: clicking a size field blurs the overlay, blur commits, and
|
|
158
|
+
* the caret the control was about to act on is gone. Toggle buttons can
|
|
159
|
+
* dodge it by `preventDefault()`-ing their own mousedown, but a field the
|
|
160
|
+
* user has to type into cannot.
|
|
161
|
+
*
|
|
162
|
+
* Chrome focus does not disturb the reported `selection` either — see
|
|
163
|
+
* `UseTextEditReturn.selection`.
|
|
164
|
+
*/
|
|
165
|
+
isEditorChrome?: (el: Element) => boolean;
|
|
166
|
+
/** What Escape does to the edit: `'cancel'` (the default) drops it,
|
|
167
|
+
* `'commit'` keeps the text as Enter would. */
|
|
168
|
+
escape?: 'cancel' | 'commit';
|
|
169
|
+
/**
|
|
170
|
+
* How long, in ms, an edit opening waits for the overlay's face before
|
|
171
|
+
* showing the text in the fallback. Defaults to {@link TEXT_EDIT_FONT_HOLD}.
|
|
172
|
+
*
|
|
173
|
+
* The overlay stands in for canvas text the host has hidden, so the wait
|
|
174
|
+
* shows nothing at all; past it, the fallback shows and reflows to the real
|
|
175
|
+
* face when that lands. A face already loaded — the usual case, since
|
|
176
|
+
* `@weasel-js/font` builds it as soon as its bytes arrive — shows at once.
|
|
177
|
+
* `0` never waits.
|
|
178
|
+
*/
|
|
179
|
+
fontHold?: number;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The default {@link UseTextEditOptions.fontHold}: at 100ms a blank edit
|
|
183
|
+
* still reads as an instant response, and it is the block period CSS gives
|
|
184
|
+
* `font-display: fallback` for the same trade.
|
|
185
|
+
*/
|
|
186
|
+
declare const TEXT_EDIT_FONT_HOLD = 100;
|
|
187
|
+
/** Options for `useTextEdit().startEdit`. */
|
|
188
|
+
interface StartEditOptions {
|
|
189
|
+
/**
|
|
190
|
+
* Where to place the caret on edit start. Number = caret offset (0..text.length);
|
|
191
|
+
* `'all'` selects the whole text (default — preserves the prior behavior).
|
|
192
|
+
*/
|
|
193
|
+
caret?: number | 'all';
|
|
194
|
+
}
|
|
195
|
+
/** Return shape of `useTextEdit`. */
|
|
196
|
+
interface UseTextEditReturn {
|
|
197
|
+
editingId: string | null;
|
|
198
|
+
startEdit: (id: string, opts?: StartEditOptions) => void;
|
|
199
|
+
cancelEdit: () => void;
|
|
200
|
+
/** End the edit and write it. An edit that leaves the text, the runs, and
|
|
201
|
+
* the node's style as it found them writes nothing. */
|
|
202
|
+
commit: () => void;
|
|
203
|
+
isEditing: (id: string) => boolean;
|
|
204
|
+
/**
|
|
205
|
+
* The node-wide style this edit has rewritten and not yet written back, or
|
|
206
|
+
* `null` when it has rewritten none (the node's own style stands) or
|
|
207
|
+
* nothing is being edited. Lowering a flag the node sets rewrites it; the
|
|
208
|
+
* write waits for the commit. A control showing the node's style during an
|
|
209
|
+
* edit reads this first.
|
|
210
|
+
*/
|
|
211
|
+
nodeStyle: TextStyle | null;
|
|
212
|
+
/**
|
|
213
|
+
* The caret's character range, or `null` when nothing is being edited. A
|
|
214
|
+
* collapsed caret reports `{ start: n, end: n }`, so `null` and "caret at
|
|
215
|
+
* n" stay distinguishable — a styling written at a collapsed caret arms
|
|
216
|
+
* {@link UseTextEditReturn.pendingStyle} instead of restyling a range.
|
|
217
|
+
*
|
|
218
|
+
* Follows the DOM selection, which browsers (and jsdom) report from a task
|
|
219
|
+
* rather than synchronously; anything this hook writes itself updates it
|
|
220
|
+
* synchronously. A DOM selection that leaves the overlay does **not** clear
|
|
221
|
+
* it: that is what happens when the user clicks a styling control, and
|
|
222
|
+
* reporting `null` there would read as "collapsed caret" and send the
|
|
223
|
+
* control's patch to the node instead of to the range. It is cleared on
|
|
224
|
+
* `startEdit` and when the edit ends.
|
|
225
|
+
*/
|
|
226
|
+
selection: TextEditSelection | null;
|
|
227
|
+
/**
|
|
228
|
+
* The styling shared by every run in `selection` — a concrete value where
|
|
229
|
+
* the range agrees, `MIXED` where it doesn't. `null` exactly when
|
|
230
|
+
* `selection` is `null`. A collapsed caret reports the styling *at* the
|
|
231
|
+
* caret (the run to its left, or the run to its right at offset 0), which
|
|
232
|
+
* is what the next typed character inherits.
|
|
233
|
+
*
|
|
234
|
+
* This does not include {@link UseTextEditReturn.pendingStyle}. A control
|
|
235
|
+
* showing what the next character will look like wants both, merged in
|
|
236
|
+
* that order; one showing what is already written wants only this.
|
|
237
|
+
*/
|
|
238
|
+
rangeStyle: RangeStyle | null;
|
|
239
|
+
/**
|
|
240
|
+
* Styling armed at a collapsed caret, applied to the next character typed
|
|
241
|
+
* and then dropped. `{}` when nothing is armed — which is always the case
|
|
242
|
+
* while `selection` covers a real range, since a range is styled directly.
|
|
243
|
+
*
|
|
244
|
+
* Moving the caret abandons it, as in any other editor.
|
|
245
|
+
*/
|
|
246
|
+
pendingStyle: RunStylePatch;
|
|
247
|
+
/**
|
|
248
|
+
* Write `patch` over `selection`. A no-op with no active edit or an empty
|
|
249
|
+
* patch. Over a real range this restyles the runs under it; at a collapsed
|
|
250
|
+
* caret it merges into `pendingStyle` instead, so the styling lands on
|
|
251
|
+
* what gets typed next rather than on text the user didn't select.
|
|
252
|
+
*
|
|
253
|
+
* Lowering a flag the *node* sets is neither of those — a run cannot say
|
|
254
|
+
* "not italic" — so it rewrites instead: the node flag is cleared and raised
|
|
255
|
+
* on every run outside the range. Bold is spared that: a run's own
|
|
256
|
+
* `fontWeight` can say "regular", so it is written over the range.
|
|
257
|
+
*
|
|
258
|
+
* The caret survives, so a second style can be applied without
|
|
259
|
+
* re-selecting, and `rangeStyle` reflects the write before this returns.
|
|
260
|
+
*/
|
|
261
|
+
applyStyleToSelection: (patch: RunStylePatch) => void;
|
|
262
|
+
/**
|
|
263
|
+
* Toggle one styling over `selection` — set it if the range doesn't
|
|
264
|
+
* uniformly carry it, clear it if it does. The shape a B / I / x² control
|
|
265
|
+
* or a keyboard shortcut wants, and what this hook's own shortcuts call.
|
|
266
|
+
*/
|
|
267
|
+
toggleStyle: (toggle: StyleToggle) => void;
|
|
268
|
+
}
|
|
269
|
+
/** In-place text editing via a contenteditable overlay positioned over the text node's screen-space pose. */
|
|
270
|
+
declare function useTextEdit(opts: UseTextEditOptions): UseTextEditReturn;
|
|
271
|
+
/**
|
|
272
|
+
* The overlay's `top`, in container pixels, for content `contentHeight` tall
|
|
273
|
+
* (pre-scale, like the rest of `pose`): the pose's top, shifted by the same
|
|
274
|
+
* `verticalAlign` slack the canvas applies, plus `drop` — how far below the
|
|
275
|
+
* CSS line's baseline the canvas puts its own, from {@link baselineDrop}.
|
|
276
|
+
* @internal
|
|
277
|
+
*/
|
|
278
|
+
declare function overlayTop(pose: TextEditScreenPose, contentHeight: number, drop?: number): number;
|
|
279
|
+
/**
|
|
280
|
+
* The canvas and CSS both hang a line's baseline half the leading plus one
|
|
281
|
+
* ascent below the line top, from the same ascent rule, but each engine
|
|
282
|
+
* rounds the font's metrics and that leading its own way, and a face that
|
|
283
|
+
* states no ascent and descent hangs from its ascent alone. So the difference
|
|
284
|
+
* is read from both sides rather than predicted: the canvas's from its own layout, the
|
|
285
|
+
* overlay's from a zero-height inline box on a hidden line set exactly as the
|
|
286
|
+
* overlay is. Pre-scale; `0` when either side has nothing to measure.
|
|
287
|
+
* @internal
|
|
288
|
+
*/
|
|
289
|
+
declare function baselineDrop(el: HTMLElement, style: ResolvedTextStyle, pose: TextEditScreenPose, lineHeight: number): number;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The DOM position of plain-text character position `offset`: a text node
|
|
293
|
+
* and an offset into it wherever one holds the position, otherwise an element
|
|
294
|
+
* and a child index (just past a `<br>` that ends the text). Offsets beyond
|
|
295
|
+
* the text clamp to its end. Returns `{ node: parent, offset: 0 }` for an
|
|
296
|
+
* overlay with no text.
|
|
297
|
+
*/
|
|
298
|
+
declare function charOffsetToDomPosition(parent: HTMLElement, offset: number): {
|
|
299
|
+
node: Node;
|
|
300
|
+
offset: number;
|
|
301
|
+
} | null;
|
|
302
|
+
/**
|
|
303
|
+
* Inverse of `charOffsetToDomPosition`: the characters that precede the DOM
|
|
304
|
+
* position `(node, offset)`.
|
|
305
|
+
*
|
|
306
|
+
* When `node` is an element the DOM offset indexes *child nodes*, not
|
|
307
|
+
* characters — `(overlay, 0)` is the start of the text and `(overlay,
|
|
308
|
+
* childNodes.length)` its end. That shape is not exotic: `Range.
|
|
309
|
+
* selectNodeContents`, which `useTextEdit` uses for its select-all caret,
|
|
310
|
+
* produces exactly it. So an element position is resolved by counting the
|
|
311
|
+
* pieces that end at or before the boundary point.
|
|
312
|
+
*/
|
|
313
|
+
declare function domPositionToCharOffset(parent: HTMLElement, node: Node, offset: number): number;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Pure DOM ↔ `StyledRun[]` serializers for the contenteditable overlay
|
|
317
|
+
* used by `useTextEdit`. The overlay's children are a flat sequence of
|
|
318
|
+
* `<span data-run>` elements, each carrying one run's text and inline
|
|
319
|
+
* styles. Newlines inside a run are literal `\n` characters; the overlay
|
|
320
|
+
* has `white-space: pre-wrap` so they render as line breaks. The browser
|
|
321
|
+
* breaks at no other UAX #14 hard break, so each of those is written as a
|
|
322
|
+
* `<span data-break>` holding a `\n` in its place — one character for one,
|
|
323
|
+
* which keeps DOM offsets equal to source offsets.
|
|
324
|
+
*
|
|
325
|
+
* A small-caps run is the one exception to one text node per span: its
|
|
326
|
+
* lowercase letters sit in `<span data-small-caps>` pieces, uppercased and
|
|
327
|
+
* sized at the scale the canvas draws them with, since a browser's own
|
|
328
|
+
* synthesis uses a fixed factor of its own. The pieces are presentation
|
|
329
|
+
* only — `domToRuns` reads straight through them.
|
|
330
|
+
*/
|
|
331
|
+
|
|
332
|
+
/** Walk an overlay tree and emit a coalesced `StyledRun[]`. */
|
|
333
|
+
declare function domToRuns(parent: HTMLElement): StyledRun[];
|
|
334
|
+
/** What a run inherits from its node, for the overlay's small caps: which
|
|
335
|
+
* runs have it, and the face and transform that decide their pieces. */
|
|
336
|
+
type OverlayRunBase = Pick<ResolvedTextStyle, 'fontFamily' | 'fontWeight' | 'fontStyle' | 'textTransform' | 'fontVariantCaps'>;
|
|
337
|
+
/**
|
|
338
|
+
* Build a flat sequence of `<span data-run>` children from `runs`, replacing
|
|
339
|
+
* any existing children of `parent`. `base` is the node's style, which a run
|
|
340
|
+
* inheriting small caps needs to be split by; without it the kit defaults
|
|
341
|
+
* stand in.
|
|
342
|
+
*/
|
|
343
|
+
declare function runsToDom(runs: readonly StyledRun[], parent: HTMLElement, base?: OverlayRunBase): void;
|
|
344
|
+
|
|
345
|
+
export { type StartEditOptions, type StyleToggle, TEXT_EDIT_FONT_HOLD, type TextEditClipRect, type TextEditCommit, type TextEditScreenPose, type TextEditSelection, type UseTextEditOptions, type UseTextEditReturn, baselineDrop, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, overlayTop, runsToDom, useTextEdit };
|