@quillmark/svelte 0.7.0 → 0.7.1
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/dist/core/codec/field.d.ts +15 -4
- package/dist/core/codec/field.js +11 -9
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +1 -1
- package/dist/visual/ArrayField.svelte +17 -14
- package/dist/visual/ArrayField.svelte.d.ts +9 -7
- package/dist/visual/Card.svelte +50 -22
- package/dist/visual/Field.svelte +22 -2
- package/dist/visual/ObjectField.svelte +70 -21
- package/dist/visual/ObjectField.svelte.d.ts +7 -1
- package/dist/visual/ProseValue.svelte +165 -0
- package/dist/visual/ProseValue.svelte.d.ts +33 -0
- package/dist/visual/VariantField.svelte +11 -3
- package/dist/visual/VariantField.svelte.d.ts +5 -1
- package/dist/visual/strings.d.ts +4 -3
- package/package.json +2 -2
- package/dist/visual/ProseArrayElement.svelte +0 -139
- package/dist/visual/ProseArrayElement.svelte.d.ts +0 -25
|
@@ -135,13 +135,24 @@ export interface LeafViews {
|
|
|
135
135
|
/** The canonical empty `Content`: one empty `para` line. The empty a prose
|
|
136
136
|
* leaf (or an array's prose element) seeds from. */
|
|
137
137
|
export declare function emptyContent(): Content;
|
|
138
|
+
/** The naming attributes for the `contenteditable`, shared by every prose leaf
|
|
139
|
+
* (`ProseValue` takes it too, so an addressed leaf and a by-value one name their
|
|
140
|
+
* regions by one rule). `aria-labelledby` supersedes `aria-label`: a leaf carrying
|
|
141
|
+
* both is the ambiguity where implementations disagree about which wins, and
|
|
142
|
+
* `undefined` is returned whole when a leaf takes neither, since ProseMirror reads
|
|
143
|
+
* the absence, not an empty object. */
|
|
144
|
+
export declare function proseAttributes(opts: {
|
|
145
|
+
label?: string;
|
|
146
|
+
labelledBy?: string;
|
|
147
|
+
describedBy?: string;
|
|
148
|
+
}): Record<string, string> | undefined;
|
|
138
149
|
export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
139
150
|
/**
|
|
140
151
|
* The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
|
|
141
|
-
* {@link createField} and the
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
152
|
+
* {@link createField} and the by-value inline editor (`ProseValue`), so the two
|
|
153
|
+
* never fork the keymap/plugin ordering. History first, then any leaf-specific
|
|
154
|
+
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
155
|
+
* a by-value leaf passes none), the
|
|
145
156
|
* markdown-shorthand input rules, then the field keymap over the base keymap, and
|
|
146
157
|
* last the two that answer to a caret or a selection beside a block: the gap cursor
|
|
147
158
|
* and {@link pastAtomPlugin}.
|
package/dist/core/codec/field.js
CHANGED
|
@@ -89,11 +89,13 @@ function opsCommittable(doc, addr) {
|
|
|
89
89
|
const stored = doc.getStored(addr);
|
|
90
90
|
return stored !== null && typeof stored === 'object';
|
|
91
91
|
}
|
|
92
|
-
/** The naming attributes for the `contenteditable
|
|
93
|
-
* `
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
|
|
92
|
+
/** The naming attributes for the `contenteditable`, shared by every prose leaf
|
|
93
|
+
* (`ProseValue` takes it too, so an addressed leaf and a by-value one name their
|
|
94
|
+
* regions by one rule). `aria-labelledby` supersedes `aria-label`: a leaf carrying
|
|
95
|
+
* both is the ambiguity where implementations disagree about which wins, and
|
|
96
|
+
* `undefined` is returned whole when a leaf takes neither, since ProseMirror reads
|
|
97
|
+
* the absence, not an empty object. */
|
|
98
|
+
export function proseAttributes(opts) {
|
|
97
99
|
const attrs = {};
|
|
98
100
|
if (opts.labelledBy)
|
|
99
101
|
attrs['aria-labelledby'] = opts.labelledBy;
|
|
@@ -386,10 +388,10 @@ export function createField(opts) {
|
|
|
386
388
|
}
|
|
387
389
|
/**
|
|
388
390
|
* The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
|
|
389
|
-
* {@link createField} and the
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
391
|
+
* {@link createField} and the by-value inline editor (`ProseValue`), so the two
|
|
392
|
+
* never fork the keymap/plugin ordering. History first, then any leaf-specific
|
|
393
|
+
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
394
|
+
* a by-value leaf passes none), the
|
|
393
395
|
* markdown-shorthand input rules, then the field keymap over the base keymap, and
|
|
394
396
|
* last the two that answer to a caret or a selection beside a block: the gap cursor
|
|
395
397
|
* and {@link pastAtomPlugin}.
|
|
@@ -3,7 +3,7 @@ import 'prosemirror-gapcursor/style/gapcursor.css';
|
|
|
3
3
|
import './prose.css';
|
|
4
4
|
export { rangeAnchor } from './anchor.js';
|
|
5
5
|
export type { RangeAnchor } from './anchor.js';
|
|
6
|
-
export { createField, emptyContent, proseLeafPlugins } from './field.js';
|
|
6
|
+
export { createField, emptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
7
7
|
export type { CreateFieldOpts, FieldController, LeafViews } from './field.js';
|
|
8
8
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
9
9
|
export type { TableChromeStrings } from './table-view.js';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import './prose.css';
|
|
|
13
13
|
// selection (`visual/FormatPopover.svelte`), so it is reached off-barrel.
|
|
14
14
|
export { rangeAnchor } from './anchor.js';
|
|
15
15
|
// The prose leaf.
|
|
16
|
-
export { createField, emptyContent, proseLeafPlugins } from './field.js';
|
|
16
|
+
export { createField, emptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
17
17
|
// The table island's chrome vocabulary: the visual tier's `strings` set extends it,
|
|
18
18
|
// so the island's wording is overridden beside every other key.
|
|
19
19
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
edit / add / remove rebuilds the whole array and hands it to the parent's typed
|
|
4
4
|
`writer.set(field, wholeArray)` (arrays are not op-addressed). Element control
|
|
5
5
|
by `items.type`: `richtext` / `plaintext` → a prose element
|
|
6
|
-
({@link
|
|
6
|
+
({@link ProseValue}), `object` → a summary row that opens onto a subform,
|
|
7
7
|
everything else → a text input. The
|
|
8
8
|
add affordance sits in the label header row (space-between with the field label);
|
|
9
9
|
{@link Field} skips its own label for array controls and hands this component the
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// off-tree, so this component renders standalone too.
|
|
39
39
|
const t = wording();
|
|
40
40
|
import { onDestroy, tick } from 'svelte';
|
|
41
|
-
import type { Content, QuillFieldSchema } from '@quillmark/wasm';
|
|
41
|
+
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
42
42
|
import { emptyContent } from '../core/codec/index.js';
|
|
43
43
|
import { createLifespan } from '../core/teardown.js';
|
|
44
44
|
import { IdSeq, controlKind } from './structure.js';
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import Icon from './icons/Icon.svelte';
|
|
47
47
|
import TextField from './TextField.svelte';
|
|
48
48
|
import ObjectField from './ObjectField.svelte';
|
|
49
|
-
import
|
|
49
|
+
import ProseValue from './ProseValue.svelte';
|
|
50
50
|
import FieldLabel from './FieldLabel.svelte';
|
|
51
51
|
import './controls.css';
|
|
52
52
|
|
|
@@ -73,12 +73,14 @@
|
|
|
73
73
|
* properties' own names from, one `-e-<element id>` segment down, so each open
|
|
74
74
|
* record's cells carry real `<label for>` pairs like every other control. */
|
|
75
75
|
idBase?: string;
|
|
76
|
-
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
76
|
+
/** The boundary's nested content read, rooted at this field: `path` is a
|
|
77
|
+
* `PathStep[]` from the field to the leaf, so `[i]` is element `i` and
|
|
78
|
+
* `[i, key]` a content cell of an `object` element's subform (`Field`,
|
|
79
|
+
* `reader.getContentAt`). The codec is the leaf's own declared type's. Asked
|
|
80
|
+
* only for a content-typed leaf: on any other the read is not content and
|
|
81
|
+
* throws. `undefined` for a leaf the stored value does not reach — a slot this
|
|
82
|
+
* control has spliced in and whose commit has yet to land, or was refused. */
|
|
83
|
+
contentAt: (path: PathStep[]) => Content | undefined;
|
|
82
84
|
onCommit: (arr: unknown[]) => void;
|
|
83
85
|
}
|
|
84
86
|
let {
|
|
@@ -90,13 +92,13 @@
|
|
|
90
92
|
labelId,
|
|
91
93
|
descriptionId,
|
|
92
94
|
idBase,
|
|
93
|
-
|
|
95
|
+
contentAt,
|
|
94
96
|
onCommit
|
|
95
97
|
}: Props = $props();
|
|
96
98
|
|
|
97
99
|
// The element control is the item schema's own, with no departure: a content-typed
|
|
98
100
|
// element mounts the prose leaf its scalar field mounts, reading through
|
|
99
|
-
// `
|
|
101
|
+
// `contentAt` whatever the element rests as. An array declaring no `items`
|
|
100
102
|
// has text elements.
|
|
101
103
|
const control = $derived(items ? controlKind(items) : 'text');
|
|
102
104
|
const arr = $derived((value ?? []) as unknown[]);
|
|
@@ -122,7 +124,7 @@
|
|
|
122
124
|
// The focus targets, keyed by element ID rather than index: an index goes stale on
|
|
123
125
|
// the splice that focus is chasing. An element control exposes `focus()` because a
|
|
124
126
|
// text element and a prose element disagree on what focusing is: the difference
|
|
125
|
-
// is stated on `
|
|
127
|
+
// is stated on `ProseValue.focus`, which owns it.
|
|
126
128
|
//
|
|
127
129
|
// Every path that drops an id deletes its entry: `bind:this` teardown nulls the
|
|
128
130
|
// value on unmount and leaves the key, so a card that outlives its elements
|
|
@@ -396,6 +398,7 @@
|
|
|
396
398
|
properties={items?.properties}
|
|
397
399
|
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
398
400
|
idBase={idBase != null ? `${idBase}-e-${id}` : undefined}
|
|
401
|
+
contentAt={(path) => contentAt([k, ...path])}
|
|
399
402
|
onCommit={(obj) => commitElement(k, obj)}
|
|
400
403
|
/>
|
|
401
404
|
{/if}
|
|
@@ -403,9 +406,9 @@
|
|
|
403
406
|
{:else}
|
|
404
407
|
<div class="qm-array-row">
|
|
405
408
|
{#if control === 'prose'}
|
|
406
|
-
<
|
|
409
|
+
<ProseValue
|
|
407
410
|
bind:this={els[id]}
|
|
408
|
-
content={() =>
|
|
411
|
+
content={() => contentAt([k]) ?? emptyContent()}
|
|
409
412
|
plaintext={items?.type === 'plaintext'}
|
|
410
413
|
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
411
414
|
onChange={(rt) => commitElement(k, rt)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Content, QuillFieldSchema } from '@quillmark/wasm';
|
|
1
|
+
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
2
2
|
import './controls.css';
|
|
3
3
|
interface Props {
|
|
4
4
|
value: unknown[] | undefined;
|
|
@@ -19,12 +19,14 @@ interface Props {
|
|
|
19
19
|
* properties' own names from, one `-e-<element id>` segment down, so each open
|
|
20
20
|
* record's cells carry real `<label for>` pairs like every other control. */
|
|
21
21
|
idBase?: string;
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
22
|
+
/** The boundary's nested content read, rooted at this field: `path` is a
|
|
23
|
+
* `PathStep[]` from the field to the leaf, so `[i]` is element `i` and
|
|
24
|
+
* `[i, key]` a content cell of an `object` element's subform (`Field`,
|
|
25
|
+
* `reader.getContentAt`). The codec is the leaf's own declared type's. Asked
|
|
26
|
+
* only for a content-typed leaf: on any other the read is not content and
|
|
27
|
+
* throws. `undefined` for a leaf the stored value does not reach — a slot this
|
|
28
|
+
* control has spliced in and whose commit has yet to land, or was refused. */
|
|
29
|
+
contentAt: (path: PathStep[]) => Content | undefined;
|
|
28
30
|
onCommit: (arr: unknown[]) => void;
|
|
29
31
|
}
|
|
30
32
|
declare const ArrayField: import("svelte").Component<Props, {
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -128,9 +128,9 @@
|
|
|
128
128
|
const ungrouped = $derived(card.sections.filter((s) => s.group == null));
|
|
129
129
|
const grouped = $derived(card.sections.filter((s) => s.group != null));
|
|
130
130
|
|
|
131
|
-
// A schema that declares no fields still gets a body, and an empty
|
|
132
|
-
//
|
|
133
|
-
//
|
|
131
|
+
// A schema that declares no fields still gets a body, and an empty payload block is
|
|
132
|
+
// worse than none: its two rules land on one y and paint as a single stroke of twice
|
|
133
|
+
// the width, and the card's gap counts the block twice besides.
|
|
134
134
|
const hasMeta = $derived(ungrouped.length > 0 || grouped.length > 0);
|
|
135
135
|
|
|
136
136
|
// Seeded once from the card's shape. Card is keyed by stable id, so this survives the
|
|
@@ -251,7 +251,11 @@
|
|
|
251
251
|
|
|
252
252
|
<div class="qm-card-body">
|
|
253
253
|
{#if hasMeta}
|
|
254
|
-
<div
|
|
254
|
+
<div
|
|
255
|
+
class="qm-card-meta"
|
|
256
|
+
class:qm-meta-top={!card.isMain}
|
|
257
|
+
class:qm-meta-bottom={card.hasBody}
|
|
258
|
+
>
|
|
255
259
|
{#each ungrouped as section (section.group ?? '_ungrouped')}
|
|
256
260
|
<div class="qm-section">
|
|
257
261
|
{@render sectionFields(section.fields)}
|
|
@@ -511,17 +515,31 @@
|
|
|
511
515
|
flex-direction: column;
|
|
512
516
|
gap: var(--_qm-space);
|
|
513
517
|
}
|
|
514
|
-
/*
|
|
515
|
-
|
|
516
|
-
the
|
|
517
|
-
|
|
518
|
+
/* One rung in two places: what a rule insets its own edge by, and what a vertical
|
|
519
|
+
reaches past the box it is drawn on. `--_qm-nest` is the distance a stroke stands off
|
|
520
|
+
the content it holds — the same one the fields keep off the vertical inline — and a
|
|
521
|
+
section's box already spends `--_qm-space` of it capping its own ends, so both spend
|
|
522
|
+
the difference. That is what squares the corner: the field stands the same rung off
|
|
523
|
+
the rule above it as off the vertical beside it. */
|
|
524
|
+
.qm-card-meta {
|
|
525
|
+
--stroke-reach: calc(var(--_qm-nest) - var(--_qm-space));
|
|
526
|
+
}
|
|
527
|
+
/* The card's two horizontals, and ranking is the one thing a horizontal is for: they
|
|
528
|
+
stand the payload against the title above it and the body below it, where saying what
|
|
529
|
+
sits inside what is the verticals' (ARCHITECTURE §"A plane is a tone"). Each is
|
|
530
|
+
conditional on what it ranks against: `main` is headerless, and a card with no body
|
|
531
|
+
has nothing under the payload to divide it from.
|
|
518
532
|
|
|
519
|
-
The inset
|
|
520
|
-
|
|
521
|
-
|
|
533
|
+
The inset is the rule's own, so it goes with it: a card drawing one horizontal insets
|
|
534
|
+
that end and not the other. The air outside the pair is the `--_qm-space-3` the card
|
|
535
|
+
spends between its blocks. */
|
|
536
|
+
.qm-meta-top {
|
|
537
|
+
border-top: var(--_qm-border-width) solid var(--_qm-border);
|
|
538
|
+
padding-top: var(--stroke-reach);
|
|
539
|
+
}
|
|
522
540
|
.qm-meta-bottom {
|
|
523
541
|
border-bottom: var(--_qm-border-width) solid var(--_qm-border);
|
|
524
|
-
padding-bottom: var(--
|
|
542
|
+
padding-bottom: var(--stroke-reach);
|
|
525
543
|
}
|
|
526
544
|
/* A section's share of the boundary between sections: one rung at each end, so two of
|
|
527
545
|
them stand three of it apart whichever kind they are (above). The accordion spends the
|
|
@@ -577,12 +595,22 @@
|
|
|
577
595
|
On the group rather than on its panel: what the stroke marks is the section, and a
|
|
578
596
|
section is its header and what hangs under it. Drawn transparent when closed rather
|
|
579
597
|
than absent, so a toggle moves no text — and closed, there is nothing under the header
|
|
580
|
-
for it to gather.
|
|
581
|
-
|
|
598
|
+
for it to gather.
|
|
599
|
+
|
|
600
|
+
The stroke caps at `--_qm-nest`, at both ends and on every section alike. Its box
|
|
601
|
+
already spends `--_qm-space` of that — the header's block padding above, the panel's
|
|
602
|
+
below, the rung the ungrouped section spends on its own two ends — so the rest is
|
|
603
|
+
grown here and handed straight back as margin: the stroke lengthens, and a section
|
|
604
|
+
boundary keeps the three rungs it stood at. What the stroke reaches into is the air of
|
|
605
|
+
that boundary, or, where the section is the payload's first or last, exactly the inset
|
|
606
|
+
its rule took — so it lands on the rule at a corner with nothing drawn longer for it.
|
|
607
|
+
One section open at a time, so no two strokes are ever visible to meet in between. */
|
|
582
608
|
.qm-group {
|
|
583
609
|
display: flex;
|
|
584
610
|
flex-direction: column;
|
|
585
611
|
border-left: var(--_qm-border-width) solid transparent;
|
|
612
|
+
padding-block: var(--stroke-reach);
|
|
613
|
+
margin-block: calc(-1 * var(--stroke-reach));
|
|
586
614
|
transition: border-color var(--_qm-duration-slow) var(--_qm-ease-reverse);
|
|
587
615
|
}
|
|
588
616
|
.qm-group.qm-open {
|
|
@@ -590,13 +618,13 @@
|
|
|
590
618
|
}
|
|
591
619
|
/* Symmetric vertical padding at the tightest rung that still clears WCAG 2.5.8's
|
|
592
620
|
24×24 floor: the header is the whole row, so adjacent labels share one rhythm with no
|
|
593
|
-
dead strip outside the button. Horizontal is
|
|
594
|
-
because
|
|
595
|
-
|
|
596
|
-
stroke; right, because the row is the target and an
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
621
|
+
dead strip outside the button. Horizontal is the nesting step left and zero right —
|
|
622
|
+
left, because the chevron opening the heading starts where the fields under it start,
|
|
623
|
+
so everything the section holds stands off its vertical by one rung and the corner the
|
|
624
|
+
stroke closes against a rule reads square; right, because the row is the target and an
|
|
625
|
+
inset there is target given back. What the glyph gives up by taking the step is the
|
|
626
|
+
gutter: at a tighter rung it stands in the clearance itself, with only its own bearing
|
|
627
|
+
between ink and stroke, and the rotation swaps which bearing faces which.
|
|
600
628
|
|
|
601
629
|
`font: inherit` because a UA button inherits no face, then the body rung, one step
|
|
602
630
|
over the field labels beneath it: at the label rung the two read as one register and
|
|
@@ -609,7 +637,7 @@
|
|
|
609
637
|
width: 100%;
|
|
610
638
|
border: none;
|
|
611
639
|
background: transparent;
|
|
612
|
-
padding: var(--_qm-space) 0 var(--_qm-space) var(--_qm-
|
|
640
|
+
padding: var(--_qm-space) 0 var(--_qm-space) var(--_qm-nest);
|
|
613
641
|
font: inherit;
|
|
614
642
|
font-size: var(--_qm-text-body);
|
|
615
643
|
font-weight: var(--_qm-weight-mid);
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -14,7 +14,15 @@
|
|
|
14
14
|
`DiagnosticList`, non-gating.
|
|
15
15
|
-->
|
|
16
16
|
<script lang="ts">
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
Document,
|
|
19
|
+
Quill,
|
|
20
|
+
Addr,
|
|
21
|
+
Content,
|
|
22
|
+
Diagnostic,
|
|
23
|
+
PathStep,
|
|
24
|
+
ResolvedField
|
|
25
|
+
} from '@quillmark/wasm';
|
|
18
26
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
19
27
|
import type { LeafRegistry } from './leaves.js';
|
|
20
28
|
import type { FieldModel, FieldSpan } from './structure.js';
|
|
@@ -116,6 +124,16 @@
|
|
|
116
124
|
// nothing, and silently.
|
|
117
125
|
const describedBy = $derived(field.description ? domIds.description : undefined);
|
|
118
126
|
|
|
127
|
+
/** The boundary's nested content read with this field's address already bound: an
|
|
128
|
+
* array's elements, an object's properties, a variant's cells, and a subform's cells
|
|
129
|
+
* under an open element. One door rather than one per depth, since `getContentAt`
|
|
130
|
+
* takes the whole path and the walk down is each container prefixing its own step.
|
|
131
|
+
* Read per call and never held: a leaf takes its state at mount, and a control asks
|
|
132
|
+
* once. */
|
|
133
|
+
function contentAt(path: PathStep[]): Content | undefined {
|
|
134
|
+
return quill.reader(doc).getContentAt(addr, path);
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
// ── Focus: one answer, two callers (`leaves.ts`) ─────────────────────────────
|
|
120
138
|
// A label click and the editor's `focusField`/`setCaret` ask the same question, so
|
|
121
139
|
// they read the same function and cannot land in different places. The four
|
|
@@ -247,6 +265,7 @@
|
|
|
247
265
|
id={domIds.control}
|
|
248
266
|
labelledBy={domIds.label}
|
|
249
267
|
{describedBy}
|
|
268
|
+
{contentAt}
|
|
250
269
|
onCommit={onCommitScalar}
|
|
251
270
|
{optionAllowed}
|
|
252
271
|
{enumDisallowed}
|
|
@@ -288,7 +307,7 @@
|
|
|
288
307
|
labelId={domIds.label}
|
|
289
308
|
descriptionId={domIds.description}
|
|
290
309
|
idBase={domIds.control}
|
|
291
|
-
|
|
310
|
+
{contentAt}
|
|
292
311
|
onCommit={onCommitScalar}
|
|
293
312
|
/>
|
|
294
313
|
{:else if field.control === 'object'}
|
|
@@ -300,6 +319,7 @@
|
|
|
300
319
|
idBase={domIds.control}
|
|
301
320
|
labelledBy={domIds.label}
|
|
302
321
|
{describedBy}
|
|
322
|
+
{contentAt}
|
|
303
323
|
onCommit={onCommitScalar}
|
|
304
324
|
/>
|
|
305
325
|
{:else}
|
|
@@ -1,8 +1,9 @@
|
|
|
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
|
-
|
|
3
|
+
committing the whole object by value on any nested change. Leaf properties: a
|
|
4
|
+
scalar draws its own control, a content property mounts the by-value prose leaf
|
|
5
|
+
({@link ProseValue}) over the boundary's nested read, and a nested `array`/`object`
|
|
6
|
+
property renders a placeholder instead of recursing.
|
|
6
7
|
|
|
7
8
|
The nesting is a vertical at `--_qm-border`, the ladder's last stroke: the card's edge,
|
|
8
9
|
an open section's vertical one `--_qm-nest` in where the field is in one, this one a rung
|
|
@@ -16,9 +17,15 @@
|
|
|
16
17
|
resolved provenance the top-level ghosts read (FIELD_PROVENANCE): `resolve`
|
|
17
18
|
carries no per-property row (an object field resolves as one row whose value is
|
|
18
19
|
the whole object).
|
|
20
|
+
|
|
21
|
+
Every caller hands down `contentAt`, which is `reader.getContentAt` with the field
|
|
22
|
+
already bound: a field-level subform passes the property key through untouched, a
|
|
23
|
+
variant passes its cell's, and an array's open row prefixes its index. So a cell
|
|
24
|
+
reads at whatever depth it sits, which nothing here knows.
|
|
19
25
|
-->
|
|
20
26
|
<script lang="ts">
|
|
21
|
-
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
27
|
+
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
28
|
+
import { emptyContent } from '../core/codec/index.js';
|
|
22
29
|
import { controlKind, humanize, obliged } from './structure.js';
|
|
23
30
|
import { wording } from './strings.js';
|
|
24
31
|
import { propertyDomIds } from './domid.js';
|
|
@@ -28,6 +35,7 @@
|
|
|
28
35
|
import NumberField from './NumberField.svelte';
|
|
29
36
|
import BooleanField from './BooleanField.svelte';
|
|
30
37
|
import DateField from './DateField.svelte';
|
|
38
|
+
import ProseValue from './ProseValue.svelte';
|
|
31
39
|
|
|
32
40
|
interface Props {
|
|
33
41
|
value: Record<string, unknown> | undefined;
|
|
@@ -47,9 +55,16 @@
|
|
|
47
55
|
idBase?: string;
|
|
48
56
|
/** Accessible-name prefix used only on the `aria-label` fallback above. */
|
|
49
57
|
label?: string;
|
|
58
|
+
/** The boundary's nested content read, rooted at this subform: `path` is a
|
|
59
|
+
* `PathStep[]` from here to the leaf, which the caller prefixes with whatever
|
|
60
|
+
* stands between this subform and the field (`reader.getContentAt`). Asked only
|
|
61
|
+
* for a content-typed property: on any other the read is not content and throws.
|
|
62
|
+
* `undefined` for a property the stored value does not reach. */
|
|
63
|
+
contentAt: (path: PathStep[]) => Content | undefined;
|
|
50
64
|
onCommit: (obj: Record<string, unknown>) => void;
|
|
51
65
|
}
|
|
52
|
-
let { value, properties, labelledBy, describedBy, idBase, label, onCommit }: Props =
|
|
66
|
+
let { value, properties, labelledBy, describedBy, idBase, label, contentAt, onCommit }: Props =
|
|
67
|
+
$props();
|
|
53
68
|
|
|
54
69
|
const t = wording();
|
|
55
70
|
const entries = $derived(Object.entries(properties ?? {}));
|
|
@@ -68,26 +83,48 @@
|
|
|
68
83
|
`${label != null ? `${label} ` : ''}${title(key, sub)}` +
|
|
69
84
|
(required(sub) ? ` ${t.strings.fieldRequired}` : '');
|
|
70
85
|
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
86
|
+
/** The prose cells' handles, keyed by property name — which is stable, a subform
|
|
87
|
+
* drawing its properties by declaration and never by position. A ref only where
|
|
88
|
+
* the DOM cannot answer: a `contenteditable` matches no focusable selector, and
|
|
89
|
+
* focusing the node is not focusing the view ({@link ProseValue.focus}). A scalar
|
|
90
|
+
* control is resolved off the markup, so a handle each would be five refs
|
|
91
|
+
* restating document order.
|
|
92
|
+
*
|
|
93
|
+
* `$state` for the binding's sake: `bind:this` into a property of a plain object
|
|
94
|
+
* is a write Svelte cannot track, and it says so once per cell per render
|
|
95
|
+
* (`ArrayField` keeps its element refs the same way). */
|
|
96
|
+
const proseEls: Record<string, { focus: () => void } | undefined> = $state({});
|
|
77
97
|
let rootEl = $state<HTMLElement | undefined>();
|
|
78
98
|
const FOCUSABLE = 'input, select, button, [tabindex]:not([tabindex="-1"])';
|
|
99
|
+
/** 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, and a property
|
|
101
|
+
* standing the unsupported line has none either, so the walk steps past it. An
|
|
102
|
+
* empty or wholly unsupported subform lands nothing. */
|
|
79
103
|
export function focus(): void {
|
|
80
|
-
|
|
104
|
+
for (const cell of propCells()) if (landOn(cell)) return;
|
|
81
105
|
}
|
|
82
|
-
/** A label click for the
|
|
83
|
-
* focus lives on a segment
|
|
84
|
-
* where the label is rather than on
|
|
106
|
+
/** A label click for the properties `for` cannot reach: the date control, whose
|
|
107
|
+
* focus lives on a segment, and a prose cell, which is a `contenteditable`. Scoped
|
|
108
|
+
* to that property's own cell, so the click lands where the label is rather than on
|
|
109
|
+
* the subform's first control. */
|
|
85
110
|
function focusProp(key: string): void {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
for (const cell of propCells()) if (cell.dataset.qmProp === key) return void landOn(cell);
|
|
112
|
+
}
|
|
113
|
+
// 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.
|
|
115
|
+
function propCells(): HTMLElement[] {
|
|
116
|
+
return [...(rootEl?.querySelectorAll<HTMLElement>('[data-qm-prop]') ?? [])];
|
|
117
|
+
}
|
|
118
|
+
/** Land in one property's cell, answering whether it took the caret. */
|
|
119
|
+
function landOn(cell: HTMLElement): boolean {
|
|
120
|
+
const prose = proseEls[cell.dataset.qmProp ?? ''];
|
|
121
|
+
if (prose) {
|
|
122
|
+
prose.focus();
|
|
123
|
+
return true;
|
|
90
124
|
}
|
|
125
|
+
const el = cell.querySelector<HTMLElement>(FOCUSABLE);
|
|
126
|
+
el?.focus();
|
|
127
|
+
return !!el;
|
|
91
128
|
}
|
|
92
129
|
|
|
93
130
|
function commitProp(key: string, v: unknown): void {
|
|
@@ -117,8 +154,8 @@
|
|
|
117
154
|
{@const named = ids ? undefined : fallbackName(key, sub)}
|
|
118
155
|
{@const describes = sub.description && ids ? ids.description : undefined}
|
|
119
156
|
<!-- `for` reaches the four labelable controls; the date field's focus lives on a
|
|
120
|
-
segment
|
|
121
|
-
level up. -->
|
|
157
|
+
segment and a prose cell is a `contenteditable`, so those take the click
|
|
158
|
+
handoff instead, exactly as `Field` does one level up. -->
|
|
122
159
|
{@const labelable =
|
|
123
160
|
kind === 'text' || kind === 'enum' || kind === 'number' || kind === 'boolean'}
|
|
124
161
|
<div class="qm-object-prop" data-qm-prop={key}>
|
|
@@ -179,6 +216,18 @@
|
|
|
179
216
|
placeholder={sub.default != null ? String(sub.default) : undefined}
|
|
180
217
|
onCommit={(v) => commitProp(key, v)}
|
|
181
218
|
/>
|
|
219
|
+
{:else if kind === 'prose'}
|
|
220
|
+
<!-- Keyed by name, so a re-derive of the container this cell commits whole
|
|
221
|
+
leaves the leaf mounted and the caret in it. -->
|
|
222
|
+
<ProseValue
|
|
223
|
+
bind:this={proseEls[key]}
|
|
224
|
+
content={() => contentAt([key]) ?? emptyContent()}
|
|
225
|
+
plaintext={sub.type === 'plaintext'}
|
|
226
|
+
label={named}
|
|
227
|
+
labelledBy={ids?.label}
|
|
228
|
+
describedBy={describes}
|
|
229
|
+
onChange={(rt) => commitProp(key, rt)}
|
|
230
|
+
/>
|
|
182
231
|
{:else}
|
|
183
232
|
<!-- A property the subform does not recurse into. What it says is what the
|
|
184
233
|
user can do about it, which is edit the field from the source surface;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
1
|
+
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
2
2
|
interface Props {
|
|
3
3
|
value: Record<string, unknown> | undefined;
|
|
4
4
|
properties: Record<string, QuillFieldSchema> | undefined;
|
|
@@ -17,6 +17,12 @@ interface Props {
|
|
|
17
17
|
idBase?: string;
|
|
18
18
|
/** Accessible-name prefix used only on the `aria-label` fallback above. */
|
|
19
19
|
label?: string;
|
|
20
|
+
/** The boundary's nested content read, rooted at this subform: `path` is a
|
|
21
|
+
* `PathStep[]` from here to the leaf, which the caller prefixes with whatever
|
|
22
|
+
* stands between this subform and the field (`reader.getContentAt`). Asked only
|
|
23
|
+
* for a content-typed property: on any other the read is not content and throws.
|
|
24
|
+
* `undefined` for a property the stored value does not reach. */
|
|
25
|
+
contentAt: (path: PathStep[]) => Content | undefined;
|
|
20
26
|
onCommit: (obj: Record<string, unknown>) => void;
|
|
21
27
|
}
|
|
22
28
|
declare const ObjectField: import("svelte").Component<Props, {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A prose leaf the parent commits by value: an element of a `richtext` (fixture
|
|
3
|
+
`keywords`) or `plaintext` (`errata`) array, and a content cell of a subform — an
|
|
4
|
+
`object`'s property, a variant's cell ({@link ObjectField}). None of them is
|
|
5
|
+
`applyChange`-addressable: `Addr.field` is a flat name, so `keywords[0]` and
|
|
6
|
+
`contact.note` have no op address. So this is not a `createField` leaf: it mounts a
|
|
7
|
+
minimal PM view over the codec's decode/encode + inline schema, and on every edit
|
|
8
|
+
hands the re-encoded `Content` up to the parent — {@link ArrayField}, which commits
|
|
9
|
+
the whole array, or {@link ObjectField}, which commits the whole container
|
|
10
|
+
(`writer.set(field, next)`). Anchors inside a value are dropped on that write:
|
|
11
|
+
acceptable for inline refs. Mounts once per stable key (an element's session id, a
|
|
12
|
+
property's name), so the parent's re-derive leaves the caret where it is.
|
|
13
|
+
|
|
14
|
+
The content is read, not passed: `reader.getContentAt(addr, path)` decodes through
|
|
15
|
+
the codec the leaf's own declared type names, so what it rests as — the content
|
|
16
|
+
object, a `plaintext` literal, the authored string a transport door left — stops
|
|
17
|
+
being the row's or the cell's business. Read once, at mount, since that is when this
|
|
18
|
+
leaf takes its state.
|
|
19
|
+
|
|
20
|
+
One textblock wherever it mounts, no islands and no slash menu. `plaintext` is
|
|
21
|
+
inline at the codec, so that is the declared shape for every plaintext leaf; a block
|
|
22
|
+
`richtext` is held one rung narrower than it declares, having no room for a second
|
|
23
|
+
paragraph in the figure a repeater row and a subform cell draw.
|
|
24
|
+
-->
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import { onMount } from 'svelte';
|
|
27
|
+
import { EditorState, Selection } from 'prosemirror-state';
|
|
28
|
+
import { EditorView } from 'prosemirror-view';
|
|
29
|
+
import {
|
|
30
|
+
decode,
|
|
31
|
+
pmToContent,
|
|
32
|
+
inlineSchema,
|
|
33
|
+
plaintextSchema,
|
|
34
|
+
proseAttributes,
|
|
35
|
+
proseLeafPlugins,
|
|
36
|
+
buildLineIndex,
|
|
37
|
+
usvToPM
|
|
38
|
+
} from '../core/codec/index.js';
|
|
39
|
+
import './controls.css';
|
|
40
|
+
import type { Content } from '@quillmark/wasm';
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
/** This leaf's content, read at mount (the parent's boundary read). A thunk
|
|
44
|
+
* rather than a value: the parent re-derives per revision and this leaf takes
|
|
45
|
+
* its state once, so a value prop would be a boundary read per leaf per render,
|
|
46
|
+
* all but one of them discarded. */
|
|
47
|
+
content: () => Content;
|
|
48
|
+
/** The mark-free schema (a `plaintext` leaf): literal text, no formatting,
|
|
49
|
+
* exactly as the scalar field of that type mounts. */
|
|
50
|
+
plaintext?: boolean;
|
|
51
|
+
/** Accessible name for the editable region, where nothing else names it: an
|
|
52
|
+
* array has no per-element label. A cell with a label element takes
|
|
53
|
+
* `labelledBy`. */
|
|
54
|
+
label?: string;
|
|
55
|
+
/** The label's own id → `aria-labelledby`. `for` cannot reach a
|
|
56
|
+
* `contenteditable`, so a subform cell's label associates the other way and
|
|
57
|
+
* hands its click back through {@link focus}. */
|
|
58
|
+
labelledBy?: string;
|
|
59
|
+
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
60
|
+
describedBy?: string;
|
|
61
|
+
onChange: (rt: Content) => void;
|
|
62
|
+
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
63
|
+
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
64
|
+
* keymap, so the state it reads is the one this keystroke has yet to change. */
|
|
65
|
+
onKey?: (e: KeyboardEvent) => void;
|
|
66
|
+
}
|
|
67
|
+
let {
|
|
68
|
+
content,
|
|
69
|
+
plaintext = false,
|
|
70
|
+
label,
|
|
71
|
+
labelledBy,
|
|
72
|
+
describedBy,
|
|
73
|
+
onChange,
|
|
74
|
+
onKey
|
|
75
|
+
}: Props = $props();
|
|
76
|
+
|
|
77
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
78
|
+
let view: EditorView | undefined;
|
|
79
|
+
/** Take the caret: what a parent placing focus on this leaf calls. The view's
|
|
80
|
+
* focus, not the element's: a PM view restores its selection, where a bare DOM
|
|
81
|
+
* focus on a contenteditable leaves the caret unplaced. And the reveal beside it,
|
|
82
|
+
* since PM's focus prevents the scroll a `string` element's input takes by
|
|
83
|
+
* default — an array whose two item types revealed differently is one row of it
|
|
84
|
+
* landing off screen. */
|
|
85
|
+
export function focus(): void {
|
|
86
|
+
if (!view) return;
|
|
87
|
+
view.focus();
|
|
88
|
+
view.dom.scrollIntoView({ block: 'nearest' });
|
|
89
|
+
}
|
|
90
|
+
/** Take the caret to USV `pos` in this leaf's own content: what a landing on an
|
|
91
|
+
* element address resolves to when the compile answered a cluster-exact offset
|
|
92
|
+
* (`leaves.ts`). `createField`'s body over the view this component owns.
|
|
93
|
+
*
|
|
94
|
+
* The index is built here rather than held: a landing is a discrete act, and one
|
|
95
|
+
* doc walk per click is cheaper than a cache to invalidate per keystroke. `usvToPM`
|
|
96
|
+
* clamps, so an offset past a value the compile has moved on from lands the end.
|
|
97
|
+
*
|
|
98
|
+
* `Selection.near`, not `TextSelection.create`: the mapped position can be
|
|
99
|
+
* non-inline. Focus first, then the caret, flagged for scroll — the order and the
|
|
100
|
+
* reasons are `FieldController.setCaret`'s (`codec/field.ts`). */
|
|
101
|
+
export function setCaret(pos: number): void {
|
|
102
|
+
if (!view) return;
|
|
103
|
+
const pm = usvToPM(buildLineIndex(view.state.doc), pos);
|
|
104
|
+
const sel = Selection.near(view.state.doc.resolve(pm));
|
|
105
|
+
view.focus();
|
|
106
|
+
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onMount(() => {
|
|
110
|
+
if (!containerEl) return;
|
|
111
|
+
// An inline, value-by-value leaf: the same keymap and plugin stack a
|
|
112
|
+
// `createField` leaf mounts (shared `proseLeafPlugins`), minus the
|
|
113
|
+
// anchor-position plugin (anchors are dropped on the parent's value write, per
|
|
114
|
+
// the header). Its own `dispatchTransaction` hands `Content` up.
|
|
115
|
+
//
|
|
116
|
+
// The schema is the declared type's, as `createField` picks it: `plaintext`
|
|
117
|
+
// declares no mark types, so there is nothing to toggle, to paste in, or to
|
|
118
|
+
// mint a shorthand with.
|
|
119
|
+
const schema = plaintext ? plaintextSchema : inlineSchema;
|
|
120
|
+
const pmDoc = decode(content(), schema);
|
|
121
|
+
const state = EditorState.create({
|
|
122
|
+
doc: pmDoc,
|
|
123
|
+
plugins: proseLeafPlugins(schema, { inline: true })
|
|
124
|
+
});
|
|
125
|
+
const mounted = new EditorView(containerEl, {
|
|
126
|
+
state,
|
|
127
|
+
// Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
|
|
128
|
+
// (`proseAttributes`), so a cell carrying a label element and a row carrying
|
|
129
|
+
// none cannot name their regions by different rules.
|
|
130
|
+
attributes: proseAttributes({ label, labelledBy, describedBy }),
|
|
131
|
+
dispatchTransaction(tr) {
|
|
132
|
+
const next = mounted.state.apply(tr);
|
|
133
|
+
mounted.updateState(next);
|
|
134
|
+
if (tr.docChanged) onChange(pmToContent(next.doc));
|
|
135
|
+
},
|
|
136
|
+
handleDOMEvents: {
|
|
137
|
+
keydown: (_v, e) => {
|
|
138
|
+
onKey?.(e);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
view = mounted;
|
|
144
|
+
return () => {
|
|
145
|
+
view = undefined;
|
|
146
|
+
mounted.destroy();
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<!-- `.qm-control-box` (controls.css) is the whole box, so an array of `richtext` and
|
|
152
|
+
an array of `string` render rows of equal height, and a subform's prose cell measures
|
|
153
|
+
like the text cell beside it. No floor: the reset in `core/codec/prose.css` makes one
|
|
154
|
+
line of prose measure one line. Width is the row's or the cell's to give: the leaf
|
|
155
|
+
fills the track it is placed in. -->
|
|
156
|
+
<div bind:this={containerEl} class="qm-prose-value qm-control-box qm-focus-ring-within"></div>
|
|
157
|
+
|
|
158
|
+
<style>
|
|
159
|
+
/* Caret-primary, matching ProseField: the contenteditable's own outline is
|
|
160
|
+
dropped and the ring rides the wrapper (`qm-focus-ring-within`,
|
|
161
|
+
controls.css), which is where the box is. */
|
|
162
|
+
.qm-prose-value :global(.ProseMirror) {
|
|
163
|
+
outline: none;
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import './controls.css';
|
|
2
|
+
import type { Content } from '@quillmark/wasm';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** This leaf's content, read at mount (the parent's boundary read). A thunk
|
|
5
|
+
* rather than a value: the parent re-derives per revision and this leaf takes
|
|
6
|
+
* its state once, so a value prop would be a boundary read per leaf per render,
|
|
7
|
+
* all but one of them discarded. */
|
|
8
|
+
content: () => Content;
|
|
9
|
+
/** The mark-free schema (a `plaintext` leaf): literal text, no formatting,
|
|
10
|
+
* exactly as the scalar field of that type mounts. */
|
|
11
|
+
plaintext?: boolean;
|
|
12
|
+
/** Accessible name for the editable region, where nothing else names it: an
|
|
13
|
+
* array has no per-element label. A cell with a label element takes
|
|
14
|
+
* `labelledBy`. */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** The label's own id → `aria-labelledby`. `for` cannot reach a
|
|
17
|
+
* `contenteditable`, so a subform cell's label associates the other way and
|
|
18
|
+
* hands its click back through {@link focus}. */
|
|
19
|
+
labelledBy?: string;
|
|
20
|
+
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
21
|
+
describedBy?: string;
|
|
22
|
+
onChange: (rt: Content) => void;
|
|
23
|
+
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
24
|
+
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
25
|
+
* keymap, so the state it reads is the one this keystroke has yet to change. */
|
|
26
|
+
onKey?: (e: KeyboardEvent) => void;
|
|
27
|
+
}
|
|
28
|
+
declare const ProseValue: import("svelte").Component<Props, {
|
|
29
|
+
focus: () => void;
|
|
30
|
+
setCaret: (pos: number) => void;
|
|
31
|
+
}, "">;
|
|
32
|
+
type ProseValue = ReturnType<typeof ProseValue>;
|
|
33
|
+
export default ProseValue;
|
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
a container, `{value: <member>, …that member's fields}`, and commits whole: it is one
|
|
5
5
|
cell to `Addr`, so there is no per-cell write address.
|
|
6
6
|
|
|
7
|
-
The cells are {@link ObjectField}, which already draws a
|
|
7
|
+
The cells are {@link ObjectField}, which already draws a leaf field set keyed by
|
|
8
8
|
name over a container its parent commits by value. It takes the whole container as its
|
|
9
9
|
`value` and the world's declaration as its `properties`, so what it hands back is the
|
|
10
10
|
container with one cell written — and cells outside the drawn world ride through
|
|
11
|
-
untouched rather than needing to be merged back.
|
|
11
|
+
untouched rather than needing to be merged back. A content cell reads at the key it
|
|
12
|
+
declares: the boundary's schema walk unions the worlds, so a dormant cell reads absent
|
|
13
|
+
rather than raising.
|
|
12
14
|
-->
|
|
13
15
|
<script lang="ts">
|
|
14
|
-
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
16
|
+
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
15
17
|
import EnumField from './EnumField.svelte';
|
|
16
18
|
import ObjectField from './ObjectField.svelte';
|
|
17
19
|
import {
|
|
@@ -34,6 +36,10 @@
|
|
|
34
36
|
id?: string;
|
|
35
37
|
labelledBy?: string;
|
|
36
38
|
describedBy?: string;
|
|
39
|
+
/** The boundary's nested content read for this field, which the cells take
|
|
40
|
+
* unprefixed: a variant's cell is one key in from the container it rests as, so
|
|
41
|
+
* a `plaintext` cell's path is its own name ({@link ObjectField}). */
|
|
42
|
+
contentAt: (path: PathStep[]) => Content | undefined;
|
|
37
43
|
onCommit: (v: Record<string, unknown> | undefined) => void;
|
|
38
44
|
optionAllowed?: (value: string) => boolean;
|
|
39
45
|
enumDisallowed?: 'hide' | 'disable';
|
|
@@ -46,6 +52,7 @@
|
|
|
46
52
|
id,
|
|
47
53
|
labelledBy,
|
|
48
54
|
describedBy,
|
|
55
|
+
contentAt,
|
|
49
56
|
onCommit,
|
|
50
57
|
optionAllowed,
|
|
51
58
|
enumDisallowed
|
|
@@ -80,6 +87,7 @@
|
|
|
80
87
|
idBase={id}
|
|
81
88
|
{labelledBy}
|
|
82
89
|
{describedBy}
|
|
90
|
+
{contentAt}
|
|
83
91
|
onCommit={(obj) => onCommit(obj)}
|
|
84
92
|
/>
|
|
85
93
|
{/key}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
1
|
+
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
2
2
|
interface Props {
|
|
3
3
|
/** The stored container, or undefined while the field is unset. */
|
|
4
4
|
value: Record<string, unknown> | undefined;
|
|
@@ -12,6 +12,10 @@ interface Props {
|
|
|
12
12
|
id?: string;
|
|
13
13
|
labelledBy?: string;
|
|
14
14
|
describedBy?: string;
|
|
15
|
+
/** The boundary's nested content read for this field, which the cells take
|
|
16
|
+
* unprefixed: a variant's cell is one key in from the container it rests as, so
|
|
17
|
+
* a `plaintext` cell's path is its own name ({@link ObjectField}). */
|
|
18
|
+
contentAt: (path: PathStep[]) => Content | undefined;
|
|
15
19
|
onCommit: (v: Record<string, unknown> | undefined) => void;
|
|
16
20
|
optionAllowed?: (value: string) => boolean;
|
|
17
21
|
enumDisallowed?: 'hide' | 'disable';
|
package/dist/visual/strings.d.ts
CHANGED
|
@@ -25,9 +25,10 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
25
25
|
* ghosts ({@link EnumField}). */
|
|
26
26
|
enumUnsetTag: string;
|
|
27
27
|
/**
|
|
28
|
-
* What stands where a subform declines to recurse
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* What stands where a subform declines to recurse — a nested `array` or `object`
|
|
29
|
+
* property, whose own figure would nest a field past the depth this one draws —
|
|
30
|
+
* and where to edit that property instead. The whole sentence, since a translator
|
|
31
|
+
* orders the kind within it.
|
|
31
32
|
*/
|
|
32
33
|
nestedUnsupported: (kind: string) => string;
|
|
33
34
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/svelte",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@quillmark/wasm": ">=0.108.
|
|
69
|
+
"@quillmark/wasm": ">=0.108.1-0",
|
|
70
70
|
"svelte": "^5.0.0"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
One element of an array of `richtext` (fixture `keywords`) or `plaintext`
|
|
3
|
-
(`errata`). Array elements are not `applyChange`-addressable: `Addr.field` is a
|
|
4
|
-
flat name, so `keywords[0]` has no op address. So
|
|
5
|
-
this is not a `createField` leaf: it mounts a minimal PM view over the codec's
|
|
6
|
-
decode/encode + inline schema, and on every edit hands the re-encoded
|
|
7
|
-
`Content` up to the parent {@link ArrayField}, which commits the whole array
|
|
8
|
-
by value (`writer.set(field, arrayWithElementReplaced)`). Anchors within an
|
|
9
|
-
element are dropped on that value write: acceptable for inline refs. Mounts
|
|
10
|
-
once per stable element id (no reset on the parent's re-derive).
|
|
11
|
-
|
|
12
|
-
The content is read, not passed: `reader.getContentAt(addr, [k])` decodes an
|
|
13
|
-
element through the codec its `items` type declares, so what the element rests
|
|
14
|
-
as — the content object, a `plaintext` literal, the authored string a transport
|
|
15
|
-
door left — stops being this row's business. Read once, at mount, since that is
|
|
16
|
-
when this leaf takes its state.
|
|
17
|
-
-->
|
|
18
|
-
<script lang="ts">
|
|
19
|
-
import { onMount } from 'svelte';
|
|
20
|
-
import { EditorState, Selection } from 'prosemirror-state';
|
|
21
|
-
import { EditorView } from 'prosemirror-view';
|
|
22
|
-
import {
|
|
23
|
-
decode,
|
|
24
|
-
pmToContent,
|
|
25
|
-
inlineSchema,
|
|
26
|
-
plaintextSchema,
|
|
27
|
-
proseLeafPlugins,
|
|
28
|
-
buildLineIndex,
|
|
29
|
-
usvToPM
|
|
30
|
-
} from '../core/codec/index.js';
|
|
31
|
-
import './controls.css';
|
|
32
|
-
import type { Content } from '@quillmark/wasm';
|
|
33
|
-
|
|
34
|
-
interface Props {
|
|
35
|
-
/** This element's content, read at mount (`ArrayField`, through the boundary's
|
|
36
|
-
* element read). A thunk rather than a value: the parent re-derives per
|
|
37
|
-
* revision and this leaf takes its state once, so a value prop would be a
|
|
38
|
-
* boundary read per row per render, all but one of them discarded. */
|
|
39
|
-
content: () => Content;
|
|
40
|
-
/** The mark-free schema (a `plaintext` item): literal text, no formatting,
|
|
41
|
-
* exactly as the scalar field of that type mounts. */
|
|
42
|
-
plaintext?: boolean;
|
|
43
|
-
/** Accessible name for the editable region (the array has no per-element label). */
|
|
44
|
-
label?: string;
|
|
45
|
-
onChange: (rt: Content) => void;
|
|
46
|
-
/** Raw keydown, for a container whose own keys run through this element: the
|
|
47
|
-
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
48
|
-
* keymap, so the state it reads is the one this keystroke has yet to change. */
|
|
49
|
-
onKey?: (e: KeyboardEvent) => void;
|
|
50
|
-
}
|
|
51
|
-
let { content, plaintext = false, label, onChange, onKey }: Props = $props();
|
|
52
|
-
|
|
53
|
-
let containerEl: HTMLDivElement | undefined = $state();
|
|
54
|
-
let view: EditorView | undefined;
|
|
55
|
-
/** Take the caret: what a parent placing focus on this element calls. The view's
|
|
56
|
-
* focus, not the element's: a PM view restores its selection, where a bare DOM
|
|
57
|
-
* focus on a contenteditable leaves the caret unplaced. And the reveal beside it,
|
|
58
|
-
* since PM's focus prevents the scroll a `string` element's input takes by
|
|
59
|
-
* default — an array whose two item types revealed differently is one row of it
|
|
60
|
-
* landing off screen. */
|
|
61
|
-
export function focus(): void {
|
|
62
|
-
if (!view) return;
|
|
63
|
-
view.focus();
|
|
64
|
-
view.dom.scrollIntoView({ block: 'nearest' });
|
|
65
|
-
}
|
|
66
|
-
/** Take the caret to USV `pos` in this element's own content: what a landing on an
|
|
67
|
-
* element address resolves to when the compile answered a cluster-exact offset
|
|
68
|
-
* (`leaves.ts`). `createField`'s body over the view this component owns.
|
|
69
|
-
*
|
|
70
|
-
* The index is built here rather than held: a landing is a discrete act, and one
|
|
71
|
-
* doc walk per click is cheaper than a cache to invalidate per keystroke. `usvToPM`
|
|
72
|
-
* clamps, so an offset past a value the compile has moved on from lands the end.
|
|
73
|
-
*
|
|
74
|
-
* `Selection.near`, not `TextSelection.create`: the mapped position can be
|
|
75
|
-
* non-inline. Focus first, then the caret, flagged for scroll — the order and the
|
|
76
|
-
* reasons are `FieldController.setCaret`'s (`codec/field.ts`). */
|
|
77
|
-
export function setCaret(pos: number): void {
|
|
78
|
-
if (!view) return;
|
|
79
|
-
const pm = usvToPM(buildLineIndex(view.state.doc), pos);
|
|
80
|
-
const sel = Selection.near(view.state.doc.resolve(pm));
|
|
81
|
-
view.focus();
|
|
82
|
-
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
onMount(() => {
|
|
86
|
-
if (!containerEl) return;
|
|
87
|
-
// An array element is an inline, value-by-value leaf: the same keymap and
|
|
88
|
-
// plugin stack a `createField` leaf mounts (shared `proseLeafPlugins`), minus
|
|
89
|
-
// the anchor-position plugin (element anchors are dropped on the array's value
|
|
90
|
-
// write, per the header). Its own `dispatchTransaction` hands `Content` up.
|
|
91
|
-
//
|
|
92
|
-
// The schema is the declared type's, as `createField` picks it: `plaintext`
|
|
93
|
-
// declares no mark types, so there is nothing to toggle, to paste in, or to
|
|
94
|
-
// mint a shorthand with.
|
|
95
|
-
const schema = plaintext ? plaintextSchema : inlineSchema;
|
|
96
|
-
const pmDoc = decode(content(), schema);
|
|
97
|
-
const state = EditorState.create({
|
|
98
|
-
doc: pmDoc,
|
|
99
|
-
plugins: proseLeafPlugins(schema, { inline: true })
|
|
100
|
-
});
|
|
101
|
-
const mounted = new EditorView(containerEl, {
|
|
102
|
-
state,
|
|
103
|
-
// Names the `contenteditable` for assistive tech (the array row has no
|
|
104
|
-
// label element to associate).
|
|
105
|
-
attributes: label ? { 'aria-label': label } : undefined,
|
|
106
|
-
dispatchTransaction(tr) {
|
|
107
|
-
const next = mounted.state.apply(tr);
|
|
108
|
-
mounted.updateState(next);
|
|
109
|
-
if (tr.docChanged) onChange(pmToContent(next.doc));
|
|
110
|
-
},
|
|
111
|
-
handleDOMEvents: {
|
|
112
|
-
keydown: (_v, e) => {
|
|
113
|
-
onKey?.(e);
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
view = mounted;
|
|
119
|
-
return () => {
|
|
120
|
-
view = undefined;
|
|
121
|
-
mounted.destroy();
|
|
122
|
-
};
|
|
123
|
-
});
|
|
124
|
-
</script>
|
|
125
|
-
|
|
126
|
-
<!-- `.qm-control-box` (controls.css) is the whole box, so an array of `richtext` and
|
|
127
|
-
an array of `string` render rows of equal height. No floor: the reset in
|
|
128
|
-
`core/codec/prose.css` makes one line of prose measure one line. Width is the
|
|
129
|
-
row's to give: the element fills the track it is placed in (`ArrayField`). -->
|
|
130
|
-
<div bind:this={containerEl} class="qm-array-prose qm-control-box qm-focus-ring-within"></div>
|
|
131
|
-
|
|
132
|
-
<style>
|
|
133
|
-
/* Caret-primary, matching ProseField: the contenteditable's own outline is
|
|
134
|
-
dropped and the ring rides the wrapper (`qm-focus-ring-within`,
|
|
135
|
-
controls.css), which is where the box is. */
|
|
136
|
-
.qm-array-prose :global(.ProseMirror) {
|
|
137
|
-
outline: none;
|
|
138
|
-
}
|
|
139
|
-
</style>
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import './controls.css';
|
|
2
|
-
import type { Content } from '@quillmark/wasm';
|
|
3
|
-
interface Props {
|
|
4
|
-
/** This element's content, read at mount (`ArrayField`, through the boundary's
|
|
5
|
-
* element read). A thunk rather than a value: the parent re-derives per
|
|
6
|
-
* revision and this leaf takes its state once, so a value prop would be a
|
|
7
|
-
* boundary read per row per render, all but one of them discarded. */
|
|
8
|
-
content: () => Content;
|
|
9
|
-
/** The mark-free schema (a `plaintext` item): literal text, no formatting,
|
|
10
|
-
* exactly as the scalar field of that type mounts. */
|
|
11
|
-
plaintext?: boolean;
|
|
12
|
-
/** Accessible name for the editable region (the array has no per-element label). */
|
|
13
|
-
label?: string;
|
|
14
|
-
onChange: (rt: Content) => void;
|
|
15
|
-
/** Raw keydown, for a container whose own keys run through this element: the
|
|
16
|
-
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
17
|
-
* keymap, so the state it reads is the one this keystroke has yet to change. */
|
|
18
|
-
onKey?: (e: KeyboardEvent) => void;
|
|
19
|
-
}
|
|
20
|
-
declare const ProseArrayElement: import("svelte").Component<Props, {
|
|
21
|
-
focus: () => void;
|
|
22
|
-
setCaret: (pos: number) => void;
|
|
23
|
-
}, "">;
|
|
24
|
-
type ProseArrayElement = ReturnType<typeof ProseArrayElement>;
|
|
25
|
-
export default ProseArrayElement;
|