@quillmark/svelte 0.21.0 → 0.23.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/NOTICE +6 -7
- package/README.md +1 -1
- package/dist/core/address.d.ts +6 -1
- package/dist/core/address.js +12 -3
- package/dist/core/bloom.d.ts +9 -4
- package/dist/core/bloom.js +18 -49
- package/dist/core/codec/atoms.d.ts +1 -1
- package/dist/core/codec/atoms.js +1 -1
- package/dist/core/codec/blocks.js +4 -2
- package/dist/core/codec/cap.d.ts +21 -0
- package/dist/core/codec/cap.js +63 -0
- package/dist/core/codec/code.d.ts +1 -1
- package/dist/core/codec/code.js +1 -1
- package/dist/core/codec/decode.d.ts +5 -5
- package/dist/core/codec/decode.js +18 -12
- package/dist/core/codec/encode.js +6 -1
- package/dist/core/codec/field.d.ts +22 -10
- package/dist/core/codec/field.js +54 -18
- package/dist/core/codec/index.d.ts +3 -1
- package/dist/core/codec/index.js +3 -2
- package/dist/core/codec/inputrules.js +1 -1
- package/dist/core/codec/lists.d.ts +3 -2
- package/dist/core/codec/lists.js +21 -5
- package/dist/core/codec/nesting.d.ts +20 -0
- package/dist/core/codec/nesting.js +62 -0
- package/dist/core/codec/prose.css +74 -20
- package/dist/core/codec/schema.d.ts +2 -2
- package/dist/core/codec/schema.js +16 -7
- package/dist/core/codec/table-copy.d.ts +17 -0
- package/dist/core/codec/table-copy.js +84 -0
- package/dist/core/codec/table-view.d.ts +1 -7
- package/dist/core/codec/table-view.js +70 -119
- package/dist/core/codec/table.d.ts +2 -9
- package/dist/core/codec/table.js +10 -19
- package/dist/core/codec/tasks.d.ts +19 -0
- package/dist/core/codec/tasks.js +140 -0
- package/dist/core/errors.d.ts +2 -2
- package/dist/visual/ArrayField.svelte +103 -63
- package/dist/visual/ArrayField.svelte.d.ts +0 -2
- package/dist/visual/Card.svelte +9 -3
- package/dist/visual/Card.svelte.d.ts +2 -2
- package/dist/visual/Field.svelte +33 -17
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldLabel.svelte +19 -33
- package/dist/visual/FieldLabel.svelte.d.ts +4 -2
- package/dist/visual/FormatPopover.svelte +54 -73
- package/dist/visual/MatrixField.svelte +18 -12
- package/dist/visual/NumberField.svelte +32 -3
- package/dist/visual/ObjectField.svelte +50 -22
- package/dist/visual/ObjectField.svelte.d.ts +6 -0
- package/dist/visual/ProseField.svelte +20 -14
- package/dist/visual/ProseField.svelte.d.ts +8 -2
- package/dist/visual/ProseValue.svelte +30 -19
- package/dist/visual/ProseValue.svelte.d.ts +7 -4
- package/dist/visual/TextField.svelte +3 -2
- package/dist/visual/TipsCard.svelte +5 -2
- package/dist/visual/VariantField.svelte +1 -1
- package/dist/visual/VariantField.svelte.d.ts +1 -1
- package/dist/visual/VisualEditorInner.svelte +13 -13
- package/dist/visual/controls.css +19 -1
- package/dist/visual/icons/nodes.d.ts +0 -21
- package/dist/visual/icons/nodes.js +1 -7
- package/dist/visual/leaves.d.ts +12 -9
- package/dist/visual/leaves.js +19 -0
- package/dist/visual/strings.d.ts +16 -5
- package/dist/visual/strings.js +17 -9
- package/dist/visual/structure.d.ts +26 -32
- package/dist/visual/structure.js +71 -53
- package/package.json +3 -3
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
(`-`, `1.`) is never a document state worth a boundary round-trip, and
|
|
5
5
|
committing it live flashes a coercion diagnostic + `console.error` on every
|
|
6
6
|
intermediate prefix, announced by `DiagnosticList`'s `role="status"` live
|
|
7
|
-
region.
|
|
7
|
+
region. Escape takes a pending entry back to what the field holds, as it takes a
|
|
8
|
+
card rename back, and the focus stays; with nothing pending the key is not claimed.
|
|
8
9
|
|
|
9
10
|
An unset field holds its resolved `default:` as its text, at the default rung
|
|
10
11
|
(theme.css), and an edit that settles writes it as authored, as `TextField` takes
|
|
@@ -52,7 +53,8 @@
|
|
|
52
53
|
}
|
|
53
54
|
let { value, integer, fallback, placeholder, label, id, describedBy, onCommit }: Props = $props();
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
const resting = $derived(value != null ? String(value) : (fallback ?? ''));
|
|
57
|
+
let local = $derived(resting);
|
|
56
58
|
const defaulted = $derived(value == null && !!fallback && local === fallback);
|
|
57
59
|
|
|
58
60
|
const partial = $derived(
|
|
@@ -76,13 +78,37 @@
|
|
|
76
78
|
local = el.value;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
// Where the reverted text differs from the last settled one, a browser fires `change`
|
|
82
|
+
// on leaving; that one commits nothing. An entry that settled outside the grammar was
|
|
83
|
+
// handed up as a string and refused, so taking it back settles the held value again,
|
|
84
|
+
// which clears the refusal drawn on the field.
|
|
85
|
+
let reverted = false;
|
|
86
|
+
let offered = false;
|
|
87
|
+
function revert(e: KeyboardEvent): void {
|
|
88
|
+
if (e.key !== 'Escape' || e.isComposing) return;
|
|
89
|
+
const el = e.currentTarget as HTMLInputElement;
|
|
90
|
+
if (el.value === resting) return;
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
local = el.value = resting;
|
|
93
|
+
reverted = true;
|
|
94
|
+
if (offered) {
|
|
95
|
+
offered = false;
|
|
96
|
+
onCommit(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
// Parse a settled entry and emit it; `local` is owned by `oninput`. Blank →
|
|
80
101
|
// `undefined`, and the input takes the default back: nothing else reconciles it, the
|
|
81
102
|
// projection having read the default throughout. Onto the element as well, since the
|
|
82
103
|
// attribute Svelte last wrote may already be the default, and it writes no repeat.
|
|
83
104
|
function commit(el: HTMLInputElement): void {
|
|
105
|
+
if (reverted) {
|
|
106
|
+
reverted = false;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
84
109
|
const raw = el.value;
|
|
85
110
|
const entry = raw.trim();
|
|
111
|
+
offered = false;
|
|
86
112
|
if (entry === '') {
|
|
87
113
|
onCommit(undefined);
|
|
88
114
|
local = el.value = fallback ?? '';
|
|
@@ -90,7 +116,8 @@
|
|
|
90
116
|
}
|
|
91
117
|
// Finite as well: a digit run past `Number.MAX_VALUE` reads as `Infinity`.
|
|
92
118
|
const n = whole.test(entry) ? Number(entry) : NaN;
|
|
93
|
-
|
|
119
|
+
offered = !Number.isFinite(n);
|
|
120
|
+
onCommit(offered ? raw : n);
|
|
94
121
|
}
|
|
95
122
|
</script>
|
|
96
123
|
|
|
@@ -107,6 +134,8 @@
|
|
|
107
134
|
onbeforeinput={filter}
|
|
108
135
|
oninput={(e) => {
|
|
109
136
|
local = (e.currentTarget as HTMLInputElement).value;
|
|
137
|
+
reverted = false;
|
|
110
138
|
}}
|
|
139
|
+
onkeydown={revert}
|
|
111
140
|
onchange={(e) => commit(e.currentTarget as HTMLInputElement)}
|
|
112
141
|
/>
|
|
@@ -41,18 +41,22 @@
|
|
|
41
41
|
baseType,
|
|
42
42
|
controlKind,
|
|
43
43
|
declaredContent,
|
|
44
|
+
fieldModels,
|
|
44
45
|
flatObject,
|
|
45
46
|
humanize,
|
|
46
47
|
isContainer,
|
|
48
|
+
literalText,
|
|
47
49
|
mountKey,
|
|
48
|
-
obliged,
|
|
49
50
|
optionalCell,
|
|
51
|
+
placeFields,
|
|
50
52
|
printedText,
|
|
51
53
|
renderedBoolean,
|
|
52
|
-
shortCell
|
|
54
|
+
shortCell,
|
|
55
|
+
textCap
|
|
53
56
|
} from './structure.js';
|
|
54
57
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
55
58
|
import type { LandingBox } from './leaves.js';
|
|
59
|
+
import { isEmptyContent } from '../core/codec/index.js';
|
|
56
60
|
import { wording } from './strings.js';
|
|
57
61
|
import { propertyDomIds } from './domid.js';
|
|
58
62
|
import FieldLabel from './FieldLabel.svelte';
|
|
@@ -102,6 +106,12 @@
|
|
|
102
106
|
/** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
|
|
103
107
|
* list the subform stands in. */
|
|
104
108
|
flat?: boolean;
|
|
109
|
+
/** A card-level object, or one nested in it: its properties are placed as a
|
|
110
|
+
* card's fields are (`placeFields`), so moving a field into a group leaves its row
|
|
111
|
+
* as it was. Unset,
|
|
112
|
+
* every short cell packs: an array's record, a variant's world and a matrix
|
|
113
|
+
* member's columns are one record's cells, not a card's fields. */
|
|
114
|
+
placed?: boolean;
|
|
105
115
|
/** Raw keydown off a text or prose cell, for a row whose own keys run through its
|
|
106
116
|
* cells: the table's Enter and Backspace ({@link ArrayField}). */
|
|
107
117
|
onCellKey?: (e: KeyboardEvent, key: string) => void;
|
|
@@ -121,23 +131,27 @@
|
|
|
121
131
|
onCommit,
|
|
122
132
|
bare = false,
|
|
123
133
|
flat = false,
|
|
134
|
+
placed = false,
|
|
124
135
|
onCellKey,
|
|
125
136
|
diagnostics
|
|
126
137
|
}: Props = $props();
|
|
127
138
|
|
|
128
139
|
const t = wording();
|
|
140
|
+
/** A capped prose cell's character count, by property. */
|
|
141
|
+
const counts: Record<string, number> = $state({});
|
|
129
142
|
const entries = $derived(Object.entries(properties ?? {}));
|
|
130
143
|
const obj = $derived((value ?? {}) as Record<string, unknown>);
|
|
131
144
|
const has = (key: string): boolean => Object.hasOwn(properties ?? {}, key);
|
|
145
|
+
const spans = $derived(
|
|
146
|
+
placed
|
|
147
|
+
? new Map(
|
|
148
|
+
placeFields(fieldModels({ fields: properties ?? {} })).map((p) => [p.field.name, p.span])
|
|
149
|
+
)
|
|
150
|
+
: undefined
|
|
151
|
+
);
|
|
132
152
|
const routed = $derived(splitDeep(diagnostics));
|
|
133
153
|
const foot = $derived(unrouted(routed, (step) => typeof step === 'string' && has(step)));
|
|
134
154
|
|
|
135
|
-
/** Obligation, read off the cell exactly as `fieldModels` reads it off a field
|
|
136
|
-
* ({@link obliged}). `validate` anchors it per leaf, so a property with no
|
|
137
|
-
* `default:` is obliged in its own right and the container around it holds none
|
|
138
|
-
* (VISUAL_EDITOR §"Enum variants"). */
|
|
139
|
-
const required = obliged;
|
|
140
|
-
|
|
141
155
|
const title = (key: string, sub: QuillFieldSchema): string => sub.title ?? humanize(key);
|
|
142
156
|
/** What an unset optional cell ghosts, which is what it prints: `none`. An answered
|
|
143
157
|
* cell ghosts nothing, an empty answer printing empty. */
|
|
@@ -151,8 +165,7 @@
|
|
|
151
165
|
/** The `aria-label` fallback, for a subform mounted without a field's id space:
|
|
152
166
|
* the field's name and the property's, since nothing else names the control. */
|
|
153
167
|
const fallbackName = (key: string, sub: QuillFieldSchema): string =>
|
|
154
|
-
`${label != null ? `${label} ` : ''}${title(key, sub)}
|
|
155
|
-
(required(sub) ? ` ${t.strings.fieldRequired}` : '');
|
|
168
|
+
`${label != null ? `${label} ` : ''}${title(key, sub)}`;
|
|
156
169
|
|
|
157
170
|
/** The handles of the cells the DOM cannot answer for, keyed by property name —
|
|
158
171
|
* which is stable, a subform drawing its properties by declaration and never by
|
|
@@ -285,10 +298,19 @@
|
|
|
285
298
|
kind === 'boolean'}
|
|
286
299
|
<!-- A container takes the full span at every depth: its rows are the document's
|
|
287
300
|
to count, and a track beside it would stand in a column of whitespace
|
|
288
|
-
(`packable`). A block prose cell spans it too, and keeps its label row
|
|
301
|
+
(`packable`). A block prose cell spans it too, and keeps its label row, as
|
|
302
|
+
does a placed cell `placeFields` gives the whole row. -->
|
|
289
303
|
{@const block = kind === 'prose' && !shortCell(sub)}
|
|
304
|
+
{@const cap = kind === 'prose' ? textCap(sub) : undefined}
|
|
305
|
+
{@const countId = cap && ids ? `${ids.control}-count` : undefined}
|
|
290
306
|
<div
|
|
291
|
-
class={[
|
|
307
|
+
class={[
|
|
308
|
+
'qm-object-prop',
|
|
309
|
+
{
|
|
310
|
+
'qm-prop-full': isContainer(kind),
|
|
311
|
+
'qm-prop-wide': block || (!isContainer(kind) && spans?.get(key) === 'full')
|
|
312
|
+
}
|
|
313
|
+
]}
|
|
292
314
|
data-qm-prop={key}
|
|
293
315
|
>
|
|
294
316
|
{#if ids && kind !== 'array' && kind !== 'matrix'}
|
|
@@ -298,8 +320,11 @@
|
|
|
298
320
|
id={ids.label}
|
|
299
321
|
descriptionId={describes}
|
|
300
322
|
onActivate={labelable ? undefined : () => focusProp(key)}
|
|
301
|
-
required={required(sub)}
|
|
302
323
|
description={sub.description}
|
|
324
|
+
count={cap && counts[key] != null
|
|
325
|
+
? t.strings.textCount(counts[key], cap.max)
|
|
326
|
+
: undefined}
|
|
327
|
+
{countId}
|
|
303
328
|
/>
|
|
304
329
|
{/if}
|
|
305
330
|
{#if kind === 'enum'}
|
|
@@ -364,13 +389,15 @@
|
|
|
364
389
|
content={() => contentAt([key])}
|
|
365
390
|
value={obj[key]}
|
|
366
391
|
fallback={contentDefault(sub)}
|
|
367
|
-
plaintext={
|
|
392
|
+
plaintext={literalText(sub)}
|
|
368
393
|
placeholder={noneOf(key, sub)}
|
|
369
394
|
{block}
|
|
370
395
|
label={named}
|
|
371
396
|
labelledBy={ids?.label}
|
|
372
|
-
describedBy={describes}
|
|
373
|
-
|
|
397
|
+
describedBy={countId ? [describes, countId].filter(Boolean).join(' ') : describes}
|
|
398
|
+
cap={cap && { ...cap, onCount: (n) => (counts[key] = n) }}
|
|
399
|
+
onChange={(rt) =>
|
|
400
|
+
commitProp(key, optionalCell(sub) && isEmptyContent(rt) ? undefined : rt)}
|
|
374
401
|
onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
|
|
375
402
|
/>
|
|
376
403
|
{:else if kind === 'array'}
|
|
@@ -386,7 +413,6 @@
|
|
|
386
413
|
layout={arrayLayout(sub)}
|
|
387
414
|
max={sub.max}
|
|
388
415
|
label={title(key, sub)}
|
|
389
|
-
required={required(sub)}
|
|
390
416
|
description={sub.description}
|
|
391
417
|
labelId={ids?.label}
|
|
392
418
|
descriptionId={describes}
|
|
@@ -401,6 +427,7 @@
|
|
|
401
427
|
value={obj[key] as Record<string, unknown> | undefined}
|
|
402
428
|
properties={sub.properties}
|
|
403
429
|
flat={flatObject(sub)}
|
|
430
|
+
{placed}
|
|
404
431
|
label={ids ? undefined : fallbackName(key, sub)}
|
|
405
432
|
idBase={ids?.control}
|
|
406
433
|
labelledBy={ids?.label}
|
|
@@ -514,15 +541,16 @@
|
|
|
514
541
|
align-items: start;
|
|
515
542
|
min-width: 0;
|
|
516
543
|
}
|
|
517
|
-
/* A
|
|
518
|
-
truncated, and there are no sibling properties for it to line up with. A
|
|
519
|
-
|
|
520
|
-
|
|
544
|
+
/* A record of one cell takes half the capacity: at full capacity a single track reads
|
|
545
|
+
as truncated, and there are no sibling properties for it to line up with. A placed
|
|
546
|
+
property's span is `placeFields`', which strands a lone one to the whole row as it
|
|
547
|
+
does a card's field. */
|
|
521
548
|
.qm-object-prop:only-child {
|
|
522
549
|
grid-column: span var(--cols-half);
|
|
523
550
|
}
|
|
524
551
|
/* A block prose property holds paragraphs, so a track beside it stands in whitespace
|
|
525
|
-
once it grows (`packable`); its label still rides the row above it.
|
|
552
|
+
once it grows (`packable`); its label still rides the row above it. A placed
|
|
553
|
+
property `placeFields` gives the whole row spans it the same way. */
|
|
526
554
|
.qm-object-prop.qm-prop-wide,
|
|
527
555
|
.qm-object-prop.qm-prop-wide:only-child {
|
|
528
556
|
grid-column: 1 / -1;
|
|
@@ -35,6 +35,12 @@ interface Props {
|
|
|
35
35
|
/** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
|
|
36
36
|
* list the subform stands in. */
|
|
37
37
|
flat?: boolean;
|
|
38
|
+
/** A card-level object, or one nested in it: its properties are placed as a
|
|
39
|
+
* card's fields are (`placeFields`), so moving a field into a group leaves its row
|
|
40
|
+
* as it was. Unset,
|
|
41
|
+
* every short cell packs: an array's record, a variant's world and a matrix
|
|
42
|
+
* member's columns are one record's cells, not a card's fields. */
|
|
43
|
+
placed?: boolean;
|
|
38
44
|
/** Raw keydown off a text or prose cell, for a row whose own keys run through its
|
|
39
45
|
* cells: the table's Enter and Backspace ({@link ArrayField}). */
|
|
40
46
|
onCellKey?: (e: KeyboardEvent, key: string) => void;
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
-->
|
|
7
7
|
<script lang="ts">
|
|
8
8
|
import { untrack } from 'svelte';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createField,
|
|
11
|
+
type FieldController,
|
|
12
|
+
type SlashState,
|
|
13
|
+
type TextCap
|
|
14
|
+
} from '../core/codec/index.js';
|
|
10
15
|
import type { LeafRegistry } from './leaves.js';
|
|
11
16
|
import SlashMenu from './SlashMenu.svelte';
|
|
12
17
|
import { wording } from './strings.js';
|
|
@@ -24,8 +29,9 @@
|
|
|
24
29
|
plaintext?: boolean;
|
|
25
30
|
/**
|
|
26
31
|
* Draw no box: the card body's leaf, and nothing else. Not `block`, which is a
|
|
27
|
-
* different predicate: a `richtext` field
|
|
28
|
-
* still a control in a row of controls, so only the caller knows what
|
|
32
|
+
* different predicate: a `richtext` field declaring `multiline: true` is block
|
|
33
|
+
* prose and still a control in a row of controls, so only the caller knows what
|
|
34
|
+
* is paper.
|
|
29
35
|
*/
|
|
30
36
|
unframed?: boolean;
|
|
31
37
|
/** Accessible name for a leaf nothing else names: an array element, or the
|
|
@@ -45,6 +51,10 @@
|
|
|
45
51
|
placeholder?: string;
|
|
46
52
|
/** The placeholder goes at the leaf's first edit (`createField`). */
|
|
47
53
|
placeholderUntilEdit?: boolean;
|
|
54
|
+
/** An emptied leaf removes the field (`createField`). */
|
|
55
|
+
emptyUnsets?: boolean;
|
|
56
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
57
|
+
cap?: TextCap;
|
|
48
58
|
/** Registry identity, stamped on the DOM node so a remount is visible as one. */
|
|
49
59
|
leafKey: string;
|
|
50
60
|
onFocus?: (addr: Addr) => void;
|
|
@@ -73,6 +83,8 @@
|
|
|
73
83
|
fallback,
|
|
74
84
|
placeholder,
|
|
75
85
|
placeholderUntilEdit,
|
|
86
|
+
emptyUnsets,
|
|
87
|
+
cap,
|
|
76
88
|
leafKey,
|
|
77
89
|
onFocus,
|
|
78
90
|
onCaretMove,
|
|
@@ -122,6 +134,8 @@
|
|
|
122
134
|
fallback,
|
|
123
135
|
placeholder,
|
|
124
136
|
placeholderUntilEdit,
|
|
137
|
+
emptyUnsets,
|
|
138
|
+
cap,
|
|
125
139
|
tableStrings: () => t.strings,
|
|
126
140
|
slashStrings: () => t.strings,
|
|
127
141
|
onSlash: (next) => {
|
|
@@ -166,7 +180,7 @@
|
|
|
166
180
|
<div
|
|
167
181
|
{@attach mountField}
|
|
168
182
|
class={[
|
|
169
|
-
'qm-prose',
|
|
183
|
+
'qm-prose qm-wash',
|
|
170
184
|
{ 'qm-control-box': !unframed, 'qm-focus-ring-within': !unframed, 'qm-prose-block': block }
|
|
171
185
|
]}
|
|
172
186
|
data-leaf-key={leafKey}
|
|
@@ -178,14 +192,6 @@
|
|
|
178
192
|
<SlashMenu menu={slash} leaf={() => controller} label={t.strings.slashLabel} />
|
|
179
193
|
|
|
180
194
|
<style>
|
|
181
|
-
/* The box is `.qm-control-box` (controls.css), the same rule the input beside it
|
|
182
|
-
draws, so the two agree on height by construction. No `min-height` with it:
|
|
183
|
-
`core/codec/prose.css` resets the paragraph box, so one line of prose in this box
|
|
184
|
-
measures one line of text in that one. */
|
|
185
|
-
.qm-prose {
|
|
186
|
-
/* Containing block for the arrival wash's inset child (`core/bloom.ts`). */
|
|
187
|
-
position: relative;
|
|
188
|
-
}
|
|
189
195
|
/* A leaf the inline schema does not constrain grows, and opens at one line box: the
|
|
190
196
|
empty leaf is the height its first line will take, so the first keystroke displaces
|
|
191
197
|
nothing below it. `1em`, not a size rung, which is the rule below's to name.
|
|
@@ -196,8 +202,8 @@
|
|
|
196
202
|
.qm-prose-block :global(.ProseMirror) {
|
|
197
203
|
min-height: calc(1em * var(--_qm-leading-body));
|
|
198
204
|
}
|
|
199
|
-
/* Paper, and the withheld box is what says so. Not `block` alone: `
|
|
200
|
-
quill's to declare, so a schema
|
|
205
|
+
/* Paper, and the withheld box is what says so. Not `block` alone: `multiline` is the
|
|
206
|
+
quill's to declare, so a schema spelling it on a richtext field puts a block leaf in
|
|
201
207
|
a field row, where this rung would break the height agreement above. */
|
|
202
208
|
.qm-prose-block:not(.qm-control-box) {
|
|
203
209
|
font-size: var(--_qm-text-paper);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TextCap } from '../core/codec/index.js';
|
|
1
2
|
import type { LeafRegistry } from './leaves.js';
|
|
2
3
|
import type { Document, Quill, Addr, Content, Diagnostic } from '@quillmark/wasm';
|
|
3
4
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
@@ -11,8 +12,9 @@ interface Props {
|
|
|
11
12
|
plaintext?: boolean;
|
|
12
13
|
/**
|
|
13
14
|
* Draw no box: the card body's leaf, and nothing else. Not `block`, which is a
|
|
14
|
-
* different predicate: a `richtext` field
|
|
15
|
-
* still a control in a row of controls, so only the caller knows what
|
|
15
|
+
* different predicate: a `richtext` field declaring `multiline: true` is block
|
|
16
|
+
* prose and still a control in a row of controls, so only the caller knows what
|
|
17
|
+
* is paper.
|
|
16
18
|
*/
|
|
17
19
|
unframed?: boolean;
|
|
18
20
|
/** Accessible name for a leaf nothing else names: an array element, or the
|
|
@@ -32,6 +34,10 @@ interface Props {
|
|
|
32
34
|
placeholder?: string;
|
|
33
35
|
/** The placeholder goes at the leaf's first edit (`createField`). */
|
|
34
36
|
placeholderUntilEdit?: boolean;
|
|
37
|
+
/** An emptied leaf removes the field (`createField`). */
|
|
38
|
+
emptyUnsets?: boolean;
|
|
39
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
40
|
+
cap?: TextCap;
|
|
35
41
|
/** Registry identity, stamped on the DOM node so a remount is visible as one. */
|
|
36
42
|
leafKey: string;
|
|
37
43
|
onFocus?: (addr: Addr) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
A prose leaf the parent commits by value: an element of a `richtext` (fixture
|
|
3
|
-
`keywords`) or `
|
|
3
|
+
`keywords`) or `text` (`authors`, `errata`) array, and a content cell of a subform — an
|
|
4
4
|
`object`'s property, a variant's cell ({@link ObjectField}). None of them is
|
|
5
5
|
`applyChange`-addressable: `Addr.field` is a flat name, so `keywords[0]` and
|
|
6
6
|
`contact.note` have no op address. So this is not a `createField` leaf: it mounts a
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
The content is read, not passed: `reader.getContentAt(addr, path)` decodes through
|
|
21
21
|
the codec the leaf's own declared type names, so what it rests as — the content
|
|
22
|
-
object, a `
|
|
22
|
+
object, a `text` literal, the authored string a transport door left — stops
|
|
23
23
|
being the row's or the cell's business. It is read at mount and again wherever the
|
|
24
24
|
parent's re-derive hands down a `value` that moved, and a read differing from the
|
|
25
25
|
content the leaf last committed or took replaces the view, the gate `createField`
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
it, so the echo replaces nothing and the caret and the history stay.
|
|
29
29
|
|
|
30
30
|
The schema is the declared type's where the parent has room for it: a subform cell
|
|
31
|
-
declaring
|
|
32
|
-
block schema and a `
|
|
31
|
+
declaring `multiline: true` takes the full row ({@link ObjectField}), a `richtext` one
|
|
32
|
+
on the block schema and a `text` one on the plain schema, paragraphs and hard breaks.
|
|
33
33
|
An array element is one textblock whatever it declares, since Enter there is the
|
|
34
34
|
repeater's. There is no slash menu at either width, and no island view.
|
|
35
35
|
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
pmToUsv,
|
|
61
61
|
usvToPM,
|
|
62
62
|
valueEqual,
|
|
63
|
-
type LeafViews
|
|
63
|
+
type LeafViews,
|
|
64
|
+
type TextCap
|
|
64
65
|
} from '../core/codec/index.js';
|
|
65
66
|
import { valueLeafSeat } from './leaves.js';
|
|
66
67
|
import { wording } from './strings.js';
|
|
@@ -80,8 +81,8 @@
|
|
|
80
81
|
/** The default an unset leaf holds, where it prints: drawn at the default rung,
|
|
81
82
|
* and handed up whole with the first edit in it, as `createField` takes one. */
|
|
82
83
|
fallback?: Content;
|
|
83
|
-
/** The mark-free schema (a `
|
|
84
|
-
*
|
|
84
|
+
/** The mark-free schema (a `text` leaf): literal text, no formatting, exactly
|
|
85
|
+
* as the field of that type mounts. */
|
|
85
86
|
plaintext?: boolean;
|
|
86
87
|
/** A multi-block schema, for a cell the parent draws at full width: the block
|
|
87
88
|
* schema, or the plain one with `plaintext`. Absent, the leaf is one textblock. */
|
|
@@ -96,9 +97,11 @@
|
|
|
96
97
|
labelledBy?: string;
|
|
97
98
|
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
98
99
|
describedBy?: string;
|
|
99
|
-
/** The empty leaf's ghost, a subform cell's
|
|
100
|
-
*
|
|
100
|
+
/** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
|
|
101
|
+
* unset. Read per decoration pass. */
|
|
101
102
|
placeholder?: string;
|
|
103
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
104
|
+
cap?: TextCap;
|
|
102
105
|
onChange: (rt: Content) => void;
|
|
103
106
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
104
107
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
@@ -115,6 +118,7 @@
|
|
|
115
118
|
labelledBy,
|
|
116
119
|
describedBy,
|
|
117
120
|
placeholder,
|
|
121
|
+
cap,
|
|
118
122
|
onChange,
|
|
119
123
|
onKey
|
|
120
124
|
}: Props = $props();
|
|
@@ -130,9 +134,8 @@
|
|
|
130
134
|
/** Take the caret: what a parent placing focus on this leaf calls. The view's
|
|
131
135
|
* focus, not the element's: a PM view restores its selection, where a bare DOM
|
|
132
136
|
* focus on a contenteditable leaves the caret unplaced. And the reveal beside it,
|
|
133
|
-
* since PM's focus prevents the scroll a
|
|
134
|
-
*
|
|
135
|
-
* landing off screen. */
|
|
137
|
+
* since PM's focus prevents the scroll a form control takes by default — an array
|
|
138
|
+
* whose two item types revealed differently is one row of it landing off screen. */
|
|
136
139
|
export function focus(): void {
|
|
137
140
|
if (!view) return;
|
|
138
141
|
view.focus();
|
|
@@ -157,11 +160,18 @@
|
|
|
157
160
|
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
$effect(() => {
|
|
164
|
+
void [label, labelledBy, describedBy, placeholder];
|
|
165
|
+
view?.setProps({});
|
|
166
|
+
});
|
|
167
|
+
|
|
160
168
|
function mountView(mount: HTMLDivElement): () => void {
|
|
161
169
|
return untrack(() => {
|
|
162
170
|
const inline = !block;
|
|
163
171
|
const leaf = { inline, plaintext, islands: false };
|
|
164
|
-
|
|
172
|
+
// Read per attribute pass, not captured: an element's name carries its index,
|
|
173
|
+
// which a remove above it moves.
|
|
174
|
+
const names = () => ({ label, labelledBy, describedBy });
|
|
165
175
|
const shown = (stored: Content | undefined): Content => stored ?? fallback ?? emptyContent();
|
|
166
176
|
const stored = content();
|
|
167
177
|
let seen = value;
|
|
@@ -171,8 +181,8 @@
|
|
|
171
181
|
// Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
|
|
172
182
|
// (`proseAttributes`), so a cell carrying a label element and a row carrying
|
|
173
183
|
// none cannot name their regions by different rules.
|
|
174
|
-
const plainNamed = proseAttributes(names) ?? {};
|
|
175
|
-
const heldNamed = heldAttributes(names, heldId);
|
|
184
|
+
const plainNamed = () => proseAttributes(names()) ?? {};
|
|
185
|
+
const heldNamed = () => heldAttributes(names(), heldId);
|
|
176
186
|
const build = (rt: Content): EditorState => {
|
|
177
187
|
// The same keymap and plugin stack a `createField` leaf mounts (shared
|
|
178
188
|
// `proseLeafPlugins`), minus the anchor-position plugin: anchors are dropped on
|
|
@@ -185,7 +195,7 @@
|
|
|
185
195
|
: proseLeafPlugins(schema, {
|
|
186
196
|
inline,
|
|
187
197
|
placeholder: () => placeholder,
|
|
188
|
-
|
|
198
|
+
cap
|
|
189
199
|
})
|
|
190
200
|
});
|
|
191
201
|
};
|
|
@@ -195,11 +205,12 @@
|
|
|
195
205
|
state: build(reconciler.last),
|
|
196
206
|
editable: () => !held,
|
|
197
207
|
attributes: () => {
|
|
198
|
-
const named = held ? heldNamed : plainNamed;
|
|
208
|
+
const named = held ? heldNamed() : plainNamed();
|
|
199
209
|
return defaulted ? { ...named, 'data-default': '' } : named;
|
|
200
210
|
},
|
|
201
211
|
dispatchTransaction(tr) {
|
|
202
212
|
const next = mounted.state.apply(tr);
|
|
213
|
+
if (next === mounted.state) return;
|
|
203
214
|
const edit = tr.docChanged && !held;
|
|
204
215
|
if (edit) defaulted = false;
|
|
205
216
|
mounted.updateState(next);
|
|
@@ -265,8 +276,8 @@
|
|
|
265
276
|
</script>
|
|
266
277
|
|
|
267
278
|
<!-- `.qm-control-box` (controls.css) is the whole box, so an array of `richtext` and
|
|
268
|
-
an array of `
|
|
269
|
-
like the
|
|
279
|
+
an array of `text` render rows of equal height, and a subform's prose cell measures
|
|
280
|
+
like the form control beside it. No floor: the reset in `core/codec/prose.css` makes one
|
|
270
281
|
line of prose measure one line. Width is the row's or the cell's to give: the leaf
|
|
271
282
|
fills the track it is placed in. -->
|
|
272
283
|
<div class="qm-prose-value qm-control-box qm-focus-ring-within">
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TextCap } from '../core/codec/index.js';
|
|
1
2
|
import type { Content } from '@quillmark/wasm';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** This leaf's content (the parent's boundary read), `undefined` where nothing is
|
|
@@ -12,8 +13,8 @@ interface Props {
|
|
|
12
13
|
/** The default an unset leaf holds, where it prints: drawn at the default rung,
|
|
13
14
|
* and handed up whole with the first edit in it, as `createField` takes one. */
|
|
14
15
|
fallback?: Content;
|
|
15
|
-
/** The mark-free schema (a `
|
|
16
|
-
*
|
|
16
|
+
/** The mark-free schema (a `text` leaf): literal text, no formatting, exactly
|
|
17
|
+
* as the field of that type mounts. */
|
|
17
18
|
plaintext?: boolean;
|
|
18
19
|
/** A multi-block schema, for a cell the parent draws at full width: the block
|
|
19
20
|
* schema, or the plain one with `plaintext`. Absent, the leaf is one textblock. */
|
|
@@ -28,9 +29,11 @@ interface Props {
|
|
|
28
29
|
labelledBy?: string;
|
|
29
30
|
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
30
31
|
describedBy?: string;
|
|
31
|
-
/** The empty leaf's ghost, a subform cell's
|
|
32
|
-
*
|
|
32
|
+
/** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
|
|
33
|
+
* unset. Read per decoration pass. */
|
|
33
34
|
placeholder?: string;
|
|
35
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
36
|
+
cap?: TextCap;
|
|
34
37
|
onChange: (rt: Content) => void;
|
|
35
38
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
36
39
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
A
|
|
3
|
-
typed `writer.set`, so the
|
|
2
|
+
A text input: the control a field of a type this build does not know falls to.
|
|
3
|
+
Commits every edit live (on input) via the parent's typed `writer.set`, so the
|
|
4
|
+
preview tracks typing.
|
|
4
5
|
|
|
5
6
|
An unset field whose `default:` prints holds that default as its text, at the
|
|
6
7
|
default rung (theme.css): the value it prints, drawn as one and a step off a written
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
{#each tips as _, i (i)}
|
|
70
70
|
<button
|
|
71
71
|
type="button"
|
|
72
|
-
class="qm-tips-dot"
|
|
72
|
+
class="qm-tips-dot qm-focus-ring"
|
|
73
73
|
aria-label={t.strings.tipsDot(i + 1, tips.length)}
|
|
74
74
|
aria-current={i === index}
|
|
75
75
|
onclick={() => (cursor = i)}
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
{/each}
|
|
78
78
|
</div>
|
|
79
79
|
{/if}
|
|
80
|
-
<button type="button" class="qm-icon-btn qm-tips-action" onclick={advance}>
|
|
80
|
+
<button type="button" class="qm-icon-btn qm-tips-action qm-focus-ring" onclick={advance}>
|
|
81
81
|
{#if isLast}
|
|
82
82
|
{t.strings.tipsDismiss} <Icon name="trash-2" />
|
|
83
83
|
{:else}
|
|
@@ -193,6 +193,9 @@
|
|
|
193
193
|
.qm-tips-dot:hover {
|
|
194
194
|
opacity: var(--_qm-opacity-muted);
|
|
195
195
|
}
|
|
196
|
+
.qm-tips-dot:focus-visible {
|
|
197
|
+
opacity: 1;
|
|
198
|
+
}
|
|
196
199
|
.qm-tips-dot[aria-current='true'] {
|
|
197
200
|
opacity: 1;
|
|
198
201
|
}
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
describedBy?: string;
|
|
43
43
|
/** The boundary's nested content read for this field, which the cells take
|
|
44
44
|
* unprefixed: a variant's cell is one key in from the container it rests as, so
|
|
45
|
-
* a `
|
|
45
|
+
* a `text` cell's path is its own name ({@link ObjectField}). */
|
|
46
46
|
contentAt: (path: PathStep[]) => Content | undefined;
|
|
47
47
|
onCommit: (v: Record<string, unknown> | undefined) => void;
|
|
48
48
|
optionAllowed?: (value: string) => boolean;
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
|
16
16
|
describedBy?: string;
|
|
17
17
|
/** The boundary's nested content read for this field, which the cells take
|
|
18
18
|
* unprefixed: a variant's cell is one key in from the container it rests as, so
|
|
19
|
-
* a `
|
|
19
|
+
* a `text` cell's path is its own name ({@link ObjectField}). */
|
|
20
20
|
contentAt: (path: PathStep[]) => Content | undefined;
|
|
21
21
|
onCommit: (v: Record<string, unknown> | undefined) => void;
|
|
22
22
|
optionAllowed?: (value: string) => boolean;
|