@quillmark/svelte 0.17.0 → 0.18.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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/dist/core/codec/index.d.ts +0 -3
  3. package/dist/core/rebind.svelte.js +5 -8
  4. package/dist/preset/index.d.ts +1 -0
  5. package/dist/preview/Preview.svelte +14 -19
  6. package/dist/preview/Preview.svelte.d.ts +3 -3
  7. package/dist/preview/index.d.ts +0 -1
  8. package/dist/visual/ArrayField.svelte +35 -43
  9. package/dist/visual/ArrayField.svelte.d.ts +0 -1
  10. package/dist/visual/BooleanField.svelte +9 -14
  11. package/dist/visual/BooleanField.svelte.d.ts +0 -1
  12. package/dist/visual/Card.svelte +5 -18
  13. package/dist/visual/CardControls.svelte.d.ts +0 -1
  14. package/dist/visual/DateField.svelte +14 -17
  15. package/dist/visual/DateField.svelte.d.ts +0 -1
  16. package/dist/visual/EnumField.svelte +9 -12
  17. package/dist/visual/EnumField.svelte.d.ts +0 -1
  18. package/dist/visual/Field.svelte +7 -8
  19. package/dist/visual/FieldHint.svelte.d.ts +0 -1
  20. package/dist/visual/FieldLabel.svelte +1 -2
  21. package/dist/visual/FormatPopover.svelte +22 -27
  22. package/dist/visual/FormatPopover.svelte.d.ts +0 -1
  23. package/dist/visual/MatrixAdd.svelte +1 -2
  24. package/dist/visual/MatrixAdd.svelte.d.ts +0 -1
  25. package/dist/visual/MatrixField.svelte +3 -5
  26. package/dist/visual/MatrixField.svelte.d.ts +0 -1
  27. package/dist/visual/NumberField.svelte +6 -10
  28. package/dist/visual/NumberField.svelte.d.ts +0 -1
  29. package/dist/visual/ObjectField.svelte +8 -12
  30. package/dist/visual/ObjectField.svelte.d.ts +0 -1
  31. package/dist/visual/ProseField.svelte +42 -41
  32. package/dist/visual/ProseField.svelte.d.ts +0 -1
  33. package/dist/visual/ProseValue.svelte +56 -56
  34. package/dist/visual/ProseValue.svelte.d.ts +0 -1
  35. package/dist/visual/SlashMenu.svelte +5 -15
  36. package/dist/visual/SlashMenu.svelte.d.ts +0 -1
  37. package/dist/visual/TextField.svelte +5 -9
  38. package/dist/visual/TextField.svelte.d.ts +0 -1
  39. package/dist/visual/TipsCard.svelte +10 -11
  40. package/dist/visual/TipsCard.svelte.d.ts +0 -1
  41. package/dist/visual/VisualEditorInner.svelte +6 -14
  42. package/dist/visual/VisualEditorInner.svelte.d.ts +0 -1
  43. package/dist/visual/index.d.ts +0 -1
  44. package/dist/visual/structure.d.ts +4 -0
  45. package/dist/visual/structure.js +12 -0
  46. package/package.json +20 -16
  47. package/dist/visual/synced.svelte.d.ts +0 -15
  48. package/dist/visual/synced.svelte.js +0 -34
@@ -38,6 +38,7 @@
38
38
  isContainer,
39
39
  optionalCell,
40
40
  printedText,
41
+ renderedBoolean,
41
42
  stringifyGhost,
42
43
  titleText
43
44
  } from './structure.js';
@@ -235,12 +236,10 @@
235
236
  * an address to name. `el` is the field-wide box, which a landing at depth does not
236
237
  * wash: that lane hands back its own row (`leaves.ts`).
237
238
  */
238
- let controlEl = $state<HTMLElement | undefined>();
239
- $effect(() => {
240
- if (field.control === 'prose' || !controlEl || !leaves) return;
239
+ function registerControl(wrapper: HTMLElement): (() => void) | undefined {
240
+ if (field.control === 'prose' || !leaves) return;
241
241
  const key = leafKey;
242
242
  const registry = leaves;
243
- const wrapper = controlEl;
244
243
  registry.registerControl(key, {
245
244
  focus: focusControl,
246
245
  focusPath: isContainer(field.control) ? focusPath : undefined,
@@ -249,7 +248,7 @@
249
248
  }
250
249
  });
251
250
  return () => registry.unregisterControl(key);
252
- });
251
+ }
253
252
 
254
253
  /**
255
254
  * A form control has no controller to report its focus through, so the wrapper
@@ -262,7 +261,7 @@
262
261
  const reportFocus = $derived(field.control === 'prose' ? undefined : () => onFocus?.(addr));
263
262
  </script>
264
263
 
265
- <div class="qm-field" class:cell={span === 'cell'}>
264
+ <div class={['qm-field', { cell: span === 'cell' }]}>
266
265
  {#if !ownsLabel && !headed}
267
266
  <FieldLabel
268
267
  label={field.label}
@@ -279,7 +278,7 @@
279
278
  rule below holds why). One markup for both spans; a field owning its row nests a
280
279
  level and measures the same. -->
281
280
  <div class="qm-field-stack">
282
- <div class="qm-field-control" bind:this={controlEl} onfocusin={reportFocus}>
281
+ <div class="qm-field-control" {@attach registerControl} onfocusin={reportFocus}>
283
282
  {#if field.control === 'prose'}
284
283
  <ProseField
285
284
  {quill}
@@ -342,7 +341,7 @@
342
341
  />
343
342
  {:else if field.control === 'boolean'}
344
343
  <BooleanField
345
- value={value as boolean | undefined}
344
+ value={renderedBoolean(value)}
346
345
  fallback={ghost as boolean | undefined}
347
346
  {optional}
348
347
  id={domIds.control}
@@ -1,4 +1,3 @@
1
- import './controls.css';
2
1
  interface Props {
3
2
  description: string;
4
3
  /** The field's label: the trigger's own name, so 30 info buttons on a card
@@ -65,8 +65,7 @@
65
65
  label is not a tab stop, and the control it hands focus to is already the
66
66
  keyboard's own way in. -->
67
67
  <label
68
- class="qm-field-label"
69
- class:targeted={controlId != null || onActivate != null}
68
+ class={['qm-field-label', { targeted: controlId != null || onActivate != null }]}
70
69
  {id}
71
70
  for={controlId}
72
71
  onclick={onActivate}
@@ -52,6 +52,7 @@
52
52
  selection the command is supposed to act on.
53
53
  -->
54
54
  <script lang="ts">
55
+ import { onDestroy } from 'svelte';
55
56
  import { wording } from './strings.js';
56
57
 
57
58
  // The surface's words, ambient from the editor root; the package's English
@@ -105,7 +106,6 @@
105
106
  /** Whether the selection carried a link when the prompt was raised. */
106
107
  let linkPresent = $state(false);
107
108
  let linkValue = $state('');
108
- let linkInputEl = $state<HTMLInputElement | undefined>(undefined);
109
109
  let contentEl = $state<HTMLElement | undefined>(undefined);
110
110
  /** The root to portal into: `document.body` escapes the editor's subtree and
111
111
  * the consumer's dials with it, so a pane-scoped palette misses this surface.
@@ -212,25 +212,11 @@
212
212
  deferredSync();
213
213
  }
214
214
 
215
- $effect(() => {
216
- document.addEventListener('selectionchange', deferredSync);
217
- document.addEventListener('focusout', deferredSync);
218
- // Capture, so the gesture is seen whatever a view in the middle of it does with
219
- // the event. `pointercancel` is the release a gesture the browser takes over gets.
220
- document.addEventListener('pointerdown', onPointerDown, true);
221
- document.addEventListener('pointerup', onPointerUp, true);
222
- document.addEventListener('pointercancel', onPointerUp, true);
223
- return () => {
224
- document.removeEventListener('selectionchange', deferredSync);
225
- document.removeEventListener('focusout', deferredSync);
226
- document.removeEventListener('pointerdown', onPointerDown, true);
227
- document.removeEventListener('pointerup', onPointerUp, true);
228
- document.removeEventListener('pointercancel', onPointerUp, true);
229
- // A burst-coalescing rAF may still be in flight at teardown; cancel it so
230
- // `sync()` never runs against the destroyed component.
231
- if (pending) cancelAnimationFrame(rafHandle);
232
- pending = false;
233
- };
215
+ // A burst-coalescing rAF may still be in flight at teardown; cancel it so
216
+ // `sync()` never runs against the destroyed component.
217
+ onDestroy(() => {
218
+ if (pending) cancelAnimationFrame(rafHandle);
219
+ pending = false;
234
220
  });
235
221
 
236
222
  // Any close path that bypasses `sync` (e.g. Escape while the link input holds
@@ -245,12 +231,10 @@
245
231
  // surface already open, so without this the URL is typed into the document.
246
232
  // Selected rather than merely focused, the input arriving seeded and replacing
247
233
  // that value being what the prompt is usually raised for.
248
- $effect(() => {
249
- if (linkPromptOpen && linkInputEl) {
250
- linkInputEl.focus();
251
- linkInputEl.select();
252
- }
253
- });
234
+ function focusPrompt(input: HTMLInputElement): void {
235
+ input.focus();
236
+ input.select();
237
+ }
254
238
 
255
239
  /** Swallow the button's mousedown so focus/selection never leave the leaf. */
256
240
  function keepFocus(e: MouseEvent): void {
@@ -338,6 +322,17 @@
338
322
  }
339
323
  </script>
340
324
 
325
+ <!-- The pointer handlers capture, so the gesture is seen whatever a view in the middle of
326
+ it does with the event. `pointercancel` is the release a gesture the browser takes
327
+ over gets. -->
328
+ <svelte:document
329
+ onselectionchange={deferredSync}
330
+ onfocusout={deferredSync}
331
+ onpointerdowncapture={onPointerDown}
332
+ onpointerupcapture={onPointerUp}
333
+ onpointercancelcapture={onPointerUp}
334
+ />
335
+
341
336
  <Popover.Root bind:open>
342
337
  <Popover.Portal to={portalTarget}>
343
338
  <!-- A flip is measured against the root the surface portals into, which is the box
@@ -383,7 +378,7 @@
383
378
  }}
384
379
  >
385
380
  <input
386
- bind:this={linkInputEl}
381
+ {@attach focusPrompt}
387
382
  class="qm-link-input"
388
383
  type="text"
389
384
  placeholder={t.strings.linkPlaceholder}
@@ -1,5 +1,4 @@
1
1
  import { type FieldController } from '../core/codec/index.js';
2
- import './controls.css';
3
2
  interface Props {
4
3
  /** The observation seam VisualEditor exposes over its `leaves` registry. */
5
4
  getActiveLeaf: () => FieldController | undefined;
@@ -114,8 +114,7 @@
114
114
  <!-- The input keeps focus and owns the keys; a press picks without taking it. -->
115
115
  <!-- svelte-ignore a11y_click_events_have_key_events -->
116
116
  <div
117
- class="qm-menu-item qm-matrix-add-option"
118
- class:add={option.add}
117
+ class={['qm-menu-item qm-matrix-add-option', { add: option.add }]}
119
118
  role="option"
120
119
  id={optionId(k)}
121
120
  tabindex="-1"
@@ -1,5 +1,4 @@
1
1
  import type { AddOption } from './checklists.js';
2
- import './controls.css';
3
2
  interface Props {
4
3
  /** The input's id, and the base its listbox and options derive theirs from. */
5
4
  id: string;
@@ -191,7 +191,7 @@
191
191
  /** A native checkbox carries its own state, so a commit the document declines leaves
192
192
  * the face ticked over a map that says otherwise, `checked={held(id)}` having nothing
193
193
  * new to write. The reassert after the flush is what the styled controls get from
194
- * their synced local (`synced.svelte.ts`): the document is what the face reads. */
194
+ * their local, a `$derived` of the value: the document is what the face reads. */
195
195
  async function tick(id: string, on: boolean, el: HTMLInputElement): Promise<void> {
196
196
  commit(on ? commitMember(map, id, ticked(id)) : unticked(id));
197
197
  await flush();
@@ -333,15 +333,13 @@
333
333
  </span>
334
334
  </div>
335
335
  <div
336
- class="qm-matrix-roster"
337
- class:qm-tracks={compact}
338
- class:compact
336
+ class={['qm-matrix-roster', { 'qm-tracks': compact, compact }]}
339
337
  data-default={defaulted ? '' : undefined}
340
338
  bind:this={rosterEl}
341
339
  >
342
340
  {#each members as m (m.id)}
343
341
  {@const on = held(m.id)}
344
- <div class="qm-member" class:held={on} bind:this={memberEls[m.id]}>
342
+ <div class={['qm-member', { held: on }]} bind:this={memberEls[m.id]}>
345
343
  <div class="qm-member-head">
346
344
  <!-- A real checkbox with its face drawn here: the UA's face is shadow DOM no
347
345
  dial reaches, so the input is `appearance: none` and the box and the
@@ -1,7 +1,6 @@
1
1
  import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
2
2
  import { type DeepDiagnostic } from './diagnostics.js';
3
3
  import type { LandingBox } from './leaves.js';
4
- import './controls.css';
5
4
  interface Props {
6
5
  /** The stored sparse map, or undefined while the field is unset. */
7
6
  value: Record<string, unknown> | undefined;
@@ -29,7 +29,6 @@
29
29
  numbers, and `1.000` as a whole one.
30
30
  -->
31
31
  <script lang="ts">
32
- import { syncedLocal } from './synced.svelte.js';
33
32
  import './controls.css';
34
33
 
35
34
  interface Props {
@@ -53,11 +52,8 @@
53
52
  }
54
53
  let { value, integer, fallback, placeholder, label, id, describedBy, onCommit }: Props = $props();
55
54
 
56
- // Local input state synced to `value` (as a string projection), or to the default
57
- // an unset field holds; own-typing stays local, only an external change reconciles
58
- // back in (see `syncedLocal`).
59
- const local = syncedLocal(() => (value != null ? String(value) : (fallback ?? '')));
60
- const defaulted = $derived(value == null && !!fallback && local.value === fallback);
55
+ let local = $derived(value != null ? String(value) : (fallback ?? ''));
56
+ const defaulted = $derived(value == null && !!fallback && local === fallback);
61
57
 
62
58
  const partial = $derived(
63
59
  integer ? /^\s*[+-]?\d*\s*$/ : /^\s*[+-]?\d*\.?\d*(?:[eE][+-]?\d*)?\s*$/
@@ -77,7 +73,7 @@
77
73
  // `insertText` lands on the undo stack and fires `input`; `setRangeText` does neither.
78
74
  if (document.execCommand?.('insertText', false, data)) return;
79
75
  el.setRangeText(data, from, to, 'end');
80
- local.value = el.value;
76
+ local = el.value;
81
77
  }
82
78
 
83
79
  // Parse a settled entry and emit it; `local` is owned by `oninput`. Blank →
@@ -89,7 +85,7 @@
89
85
  const entry = raw.trim();
90
86
  if (entry === '') {
91
87
  onCommit(undefined);
92
- local.value = el.value = fallback ?? '';
88
+ local = el.value = fallback ?? '';
93
89
  return;
94
90
  }
95
91
  // Finite as well: a digit run past `Number.MAX_VALUE` reads as `Infinity`.
@@ -102,7 +98,7 @@
102
98
  class="qm-input qm-focus-ring"
103
99
  type="text"
104
100
  inputmode={integer ? 'numeric' : 'decimal'}
105
- value={local.value}
101
+ value={local}
106
102
  {id}
107
103
  {placeholder}
108
104
  data-default={defaulted ? '' : undefined}
@@ -110,7 +106,7 @@
110
106
  aria-describedby={describedBy}
111
107
  onbeforeinput={filter}
112
108
  oninput={(e) => {
113
- local.value = (e.currentTarget as HTMLInputElement).value;
109
+ local = (e.currentTarget as HTMLInputElement).value;
114
110
  }}
115
111
  onchange={(e) => commit(e.currentTarget as HTMLInputElement)}
116
112
  />
@@ -1,4 +1,3 @@
1
- import './controls.css';
2
1
  interface Props {
3
2
  value: number | undefined;
4
3
  integer?: boolean;
@@ -5,8 +5,8 @@
5
5
  ProseValue}) over the boundary's nested read, and a container property — an `array`,
6
6
  an `object`, a `matrix`, a variant — mounts its own control here, at the next rung of
7
7
  the same figure. Depth is bounded by what is open, not by the schema: a repeater inside
8
- a subform is collapsed rows again, one open at a time, so an open row under an open
9
- row is one figure two rungs in.
8
+ a subform is collapsed rows again, so an open row under an open row is one figure two
9
+ rungs in.
10
10
 
11
11
  The nesting is a vertical at `--_qm-border`, the ladder's last stroke: the card's edge,
12
12
  an open section's vertical one `--_qm-nest` in where the field is in one, this one a rung
@@ -48,6 +48,7 @@
48
48
  obliged,
49
49
  optionalCell,
50
50
  printedText,
51
+ renderedBoolean,
51
52
  shortCell
52
53
  } from './structure.js';
53
54
  import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
@@ -255,9 +256,7 @@
255
256
 
256
257
  <div
257
258
  bind:this={rootEl}
258
- class="qm-object"
259
- class:qm-object-bare={bare}
260
- class:qm-object-flat={flat}
259
+ class={['qm-object', { 'qm-object-bare': bare, 'qm-object-flat': flat }]}
261
260
  role="group"
262
261
  aria-labelledby={labelledBy}
263
262
  aria-describedby={describedBy}
@@ -286,9 +285,7 @@
286
285
  (`packable`). A block prose cell spans it too, and keeps its label row. -->
287
286
  {@const block = kind === 'prose' && !shortCell(sub)}
288
287
  <div
289
- class="qm-object-prop"
290
- class:qm-prop-full={isContainer(kind)}
291
- class:qm-prop-wide={block}
288
+ class={['qm-object-prop', { 'qm-prop-full': isContainer(kind), 'qm-prop-wide': block }]}
292
289
  data-qm-prop={key}
293
290
  >
294
291
  {#if ids && kind !== 'array' && kind !== 'matrix'}
@@ -330,7 +327,7 @@
330
327
  label={named}
331
328
  id={ids?.control}
332
329
  describedBy={describes}
333
- value={obj[key] as boolean | undefined}
330
+ value={renderedBoolean(obj[key])}
334
331
  fallback={sub.default as boolean | undefined}
335
332
  optional={optionalCell(sub)}
336
333
  onCommit={(v) => commitProp(key, v)}
@@ -375,9 +372,8 @@
375
372
  {:else if kind === 'array'}
376
373
  <!-- The repeater owns its label row, the add chip sharing it (`Field` skips
377
374
  its own label for one too), so the names are handed down rather than
378
- drawn here. Its rows are collapsed rows again, one open at a time: the
379
- depth an open row under an open row costs is two rungs of the ladder
380
- and one figure. -->
375
+ drawn here. Its rows are collapsed rows again: the depth an open row
376
+ under an open row costs is two rungs of the ladder and one figure. -->
381
377
  <ArrayField
382
378
  bind:this={nestedEls[key]}
383
379
  value={obj[key] as unknown[] | undefined}
@@ -2,7 +2,6 @@ import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
2
2
  import { type DeepDiagnostic } from './diagnostics.js';
3
3
  import type { LandingBox } from './leaves.js';
4
4
  import ObjectField from './ObjectField.svelte';
5
- import './controls.css';
6
5
  interface Props {
7
6
  value: Record<string, unknown> | undefined;
8
7
  properties: Record<string, QuillFieldSchema> | undefined;
@@ -5,7 +5,7 @@
5
5
  this leaf's commits with the caret riding the untouched view.
6
6
  -->
7
7
  <script lang="ts">
8
- import { onMount } from 'svelte';
8
+ import { untrack } from 'svelte';
9
9
  import { createField, type FieldController, type SlashState } from '../core/codec/index.js';
10
10
  import type { LeafRegistry } from './leaves.js';
11
11
  import SlashMenu from './SlashMenu.svelte';
@@ -82,7 +82,6 @@
82
82
  diagnostics
83
83
  }: Props = $props();
84
84
 
85
- let containerEl: HTMLDivElement | undefined = $state();
86
85
  let held = $state(false);
87
86
  const uid = $props.id();
88
87
  const heldId = `${uid}-held`;
@@ -108,41 +107,43 @@
108
107
  controller?.focus();
109
108
  }
110
109
 
111
- onMount(() => {
112
- if (!containerEl) return;
113
- controller = createField({
114
- doc,
115
- quill,
116
- addr,
117
- container: containerEl,
118
- inline,
119
- plaintext,
120
- label,
121
- labelledBy,
122
- describedBy,
123
- fallback,
124
- placeholder,
125
- placeholderUntilEdit,
126
- tableStrings: () => t.strings,
127
- onSlash: (next) => {
128
- slash = next;
129
- },
130
- onFocus,
131
- onCaretMove,
132
- onChange,
133
- onError,
134
- heldNoteId: heldId,
135
- onHold: (next) => {
136
- held = next;
137
- }
110
+ function mountField(container: HTMLDivElement): () => void {
111
+ return untrack(() => {
112
+ const mounted = createField({
113
+ doc,
114
+ quill,
115
+ addr,
116
+ container,
117
+ inline,
118
+ plaintext,
119
+ label,
120
+ labelledBy,
121
+ describedBy,
122
+ fallback,
123
+ placeholder,
124
+ placeholderUntilEdit,
125
+ tableStrings: () => t.strings,
126
+ onSlash: (next) => {
127
+ slash = next;
128
+ },
129
+ onFocus,
130
+ onCaretMove,
131
+ onChange,
132
+ onError,
133
+ heldNoteId: heldId,
134
+ onHold: (next) => {
135
+ held = next;
136
+ }
137
+ });
138
+ controller = mounted;
139
+ leaves?.registerProse(leafKey, mounted);
140
+ return () => {
141
+ leaves?.unregisterProse(leafKey);
142
+ mounted.destroy();
143
+ controller = undefined;
144
+ };
138
145
  });
139
- leaves?.registerProse(leafKey, controller);
140
- return () => {
141
- leaves?.unregisterProse(leafKey);
142
- controller?.destroy();
143
- controller = undefined;
144
- };
145
- });
146
+ }
146
147
 
147
148
  // A retype does not remount the leaf (its key is the card's session id), so the new
148
149
  // kind's ghost and default are pushed into the live view rather than paid for with
@@ -162,11 +163,11 @@
162
163
  </script>
163
164
 
164
165
  <div
165
- bind:this={containerEl}
166
- class="qm-prose"
167
- class:qm-control-box={!unframed}
168
- class:qm-focus-ring-within={!unframed}
169
- class:qm-prose-block={block}
166
+ {@attach mountField}
167
+ class={[
168
+ 'qm-prose',
169
+ { 'qm-control-box': !unframed, 'qm-focus-ring-within': !unframed, 'qm-prose-block': block }
170
+ ]}
170
171
  data-leaf-key={leafKey}
171
172
  >
172
173
  {#if held}
@@ -1,5 +1,4 @@
1
1
  import type { LeafRegistry } from './leaves.js';
2
- import './controls.css';
3
2
  import type { Document, Quill, Addr, Content, Diagnostic } from '@quillmark/wasm';
4
3
  import type { EditorErrorHandler } from '../core/errors.js';
5
4
  interface Props {
@@ -31,7 +31,7 @@
31
31
  schema, read-only, with a note inside its box, and commits nothing.
32
32
  -->
33
33
  <script lang="ts">
34
- import { onMount } from 'svelte';
34
+ import { untrack } from 'svelte';
35
35
  import { EditorState, Selection } from 'prosemirror-state';
36
36
  import { EditorView } from 'prosemirror-view';
37
37
  import {
@@ -101,7 +101,6 @@
101
101
  const t = wording();
102
102
  const uid = $props.id();
103
103
  const heldId = `${uid}-held`;
104
- let editorEl: HTMLDivElement | undefined = $state();
105
104
  let held = $state(false);
106
105
  let view: EditorView | undefined;
107
106
  /** Take the caret: what a parent placing focus on this leaf calls. The view's
@@ -134,61 +133,62 @@
134
133
  view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
135
134
  }
136
135
 
137
- onMount(() => {
138
- if (!editorEl) return;
139
- // The same keymap and plugin stack a `createField` leaf mounts (shared
140
- // `proseLeafPlugins`), minus the anchor-position plugin: anchors are dropped on
141
- // the parent's value write, per the header.
142
- const inline = !block;
143
- const stored = content();
144
- const rt = stored ?? fallback ?? emptyContent();
145
- let defaulted = stored === undefined && !!fallback;
146
- held = !fitsLeaf(rt, { inline, plaintext });
147
- const named = held
148
- ? heldAttributes({ label, labelledBy, describedBy }, heldId)
149
- : (proseAttributes({ label, labelledBy, describedBy }) ?? {});
150
- const defaultNamed = { ...named, 'data-default': '' };
151
- const schema = held ? blockSchema : leafSchema({ plaintext, inline });
152
- const state = EditorState.create({
153
- doc: decode(rt, schema),
154
- plugins: held
155
- ? []
156
- : proseLeafPlugins(schema, {
157
- inline,
158
- placeholder: () => placeholder,
159
- placeholderUntilEdit: true
160
- })
161
- });
162
- const mounted = new EditorView(
163
- { mount: editorEl },
164
- {
165
- state,
166
- editable: () => !held,
167
- // Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
168
- // (`proseAttributes`), so a cell carrying a label element and a row carrying
169
- // none cannot name their regions by different rules.
170
- attributes: () => (defaulted ? defaultNamed : named),
171
- dispatchTransaction(tr) {
172
- const next = mounted.state.apply(tr);
173
- const edit = tr.docChanged && !held;
174
- if (edit) defaulted = false;
175
- mounted.updateState(next);
176
- if (edit) onChange(pmToContent(next.doc));
177
- },
178
- handleDOMEvents: {
179
- keydown: (_v, e) => {
180
- onKey?.(e);
181
- return false;
136
+ function mountView(mount: HTMLDivElement): () => void {
137
+ return untrack(() => {
138
+ // The same keymap and plugin stack a `createField` leaf mounts (shared
139
+ // `proseLeafPlugins`), minus the anchor-position plugin: anchors are dropped on
140
+ // the parent's value write, per the header.
141
+ const inline = !block;
142
+ const stored = content();
143
+ const rt = stored ?? fallback ?? emptyContent();
144
+ let defaulted = stored === undefined && !!fallback;
145
+ held = !fitsLeaf(rt, { inline, plaintext });
146
+ const named = held
147
+ ? heldAttributes({ label, labelledBy, describedBy }, heldId)
148
+ : (proseAttributes({ label, labelledBy, describedBy }) ?? {});
149
+ const defaultNamed = { ...named, 'data-default': '' };
150
+ const schema = held ? blockSchema : leafSchema({ plaintext, inline });
151
+ const state = EditorState.create({
152
+ doc: decode(rt, schema),
153
+ plugins: held
154
+ ? []
155
+ : proseLeafPlugins(schema, {
156
+ inline,
157
+ placeholder: () => placeholder,
158
+ placeholderUntilEdit: true
159
+ })
160
+ });
161
+ const mounted = new EditorView(
162
+ { mount },
163
+ {
164
+ state,
165
+ editable: () => !held,
166
+ // Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
167
+ // (`proseAttributes`), so a cell carrying a label element and a row carrying
168
+ // none cannot name their regions by different rules.
169
+ attributes: () => (defaulted ? defaultNamed : named),
170
+ dispatchTransaction(tr) {
171
+ const next = mounted.state.apply(tr);
172
+ const edit = tr.docChanged && !held;
173
+ if (edit) defaulted = false;
174
+ mounted.updateState(next);
175
+ if (edit) onChange(pmToContent(next.doc));
176
+ },
177
+ handleDOMEvents: {
178
+ keydown: (_v, e) => {
179
+ onKey?.(e);
180
+ return false;
181
+ }
182
182
  }
183
183
  }
184
- }
185
- );
186
- view = mounted;
187
- return () => {
188
- view = undefined;
189
- mounted.destroy();
190
- };
191
- });
184
+ );
185
+ view = mounted;
186
+ return () => {
187
+ view = undefined;
188
+ mounted.destroy();
189
+ };
190
+ });
191
+ }
192
192
  </script>
193
193
 
194
194
  <!-- `.qm-control-box` (controls.css) is the whole box, so an array of `richtext` and
@@ -197,7 +197,7 @@
197
197
  line of prose measure one line. Width is the row's or the cell's to give: the leaf
198
198
  fills the track it is placed in. -->
199
199
  <div class="qm-prose-value qm-control-box qm-focus-ring-within">
200
- <div bind:this={editorEl}></div>
200
+ <div {@attach mountView}></div>
201
201
  <!-- Inside the box, so the cell or row holds one child either way: a subform cell's
202
202
  subgrid has a row for the label and one for the box, and an array row's slabs
203
203
  stand the box's height. -->
@@ -1,4 +1,3 @@
1
- import './controls.css';
2
1
  import type { Content } from '@quillmark/wasm';
3
2
  interface Props {
4
3
  /** This leaf's content, read at mount (the parent's boundary read), `undefined`