@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.
- package/README.md +1 -1
- package/dist/core/codec/index.d.ts +0 -3
- package/dist/core/rebind.svelte.js +5 -8
- package/dist/preset/index.d.ts +1 -0
- package/dist/preview/Preview.svelte +14 -19
- package/dist/preview/Preview.svelte.d.ts +3 -3
- package/dist/preview/index.d.ts +0 -1
- package/dist/visual/ArrayField.svelte +35 -43
- package/dist/visual/ArrayField.svelte.d.ts +0 -1
- package/dist/visual/BooleanField.svelte +9 -14
- package/dist/visual/BooleanField.svelte.d.ts +0 -1
- package/dist/visual/Card.svelte +5 -18
- package/dist/visual/CardControls.svelte.d.ts +0 -1
- package/dist/visual/DateField.svelte +14 -17
- package/dist/visual/DateField.svelte.d.ts +0 -1
- package/dist/visual/EnumField.svelte +9 -12
- package/dist/visual/EnumField.svelte.d.ts +0 -1
- package/dist/visual/Field.svelte +7 -8
- package/dist/visual/FieldHint.svelte.d.ts +0 -1
- package/dist/visual/FieldLabel.svelte +1 -2
- package/dist/visual/FormatPopover.svelte +22 -27
- package/dist/visual/FormatPopover.svelte.d.ts +0 -1
- package/dist/visual/MatrixAdd.svelte +1 -2
- package/dist/visual/MatrixAdd.svelte.d.ts +0 -1
- package/dist/visual/MatrixField.svelte +3 -5
- package/dist/visual/MatrixField.svelte.d.ts +0 -1
- package/dist/visual/NumberField.svelte +6 -10
- package/dist/visual/NumberField.svelte.d.ts +0 -1
- package/dist/visual/ObjectField.svelte +8 -12
- package/dist/visual/ObjectField.svelte.d.ts +0 -1
- package/dist/visual/ProseField.svelte +42 -41
- package/dist/visual/ProseField.svelte.d.ts +0 -1
- package/dist/visual/ProseValue.svelte +56 -56
- package/dist/visual/ProseValue.svelte.d.ts +0 -1
- package/dist/visual/SlashMenu.svelte +5 -15
- package/dist/visual/SlashMenu.svelte.d.ts +0 -1
- package/dist/visual/TextField.svelte +5 -9
- package/dist/visual/TextField.svelte.d.ts +0 -1
- package/dist/visual/TipsCard.svelte +10 -11
- package/dist/visual/TipsCard.svelte.d.ts +0 -1
- package/dist/visual/VisualEditorInner.svelte +6 -14
- package/dist/visual/VisualEditorInner.svelte.d.ts +0 -1
- package/dist/visual/index.d.ts +0 -1
- package/dist/visual/structure.d.ts +4 -0
- package/dist/visual/structure.js +12 -0
- package/package.json +20 -16
- package/dist/visual/synced.svelte.d.ts +0 -15
- package/dist/visual/synced.svelte.js +0 -34
package/dist/visual/Field.svelte
CHANGED
|
@@ -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
|
-
|
|
239
|
-
|
|
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=
|
|
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"
|
|
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
|
|
344
|
+
value={renderedBoolean(value)}
|
|
346
345
|
fallback={ghost as boolean | undefined}
|
|
347
346
|
{optional}
|
|
348
347
|
id={domIds.control}
|
|
@@ -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=
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
381
|
+
{@attach focusPrompt}
|
|
387
382
|
class="qm-link-input"
|
|
388
383
|
type="text"
|
|
389
384
|
placeholder={t.strings.linkPlaceholder}
|
|
@@ -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=
|
|
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"
|
|
@@ -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
|
|
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=
|
|
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=
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
109
|
+
local = (e.currentTarget as HTMLInputElement).value;
|
|
114
110
|
}}
|
|
115
111
|
onchange={(e) => commit(e.currentTarget as HTMLInputElement)}
|
|
116
112
|
/>
|
|
@@ -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,
|
|
9
|
-
|
|
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=
|
|
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=
|
|
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]
|
|
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
|
|
379
|
-
|
|
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 {
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
class=
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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}
|
|
@@ -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 {
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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. -->
|