@quillmark/svelte 0.16.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 +4 -2
- package/dist/core/codec/index.d.ts +0 -3
- package/dist/core/rebind.svelte.js +5 -8
- package/dist/core/theme.css +12 -1
- 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 +44 -41
- package/dist/visual/ArrayField.svelte.d.ts +0 -1
- package/dist/visual/BooleanField.svelte +16 -14
- package/dist/visual/BooleanField.svelte.d.ts +0 -1
- package/dist/visual/Card.svelte +65 -35
- 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 +17 -10
- package/dist/visual/Field.svelte.d.ts +3 -0
- 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 +210 -0
- package/dist/visual/MatrixAdd.svelte.d.ts +15 -0
- package/dist/visual/MatrixField.svelte +329 -84
- package/dist/visual/MatrixField.svelte.d.ts +8 -6
- package/dist/visual/NumberField.svelte +6 -10
- package/dist/visual/NumberField.svelte.d.ts +0 -1
- package/dist/visual/ObjectField.svelte +30 -17
- package/dist/visual/ObjectField.svelte.d.ts +3 -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 +21 -14
- package/dist/visual/VisualEditorInner.svelte.d.ts +0 -1
- package/dist/visual/checklists.d.ts +82 -0
- package/dist/visual/checklists.js +171 -0
- package/dist/visual/index.d.ts +0 -1
- package/dist/visual/strings.d.ts +15 -1
- package/dist/visual/strings.js +6 -0
- package/dist/visual/structure.d.ts +63 -31
- package/dist/visual/structure.js +122 -44
- 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/Card.svelte
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
// Ambient from the editor root, with the package's English off-tree, so this
|
|
10
10
|
// component renders standalone too.
|
|
11
11
|
const t = wording();
|
|
12
|
-
import { untrack } from 'svelte';
|
|
13
12
|
import Icon from './icons/Icon.svelte';
|
|
14
13
|
import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
|
|
15
14
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
@@ -19,6 +18,7 @@
|
|
|
19
18
|
import { holdInView } from './hold.js';
|
|
20
19
|
import type { FieldDomIds } from './domid.js';
|
|
21
20
|
import Field from './Field.svelte';
|
|
21
|
+
import FieldHint from './FieldHint.svelte';
|
|
22
22
|
import ProseField from './ProseField.svelte';
|
|
23
23
|
import CardControls from './CardControls.svelte';
|
|
24
24
|
import DiagnosticList from './DiagnosticList.svelte';
|
|
@@ -72,15 +72,7 @@
|
|
|
72
72
|
leaves
|
|
73
73
|
}: Props = $props();
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
// svelte-ignore state_referenced_locally
|
|
77
|
-
let localTitle = $state(card.titleOverride);
|
|
78
|
-
$effect(() => {
|
|
79
|
-
const incoming = card.titleOverride;
|
|
80
|
-
untrack(() => {
|
|
81
|
-
if (incoming !== localTitle) localTitle = incoming;
|
|
82
|
-
});
|
|
83
|
-
});
|
|
75
|
+
let localTitle = $derived(card.titleOverride);
|
|
84
76
|
|
|
85
77
|
// The pre-edit value Escape rolls back to; the rename itself stays live on input.
|
|
86
78
|
let titleAtFocus = '';
|
|
@@ -208,10 +200,8 @@
|
|
|
208
200
|
ancestor and nothing else marks the box its guidance belongs in. -->
|
|
209
201
|
<section
|
|
210
202
|
bind:this={el}
|
|
211
|
-
class=
|
|
203
|
+
class={['qm-card', { 'qm-main': card.isMain, 'qm-unschemable': card.unschemable }]}
|
|
212
204
|
data-qm-card
|
|
213
|
-
class:qm-main={card.isMain}
|
|
214
|
-
class:qm-unschemable={card.unschemable}
|
|
215
205
|
>
|
|
216
206
|
{#if card.unschemable}
|
|
217
207
|
{@render recoveryShell()}
|
|
@@ -252,9 +242,7 @@
|
|
|
252
242
|
<div class="qm-card-body">
|
|
253
243
|
{#if hasMeta}
|
|
254
244
|
<div
|
|
255
|
-
class=
|
|
256
|
-
class:qm-meta-top={!card.isMain}
|
|
257
|
-
class:qm-meta-bottom={card.hasBody}
|
|
245
|
+
class={['qm-card-meta', { 'qm-meta-top': !card.isMain, 'qm-meta-bottom': card.hasBody }]}
|
|
258
246
|
>
|
|
259
247
|
{#each ungrouped as section (section.group ?? '_ungrouped')}
|
|
260
248
|
<div class="qm-section">
|
|
@@ -265,26 +253,42 @@
|
|
|
265
253
|
<!-- One open at a time. Rendered only when a card has groups, so an empty
|
|
266
254
|
accordion does not stand a gap under the ungrouped fields. -->
|
|
267
255
|
{#if grouped.length}
|
|
268
|
-
<div class=
|
|
256
|
+
<div class={['qm-groups', { 'qm-instant': !animate }]}>
|
|
269
257
|
{#each grouped as section (section.group)}
|
|
270
258
|
{@const group = section.group as string}
|
|
271
259
|
{@const isOpen = expanded === group}
|
|
272
260
|
{@const panelId = ops.panelId(group)}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
261
|
+
{@const headIds = section.head ? ops.domIds(section.head.name) : undefined}
|
|
262
|
+
{@const headHint = section.head?.description}
|
|
263
|
+
<div class={['qm-group', { 'qm-open': isOpen }]}>
|
|
264
|
+
<!-- A flat field alone in its section is named by the header, which
|
|
265
|
+
takes its label id and parks its description: the field draws no
|
|
266
|
+
label of its own to say the section's name twice. -->
|
|
267
|
+
<div class="qm-group-head">
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
class="qm-group-header"
|
|
271
|
+
id={headIds?.label}
|
|
272
|
+
bind:this={headers[group]}
|
|
273
|
+
aria-expanded={isOpen}
|
|
274
|
+
aria-controls={panelId}
|
|
275
|
+
onclick={() => toggleGroup(group)}
|
|
276
|
+
>
|
|
277
|
+
<Icon
|
|
278
|
+
name="chevron-right"
|
|
279
|
+
class="qm-group-chevron"
|
|
280
|
+
size={CHEVRON}
|
|
281
|
+
/>{section.label}
|
|
282
|
+
</button>
|
|
283
|
+
{#if headIds && headHint}
|
|
284
|
+
<FieldHint
|
|
285
|
+
description={headHint}
|
|
286
|
+
label={section.label}
|
|
287
|
+
describedBy={headIds.description}
|
|
288
|
+
/>
|
|
289
|
+
<span class="qm-visually-hidden" id={headIds.description}>{headHint}</span>
|
|
290
|
+
{/if}
|
|
291
|
+
</div>
|
|
288
292
|
<!-- The panel is clipped, not unmounted, so without `inert` every field
|
|
289
293
|
in a hidden section keeps its place in the tab order and in the a11y
|
|
290
294
|
tree, under a header announcing `aria-expanded="false"`. -->
|
|
@@ -295,7 +299,7 @@
|
|
|
295
299
|
bind:this={panels[group]}
|
|
296
300
|
>
|
|
297
301
|
<div class="qm-group-panel-inner">
|
|
298
|
-
{@render sectionFields(section.fields)}
|
|
302
|
+
{@render sectionFields(section.fields, section.head)}
|
|
299
303
|
</div>
|
|
300
304
|
</div>
|
|
301
305
|
</div>
|
|
@@ -332,7 +336,7 @@
|
|
|
332
336
|
{/if}
|
|
333
337
|
</section>
|
|
334
338
|
|
|
335
|
-
{#snippet sectionFields(fields: FieldModel[])}
|
|
339
|
+
{#snippet sectionFields(fields: FieldModel[], head?: FieldModel)}
|
|
336
340
|
<div class="qm-fields qm-tracks">
|
|
337
341
|
{#each placeFields(fields) as { field: f, span } (mountKey(f.name, f.schema))}
|
|
338
342
|
<Field
|
|
@@ -354,6 +358,7 @@
|
|
|
354
358
|
{onError}
|
|
355
359
|
{leaves}
|
|
356
360
|
diagnostics={ops.diagFor(f.name)}
|
|
361
|
+
headed={f === head}
|
|
357
362
|
/>
|
|
358
363
|
{/each}
|
|
359
364
|
</div>
|
|
@@ -591,7 +596,8 @@
|
|
|
591
596
|
display: grid;
|
|
592
597
|
grid-template-columns: repeat(var(--cols), 1fr);
|
|
593
598
|
column-gap: var(--_qm-space-2);
|
|
594
|
-
row-gap: var(--_qm-space-3);
|
|
599
|
+
--row-gap: var(--_qm-space-3);
|
|
600
|
+
row-gap: var(--row-gap);
|
|
595
601
|
}
|
|
596
602
|
/* The nesting vertical, one `--_qm-nest` in from the card's edge with the panel's
|
|
597
603
|
fields the same rung off it (ARCHITECTURE §"A plane is a tone"). `--_qm-border` and
|
|
@@ -680,6 +686,30 @@
|
|
|
680
686
|
text-align: left;
|
|
681
687
|
transition: color var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
682
688
|
}
|
|
689
|
+
/* The header row: the press target, and beside it the guidance marker a headed
|
|
690
|
+
section carries (`FieldHint`), which is its own control and so not inside the
|
|
691
|
+
button. Positioned for the reason the header is, the row being the box that
|
|
692
|
+
stands in the band. */
|
|
693
|
+
.qm-group-head {
|
|
694
|
+
position: relative;
|
|
695
|
+
display: flex;
|
|
696
|
+
align-items: center;
|
|
697
|
+
gap: var(--_qm-space);
|
|
698
|
+
}
|
|
699
|
+
.qm-group-head > .qm-group-header {
|
|
700
|
+
flex: 1;
|
|
701
|
+
min-width: 0;
|
|
702
|
+
}
|
|
703
|
+
/* The parked description: `FieldLabel`'s recipe, for the one description a
|
|
704
|
+
header rather than a label parks. */
|
|
705
|
+
.qm-visually-hidden {
|
|
706
|
+
position: absolute;
|
|
707
|
+
width: 1px;
|
|
708
|
+
height: 1px;
|
|
709
|
+
overflow: hidden;
|
|
710
|
+
clip-path: inset(50%);
|
|
711
|
+
white-space: nowrap;
|
|
712
|
+
}
|
|
683
713
|
.qm-group-header :global(.qm-group-chevron) {
|
|
684
714
|
flex-shrink: 0;
|
|
685
715
|
display: block;
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
values are data, not input: `parseDate` throws on anything malformed, so a bad
|
|
26
26
|
string degrades to an empty field rather than taking the editor down with it.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
parsed value: a fresh `CalendarDate` is never `===` the last one,
|
|
30
|
-
|
|
28
|
+
The local is a `$derived`, which compares by identity, so it holds the string, not
|
|
29
|
+
the parsed value: a fresh `CalendarDate` is never `===` the last one, so the
|
|
30
|
+
re-derivation each commit causes would re-render all seven segments.
|
|
31
31
|
|
|
32
32
|
The ghost is the default's digits, not a format hint. An unset field
|
|
33
33
|
carrying a `default:` prints the default's digits in the segments at the default
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
import { getLocalTimeZone, parseDate, today, type DateValue } from '@internationalized/date';
|
|
52
52
|
import Icon from './icons/Icon.svelte';
|
|
53
53
|
import { wording } from './strings.js';
|
|
54
|
-
import { syncedLocal } from './synced.svelte.js';
|
|
55
54
|
import './controls.css';
|
|
56
55
|
|
|
57
56
|
const t = wording();
|
|
@@ -109,31 +108,29 @@
|
|
|
109
108
|
|
|
110
109
|
const fallbackDate = $derived(toDateValue(fallback?.slice(0, 10) ?? ''));
|
|
111
110
|
/** The default is the primitive's value: from entering an unset field until an
|
|
112
|
-
* edit, a cleared segment or leaving. Part of the projection, so a
|
|
111
|
+
* edit, a cleared segment or leaving. Part of the projection, so a re-derivation
|
|
113
112
|
* neither drops a seat nor restores one over the segments a clear left. */
|
|
114
113
|
let seat = $state(false);
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const local = syncedLocal(
|
|
120
|
-
() => value?.slice(0, 10) ?? (seat && fallbackDate ? fallbackDate.toString() : '')
|
|
114
|
+
// Driven controlled (`value` + `onValueChange`, never `bind:`) so reconciliation
|
|
115
|
+
// stays the package's.
|
|
116
|
+
let local = $derived(
|
|
117
|
+
value?.slice(0, 10) ?? (seat && fallbackDate ? fallbackDate.toString() : '')
|
|
121
118
|
);
|
|
122
|
-
const parsed = $derived(toDateValue(local
|
|
119
|
+
const parsed = $derived(toDateValue(local));
|
|
123
120
|
// The date the empty segments ghost, or undefined when there is nothing to ghost:
|
|
124
121
|
// the field is unset and the default has a date form. A non-blank local that
|
|
125
122
|
// fails to parse is AUTHORED-but-malformed, which the empty field states
|
|
126
123
|
// honestly: the ghost would claim it unset. Held as the parsed value rather than
|
|
127
124
|
// a boolean so the substitution below narrows on the one fact it needs.
|
|
128
|
-
const ghost = $derived(local
|
|
129
|
-
const seated = $derived(value == null && seat && local
|
|
125
|
+
const ghost = $derived(local === '' ? fallbackDate : undefined);
|
|
126
|
+
const seated = $derived(value == null && seat && local !== '');
|
|
130
127
|
/** Who put `today` here, while the field follows the render date. */
|
|
131
128
|
const follows = $derived(
|
|
132
|
-
local
|
|
129
|
+
local === TODAY ? 'authored' : value == null && fallback === TODAY ? 'default' : undefined
|
|
133
130
|
);
|
|
134
131
|
|
|
135
132
|
function write(next: string): void {
|
|
136
|
-
local
|
|
133
|
+
local = next;
|
|
137
134
|
seat = false;
|
|
138
135
|
onCommit(next);
|
|
139
136
|
}
|
|
@@ -207,7 +204,7 @@
|
|
|
207
204
|
// seated default or `today` would write it.
|
|
208
205
|
const next = d?.toString() ?? '';
|
|
209
206
|
if (next === (parsed?.toString() ?? '')) return;
|
|
210
|
-
local
|
|
207
|
+
local = next;
|
|
211
208
|
if (!d) seat = false;
|
|
212
209
|
onCommit(d?.toString());
|
|
213
210
|
}}
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
<script lang="ts">
|
|
16
16
|
import { Select } from 'bits-ui';
|
|
17
17
|
import Icon from './icons/Icon.svelte';
|
|
18
|
-
import { syncedLocal } from './synced.svelte.js';
|
|
19
18
|
import { wording } from './strings.js';
|
|
20
19
|
import './controls.css';
|
|
21
20
|
|
|
@@ -67,23 +66,21 @@
|
|
|
67
66
|
// the like), so it never collides with a real option.
|
|
68
67
|
const UNSET = '__qm_unset__';
|
|
69
68
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
// the package's rather than the primitive's.
|
|
74
|
-
const local = syncedLocal(() => value ?? UNSET);
|
|
69
|
+
// Driven controlled (`value` + `onValueChange`, never `bind:`) so reconciliation
|
|
70
|
+
// stays the package's rather than the primitive's.
|
|
71
|
+
let local = $derived(value ?? UNSET);
|
|
75
72
|
|
|
76
73
|
/** The blank has no glyph of its own: the quill's `ui.blank_title` stands in for
|
|
77
74
|
* it, else an em dash. */
|
|
78
75
|
const dash = (v: string | undefined) => v || blankTitle || '—';
|
|
79
76
|
|
|
80
|
-
const unset = $derived(local
|
|
77
|
+
const unset = $derived(local === UNSET);
|
|
81
78
|
/** The ghost is a member the render prints, rather than a word for the blank or for
|
|
82
79
|
* `none`: it draws at the default rung (theme.css). */
|
|
83
80
|
const ghostsMember = $derived(!optional && !!fallback);
|
|
84
81
|
const ghostText = $derived(optional ? t.strings.optionalGhost : dash(fallback));
|
|
85
82
|
/** What the closed trigger shows: the pick, or the ghosted default while unset. */
|
|
86
|
-
const shown = $derived(unset ? ghostText : dash(local
|
|
83
|
+
const shown = $derived(unset ? ghostText : dash(local));
|
|
87
84
|
|
|
88
85
|
/** The root to portal into: `document.body` would escape the consumer's dials
|
|
89
86
|
* along with the editor's subtree. `undefined` falls back to bits-ui's default. */
|
|
@@ -99,10 +96,10 @@
|
|
|
99
96
|
<Select.Root
|
|
100
97
|
type="single"
|
|
101
98
|
allowDeselect={false}
|
|
102
|
-
value={local
|
|
99
|
+
value={local}
|
|
103
100
|
onValueChange={(v) => {
|
|
104
101
|
if (v == null) return;
|
|
105
|
-
local
|
|
102
|
+
local = v;
|
|
106
103
|
// Sentinel → unset (parent `removeField`, default renders); any real pick
|
|
107
104
|
// (incl. the default value) → a genuine write.
|
|
108
105
|
onCommit(v === UNSET ? undefined : v);
|
|
@@ -152,12 +149,12 @@
|
|
|
152
149
|
policy when it is the one selected: a listbox whose selected value
|
|
153
150
|
has no row shows nothing for what the document says, and offers none
|
|
154
151
|
for the primitive to mark, type-ahead to, or key onto
|
|
155
|
-
(VISUAL_EDITOR §"Enum policy"). Off `local
|
|
152
|
+
(VISUAL_EDITOR §"Enum policy"). Off `local`, not `value`: the
|
|
156
153
|
row that exists is the row the control has selected, through the
|
|
157
154
|
window where an own pick has not reconciled back. -->
|
|
158
155
|
{#each values as v (v)}
|
|
159
156
|
{@const allowed = optionAllowed?.(v) !== false}
|
|
160
|
-
{#if allowed || enumDisallowed === 'disable' || v === local
|
|
157
|
+
{#if allowed || enumDisallowed === 'disable' || v === local}
|
|
161
158
|
<Select.Item
|
|
162
159
|
class="qm-menu-item qm-select-item"
|
|
163
160
|
value={v}
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
VARIANT_DISCRIMINANT,
|
|
34
34
|
arrayLayout,
|
|
35
35
|
baseType,
|
|
36
|
+
flatObject,
|
|
36
37
|
ghostDefault,
|
|
37
38
|
isContainer,
|
|
38
39
|
optionalCell,
|
|
39
40
|
printedText,
|
|
41
|
+
renderedBoolean,
|
|
40
42
|
stringifyGhost,
|
|
41
43
|
titleText
|
|
42
44
|
} from './structure.js';
|
|
@@ -88,6 +90,9 @@
|
|
|
88
90
|
* landing handle here, a prose leaf its controller from inside `ProseField`. */
|
|
89
91
|
leaves?: LeafRegistry;
|
|
90
92
|
diagnostics?: Diagnostic[];
|
|
93
|
+
/** The section's header names this field and parks its description, under this
|
|
94
|
+
* field's own label and description ids ({@link GroupSection.head}). */
|
|
95
|
+
headed?: boolean;
|
|
91
96
|
}
|
|
92
97
|
let {
|
|
93
98
|
field,
|
|
@@ -107,7 +112,8 @@
|
|
|
107
112
|
onChange,
|
|
108
113
|
onError,
|
|
109
114
|
leaves,
|
|
110
|
-
diagnostics
|
|
115
|
+
diagnostics,
|
|
116
|
+
headed = false
|
|
111
117
|
}: Props = $props();
|
|
112
118
|
|
|
113
119
|
const t = wording();
|
|
@@ -157,6 +163,7 @@
|
|
|
157
163
|
const describedBy = $derived(field.description ? domIds.description : undefined);
|
|
158
164
|
// Two controls own their label row, the add chip and the held count sharing it.
|
|
159
165
|
const ownsLabel = $derived(field.control === 'array' || field.control === 'matrix');
|
|
166
|
+
const flat = $derived(flatObject(field.schema));
|
|
160
167
|
|
|
161
168
|
// A container walks its diagnostics down to the cell each names and draws the rest
|
|
162
169
|
// at its own foot (`diagnostics.ts`); every other control draws them here, under
|
|
@@ -229,12 +236,10 @@
|
|
|
229
236
|
* an address to name. `el` is the field-wide box, which a landing at depth does not
|
|
230
237
|
* wash: that lane hands back its own row (`leaves.ts`).
|
|
231
238
|
*/
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (field.control === 'prose' || !controlEl || !leaves) return;
|
|
239
|
+
function registerControl(wrapper: HTMLElement): (() => void) | undefined {
|
|
240
|
+
if (field.control === 'prose' || !leaves) return;
|
|
235
241
|
const key = leafKey;
|
|
236
242
|
const registry = leaves;
|
|
237
|
-
const wrapper = controlEl;
|
|
238
243
|
registry.registerControl(key, {
|
|
239
244
|
focus: focusControl,
|
|
240
245
|
focusPath: isContainer(field.control) ? focusPath : undefined,
|
|
@@ -243,7 +248,7 @@
|
|
|
243
248
|
}
|
|
244
249
|
});
|
|
245
250
|
return () => registry.unregisterControl(key);
|
|
246
|
-
}
|
|
251
|
+
}
|
|
247
252
|
|
|
248
253
|
/**
|
|
249
254
|
* A form control has no controller to report its focus through, so the wrapper
|
|
@@ -256,8 +261,8 @@
|
|
|
256
261
|
const reportFocus = $derived(field.control === 'prose' ? undefined : () => onFocus?.(addr));
|
|
257
262
|
</script>
|
|
258
263
|
|
|
259
|
-
<div class=
|
|
260
|
-
{#if !ownsLabel}
|
|
264
|
+
<div class={['qm-field', { cell: span === 'cell' }]}>
|
|
265
|
+
{#if !ownsLabel && !headed}
|
|
261
266
|
<FieldLabel
|
|
262
267
|
label={field.label}
|
|
263
268
|
controlId={labelable ? domIds.control : undefined}
|
|
@@ -273,7 +278,7 @@
|
|
|
273
278
|
rule below holds why). One markup for both spans; a field owning its row nests a
|
|
274
279
|
level and measures the same. -->
|
|
275
280
|
<div class="qm-field-stack">
|
|
276
|
-
<div class="qm-field-control"
|
|
281
|
+
<div class="qm-field-control" {@attach registerControl} onfocusin={reportFocus}>
|
|
277
282
|
{#if field.control === 'prose'}
|
|
278
283
|
<ProseField
|
|
279
284
|
{quill}
|
|
@@ -336,7 +341,7 @@
|
|
|
336
341
|
/>
|
|
337
342
|
{:else if field.control === 'boolean'}
|
|
338
343
|
<BooleanField
|
|
339
|
-
value={value
|
|
344
|
+
value={renderedBoolean(value)}
|
|
340
345
|
fallback={ghost as boolean | undefined}
|
|
341
346
|
{optional}
|
|
342
347
|
id={domIds.control}
|
|
@@ -376,6 +381,7 @@
|
|
|
376
381
|
bind:this={objectEl}
|
|
377
382
|
value={value as Record<string, unknown> | undefined}
|
|
378
383
|
properties={field.schema.properties}
|
|
384
|
+
{flat}
|
|
379
385
|
label={field.label}
|
|
380
386
|
idBase={domIds.control}
|
|
381
387
|
labelledBy={domIds.label}
|
|
@@ -394,6 +400,7 @@
|
|
|
394
400
|
labelId={domIds.label}
|
|
395
401
|
descriptionId={domIds.description}
|
|
396
402
|
idBase={domIds.control}
|
|
403
|
+
listKey={leafKey}
|
|
397
404
|
{contentAt}
|
|
398
405
|
onCommit={onCommitScalar}
|
|
399
406
|
diagnostics={deep}
|
|
@@ -35,6 +35,9 @@ interface Props {
|
|
|
35
35
|
* landing handle here, a prose leaf its controller from inside `ProseField`. */
|
|
36
36
|
leaves?: LeafRegistry;
|
|
37
37
|
diagnostics?: Diagnostic[];
|
|
38
|
+
/** The section's header names this field and parks its description, under this
|
|
39
|
+
* field's own label and description ids ({@link GroupSection.head}). */
|
|
40
|
+
headed?: boolean;
|
|
38
41
|
}
|
|
39
42
|
declare const Field: import("svelte").Component<Props, {}, "">;
|
|
40
43
|
type Field = ReturnType<typeof Field>;
|
|
@@ -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}
|