@weasel-js/text-dom 1.9.4 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,11 @@ While an edit is open the hook reports the selection's shared style
22
22
  (`MIXED` where the selected runs disagree) and applies a style or a flag such
23
23
  as bold to the selected range.
24
24
 
25
+ A paste or a drop goes in as runs, never as the clipboard's markup: its blocks
26
+ become lines, its table cells are set apart by tabs, and it keeps its bold,
27
+ italic, underline, strikethrough, overline, and super- and subscript. Face,
28
+ size, and color come from the text it lands in.
29
+
25
30
  `runsToDom` and `domToRuns` are the conversion between runs and the overlay's
26
31
  DOM, and `charOffsetToDomPosition` / `domPositionToCharOffset` map a character
27
32
  offset in the text to a DOM position and back.
package/dist/index.d.ts CHANGED
@@ -1,15 +1,6 @@
1
1
  import { FlagKey, StyledRun, TextStyle, TextVerticalAlign, TextPaint, RangeStyle, RunStylePatch, ResolvedTextStyle } from '@weasel-js/text';
2
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
- */
3
+ /** The option, return, and pose types of `useTextEdit`. */
13
4
 
14
5
  /**
15
6
  * A styling a shortcut or a control toggles. The additive booleans, plus the
@@ -56,18 +47,19 @@ interface TextEditScreenPose {
56
47
  * transform does the world→screen conversion.
57
48
  *
58
49
  * 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.
50
+ * than 1. Omit it (the default, `1`) and the pose is plain screen pixels —
51
+ * the hook then infers the world→screen factor from
52
+ * `fontSize / style.fontSize` to scale node-level `letterSpacing`, and
53
+ * run-level overrides are left unscaled.
69
54
  */
70
55
  zoom?: number;
56
+ /**
57
+ * The frame the text is drawn in, when its box is turned or skewed: the
58
+ * linear part `[a, b, c, d]` (`x' = a·x + c·y`, `y' = b·x + d·y`) applied
59
+ * about the box's center. `x`, `y`, `width`, and `height` stay those of
60
+ * the box before it. Absent is the identity.
61
+ */
62
+ frame?: readonly [a: number, b: number, c: number, d: number];
71
63
  }
72
64
  /** A box in CSS pixels relative to `useTextEdit`'s `container`. */
73
65
  interface TextEditClipRect {
@@ -144,8 +136,7 @@ interface UseTextEditOptions {
144
136
  * commit calls both `setText` (with `runsToPlainText(runs)`) and
145
137
  * `setRuns(id, runs)` — but only when runs are actually in play: the node
146
138
  * 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.
139
+ * plain-text node still calls `setText` alone.
149
140
  */
150
141
  setRuns?: (id: string, runs: StyledRun[]) => void;
151
142
  /**
@@ -153,8 +144,7 @@ interface UseTextEditOptions {
153
144
  * color popover, anything whose whole purpose is to style the text being
154
145
  * edited? Focus moving into one does not end the edit.
155
146
  *
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
147
+ * Without this, clicking a size field blurs the overlay, blur commits, and
158
148
  * the caret the control was about to act on is gone. Toggle buttons can
159
149
  * dodge it by `preventDefault()`-ing their own mousedown, but a field the
160
150
  * user has to type into cannot.
@@ -172,9 +162,7 @@ interface UseTextEditOptions {
172
162
  *
173
163
  * The overlay stands in for canvas text the host has hidden, so the wait
174
164
  * 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.
165
+ * face when that lands. A face already loaded shows at once. `0` never waits.
178
166
  */
179
167
  fontHold?: number;
180
168
  }
@@ -186,10 +174,8 @@ interface UseTextEditOptions {
186
174
  declare const TEXT_EDIT_FONT_HOLD = 100;
187
175
  /** Options for `useTextEdit().startEdit`. */
188
176
  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
- */
177
+ /** Where to place the caret on edit start. Number = caret offset (0..text.length);
178
+ * `'all'` selects the whole text (default). */
193
179
  caret?: number | 'all';
194
180
  }
195
181
  /** Return shape of `useTextEdit`. */
@@ -215,7 +201,7 @@ interface UseTextEditReturn {
215
201
  * n" stay distinguishable — a styling written at a collapsed caret arms
216
202
  * {@link UseTextEditReturn.pendingStyle} instead of restyling a range.
217
203
  *
218
- * Follows the DOM selection, which browsers (and jsdom) report from a task
204
+ * Follows the DOM selection, which browsers report from a task
219
205
  * rather than synchronously; anything this hook writes itself updates it
220
206
  * synchronously. A DOM selection that leaves the overlay does **not** clear
221
207
  * it: that is what happens when the user clicks a styling control, and
@@ -240,10 +226,15 @@ interface UseTextEditReturn {
240
226
  * Styling armed at a collapsed caret, applied to the next character typed
241
227
  * and then dropped. `{}` when nothing is armed — which is always the case
242
228
  * while `selection` covers a real range, since a range is styled directly.
243
- *
244
- * Moving the caret abandons it, as in any other editor.
229
+ * Moving the caret abandons it.
245
230
  */
246
231
  pendingStyle: RunStylePatch;
232
+ /**
233
+ * What a styling control shows during an edit: the node's own style and
234
+ * paint, `rangeStyle` over it, and `pendingStyle` last — what the user
235
+ * will see themselves type. `null` when nothing is being edited.
236
+ */
237
+ effectiveStyle: RangeStyle | null;
247
238
  /**
248
239
  * Write `patch` over `selection`. A no-op with no active edit or an empty
249
240
  * patch. Over a real range this restyles the runs under it; at a collapsed
@@ -259,34 +250,20 @@ interface UseTextEditReturn {
259
250
  * re-selecting, and `rangeStyle` reflects the write before this returns.
260
251
  */
261
252
  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
- */
253
+ /** Toggle one styling over `selection` — set it if the range doesn't
254
+ * uniformly carry it, clear it if it does. */
267
255
  toggleStyle: (toggle: StyleToggle) => void;
268
256
  }
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;
257
+
279
258
  /**
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
259
+ * In-place text editing via a contenteditable overlay positioned over the
260
+ * text node's screen-space pose. Enter / blur commit; Shift+Enter inserts a
261
+ * newline. The text layer should hide the node it's currently editing
262
+ * (`isHidden: (n) => n.id === editingId`) so the overlay isn't drawn twice.
288
263
  */
289
- declare function baselineDrop(el: HTMLElement, style: ResolvedTextStyle, pose: TextEditScreenPose, lineHeight: number): number;
264
+
265
+ /** In-place text editing via a contenteditable overlay positioned over the text node's screen-space pose. */
266
+ declare function useTextEdit(opts: UseTextEditOptions): UseTextEditReturn;
290
267
 
291
268
  /**
292
269
  * The DOM position of plain-text character position `offset`: a text node
@@ -313,20 +290,15 @@ declare function charOffsetToDomPosition(parent: HTMLElement, offset: number): {
313
290
  declare function domPositionToCharOffset(parent: HTMLElement, node: Node, offset: number): number;
314
291
 
315
292
  /**
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
293
+ * DOM ↔ `StyledRun[]` serializers for the contenteditable overlay: a flat
294
+ * sequence of `<span data-run>` elements, one per run. The browser breaks at
295
+ * `\n` and at no other UAX #14 hard break, so each of those is written as a
322
296
  * `<span data-break>` holding a `\n` in its place — one character for one,
323
297
  * which keeps DOM offsets equal to source offsets.
324
298
  *
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.
299
+ * A small-caps run's lowercase letters sit in `<span data-small-caps>` pieces,
300
+ * uppercased and sized at the scale the canvas draws them with, since a
301
+ * browser's own synthesis uses a fixed factor of its own.
330
302
  */
331
303
 
332
304
  /** Walk an overlay tree and emit a coalesced `StyledRun[]`. */
@@ -335,11 +307,9 @@ declare function domToRuns(parent: HTMLElement): StyledRun[];
335
307
  * runs have it, and the face and transform that decide their pieces. */
336
308
  type OverlayRunBase = Pick<ResolvedTextStyle, 'fontFamily' | 'fontWeight' | 'fontStyle' | 'textTransform' | 'fontVariantCaps'>;
337
309
  /**
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.
310
+ * Replace `parent`'s children with one `<span data-run>` per run. `base` is
311
+ * the node's style, which a run inheriting small caps is split by.
342
312
  */
343
313
  declare function runsToDom(runs: readonly StyledRun[], parent: HTMLElement, base?: OverlayRunBase): void;
344
314
 
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 };
315
+ export { type OverlayRunBase, type StartEditOptions, type StyleToggle, TEXT_EDIT_FONT_HOLD, type TextEditClipRect, type TextEditCommit, type TextEditScreenPose, type TextEditSelection, type UseTextEditOptions, type UseTextEditReturn, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, runsToDom, useTextEdit };