@quillmark/svelte 0.10.1 → 0.12.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/THEMING.md +2 -2
- package/dist/core/address.d.ts +17 -10
- package/dist/core/address.js +26 -12
- package/dist/core/bloom.js +4 -3
- package/dist/core/codec/blocks.js +21 -1
- package/dist/core/codec/breaks.d.ts +10 -2
- package/dist/core/codec/breaks.js +65 -9
- package/dist/core/codec/code.d.ts +6 -4
- package/dist/core/codec/code.js +7 -4
- package/dist/core/codec/decode.js +103 -68
- package/dist/core/codec/encode.d.ts +5 -8
- package/dist/core/codec/encode.js +56 -86
- package/dist/core/codec/field.js +43 -24
- package/dist/core/codec/index.d.ts +2 -1
- package/dist/core/codec/index.js +6 -3
- package/dist/core/codec/islands.d.ts +16 -11
- package/dist/core/codec/islands.js +70 -59
- package/dist/core/codec/keymap.js +2 -1
- package/dist/core/codec/marks.d.ts +17 -13
- package/dist/core/codec/marks.js +39 -42
- package/dist/core/codec/positions.d.ts +4 -1
- package/dist/core/codec/positions.js +5 -1
- package/dist/core/codec/reconcile.d.ts +6 -0
- package/dist/core/codec/reconcile.js +19 -0
- package/dist/core/codec/schema.d.ts +5 -8
- package/dist/core/codec/schema.js +30 -156
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.js +77 -26
- package/dist/core/codec/table.d.ts +6 -5
- package/dist/core/codec/table.js +14 -31
- package/dist/core/codec/urls.d.ts +20 -0
- package/dist/core/codec/urls.js +45 -0
- package/dist/core/rungs.d.ts +8 -0
- package/dist/core/rungs.js +24 -0
- package/dist/core/theme.css +10 -0
- package/dist/preset/recipes.css +1 -1
- package/dist/preset/scale.css +7 -4
- package/dist/preview/bridge.d.ts +8 -1
- package/dist/preview/bridge.js +31 -25
- package/dist/preview/controller.js +13 -13
- package/dist/preview/geometry.d.ts +12 -1
- package/dist/preview/geometry.js +14 -1
- package/dist/preview/paint.js +6 -4
- package/dist/preview/strings.d.ts +0 -2
- package/dist/preview/strings.js +5 -6
- package/dist/visual/ArrayField.svelte +600 -173
- package/dist/visual/ArrayField.svelte.d.ts +14 -1
- package/dist/visual/BooleanField.svelte +10 -7
- package/dist/visual/Card.svelte +8 -30
- package/dist/visual/DateField.svelte +2 -2
- package/dist/visual/Field.svelte +76 -32
- package/dist/visual/FieldHint.svelte +1 -1
- package/dist/visual/MatrixField.svelte +352 -0
- package/dist/visual/MatrixField.svelte.d.ts +36 -0
- package/dist/visual/ObjectField.svelte +356 -133
- package/dist/visual/ObjectField.svelte.d.ts +17 -2
- package/dist/visual/VariantField.svelte +55 -2
- package/dist/visual/VariantField.svelte.d.ts +10 -1
- package/dist/visual/VisualEditor.svelte +1 -1
- package/dist/visual/VisualEditorInner.svelte +118 -67
- package/dist/visual/controls.css +39 -9
- package/dist/visual/diagnostics.d.ts +26 -2
- package/dist/visual/diagnostics.js +52 -5
- package/dist/visual/ext.d.ts +12 -13
- package/dist/visual/ext.js +22 -19
- package/dist/visual/hold.js +2 -2
- package/dist/visual/icons/nodes.d.ts +3 -0
- package/dist/visual/icons/nodes.js +2 -1
- package/dist/visual/index.js +2 -3
- package/dist/visual/leaves.d.ts +25 -12
- package/dist/visual/links.d.ts +4 -0
- package/dist/visual/links.js +15 -4
- package/dist/visual/motion.d.ts +14 -5
- package/dist/visual/motion.js +13 -11
- package/dist/visual/strings.d.ts +26 -13
- package/dist/visual/strings.js +7 -2
- package/dist/visual/structure.d.ts +101 -8
- package/dist/visual/structure.js +252 -13
- package/package.json +2 -2
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
An `object` field → a nested subform over `properties` (declaration order),
|
|
3
|
-
committing the whole object by value on any nested change.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
committing the whole object by value on any nested change. A scalar property draws
|
|
4
|
+
its own control, a content property mounts the by-value prose leaf ({@link
|
|
5
|
+
ProseValue}) over the boundary's nested read, and a container property — an `array`,
|
|
6
|
+
an `object`, a `matrix`, a variant — mounts its own control here, at the next rung of
|
|
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.
|
|
7
10
|
|
|
8
11
|
The nesting is a vertical at `--_qm-border`, the ladder's last stroke: the card's edge,
|
|
9
12
|
an open section's vertical one `--_qm-nest` in where the field is in one, this one a rung
|
|
10
13
|
further, the properties a rung inside it (ARCHITECTURE §"A plane is a tone").
|
|
11
14
|
|
|
12
|
-
One figure wherever it mounts
|
|
13
|
-
|
|
14
|
-
at
|
|
15
|
+
One figure wherever it mounts, and where its stroke lands is the stacker's. A
|
|
16
|
+
variant's cells and a matrix member's columns sit at the depth a field-level subform
|
|
17
|
+
sits at: each stands beside the cell it is stored with — the discriminant, the tick —
|
|
18
|
+
rather than inside it, so the box says which world or which member and not how deep.
|
|
19
|
+
An array element's properties hang a rung further in ({@link ArrayField}), the summary
|
|
20
|
+
above them standing for the whole row.
|
|
15
21
|
|
|
16
22
|
A property's ghosted `default:` is the static schema `sub.default`, not the
|
|
17
23
|
resolved provenance the top-level ghosts read (FIELD_PROVENANCE): `resolve`
|
|
@@ -20,13 +26,18 @@
|
|
|
20
26
|
|
|
21
27
|
Every caller hands down `contentAt`, which is `reader.getContentAt` with the field
|
|
22
28
|
already bound: a field-level subform passes the property key through untouched, a
|
|
23
|
-
variant passes its cell's,
|
|
24
|
-
reads at whatever depth it sits, which nothing here knows.
|
|
29
|
+
variant passes its cell's, an array's open row prefixes its index and a matrix its
|
|
30
|
+
member's id. So a cell reads at whatever depth it sits, which nothing here knows.
|
|
31
|
+
|
|
32
|
+
`bare` is the table's row: the same cells with no labels and no figure of their own,
|
|
33
|
+
laid onto the tracks the table's header names ({@link ArrayField}).
|
|
25
34
|
-->
|
|
26
35
|
<script lang="ts">
|
|
27
36
|
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
28
37
|
import { emptyContent } from '../core/codec/index.js';
|
|
29
|
-
import { controlKind, humanize, obliged } from './structure.js';
|
|
38
|
+
import { arrayLayout, controlKind, humanize, isContainer, obliged } from './structure.js';
|
|
39
|
+
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
40
|
+
import type { LandingBox } from './leaves.js';
|
|
30
41
|
import { wording } from './strings.js';
|
|
31
42
|
import { propertyDomIds } from './domid.js';
|
|
32
43
|
import FieldLabel from './FieldLabel.svelte';
|
|
@@ -36,6 +47,12 @@
|
|
|
36
47
|
import BooleanField from './BooleanField.svelte';
|
|
37
48
|
import DateField from './DateField.svelte';
|
|
38
49
|
import ProseValue from './ProseValue.svelte';
|
|
50
|
+
import ArrayField from './ArrayField.svelte';
|
|
51
|
+
import MatrixField from './MatrixField.svelte';
|
|
52
|
+
import VariantField from './VariantField.svelte';
|
|
53
|
+
import DiagnosticList from './DiagnosticList.svelte';
|
|
54
|
+
import ObjectField from './ObjectField.svelte';
|
|
55
|
+
import './controls.css';
|
|
39
56
|
|
|
40
57
|
interface Props {
|
|
41
58
|
value: Record<string, unknown> | undefined;
|
|
@@ -49,8 +66,8 @@
|
|
|
49
66
|
/**
|
|
50
67
|
* The parent control's DOM id: the base each property's own three names derive
|
|
51
68
|
* from ({@link propertyDomIds}). Absent — a subform mounted with no field around
|
|
52
|
-
* it — the properties fall back to `aria-label`, which names
|
|
53
|
-
* `for` target to click.
|
|
69
|
+
* it, or a table's row — the properties fall back to `aria-label`, which names
|
|
70
|
+
* them without a `for` target to click.
|
|
54
71
|
*/
|
|
55
72
|
idBase?: string;
|
|
56
73
|
/** Accessible-name prefix used only on the `aria-label` fallback above. */
|
|
@@ -62,13 +79,37 @@
|
|
|
62
79
|
* `undefined` for a property the stored value does not reach. */
|
|
63
80
|
contentAt: (path: PathStep[]) => Content | undefined;
|
|
64
81
|
onCommit: (obj: Record<string, unknown>) => void;
|
|
82
|
+
/** A table's row: the cells alone, on the tracks the table's header names, with
|
|
83
|
+
* no labels and no vertical of their own. */
|
|
84
|
+
bare?: boolean;
|
|
85
|
+
/** Raw keydown off a text or prose cell, for a row whose own keys run through its
|
|
86
|
+
* cells: the table's Enter and Backspace ({@link ArrayField}). */
|
|
87
|
+
onCellKey?: (e: KeyboardEvent, key: string) => void;
|
|
88
|
+
/** The diagnostics routed into this subform, each with its steps still to walk:
|
|
89
|
+
* one naming a property draws under that property's control, and one naming
|
|
90
|
+
* nothing this subform holds draws at its foot (`diagnostics.ts`). */
|
|
91
|
+
diagnostics?: readonly DeepDiagnostic[];
|
|
65
92
|
}
|
|
66
|
-
let {
|
|
67
|
-
|
|
93
|
+
let {
|
|
94
|
+
value,
|
|
95
|
+
properties,
|
|
96
|
+
labelledBy,
|
|
97
|
+
describedBy,
|
|
98
|
+
idBase,
|
|
99
|
+
label,
|
|
100
|
+
contentAt,
|
|
101
|
+
onCommit,
|
|
102
|
+
bare = false,
|
|
103
|
+
onCellKey,
|
|
104
|
+
diagnostics
|
|
105
|
+
}: Props = $props();
|
|
68
106
|
|
|
69
107
|
const t = wording();
|
|
70
108
|
const entries = $derived(Object.entries(properties ?? {}));
|
|
71
109
|
const obj = $derived((value ?? {}) as Record<string, unknown>);
|
|
110
|
+
const has = (key: string): boolean => Object.hasOwn(properties ?? {}, key);
|
|
111
|
+
const routed = $derived(splitDeep(diagnostics));
|
|
112
|
+
const foot = $derived(unrouted(routed, (step) => typeof step === 'string' && has(step)));
|
|
72
113
|
|
|
73
114
|
/** Obligation, read off the cell exactly as `fieldModels` reads it off a field
|
|
74
115
|
* ({@link obliged}). `validate` anchors it per leaf, so a property with no
|
|
@@ -83,46 +124,90 @@
|
|
|
83
124
|
`${label != null ? `${label} ` : ''}${title(key, sub)}` +
|
|
84
125
|
(required(sub) ? ` ${t.strings.fieldRequired}` : '');
|
|
85
126
|
|
|
86
|
-
/** The
|
|
87
|
-
* drawing its properties by declaration and never by
|
|
88
|
-
*
|
|
89
|
-
* focusing the node is not focusing the view ({@link ProseValue.focus})
|
|
90
|
-
*
|
|
91
|
-
* restating document
|
|
127
|
+
/** The handles of the cells the DOM cannot answer for, keyed by property name —
|
|
128
|
+
* which is stable, a subform drawing its properties by declaration and never by
|
|
129
|
+
* position. A prose cell, because a `contenteditable` matches no focusable
|
|
130
|
+
* selector and focusing the node is not focusing the view ({@link ProseValue.focus});
|
|
131
|
+
* a container cell, because its focus and its landing are its own. A scalar control
|
|
132
|
+
* is resolved off the markup, so a handle each would be five refs restating document
|
|
133
|
+
* order.
|
|
92
134
|
*
|
|
93
135
|
* `$state` for the binding's sake: `bind:this` into a property of a plain object
|
|
94
136
|
* is a write Svelte cannot track, and it says so once per cell per render
|
|
95
137
|
* (`ArrayField` keeps its element refs the same way). */
|
|
96
|
-
const proseEls: Record<
|
|
138
|
+
const proseEls: Record<
|
|
139
|
+
string,
|
|
140
|
+
{ focus: () => void; setCaret: (pos: number) => void } | undefined
|
|
141
|
+
> = $state({});
|
|
142
|
+
const nestedEls: Record<
|
|
143
|
+
string,
|
|
144
|
+
| { focus: () => void; focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox }
|
|
145
|
+
| undefined
|
|
146
|
+
> = $state({});
|
|
97
147
|
let rootEl = $state<HTMLElement | undefined>();
|
|
148
|
+
/** A handle by property name, own keys only: a property can be named `constructor`. */
|
|
149
|
+
const ref = <T,>(map: Record<string, T | undefined>, key: string): T | undefined =>
|
|
150
|
+
Object.hasOwn(map, key) ? map[key] : undefined;
|
|
98
151
|
const FOCUSABLE = 'input, select, button, [tabindex]:not([tabindex="-1"])';
|
|
99
152
|
/** Take the caret: the first property that has somewhere to put it, in document
|
|
100
|
-
* order — a subform has no single control of its own to land on
|
|
101
|
-
*
|
|
102
|
-
* empty or wholly unsupported subform lands nothing. */
|
|
153
|
+
* order — a subform has no single control of its own to land on. An empty subform
|
|
154
|
+
* lands nothing. */
|
|
103
155
|
export function focus(): void {
|
|
104
156
|
for (const cell of propCells()) if (landOn(cell)) return;
|
|
105
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Land at `steps` inside this subform: the property the first step names, and the
|
|
160
|
+
* rest of the walk inside it where that property is a container ({@link
|
|
161
|
+
* FieldControl.focusPath}). A prose cell takes the offset; a scalar takes the bare
|
|
162
|
+
* focus. A step naming no property here falls back to {@link focus}. What comes back
|
|
163
|
+
* is the innermost row the walk opened, which a property of this subform never is:
|
|
164
|
+
* the caller washes its own box for a landing that ends here.
|
|
165
|
+
*/
|
|
166
|
+
export function focusPath(steps: readonly PathStep[], pos?: number): LandingBox {
|
|
167
|
+
const [key, ...rest] = steps;
|
|
168
|
+
if (typeof key !== 'string' || !has(key)) {
|
|
169
|
+
focus();
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const nested = ref(nestedEls, key);
|
|
173
|
+
if (nested) return nested.focusPath(rest, pos);
|
|
174
|
+
const prose = ref(proseEls, key);
|
|
175
|
+
if (prose) {
|
|
176
|
+
if (pos != null) prose.setCaret(pos);
|
|
177
|
+
else prose.focus();
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
focusProp(key);
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
106
183
|
/** A label click for the properties `for` cannot reach: the date control, whose
|
|
107
|
-
* focus lives on a segment,
|
|
108
|
-
* to that property's own cell, so the click lands where the
|
|
109
|
-
* the subform's first control. */
|
|
184
|
+
* focus lives on a segment, a prose cell, which is a `contenteditable`, and a
|
|
185
|
+
* nested subform. Scoped to that property's own cell, so the click lands where the
|
|
186
|
+
* label is rather than on the subform's first control. */
|
|
110
187
|
function focusProp(key: string): void {
|
|
111
188
|
for (const cell of propCells()) if (cell.dataset.qmProp === key) return void landOn(cell);
|
|
112
189
|
}
|
|
113
190
|
// Matched on the dataset: a key is the schema's own string, and spelling one into a
|
|
114
|
-
// selector needs `CSS.escape`, which the test DOM does not carry.
|
|
191
|
+
// selector needs `CSS.escape`, which the test DOM does not carry. `:scope >` keeps a
|
|
192
|
+
// nested subform's cells out of this one's walk: they are its own to land in.
|
|
115
193
|
function propCells(): HTMLElement[] {
|
|
116
|
-
return [
|
|
194
|
+
return [
|
|
195
|
+
...(rootEl?.querySelectorAll<HTMLElement>(':scope > .qm-object-grid > [data-qm-prop]') ?? [])
|
|
196
|
+
];
|
|
117
197
|
}
|
|
118
198
|
/** Land in one property's cell, answering whether it took the caret. */
|
|
119
199
|
function landOn(cell: HTMLElement): boolean {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
200
|
+
const key = cell.dataset.qmProp ?? '';
|
|
201
|
+
const owned = ref(nestedEls, key) ?? ref(proseEls, key);
|
|
202
|
+
if (owned) {
|
|
203
|
+
owned.focus();
|
|
123
204
|
return true;
|
|
124
205
|
}
|
|
125
|
-
|
|
206
|
+
// Past the label row: a property carrying a `description` opens with the hint
|
|
207
|
+
// trigger, which is focusable and stands before the control it describes.
|
|
208
|
+
const el = [...cell.querySelectorAll<HTMLElement>(FOCUSABLE)].find(
|
|
209
|
+
(n) => !n.closest('.qm-field-label-row')
|
|
210
|
+
);
|
|
126
211
|
el?.focus();
|
|
127
212
|
return !!el;
|
|
128
213
|
}
|
|
@@ -144,98 +229,183 @@
|
|
|
144
229
|
<div
|
|
145
230
|
bind:this={rootEl}
|
|
146
231
|
class="qm-object"
|
|
232
|
+
class:qm-object-bare={bare}
|
|
147
233
|
role="group"
|
|
148
234
|
aria-labelledby={labelledBy}
|
|
149
235
|
aria-describedby={describedBy}
|
|
150
236
|
>
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
<!-- The grid is one box in, so the wrapper can be the query container it reads its
|
|
238
|
+
capacity from: a container cannot query itself. -->
|
|
239
|
+
<div class="qm-object-grid qm-tracks">
|
|
240
|
+
{#each entries as [key, sub] (key)}
|
|
241
|
+
{@const kind = controlKind(sub)}
|
|
242
|
+
{@const ids = idBase && !bare ? propertyDomIds(idBase, key) : undefined}
|
|
243
|
+
{@const named = ids ? undefined : fallbackName(key, sub)}
|
|
244
|
+
{@const describes = sub.description && ids ? ids.description : undefined}
|
|
245
|
+
{@const deep = routed.below.get(key)}
|
|
246
|
+
{@const own = isContainer(kind) ? undefined : deep?.map((d) => d.diagnostic)}
|
|
247
|
+
<!-- `for` reaches the labelable controls, a variant through its discriminant; the
|
|
248
|
+
date field's focus lives on a segment and a prose cell is a `contenteditable`,
|
|
249
|
+
so those take the click handoff instead, exactly as `Field` does one level up. -->
|
|
250
|
+
{@const labelable =
|
|
251
|
+
kind === 'text' ||
|
|
252
|
+
kind === 'enum' ||
|
|
253
|
+
kind === 'variant' ||
|
|
254
|
+
kind === 'number' ||
|
|
255
|
+
kind === 'boolean'}
|
|
256
|
+
<!-- A container takes the full span at every depth: its rows are the document's
|
|
257
|
+
to count, and a track beside it would stand in a column of whitespace
|
|
258
|
+
(`packable`). -->
|
|
259
|
+
<div class="qm-object-prop" class:qm-prop-full={isContainer(kind)} data-qm-prop={key}>
|
|
260
|
+
{#if ids && kind !== 'array' && kind !== 'matrix'}
|
|
261
|
+
<FieldLabel
|
|
262
|
+
label={title(key, sub)}
|
|
263
|
+
controlId={labelable ? ids.control : undefined}
|
|
264
|
+
id={ids.label}
|
|
265
|
+
descriptionId={describes}
|
|
266
|
+
onActivate={labelable ? undefined : () => focusProp(key)}
|
|
267
|
+
required={required(sub)}
|
|
268
|
+
description={sub.description}
|
|
269
|
+
/>
|
|
270
|
+
{/if}
|
|
271
|
+
{#if kind === 'enum'}
|
|
272
|
+
<EnumField
|
|
273
|
+
label={named}
|
|
274
|
+
id={ids?.control}
|
|
275
|
+
describedBy={describes}
|
|
276
|
+
value={obj[key] as string | undefined}
|
|
277
|
+
values={sub.values ?? []}
|
|
278
|
+
fallback={sub.default as string | undefined}
|
|
279
|
+
onCommit={(v) => commitProp(key, v)}
|
|
280
|
+
/>
|
|
281
|
+
{:else if kind === 'number'}
|
|
282
|
+
<NumberField
|
|
283
|
+
label={named}
|
|
284
|
+
id={ids?.control}
|
|
285
|
+
describedBy={describes}
|
|
286
|
+
value={obj[key] as number | undefined}
|
|
287
|
+
integer={sub.type === 'integer'}
|
|
288
|
+
fallback={sub.default as number | undefined}
|
|
289
|
+
onCommit={(v) => commitProp(key, v)}
|
|
290
|
+
/>
|
|
291
|
+
{:else if kind === 'boolean'}
|
|
292
|
+
<BooleanField
|
|
293
|
+
label={named}
|
|
294
|
+
id={ids?.control}
|
|
295
|
+
describedBy={describes}
|
|
296
|
+
value={obj[key] as boolean | undefined}
|
|
297
|
+
fallback={sub.default as boolean | undefined}
|
|
298
|
+
onCommit={(v) => commitProp(key, v)}
|
|
299
|
+
/>
|
|
300
|
+
{:else if kind === 'date'}
|
|
301
|
+
<DateField
|
|
302
|
+
label={named}
|
|
303
|
+
labelledBy={ids?.label}
|
|
304
|
+
describedBy={describes}
|
|
305
|
+
value={obj[key] as string | undefined}
|
|
306
|
+
fallback={sub.default != null ? String(sub.default) : undefined}
|
|
307
|
+
onCommit={(v) => commitProp(key, v)}
|
|
308
|
+
/>
|
|
309
|
+
{:else if kind === 'text'}
|
|
310
|
+
<TextField
|
|
311
|
+
label={named}
|
|
312
|
+
id={ids?.control}
|
|
313
|
+
describedBy={describes}
|
|
314
|
+
value={obj[key] as string | undefined}
|
|
315
|
+
placeholder={sub.default != null ? String(sub.default) : undefined}
|
|
316
|
+
onCommit={(v) => commitProp(key, v)}
|
|
317
|
+
onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
|
|
318
|
+
/>
|
|
319
|
+
{:else if kind === 'prose'}
|
|
320
|
+
<!-- Keyed by name, so a re-derive of the container this cell commits whole
|
|
321
|
+
leaves the leaf mounted and the caret in it. -->
|
|
322
|
+
<ProseValue
|
|
323
|
+
bind:this={proseEls[key]}
|
|
324
|
+
content={() => contentAt([key]) ?? emptyContent()}
|
|
325
|
+
plaintext={sub.type === 'plaintext'}
|
|
326
|
+
label={named}
|
|
327
|
+
labelledBy={ids?.label}
|
|
328
|
+
describedBy={describes}
|
|
329
|
+
onChange={(rt) => commitProp(key, rt)}
|
|
330
|
+
onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
|
|
331
|
+
/>
|
|
332
|
+
{:else if kind === 'array'}
|
|
333
|
+
<!-- The repeater owns its label row, the add chip sharing it (`Field` skips
|
|
334
|
+
its own label for one too), so the names are handed down rather than
|
|
335
|
+
drawn here. Its rows are collapsed rows again, one open at a time: the
|
|
336
|
+
depth an open row under an open row costs is two rungs of the ladder
|
|
337
|
+
and one figure. -->
|
|
338
|
+
<ArrayField
|
|
339
|
+
bind:this={nestedEls[key]}
|
|
340
|
+
value={obj[key] as unknown[] | undefined}
|
|
341
|
+
items={sub.items}
|
|
342
|
+
layout={arrayLayout(sub)}
|
|
343
|
+
max={sub.max}
|
|
344
|
+
label={title(key, sub)}
|
|
345
|
+
required={required(sub)}
|
|
346
|
+
description={sub.description}
|
|
347
|
+
labelId={ids?.label}
|
|
348
|
+
descriptionId={describes}
|
|
349
|
+
idBase={ids?.control}
|
|
350
|
+
contentAt={(path) => contentAt([key, ...path])}
|
|
351
|
+
onCommit={(v) => commitProp(key, v)}
|
|
352
|
+
diagnostics={deep}
|
|
353
|
+
/>
|
|
354
|
+
{:else if kind === 'object'}
|
|
355
|
+
<ObjectField
|
|
356
|
+
bind:this={nestedEls[key]}
|
|
357
|
+
value={obj[key] as Record<string, unknown> | undefined}
|
|
358
|
+
properties={sub.properties}
|
|
359
|
+
label={ids ? undefined : fallbackName(key, sub)}
|
|
360
|
+
idBase={ids?.control}
|
|
361
|
+
labelledBy={ids?.label}
|
|
362
|
+
describedBy={describes}
|
|
363
|
+
contentAt={(path) => contentAt([key, ...path])}
|
|
364
|
+
onCommit={(v) => commitProp(key, v)}
|
|
365
|
+
diagnostics={deep}
|
|
366
|
+
/>
|
|
367
|
+
{:else if kind === 'matrix'}
|
|
368
|
+
<MatrixField
|
|
369
|
+
bind:this={nestedEls[key]}
|
|
370
|
+
value={obj[key] as Record<string, unknown> | undefined}
|
|
371
|
+
schema={sub}
|
|
372
|
+
label={title(key, sub)}
|
|
373
|
+
description={sub.description}
|
|
374
|
+
labelId={ids?.label}
|
|
375
|
+
descriptionId={describes}
|
|
376
|
+
idBase={ids?.control}
|
|
377
|
+
contentAt={(path) => contentAt([key, ...path])}
|
|
378
|
+
onCommit={(v) => commitProp(key, v)}
|
|
379
|
+
diagnostics={deep}
|
|
380
|
+
/>
|
|
381
|
+
{:else if kind === 'variant'}
|
|
382
|
+
<VariantField
|
|
383
|
+
bind:this={nestedEls[key]}
|
|
384
|
+
value={obj[key] as Record<string, unknown> | undefined}
|
|
385
|
+
schema={sub}
|
|
386
|
+
ghostMember={sub.default as string | undefined}
|
|
387
|
+
label={title(key, sub)}
|
|
388
|
+
id={ids?.control}
|
|
389
|
+
labelledBy={ids?.label}
|
|
390
|
+
describedBy={describes}
|
|
391
|
+
contentAt={(path) => contentAt([key, ...path])}
|
|
392
|
+
onCommit={(v) => commitProp(key, v)}
|
|
393
|
+
diagnostics={deep}
|
|
394
|
+
/>
|
|
395
|
+
{:else}
|
|
396
|
+
<TextField
|
|
397
|
+
label={named}
|
|
398
|
+
id={ids?.control}
|
|
399
|
+
describedBy={describes}
|
|
400
|
+
value={obj[key] as string | undefined}
|
|
401
|
+
onCommit={(v) => commitProp(key, v)}
|
|
402
|
+
/>
|
|
403
|
+
{/if}
|
|
404
|
+
<DiagnosticList diagnostics={own} />
|
|
405
|
+
</div>
|
|
406
|
+
{/each}
|
|
407
|
+
</div>
|
|
408
|
+
<DiagnosticList diagnostics={foot} />
|
|
239
409
|
</div>
|
|
240
410
|
|
|
241
411
|
<style>
|
|
@@ -251,19 +421,32 @@
|
|
|
251
421
|
section's (`Card.svelte`). A cap and not a leading gap: what stands the subform off
|
|
252
422
|
the label or the box above it is still that stacker's own.
|
|
253
423
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
424
|
+
A query container over its own width: the capacity its grid steps is the width the
|
|
425
|
+
properties get, which three rungs in is three `--_qm-nest` and three verticals
|
|
426
|
+
narrower than the section's, and a subform reading the section's count over that
|
|
427
|
+
box draws four tracks where two fit. The ladder itself is the one shared recipe
|
|
428
|
+
(`.qm-tracks`, controls.css), so this container and the section's step at the same
|
|
429
|
+
widths. Not on a table's row, which is a subgrid of the table and sizes nothing of
|
|
430
|
+
its own. */
|
|
259
431
|
.qm-object {
|
|
260
|
-
display:
|
|
261
|
-
|
|
262
|
-
gap: var(--_qm-space
|
|
432
|
+
display: flex;
|
|
433
|
+
flex-direction: column;
|
|
434
|
+
gap: var(--_qm-space);
|
|
435
|
+
}
|
|
436
|
+
.qm-object:not(.qm-object-bare) {
|
|
437
|
+
container-type: inline-size;
|
|
263
438
|
border-inline-start: var(--_qm-vertical-width) solid var(--_qm-border);
|
|
264
439
|
padding-inline-start: var(--_qm-nest);
|
|
265
440
|
padding-block: var(--_qm-space);
|
|
266
441
|
}
|
|
442
|
+
/* The tracks are this subform's own count over its own width (`.qm-tracks`); the
|
|
443
|
+
edges are not the section's: the inset is the nesting, and a cell lining up with the
|
|
444
|
+
field above it would claim a depth it is not at. */
|
|
445
|
+
.qm-object-grid {
|
|
446
|
+
display: grid;
|
|
447
|
+
grid-template-columns: repeat(var(--cols), 1fr);
|
|
448
|
+
gap: var(--_qm-space-2);
|
|
449
|
+
}
|
|
267
450
|
/* A property measures like a field: two tracks over the subform's own rows, so a
|
|
268
451
|
label wrapping in one column leaves its control on the row's baseline rather than a
|
|
269
452
|
line below it — `Field.svelte`'s rule for a row-sharing field, one level in. The
|
|
@@ -286,9 +469,49 @@
|
|
|
286
469
|
.qm-object-prop:only-child {
|
|
287
470
|
grid-column: span var(--cols-half);
|
|
288
471
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
472
|
+
/* A container property takes the whole row and its own rows: its label rides inside
|
|
473
|
+
the control it draws (a repeater's header, a matrix's) or above a subform of its
|
|
474
|
+
own, and neither is a track a scalar's label shares. */
|
|
475
|
+
.qm-object-prop.qm-prop-full,
|
|
476
|
+
.qm-object-prop.qm-prop-full:only-child {
|
|
477
|
+
grid-column: 1 / -1;
|
|
478
|
+
grid-row: auto;
|
|
479
|
+
grid-template-rows: none;
|
|
480
|
+
row-gap: var(--_qm-space);
|
|
481
|
+
}
|
|
482
|
+
/* ── A table's row ──────────────────────────────────────────────────────────
|
|
483
|
+
The cells alone, each on the track the header names: this box and its grid both
|
|
484
|
+
subgrid onto the row that holds them, so a cell's edge is the column's. No
|
|
485
|
+
vertical, no inset, no capacity of its own, and one row per cell, the label having
|
|
486
|
+
moved up to the header. `.qm-array-row` is the row (`ArrayField`), which spends
|
|
487
|
+
the last track on the row's remove; this box takes the rest. */
|
|
488
|
+
.qm-object-bare {
|
|
489
|
+
display: grid;
|
|
490
|
+
grid-template-columns: subgrid;
|
|
491
|
+
grid-column: 1 / -2;
|
|
492
|
+
}
|
|
493
|
+
.qm-object-bare > .qm-object-grid {
|
|
494
|
+
grid-template-columns: subgrid;
|
|
495
|
+
grid-column: 1 / -1;
|
|
496
|
+
/* The table's own gutter, not the subform's: a subgrid that states a wider gap
|
|
497
|
+
than its parent's shifts its items half the difference off the tracks the
|
|
498
|
+
header cells sit on. */
|
|
499
|
+
column-gap: var(--_qm-space);
|
|
500
|
+
}
|
|
501
|
+
.qm-object-bare .qm-object-prop,
|
|
502
|
+
.qm-object-bare .qm-object-prop:only-child {
|
|
503
|
+
grid-column: auto;
|
|
504
|
+
grid-row: auto;
|
|
505
|
+
grid-template-rows: none;
|
|
506
|
+
row-gap: var(--_qm-space-half);
|
|
507
|
+
/* A row is one line of controls, so a cell shorter than the box beside it stands
|
|
508
|
+
on that line: a switch held at the top of a row a text box set the height of
|
|
509
|
+
reads as a cell that slipped its track. */
|
|
510
|
+
align-items: center;
|
|
511
|
+
}
|
|
512
|
+
/* What the row could not hand down stands across it: a diagnostic in the first
|
|
513
|
+
column alone would read as that column's. */
|
|
514
|
+
.qm-object-bare > :global(.qm-diag-list) {
|
|
515
|
+
grid-column: 1 / -1;
|
|
293
516
|
}
|
|
294
517
|
</style>
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
2
|
+
import { type DeepDiagnostic } from './diagnostics.js';
|
|
3
|
+
import type { LandingBox } from './leaves.js';
|
|
4
|
+
import ObjectField from './ObjectField.svelte';
|
|
5
|
+
import './controls.css';
|
|
2
6
|
interface Props {
|
|
3
7
|
value: Record<string, unknown> | undefined;
|
|
4
8
|
properties: Record<string, QuillFieldSchema> | undefined;
|
|
@@ -11,8 +15,8 @@ interface Props {
|
|
|
11
15
|
/**
|
|
12
16
|
* The parent control's DOM id: the base each property's own three names derive
|
|
13
17
|
* from ({@link propertyDomIds}). Absent — a subform mounted with no field around
|
|
14
|
-
* it — the properties fall back to `aria-label`, which names
|
|
15
|
-
* `for` target to click.
|
|
18
|
+
* it, or a table's row — the properties fall back to `aria-label`, which names
|
|
19
|
+
* them without a `for` target to click.
|
|
16
20
|
*/
|
|
17
21
|
idBase?: string;
|
|
18
22
|
/** Accessible-name prefix used only on the `aria-label` fallback above. */
|
|
@@ -24,9 +28,20 @@ interface Props {
|
|
|
24
28
|
* `undefined` for a property the stored value does not reach. */
|
|
25
29
|
contentAt: (path: PathStep[]) => Content | undefined;
|
|
26
30
|
onCommit: (obj: Record<string, unknown>) => void;
|
|
31
|
+
/** A table's row: the cells alone, on the tracks the table's header names, with
|
|
32
|
+
* no labels and no vertical of their own. */
|
|
33
|
+
bare?: boolean;
|
|
34
|
+
/** Raw keydown off a text or prose cell, for a row whose own keys run through its
|
|
35
|
+
* cells: the table's Enter and Backspace ({@link ArrayField}). */
|
|
36
|
+
onCellKey?: (e: KeyboardEvent, key: string) => void;
|
|
37
|
+
/** The diagnostics routed into this subform, each with its steps still to walk:
|
|
38
|
+
* one naming a property draws under that property's control, and one naming
|
|
39
|
+
* nothing this subform holds draws at its foot (`diagnostics.ts`). */
|
|
40
|
+
diagnostics?: readonly DeepDiagnostic[];
|
|
27
41
|
}
|
|
28
42
|
declare const ObjectField: import("svelte").Component<Props, {
|
|
29
43
|
focus: () => void;
|
|
44
|
+
focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
|
|
30
45
|
}, "">;
|
|
31
46
|
type ObjectField = ReturnType<typeof ObjectField>;
|
|
32
47
|
export default ObjectField;
|