@quillmark/svelte 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -2
- package/dist/core/codec/blocks.d.ts +11 -2
- package/dist/core/codec/blocks.js +45 -14
- package/dist/core/codec/breaks.d.ts +3 -2
- package/dist/core/codec/breaks.js +3 -2
- package/dist/core/codec/decode.d.ts +7 -3
- package/dist/core/codec/decode.js +34 -8
- package/dist/core/codec/field.d.ts +11 -5
- package/dist/core/codec/field.js +11 -8
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +1 -1
- package/dist/core/codec/islands.d.ts +3 -1
- package/dist/core/codec/islands.js +8 -5
- package/dist/core/codec/markdown-paste.d.ts +9 -0
- package/dist/core/codec/markdown-paste.js +49 -0
- package/dist/core/codec/prose.css +109 -1
- package/dist/core/codec/schema.d.ts +8 -2
- package/dist/core/codec/schema.js +16 -5
- package/dist/core/codec/slash.d.ts +9 -6
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-paste.d.ts +7 -0
- package/dist/core/codec/table-paste.js +132 -0
- package/dist/core/codec/table-view.d.ts +12 -0
- package/dist/core/codec/table-view.js +401 -57
- package/dist/core/codec/table.d.ts +38 -16
- package/dist/core/codec/table.js +106 -35
- package/dist/core/theme.css +1 -1
- package/dist/visual/ArrayField.svelte +17 -6
- package/dist/visual/ArrayField.svelte.d.ts +1 -1
- package/dist/visual/Field.svelte +9 -3
- package/dist/visual/Field.svelte.d.ts +1 -1
- package/dist/visual/FieldHint.svelte +62 -10
- package/dist/visual/FormatPopover.svelte +27 -31
- package/dist/visual/FormatPopover.svelte.d.ts +2 -2
- package/dist/visual/MatrixAdd.svelte +13 -132
- package/dist/visual/MatrixAdd.svelte.d.ts +2 -5
- package/dist/visual/MatrixField.svelte +5 -63
- package/dist/visual/MatrixField.svelte.d.ts +0 -3
- package/dist/visual/ObjectField.svelte +8 -4
- package/dist/visual/ObjectField.svelte.d.ts +4 -2
- package/dist/visual/ProseField.svelte +2 -1
- package/dist/visual/ProseValue.svelte +113 -40
- package/dist/visual/ProseValue.svelte.d.ts +8 -4
- package/dist/visual/VisualEditorInner.svelte +22 -22
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -2
- package/dist/visual/checklists.d.ts +0 -79
- package/dist/visual/checklists.js +0 -164
- package/dist/visual/guidance.d.ts +15 -0
- package/dist/visual/guidance.js +37 -0
- package/dist/visual/leaves.d.ts +27 -1
- package/dist/visual/leaves.js +46 -0
- package/dist/visual/strings.d.ts +3 -8
- package/dist/visual/strings.js +0 -3
- package/dist/visual/structure.js +1 -1
- package/dist/visual/tips.js +1 -1
- package/package.json +2 -2
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
The formatting selection popover (VISUAL_EDITOR §Chrome). One popover,
|
|
3
3
|
shell-owned, observing the active leaf through
|
|
4
4
|
`getActiveLeaf`: the VisualEditor's accessor over its `leaves` registry, and
|
|
5
|
-
the whole of what this surface knows about the editor.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
(
|
|
5
|
+
the whole of what this surface knows about the editor. The active leaf is a
|
|
6
|
+
field's or a by-value one (an array's element, a subform's cell): its views either
|
|
7
|
+
way, its controller only for a field's. A non-empty selection in the active leaf
|
|
8
|
+
raises it over that leaf's selection rect; each button dispatches a PM `toggleMark`
|
|
9
|
+
command straight at the leaf's `EditorView`, whose own `dispatchTransaction` commits
|
|
10
|
+
the result as it commits a keystroke: a field's lowers it to `markOps` through
|
|
11
|
+
`applyChange` (field.ts), a by-value leaf's hands the re-encoded value to its parent
|
|
12
|
+
(`ProseValue`). This component never touches the content. The keymap mirror
|
|
13
|
+
(Mod-b/i/u) already lives in the codec's keymap (field.ts); this is the pointer
|
|
14
|
+
affordance for all six marks.
|
|
13
15
|
|
|
14
|
-
Selection observation. The view's `dispatchTransaction` is the
|
|
16
|
+
Selection observation. The view's `dispatchTransaction` is the leaf's
|
|
15
17
|
(read-only), so there is no hook to observe transactions from outside.
|
|
16
18
|
Fallback: a document-level `selectionchange` listener, coalesced to one
|
|
17
19
|
`requestAnimationFrame`-deferred check (see `deferredSync` below: a bare
|
|
@@ -65,21 +67,14 @@
|
|
|
65
67
|
import { Popover } from 'bits-ui';
|
|
66
68
|
import Icon from './icons/Icon.svelte';
|
|
67
69
|
import type { IconName } from './icons/nodes.js';
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
rangeAnchor,
|
|
71
|
-
type FieldController,
|
|
72
|
-
type LeafViews,
|
|
73
|
-
type RangeAnchor
|
|
74
|
-
} from '../core/codec/index.js';
|
|
70
|
+
import { hasMarks, rangeAnchor, type RangeAnchor } from '../core/codec/index.js';
|
|
71
|
+
import type { ActiveProse } from './leaves.js';
|
|
75
72
|
import { clearLink, hrefInSelection, normalizeHref, setLink } from './links.js';
|
|
76
73
|
import './controls.css';
|
|
77
74
|
|
|
78
|
-
type LeafWithView = FieldController & Partial<LeafViews>;
|
|
79
|
-
|
|
80
75
|
interface Props {
|
|
81
76
|
/** The observation seam VisualEditor exposes over its `leaves` registry. */
|
|
82
|
-
getActiveLeaf: () =>
|
|
77
|
+
getActiveLeaf: () => ActiveProse | undefined;
|
|
83
78
|
}
|
|
84
79
|
let { getActiveLeaf }: Props = $props();
|
|
85
80
|
|
|
@@ -99,8 +94,8 @@
|
|
|
99
94
|
let open = $state(false);
|
|
100
95
|
/** A pointer is down outside this surface: the selection is still being made. */
|
|
101
96
|
let pressed = false;
|
|
102
|
-
/** Whether the selection
|
|
103
|
-
*
|
|
97
|
+
/** Whether an anchor can be minted over the selection: in a field's own view, never
|
|
98
|
+
* in a table cell or a by-value leaf (see `sync`). */
|
|
104
99
|
let anchorAvailable = $state(true);
|
|
105
100
|
let linkPromptOpen = $state(false);
|
|
106
101
|
/** Whether the selection carried a link when the prompt was raised. */
|
|
@@ -122,8 +117,7 @@
|
|
|
122
117
|
* too, so a selection in a cell raises the same popover and toggles the same
|
|
123
118
|
* commands; only `anchor` is withheld (below). */
|
|
124
119
|
function activeLeafView(): EditorView | undefined {
|
|
125
|
-
|
|
126
|
-
return leaf?.focusedView?.() ?? leaf?.view;
|
|
120
|
+
return getActiveLeaf()?.views.focusedView();
|
|
127
121
|
}
|
|
128
122
|
|
|
129
123
|
function sync(): void {
|
|
@@ -134,8 +128,8 @@
|
|
|
134
128
|
// returns early, so a surface frozen here still tracks the selection it covers.
|
|
135
129
|
if (insidePopover) return;
|
|
136
130
|
if (pressed) return;
|
|
137
|
-
const leaf = getActiveLeaf()
|
|
138
|
-
const view = leaf?.focusedView
|
|
131
|
+
const leaf = getActiveLeaf();
|
|
132
|
+
const view = leaf?.views.focusedView();
|
|
139
133
|
// A non-empty text selection. A node selection is non-empty too and has nothing
|
|
140
134
|
// to format: it covers a leaf block (an island, a rule), which is exactly where
|
|
141
135
|
// Escape out of a table cell lands.
|
|
@@ -160,11 +154,13 @@
|
|
|
160
154
|
portalTarget = view.dom.closest<HTMLElement>('[data-qm-root]') ?? undefined;
|
|
161
155
|
const { from, to } = view.state.selection;
|
|
162
156
|
anchor = rangeAnchor(view, from, to);
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
157
|
+
// An anchor is minted through a field's controller, which a by-value leaf has
|
|
158
|
+
// none of: its anchors are dropped on the parent's value write. A selection inside
|
|
159
|
+
// a table cell is not in the field's coordinate space at all: the field's position
|
|
160
|
+
// map holds one `atom` run for the whole island, so there is no USV offset to mint
|
|
161
|
+
// an anchor at. The button is withheld rather than disabled, because what it would
|
|
162
|
+
// toggle does not exist there (CODEC §Islands).
|
|
163
|
+
anchorAvailable = !!leaf?.controller && view === leaf.views.view;
|
|
168
164
|
open = true;
|
|
169
165
|
}
|
|
170
166
|
|
|
@@ -283,7 +279,7 @@
|
|
|
283
279
|
* the content, its chrome awaiting comment-thread UX.
|
|
284
280
|
*/
|
|
285
281
|
function toggleAnchor(): void {
|
|
286
|
-
const leaf = getActiveLeaf();
|
|
282
|
+
const leaf = getActiveLeaf()?.controller;
|
|
287
283
|
const view = activeLeafView();
|
|
288
284
|
if (!leaf || !view || view.state.selection.empty) return;
|
|
289
285
|
const { from, to } = leaf.selectionRange();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ActiveProse } from './leaves.js';
|
|
2
2
|
interface Props {
|
|
3
3
|
/** The observation seam VisualEditor exposes over its `leaves` registry. */
|
|
4
|
-
getActiveLeaf: () =>
|
|
4
|
+
getActiveLeaf: () => ActiveProse | undefined;
|
|
5
5
|
}
|
|
6
6
|
declare const FormatPopover: import("svelte").Component<Props, {}, "">;
|
|
7
7
|
type FormatPopover = ReturnType<typeof FormatPopover>;
|
|
@@ -1,76 +1,38 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
An open matrix's add box (VISUAL_EDITOR §"The matrix"): a
|
|
3
|
-
what
|
|
4
|
-
item they would add to this one. The list stands in flow under the input rather than
|
|
5
|
-
floating, so a closing group panel clips nothing of it, and it closes while the focus
|
|
6
|
-
is elsewhere. The input keeps the caret throughout and the active option is
|
|
7
|
-
`aria-activedescendant`, the combobox pattern: ↑/↓ walk, Enter picks, Escape clears.
|
|
8
|
-
Unwalked, the active option is the first result the words name outright, else the add
|
|
9
|
-
option: a result named only by abbreviation, which may stand in another card, is
|
|
10
|
-
taken by an arrow or a press and never by Enter alone.
|
|
2
|
+
An open matrix's add box (VISUAL_EDITOR §"The matrix"): a plain text input. Enter adds
|
|
3
|
+
what was typed, trimmed, and clears the box; Escape clears it.
|
|
11
4
|
-->
|
|
12
5
|
<script lang="ts">
|
|
13
6
|
import Icon from './icons/Icon.svelte';
|
|
14
|
-
import type { AddOption } from './checklists.js';
|
|
15
7
|
import './controls.css';
|
|
16
8
|
|
|
17
9
|
interface Props {
|
|
18
|
-
/** The input's id, and the base its listbox and options derive theirs from. */
|
|
19
10
|
id: string;
|
|
20
11
|
/** The input's accessible name and its words at rest. */
|
|
21
12
|
placeholder: string;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
onChoose: (option: AddOption, query: string) => void;
|
|
13
|
+
/** Add an item titled `title`, never empty. */
|
|
14
|
+
onAdd: (title: string) => void;
|
|
25
15
|
}
|
|
26
|
-
let { id, placeholder,
|
|
16
|
+
let { id, placeholder, onAdd }: Props = $props();
|
|
27
17
|
|
|
28
|
-
let
|
|
29
|
-
/** The walked option, `undefined` until an arrow or the pointer moves it. */
|
|
30
|
-
let walked = $state<number | undefined>();
|
|
31
|
-
let focused = $state(false);
|
|
18
|
+
let typed = $state('');
|
|
32
19
|
let inputEl = $state<HTMLInputElement | undefined>();
|
|
33
|
-
const shown = $derived(query.trim() ? options(query.trim()) : []);
|
|
34
|
-
const open = $derived(focused && shown.length > 0);
|
|
35
|
-
const active = $derived.by(() => {
|
|
36
|
-
if (walked !== undefined) return Math.min(walked, shown.length - 1);
|
|
37
|
-
const strong = shown.findIndex((o) => o.strong);
|
|
38
|
-
if (strong >= 0) return strong;
|
|
39
|
-
const add = shown.findIndex((o) => o.add);
|
|
40
|
-
return add >= 0 ? add : 0;
|
|
41
|
-
});
|
|
42
|
-
const listId = $derived(`${id}-list`);
|
|
43
|
-
const optionId = (k: number): string => `${id}-opt-${k}`;
|
|
44
20
|
|
|
45
21
|
export function focus(): void {
|
|
46
22
|
inputEl?.focus();
|
|
47
23
|
}
|
|
48
24
|
|
|
49
|
-
function choose(k: number): void {
|
|
50
|
-
const option = shown[k];
|
|
51
|
-
if (!option) return;
|
|
52
|
-
const typed = query.trim();
|
|
53
|
-
query = '';
|
|
54
|
-
walked = undefined;
|
|
55
|
-
onChoose(option, typed);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
25
|
function onkeydown(e: KeyboardEvent): void {
|
|
59
26
|
if (e.isComposing) return;
|
|
60
|
-
if (e.key === '
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
walked = (active + 1) % shown.length;
|
|
63
|
-
} else if (e.key === 'ArrowUp' && open) {
|
|
64
|
-
e.preventDefault();
|
|
65
|
-
walked = (active - 1 + shown.length) % shown.length;
|
|
66
|
-
} else if (e.key === 'Enter' && open) {
|
|
27
|
+
if (e.key === 'Enter' && typed.trim()) {
|
|
67
28
|
e.preventDefault();
|
|
68
|
-
|
|
69
|
-
|
|
29
|
+
const title = typed.trim();
|
|
30
|
+
typed = '';
|
|
31
|
+
onAdd(title);
|
|
32
|
+
} else if (e.key === 'Escape' && typed) {
|
|
70
33
|
e.preventDefault();
|
|
71
34
|
e.stopPropagation();
|
|
72
|
-
|
|
73
|
-
walked = undefined;
|
|
35
|
+
typed = '';
|
|
74
36
|
}
|
|
75
37
|
}
|
|
76
38
|
</script>
|
|
@@ -80,67 +42,21 @@
|
|
|
80
42
|
<Icon name="plus" class="qm-matrix-add-glyph" />
|
|
81
43
|
<input
|
|
82
44
|
bind:this={inputEl}
|
|
45
|
+
bind:value={typed}
|
|
83
46
|
{id}
|
|
84
47
|
class="qm-input qm-focus-ring"
|
|
85
48
|
type="text"
|
|
86
|
-
role="combobox"
|
|
87
49
|
autocomplete="off"
|
|
88
50
|
aria-label={placeholder}
|
|
89
|
-
aria-autocomplete="list"
|
|
90
|
-
aria-expanded={open}
|
|
91
|
-
aria-controls={listId}
|
|
92
|
-
aria-activedescendant={open ? optionId(active) : undefined}
|
|
93
51
|
{placeholder}
|
|
94
|
-
value={query}
|
|
95
|
-
oninput={(e) => {
|
|
96
|
-
query = e.currentTarget.value;
|
|
97
|
-
walked = undefined;
|
|
98
|
-
}}
|
|
99
|
-
onfocus={() => (focused = true)}
|
|
100
|
-
onblur={() => (focused = false)}
|
|
101
52
|
{onkeydown}
|
|
102
53
|
/>
|
|
103
54
|
</span>
|
|
104
|
-
<!-- Always mounted, so `aria-controls` names an element at rest too; empty, it draws
|
|
105
|
-
nothing. -->
|
|
106
|
-
<div
|
|
107
|
-
class="qm-menu-surface qm-matrix-add-list"
|
|
108
|
-
role="listbox"
|
|
109
|
-
id={listId}
|
|
110
|
-
aria-label={placeholder}
|
|
111
|
-
hidden={!open}
|
|
112
|
-
>
|
|
113
|
-
{#each shown as option, k (option.key)}
|
|
114
|
-
<!-- The input keeps focus and owns the keys; a press picks without taking it. -->
|
|
115
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
116
|
-
<div
|
|
117
|
-
class={['qm-menu-item qm-matrix-add-option', { add: option.add }]}
|
|
118
|
-
role="option"
|
|
119
|
-
id={optionId(k)}
|
|
120
|
-
tabindex="-1"
|
|
121
|
-
aria-selected={k === active}
|
|
122
|
-
data-highlighted={k === active ? '' : undefined}
|
|
123
|
-
onpointerdown={(e) => e.preventDefault()}
|
|
124
|
-
onpointermove={() => (walked = k)}
|
|
125
|
-
onclick={() => choose(k)}
|
|
126
|
-
>
|
|
127
|
-
{#if option.add}<Icon name="plus" class="qm-matrix-add-glyph" />{/if}
|
|
128
|
-
<span class="qm-matrix-add-title">{option.title}</span>
|
|
129
|
-
{#if option.note}<span class="qm-matrix-add-note">{option.note}</span>{/if}
|
|
130
|
-
</div>
|
|
131
|
-
{/each}
|
|
132
|
-
</div>
|
|
133
55
|
</div>
|
|
134
56
|
|
|
135
57
|
<style>
|
|
136
|
-
/* The box's text, and every option's, starts past the glyph slot at its leading edge,
|
|
137
|
-
so a result reads in the column the typed words stand in and the add option's `+`
|
|
138
|
-
stands under the box's. */
|
|
139
58
|
.qm-matrix-add {
|
|
140
59
|
--_slot: calc(var(--_qm-space-2) * 2 + var(--_qm-glyph-control));
|
|
141
|
-
display: flex;
|
|
142
|
-
flex-direction: column;
|
|
143
|
-
gap: var(--_qm-space-half);
|
|
144
60
|
min-width: 0;
|
|
145
61
|
}
|
|
146
62
|
.qm-matrix-add-field {
|
|
@@ -172,39 +88,4 @@
|
|
|
172
88
|
.qm-matrix-add-field .qm-input:placeholder-shown:not(:focus):hover {
|
|
173
89
|
background: var(--_qm-surface-hover);
|
|
174
90
|
}
|
|
175
|
-
/* The list is a menu, raised over the card rather than cut into it, so its options
|
|
176
|
-
read as choices and not as a second field. Its edge and inset come off each
|
|
177
|
-
option's, which keeps the column. */
|
|
178
|
-
.qm-matrix-add-list {
|
|
179
|
-
display: flex;
|
|
180
|
-
flex-direction: column;
|
|
181
|
-
}
|
|
182
|
-
.qm-matrix-add-list[hidden] {
|
|
183
|
-
display: none;
|
|
184
|
-
}
|
|
185
|
-
.qm-matrix-add-option {
|
|
186
|
-
position: relative;
|
|
187
|
-
display: flex;
|
|
188
|
-
align-items: baseline;
|
|
189
|
-
justify-content: space-between;
|
|
190
|
-
gap: var(--_qm-space-2);
|
|
191
|
-
padding-inline-start: calc(var(--_slot) - var(--_qm-space-half) - var(--_qm-border-width));
|
|
192
|
-
font-size: var(--_qm-text-body);
|
|
193
|
-
color: var(--_qm-ink);
|
|
194
|
-
}
|
|
195
|
-
.qm-matrix-add-option :global(.qm-matrix-add-glyph) {
|
|
196
|
-
inset-inline-start: calc(var(--_qm-space-2) - var(--_qm-space-half) - var(--_qm-border-width));
|
|
197
|
-
inset-block-start: 50%;
|
|
198
|
-
translate: 0 -50%;
|
|
199
|
-
}
|
|
200
|
-
/* The add option reads at the label ink: it offers a new item, where a result names
|
|
201
|
-
one the document already knows. */
|
|
202
|
-
.qm-matrix-add-option.add {
|
|
203
|
-
color: var(--_qm-ink-label);
|
|
204
|
-
}
|
|
205
|
-
.qm-matrix-add-note {
|
|
206
|
-
flex-shrink: 0;
|
|
207
|
-
font-size: var(--_qm-text-label);
|
|
208
|
-
color: var(--_qm-ink-label);
|
|
209
|
-
}
|
|
210
91
|
</style>
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import type { AddOption } from './checklists.js';
|
|
2
1
|
interface Props {
|
|
3
|
-
/** The input's id, and the base its listbox and options derive theirs from. */
|
|
4
2
|
id: string;
|
|
5
3
|
/** The input's accessible name and its words at rest. */
|
|
6
4
|
placeholder: string;
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
onChoose: (option: AddOption, query: string) => void;
|
|
5
|
+
/** Add an item titled `title`, never empty. */
|
|
6
|
+
onAdd: (title: string) => void;
|
|
10
7
|
}
|
|
11
8
|
declare const MatrixAdd: import("svelte").Component<Props, {
|
|
12
9
|
focus: () => void;
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
An open matrix's added items follow the roster in id order, as the plate prints them:
|
|
12
12
|
held by being present, so each draws a fixed tick, its `title` as an input, a remove,
|
|
13
|
-
and the same columns. The add box closes the list ({@link MatrixAdd}):
|
|
14
|
-
|
|
13
|
+
and the same columns. The add box closes the list ({@link MatrixAdd}): what is typed
|
|
14
|
+
there is added under an id minted from it past every id the list holds, so a title the
|
|
15
|
+
list already carries is a second item.
|
|
15
16
|
|
|
16
17
|
The control owns its label row, as an array does: the count of held items sits
|
|
17
18
|
where an array's add chip sits, so {@link Field} skips its own label for it.
|
|
@@ -48,15 +49,7 @@
|
|
|
48
49
|
onRoster,
|
|
49
50
|
schemaAt
|
|
50
51
|
} from './structure.js';
|
|
51
|
-
import {
|
|
52
|
-
checklists,
|
|
53
|
-
matrixPrinted,
|
|
54
|
-
searchChecklists,
|
|
55
|
-
type AddOption,
|
|
56
|
-
type Checklist,
|
|
57
|
-
type ChecklistHit,
|
|
58
|
-
type Searchable
|
|
59
|
-
} from './checklists.js';
|
|
52
|
+
import { matrixPrinted } from './checklists.js';
|
|
60
53
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
61
54
|
import type { LandingBox } from './leaves.js';
|
|
62
55
|
import { propertyDomIds } from './domid.js';
|
|
@@ -85,9 +78,6 @@
|
|
|
85
78
|
/** The field's control id: the base a member's tick and columns derive their own
|
|
86
79
|
* names from, one `-m-<member id>` segment down. */
|
|
87
80
|
idBase?: string;
|
|
88
|
-
/** The field's leaf key, where it is a card's own field: how the add box tells
|
|
89
|
-
* this list from the rest of the document's (`checklists.ts`). */
|
|
90
|
-
listKey?: string;
|
|
91
81
|
/** The boundary's nested content read, rooted at this field: a member's content
|
|
92
82
|
* column reads at `[id, column]` (`reader.getContentAt`). */
|
|
93
83
|
contentAt: (path: PathStep[]) => Content | undefined;
|
|
@@ -104,16 +94,11 @@
|
|
|
104
94
|
labelId,
|
|
105
95
|
descriptionId,
|
|
106
96
|
idBase,
|
|
107
|
-
listKey,
|
|
108
97
|
contentAt,
|
|
109
98
|
onCommit,
|
|
110
99
|
diagnostics
|
|
111
100
|
}: Props = $props();
|
|
112
101
|
|
|
113
|
-
// A card's own matrix searches the document's lists; one nested in a container is
|
|
114
|
-
// not indexed, and searches its own items alone.
|
|
115
|
-
const index = checklists();
|
|
116
|
-
const others = (): readonly Checklist[] => (listKey != null ? (index?.list() ?? []) : []);
|
|
117
102
|
const members = $derived(matrixMembers(schema.members));
|
|
118
103
|
const compact = $derived(!!schema.ui?.compact);
|
|
119
104
|
const hasColumns = $derived(Object.keys(schema.properties ?? {}).length > 0);
|
|
@@ -220,48 +205,6 @@
|
|
|
220
205
|
}
|
|
221
206
|
|
|
222
207
|
// ── The add box ────────────────────────────────────────────────────────────
|
|
223
|
-
/** A result's list: this one, or another of the document's. */
|
|
224
|
-
type Hit = ChecklistHit<Checklist | undefined>;
|
|
225
|
-
function options(query: string): AddOption[] {
|
|
226
|
-
const own: Searchable<Checklist | undefined> = { list: undefined, schema, value: map };
|
|
227
|
-
const lists: Searchable<Checklist | undefined>[] = others()
|
|
228
|
-
.filter((l) => l.key !== listKey)
|
|
229
|
-
.map((l) => ({ list: l, schema: l.schema, value: l.value }));
|
|
230
|
-
const hits = searchChecklists(query, [own, ...lists]);
|
|
231
|
-
const out: AddOption[] = hits.map((h, k) => ({
|
|
232
|
-
key: `hit-${k}`,
|
|
233
|
-
title: h.title,
|
|
234
|
-
note:
|
|
235
|
-
[
|
|
236
|
-
h.list ? t.strings.matrixIn(h.list.label) : undefined,
|
|
237
|
-
h.held ? t.strings.matrixHeldTag : undefined
|
|
238
|
-
]
|
|
239
|
-
.filter(Boolean)
|
|
240
|
-
.join(' · ') || undefined,
|
|
241
|
-
strong: h.strong,
|
|
242
|
-
payload: h
|
|
243
|
-
}));
|
|
244
|
-
// Offered last, and not where an item of this list already carries the words.
|
|
245
|
-
const fold = (s: string) => s.trim().toLowerCase();
|
|
246
|
-
const dup = hits.some((h) => h.list === undefined && fold(h.title) === fold(query));
|
|
247
|
-
if (!dup) out.push({ key: 'add', title: t.strings.matrixAddNew(query), add: true });
|
|
248
|
-
return out;
|
|
249
|
-
}
|
|
250
|
-
async function choose(option: AddOption, query: string): Promise<void> {
|
|
251
|
-
if (option.add) return add(query);
|
|
252
|
-
const hit = option.payload as Hit;
|
|
253
|
-
const other = hit.list;
|
|
254
|
-
if (other) {
|
|
255
|
-
if (!hit.held) other.commit(commitMember(other.value, hit.id, memberWrite({})));
|
|
256
|
-
index?.land(`${other.path}.${hit.id}`);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (!hit.held) commit(commitMember(map, hit.id, ticked(hit.id)));
|
|
260
|
-
await flush();
|
|
261
|
-
focusPath([hit.id]);
|
|
262
|
-
const box = ref(memberEls, hit.id);
|
|
263
|
-
if (box) bloomInside(box);
|
|
264
|
-
}
|
|
265
208
|
async function add(title: string): Promise<void> {
|
|
266
209
|
const taken = (id: string) => onRoster(schema, id) || (map != null && Object.hasOwn(map, id));
|
|
267
210
|
const id = addedId(title, taken);
|
|
@@ -397,8 +340,7 @@
|
|
|
397
340
|
bind:this={addEl}
|
|
398
341
|
id={`${base}-add`}
|
|
399
342
|
placeholder={t.strings.matrixAdd(label ?? '')}
|
|
400
|
-
{
|
|
401
|
-
onChoose={choose}
|
|
343
|
+
onAdd={add}
|
|
402
344
|
/>
|
|
403
345
|
{/if}
|
|
404
346
|
<DiagnosticList diagnostics={foot} />
|
|
@@ -18,9 +18,6 @@ interface Props {
|
|
|
18
18
|
/** The field's control id: the base a member's tick and columns derive their own
|
|
19
19
|
* names from, one `-m-<member id>` segment down. */
|
|
20
20
|
idBase?: string;
|
|
21
|
-
/** The field's leaf key, where it is a card's own field: how the add box tells
|
|
22
|
-
* this list from the rest of the document's (`checklists.ts`). */
|
|
23
|
-
listKey?: string;
|
|
24
21
|
/** The boundary's nested content read, rooted at this field: a member's content
|
|
25
22
|
* column reads at `[id, column]` (`reader.getContentAt`). */
|
|
26
23
|
contentAt: (path: PathStep[]) => Content | undefined;
|
|
@@ -81,8 +81,10 @@
|
|
|
81
81
|
/**
|
|
82
82
|
* The parent control's DOM id: the base each property's own three names derive
|
|
83
83
|
* from ({@link propertyDomIds}). Absent — a subform mounted with no field around
|
|
84
|
-
* it
|
|
85
|
-
*
|
|
84
|
+
* it — the properties fall back to `aria-label`, which names them without a
|
|
85
|
+
* `for` target to click. A table's row takes the table's own and names its
|
|
86
|
+
* cells the same fallback way, reading only the description off it: the
|
|
87
|
+
* header parks one per column, which every row's cell in it points at.
|
|
86
88
|
*/
|
|
87
89
|
idBase?: string;
|
|
88
90
|
/** Accessible-name prefix used only on the `aria-label` fallback above. */
|
|
@@ -268,7 +270,8 @@
|
|
|
268
270
|
{@const kind = controlKind(sub)}
|
|
269
271
|
{@const ids = idBase && !bare ? propertyDomIds(idBase, key) : undefined}
|
|
270
272
|
{@const named = ids ? undefined : fallbackName(key, sub)}
|
|
271
|
-
{@const describes =
|
|
273
|
+
{@const describes =
|
|
274
|
+
sub.description && idBase ? propertyDomIds(idBase, key).description : undefined}
|
|
272
275
|
{@const deep = routed.below.get(key)}
|
|
273
276
|
{@const own = isContainer(kind) ? undefined : deep?.map((d) => d.diagnostic)}
|
|
274
277
|
<!-- `for` reaches the labelable controls, a variant through its discriminant; the
|
|
@@ -359,6 +362,7 @@
|
|
|
359
362
|
<ProseValue
|
|
360
363
|
bind:this={proseEls[key]}
|
|
361
364
|
content={() => contentAt([key])}
|
|
365
|
+
value={obj[key]}
|
|
362
366
|
fallback={contentDefault(sub)}
|
|
363
367
|
plaintext={baseType(sub) === 'plaintext'}
|
|
364
368
|
placeholder={noneOf(key, sub)}
|
|
@@ -376,7 +380,7 @@
|
|
|
376
380
|
under an open row costs is two rungs of the ladder and one figure. -->
|
|
377
381
|
<ArrayField
|
|
378
382
|
bind:this={nestedEls[key]}
|
|
379
|
-
value={obj[key]
|
|
383
|
+
value={obj[key]}
|
|
380
384
|
fallback={Array.isArray(sub.default) ? sub.default : undefined}
|
|
381
385
|
items={sub.items}
|
|
382
386
|
layout={arrayLayout(sub)}
|
|
@@ -14,8 +14,10 @@ interface Props {
|
|
|
14
14
|
/**
|
|
15
15
|
* The parent control's DOM id: the base each property's own three names derive
|
|
16
16
|
* from ({@link propertyDomIds}). Absent — a subform mounted with no field around
|
|
17
|
-
* it
|
|
18
|
-
*
|
|
17
|
+
* it — the properties fall back to `aria-label`, which names them without a
|
|
18
|
+
* `for` target to click. A table's row takes the table's own and names its
|
|
19
|
+
* cells the same fallback way, reading only the description off it: the
|
|
20
|
+
* header parks one per column, which every row's cell in it points at.
|
|
19
21
|
*/
|
|
20
22
|
idBase?: string;
|
|
21
23
|
/** Accessible-name prefix used only on the `aria-label` fallback above. */
|
|
@@ -123,6 +123,7 @@
|
|
|
123
123
|
placeholder,
|
|
124
124
|
placeholderUntilEdit,
|
|
125
125
|
tableStrings: () => t.strings,
|
|
126
|
+
slashStrings: () => t.strings,
|
|
126
127
|
onSlash: (next) => {
|
|
127
128
|
slash = next;
|
|
128
129
|
},
|
|
@@ -155,7 +156,7 @@
|
|
|
155
156
|
controller?.setFallback(fallback);
|
|
156
157
|
});
|
|
157
158
|
|
|
158
|
-
// Only a narrowed or a plain leaf
|
|
159
|
+
// Only a narrowed or a plain leaf holds by what a re-validation says, so only one of them asks.
|
|
159
160
|
$effect(() => {
|
|
160
161
|
void diagnostics;
|
|
161
162
|
if (inline || plaintext) controller?.applyExternal();
|