@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
|
@@ -11,24 +11,35 @@
|
|
|
11
11
|
Mounts once per stable key (an element's session id, a property's name), so the
|
|
12
12
|
parent's re-derive leaves the caret where it is.
|
|
13
13
|
|
|
14
|
+
Having no address, it registers no controller: it announces its views into the
|
|
15
|
+
registry's by-value lane under the field it stands in (`leaves.ts`), the formatting
|
|
16
|
+
popover's door to it. A focus on an editable leaf announces them, and so does a write
|
|
17
|
+
releasing the leaf under the caret; a hold and an unmount withdraw them, and a blur
|
|
18
|
+
does not, the popover's link prompt taking the focus from the view it writes to.
|
|
19
|
+
|
|
14
20
|
The content is read, not passed: `reader.getContentAt(addr, path)` decodes through
|
|
15
21
|
the codec the leaf's own declared type names, so what it rests as — the content
|
|
16
22
|
object, a `plaintext` literal, the authored string a transport door left — stops
|
|
17
|
-
being the row's or the cell's business.
|
|
18
|
-
|
|
23
|
+
being the row's or the cell's business. It is read at mount and again wherever the
|
|
24
|
+
parent's re-derive hands down a `value` that moved, and a read differing from the
|
|
25
|
+
content the leaf last committed or took replaces the view, the gate `createField`
|
|
26
|
+
re-hydrates through (`reconcile.ts`): a write that lands in the store under a mounted
|
|
27
|
+
leaf is what its next keystroke edits. Its own commit is read back as the store took
|
|
28
|
+
it, so the echo replaces nothing and the caret and the history stay.
|
|
19
29
|
|
|
20
30
|
The schema is the declared type's where the parent has room for it: a subform cell
|
|
21
31
|
declaring no `inline` takes the full row ({@link ObjectField}), a `richtext` one on the
|
|
22
32
|
block schema and a `plaintext` one on the plain schema, paragraphs and hard breaks.
|
|
23
33
|
An array element is one textblock whatever it declares, since Enter there is the
|
|
24
|
-
repeater's. There is no slash menu at either width, and
|
|
25
|
-
placeholder: an atom a keystroke can delete and no view edits.
|
|
34
|
+
repeater's. There is no slash menu at either width, and no island view.
|
|
26
35
|
|
|
27
|
-
A leaf over content
|
|
28
|
-
one plain paragraph
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
schema, read-only, with a note
|
|
36
|
+
A leaf over content it cannot edit is held (`fitsLeaf`): a narrowed one over anything
|
|
37
|
+
but one plain paragraph, a plain one over anything but plain lines, where the decode
|
|
38
|
+
would join or drop what is left over for the first keystroke to write back, and a block
|
|
39
|
+
one over an island, which draws as its placeholder tag, an atom a keystroke deletes
|
|
40
|
+
unseen. A held leaf draws its content on the block schema, read-only, with a note
|
|
41
|
+
inside its box, and commits nothing. The hold is judged of what each mount and each
|
|
42
|
+
replacement reads, so a write moving the value takes it or releases it.
|
|
32
43
|
-->
|
|
33
44
|
<script lang="ts">
|
|
34
45
|
import { untrack } from 'svelte';
|
|
@@ -36,6 +47,7 @@
|
|
|
36
47
|
import { EditorView } from 'prosemirror-view';
|
|
37
48
|
import {
|
|
38
49
|
blockSchema,
|
|
50
|
+
createReconciler,
|
|
39
51
|
decode,
|
|
40
52
|
emptyContent,
|
|
41
53
|
fitsLeaf,
|
|
@@ -45,18 +57,26 @@
|
|
|
45
57
|
proseAttributes,
|
|
46
58
|
proseLeafPlugins,
|
|
47
59
|
buildLineIndex,
|
|
48
|
-
|
|
60
|
+
pmToUsv,
|
|
61
|
+
usvToPM,
|
|
62
|
+
valueEqual,
|
|
63
|
+
type LeafViews
|
|
49
64
|
} from '../core/codec/index.js';
|
|
65
|
+
import { valueLeafSeat } from './leaves.js';
|
|
50
66
|
import { wording } from './strings.js';
|
|
51
67
|
import './controls.css';
|
|
52
68
|
import type { Content } from '@quillmark/wasm';
|
|
53
69
|
|
|
54
70
|
interface Props {
|
|
55
|
-
/** This leaf's content
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* boundary read per leaf per render, all but
|
|
71
|
+
/** This leaf's content (the parent's boundary read), `undefined` where nothing is
|
|
72
|
+
* stored: read at mount and wherever {@link value} moves. A thunk rather than a
|
|
73
|
+
* value: the parent re-derives per revision, and a value prop would be a
|
|
74
|
+
* boundary read per leaf per render, all but the moved one discarded. */
|
|
59
75
|
content: () => Content | undefined;
|
|
76
|
+
/** This leaf's value as the parent's re-derive delivers it, stored form and all:
|
|
77
|
+
* compared with the last one delivered, never drawn, so a move is what re-reads
|
|
78
|
+
* {@link content}. */
|
|
79
|
+
value?: unknown;
|
|
60
80
|
/** The default an unset leaf holds, where it prints: drawn at the default rung,
|
|
61
81
|
* and handed up whole with the first edit in it, as `createField` takes one. */
|
|
62
82
|
fallback?: Content;
|
|
@@ -87,6 +107,7 @@
|
|
|
87
107
|
}
|
|
88
108
|
let {
|
|
89
109
|
content,
|
|
110
|
+
value,
|
|
90
111
|
fallback,
|
|
91
112
|
plaintext = false,
|
|
92
113
|
block = false,
|
|
@@ -99,10 +120,13 @@
|
|
|
99
120
|
}: Props = $props();
|
|
100
121
|
|
|
101
122
|
const t = wording();
|
|
123
|
+
const seat = valueLeafSeat();
|
|
102
124
|
const uid = $props.id();
|
|
103
125
|
const heldId = `${uid}-held`;
|
|
104
126
|
let held = $state(false);
|
|
105
127
|
let view: EditorView | undefined;
|
|
128
|
+
/** What the mounted view does with a value the parent's re-derive delivers. */
|
|
129
|
+
let follow: ((next: unknown) => void) | undefined;
|
|
106
130
|
/** Take the caret: what a parent placing focus on this leaf calls. The view's
|
|
107
131
|
* focus, not the element's: a PM view restores its selection, where a bare DOM
|
|
108
132
|
* focus on a contenteditable leaves the caret unplaced. And the reveal beside it,
|
|
@@ -135,46 +159,61 @@
|
|
|
135
159
|
|
|
136
160
|
function mountView(mount: HTMLDivElement): () => void {
|
|
137
161
|
return untrack(() => {
|
|
138
|
-
// The same keymap and plugin stack a `createField` leaf mounts (shared
|
|
139
|
-
// `proseLeafPlugins`), minus the anchor-position plugin: anchors are dropped on
|
|
140
|
-
// the parent's value write, per the header.
|
|
141
162
|
const inline = !block;
|
|
163
|
+
const leaf = { inline, plaintext, islands: false };
|
|
164
|
+
const names = { label, labelledBy, describedBy };
|
|
165
|
+
const shown = (stored: Content | undefined): Content => stored ?? fallback ?? emptyContent();
|
|
142
166
|
const stored = content();
|
|
143
|
-
|
|
167
|
+
let seen = value;
|
|
144
168
|
let defaulted = stored === undefined && !!fallback;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
const reconciler = createReconciler(shown(stored));
|
|
170
|
+
held = !fitsLeaf(reconciler.last, leaf);
|
|
171
|
+
// Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
|
|
172
|
+
// (`proseAttributes`), so a cell carrying a label element and a row carrying
|
|
173
|
+
// none cannot name their regions by different rules.
|
|
174
|
+
const plainNamed = proseAttributes(names) ?? {};
|
|
175
|
+
const heldNamed = heldAttributes(names, heldId);
|
|
176
|
+
const build = (rt: Content): EditorState => {
|
|
177
|
+
// The same keymap and plugin stack a `createField` leaf mounts (shared
|
|
178
|
+
// `proseLeafPlugins`), minus the anchor-position plugin: anchors are dropped on
|
|
179
|
+
// the parent's value write, per the header.
|
|
180
|
+
const schema = held ? blockSchema : leafSchema({ plaintext, inline });
|
|
181
|
+
return EditorState.create({
|
|
182
|
+
doc: decode(rt, schema),
|
|
183
|
+
plugins: held
|
|
184
|
+
? []
|
|
185
|
+
: proseLeafPlugins(schema, {
|
|
186
|
+
inline,
|
|
187
|
+
placeholder: () => placeholder,
|
|
188
|
+
placeholderUntilEdit: true
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
};
|
|
161
192
|
const mounted = new EditorView(
|
|
162
193
|
{ mount },
|
|
163
194
|
{
|
|
164
|
-
state,
|
|
195
|
+
state: build(reconciler.last),
|
|
165
196
|
editable: () => !held,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
attributes: () => {
|
|
198
|
+
const named = held ? heldNamed : plainNamed;
|
|
199
|
+
return defaulted ? { ...named, 'data-default': '' } : named;
|
|
200
|
+
},
|
|
170
201
|
dispatchTransaction(tr) {
|
|
171
202
|
const next = mounted.state.apply(tr);
|
|
172
203
|
const edit = tr.docChanged && !held;
|
|
173
204
|
if (edit) defaulted = false;
|
|
174
205
|
mounted.updateState(next);
|
|
175
|
-
if (edit)
|
|
206
|
+
if (!edit) return;
|
|
207
|
+
onChange(pmToContent(next.doc));
|
|
208
|
+
// The commit as the store took it, so its echo through the re-derive is no move.
|
|
209
|
+
seen = value;
|
|
210
|
+
reconciler.commit(shown(content()));
|
|
176
211
|
},
|
|
177
212
|
handleDOMEvents: {
|
|
213
|
+
focus: () => {
|
|
214
|
+
if (!held) seat?.focus(views);
|
|
215
|
+
return false;
|
|
216
|
+
},
|
|
178
217
|
keydown: (_v, e) => {
|
|
179
218
|
onKey?.(e);
|
|
180
219
|
return false;
|
|
@@ -182,13 +221,47 @@
|
|
|
182
221
|
}
|
|
183
222
|
}
|
|
184
223
|
);
|
|
224
|
+
const views: LeafViews = {
|
|
225
|
+
view: mounted,
|
|
226
|
+
focusedView: () => mounted,
|
|
227
|
+
nestedViews: () => []
|
|
228
|
+
};
|
|
185
229
|
view = mounted;
|
|
230
|
+
follow = (next) => {
|
|
231
|
+
if (valueEqual(next, seen)) return;
|
|
232
|
+
seen = next;
|
|
233
|
+
const read = content();
|
|
234
|
+
const current = shown(read);
|
|
235
|
+
const holds = !fitsLeaf(current, leaf);
|
|
236
|
+
const wasDefaulted = defaulted;
|
|
237
|
+
defaulted = read === undefined && !!fallback;
|
|
238
|
+
if (holds === held && !reconciler.shouldRehydrate(current)) {
|
|
239
|
+
if (defaulted !== wasDefaulted) mounted.setProps({});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
held = holds;
|
|
243
|
+
if (held) seat?.release(views);
|
|
244
|
+
else if (mounted.hasFocus()) seat?.focus(views);
|
|
245
|
+
// The caret keeps its offset, clamped, as a field's re-hydrate keeps it.
|
|
246
|
+
const caret = pmToUsv(buildLineIndex(mounted.state.doc), mounted.state.selection.head);
|
|
247
|
+
const fresh = build(current);
|
|
248
|
+
const sel = Selection.near(fresh.doc.resolve(usvToPM(buildLineIndex(fresh.doc), caret)));
|
|
249
|
+
mounted.updateState(fresh.apply(fresh.tr.setSelection(sel)));
|
|
250
|
+
reconciler.commit(current);
|
|
251
|
+
};
|
|
186
252
|
return () => {
|
|
253
|
+
seat?.release(views);
|
|
254
|
+
follow = undefined;
|
|
187
255
|
view = undefined;
|
|
188
256
|
mounted.destroy();
|
|
189
257
|
};
|
|
190
258
|
});
|
|
191
259
|
}
|
|
260
|
+
|
|
261
|
+
$effect(() => {
|
|
262
|
+
const next = value;
|
|
263
|
+
untrack(() => follow?.(next));
|
|
264
|
+
});
|
|
192
265
|
</script>
|
|
193
266
|
|
|
194
267
|
<!-- `.qm-control-box` (controls.css) is the whole box, so an array of `richtext` and
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Content } from '@quillmark/wasm';
|
|
2
2
|
interface Props {
|
|
3
|
-
/** This leaf's content
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* boundary read per leaf per render, all but
|
|
3
|
+
/** This leaf's content (the parent's boundary read), `undefined` where nothing is
|
|
4
|
+
* stored: read at mount and wherever {@link value} moves. A thunk rather than a
|
|
5
|
+
* value: the parent re-derives per revision, and a value prop would be a
|
|
6
|
+
* boundary read per leaf per render, all but the moved one discarded. */
|
|
7
7
|
content: () => Content | undefined;
|
|
8
|
+
/** This leaf's value as the parent's re-derive delivers it, stored form and all:
|
|
9
|
+
* compared with the last one delivered, never drawn, so a move is what re-reads
|
|
10
|
+
* {@link content}. */
|
|
11
|
+
value?: unknown;
|
|
8
12
|
/** The default an unset leaf holds, where it prints: drawn at the default rung,
|
|
9
13
|
* and handed up whole with the first edit in it, as `createField` takes one. */
|
|
10
14
|
fallback?: Content;
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import type { VisualEditorProps } from './props.js';
|
|
47
47
|
import { mergeStrings, setWording } from './strings.js';
|
|
48
48
|
import type { CardId, ChangeSource } from './signals.js';
|
|
49
|
-
import type { FieldController } from '../core/codec/index.js';
|
|
49
|
+
import type { FieldController, LeafViews } from '../core/codec/index.js';
|
|
50
50
|
import {
|
|
51
51
|
IdSeq,
|
|
52
52
|
controlKind,
|
|
@@ -79,11 +79,11 @@
|
|
|
79
79
|
type RoutedDiagnostic
|
|
80
80
|
} from './diagnostics.js';
|
|
81
81
|
import { fieldDomIds, groupPanelId } from './domid.js';
|
|
82
|
-
import { createLeafRegistry, type FieldControl } from './leaves.js';
|
|
82
|
+
import { createLeafRegistry, type ActiveProse, type FieldControl } from './leaves.js';
|
|
83
83
|
import { reorder, reorderArm, reorderTrips } from './motion.js';
|
|
84
84
|
import { tipsChannel } from './tips.js';
|
|
85
|
-
import { cardChecklists, setChecklists } from './checklists.js';
|
|
86
85
|
import { patchEditorExt } from './ext.js';
|
|
86
|
+
import { raiseGuidance } from './guidance.js';
|
|
87
87
|
import Card from './Card.svelte';
|
|
88
88
|
import TipsCard from './TipsCard.svelte';
|
|
89
89
|
import FormatPopover from './FormatPopover.svelte';
|
|
@@ -654,20 +654,6 @@
|
|
|
654
654
|
};
|
|
655
655
|
});
|
|
656
656
|
|
|
657
|
-
// The document's checklists, which an open matrix's add box searches whole
|
|
658
|
-
// (`checklists.ts`): off the model, so a list in a closed group or another card is
|
|
659
|
-
// reached without being mounted.
|
|
660
|
-
const lists = $derived(
|
|
661
|
-
cardChecklists(
|
|
662
|
-
[model.main, ...model.cards],
|
|
663
|
-
(c) => c.titleOverride || c.titlePlaceholder,
|
|
664
|
-
(c, field) => pathFor(makeAddr(c.id, c.isMain, field)),
|
|
665
|
-
(c, field) => fieldKeyToString({ card: c.isMain ? undefined : c.id, field }),
|
|
666
|
-
(c, field, next) => commitScalar(c.id, c.isMain, field, next)
|
|
667
|
-
)
|
|
668
|
-
);
|
|
669
|
-
setChecklists({ list: () => lists, land: (path) => void focusField(path) });
|
|
670
|
-
|
|
671
657
|
// ── Public entry points ─────────────────────────────────────────────────────
|
|
672
658
|
/**
|
|
673
659
|
* Resolve a preview {@link Landing} to a mounted leaf and land in it.
|
|
@@ -694,12 +680,18 @@
|
|
|
694
680
|
const box = await land(found, at.granularity === 'segment' ? undefined : at.pos);
|
|
695
681
|
if (span.alive) bloomInside(box);
|
|
696
682
|
}
|
|
697
|
-
/** The active leaf
|
|
698
|
-
*
|
|
699
|
-
|
|
683
|
+
/** The active prose leaf: the formatting popover's observation seam. The active
|
|
684
|
+
* field's controller where it registered one, else the by-value leaf focused last
|
|
685
|
+
* under it (`leaves.ts`); `undefined` for a form control, which holds no marks to
|
|
686
|
+
* toggle. */
|
|
687
|
+
export function getActiveLeaf(): ActiveProse | undefined {
|
|
700
688
|
if (!activeAddr) return undefined;
|
|
701
689
|
const card = activeAddr.card != null ? activeCardId : undefined;
|
|
702
|
-
|
|
690
|
+
const key = fieldKeyToString({ card, field: activeAddr.field });
|
|
691
|
+
const controller = leaves.prose(key);
|
|
692
|
+
if (controller) return { views: controller as FieldController & LeafViews, controller };
|
|
693
|
+
const views = leaves.value(key);
|
|
694
|
+
return views && { views };
|
|
703
695
|
}
|
|
704
696
|
|
|
705
697
|
/**
|
|
@@ -856,7 +848,15 @@
|
|
|
856
848
|
}
|
|
857
849
|
</script>
|
|
858
850
|
|
|
859
|
-
|
|
851
|
+
<!-- The keydown is a control's, bubbled: the root resolves which description it raises. -->
|
|
852
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
853
|
+
<div
|
|
854
|
+
class="qm-editor {className ?? ''}"
|
|
855
|
+
{style}
|
|
856
|
+
data-qm-root
|
|
857
|
+
bind:this={rootEl}
|
|
858
|
+
onkeydown={raiseGuidance}
|
|
859
|
+
>
|
|
860
860
|
<!-- `main` and the tips card are one block in the stack: the tips card tucks under
|
|
861
861
|
`main`'s bottom corners, so the two share a seam rather than a gutter and the
|
|
862
862
|
wrapper is what holds them to it. -->
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type DocPath, type Landing } from '../core/address.js';
|
|
2
2
|
import type { VisualEditorProps } from './props.js';
|
|
3
3
|
import type { CardId } from './signals.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type ActiveProse } from './leaves.js';
|
|
5
5
|
declare const VisualEditorInner: import("svelte").Component<VisualEditorProps, {
|
|
6
6
|
setCaret: (at: Landing) => Promise<void>;
|
|
7
|
-
getActiveLeaf: () =>
|
|
7
|
+
getActiveLeaf: () => ActiveProse | undefined;
|
|
8
8
|
focusField: (field: DocPath) => Promise<void>;
|
|
9
9
|
insertCard: (kind: string, at?: number) => CardId | undefined;
|
|
10
10
|
removeCard: (cardId: CardId) => void;
|
|
@@ -1,82 +1,3 @@
|
|
|
1
1
|
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
2
|
-
import type { DocPath } from '../core/address.js';
|
|
3
|
-
import { type CardModel, type MatrixMember } from './structure.js';
|
|
4
|
-
/** One matrix of the document, as the add box reaches it. */
|
|
5
|
-
export interface Checklist {
|
|
6
|
-
/** The field's leaf key: how a mounted matrix tells itself from the rest. */
|
|
7
|
-
key: string;
|
|
8
|
-
/** The field's path, the prefix a member's landing address extends. */
|
|
9
|
-
path: DocPath;
|
|
10
|
-
/** What a result from this list reads under: the field's label, and the card's
|
|
11
|
-
* where the field is not the main card's. */
|
|
12
|
-
label: string;
|
|
13
|
-
schema: QuillFieldSchema;
|
|
14
|
-
/** The held set as it prints: the stored map, else the declared `default:`. */
|
|
15
|
-
value: Record<string, unknown> | undefined;
|
|
16
|
-
/** Commit the whole map, as the field's own control does. */
|
|
17
|
-
commit: (next: Record<string, unknown> | undefined) => void;
|
|
18
|
-
}
|
|
19
|
-
export interface ChecklistIndex {
|
|
20
|
-
list: () => readonly Checklist[];
|
|
21
|
-
/** Reveal and focus a member by its address. */
|
|
22
|
-
land: (path: DocPath) => void;
|
|
23
|
-
}
|
|
24
|
-
export declare function setChecklists(index: ChecklistIndex): void;
|
|
25
|
-
/** The index under a mounted editor; `undefined` off-tree. */
|
|
26
|
-
export declare function checklists(): ChecklistIndex | undefined;
|
|
27
2
|
/** The value a matrix prints: the stored map, else a mapping `default:`. */
|
|
28
3
|
export declare function matrixPrinted(value: unknown, schema: QuillFieldSchema): Record<string, unknown> | undefined;
|
|
29
|
-
/** Every card-level matrix of the cards, in document order. */
|
|
30
|
-
export declare function cardChecklists(cards: readonly CardModel[], cardLabel: (card: CardModel) => string, path: (card: CardModel, field: string) => DocPath | undefined, leafKey: (card: CardModel, field: string) => string, commit: (card: CardModel, field: string, next: Record<string, unknown> | undefined) => void): Checklist[];
|
|
31
|
-
/** Every item a matrix can show: the roster in declaration order, then the items its
|
|
32
|
-
* map adds, in id order. */
|
|
33
|
-
export declare function checklistItems(schema: QuillFieldSchema, value: Record<string, unknown> | undefined): MatrixMember[];
|
|
34
|
-
/** One option of an add box. */
|
|
35
|
-
export interface AddOption {
|
|
36
|
-
/** Stable across one query's options: the option element's id suffix. */
|
|
37
|
-
key: string;
|
|
38
|
-
title: string;
|
|
39
|
-
/** Words after the title: the list a result stands in, its held state. */
|
|
40
|
-
note?: string;
|
|
41
|
-
/** The option that adds what was typed, rather than a result. */
|
|
42
|
-
add?: boolean;
|
|
43
|
-
/** A result the typed words name outright, which Enter takes unarrowed: an add box
|
|
44
|
-
* whose best result is weaker takes its add option instead. */
|
|
45
|
-
strong?: boolean;
|
|
46
|
-
/** What the owner reads back off a chosen option. */
|
|
47
|
-
payload?: unknown;
|
|
48
|
-
}
|
|
49
|
-
/** One search result: an item of some checklist, `list` naming which. */
|
|
50
|
-
export interface ChecklistHit<L> {
|
|
51
|
-
list: L;
|
|
52
|
-
id: string;
|
|
53
|
-
title: string;
|
|
54
|
-
held: boolean;
|
|
55
|
-
score: number;
|
|
56
|
-
/** Every query word claimed a title word whole or as its opening. */
|
|
57
|
-
strong: boolean;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* How well `query` names `title`, or `undefined` for not at all. Each query word claims
|
|
61
|
-
* a title word — whole, as its opening, or as an abbreviation of it (`flt cc` names
|
|
62
|
-
* `Flight CC`) — the stronger claims taken first, so `c cc` names `CC Candidate`; a
|
|
63
|
-
* word left over may claim a run of words whose openings it spells (`fcc`, `jqo`). The
|
|
64
|
-
* title the query spells outright ranks first, then one it opens.
|
|
65
|
-
*/
|
|
66
|
-
export declare function matchTitle(query: string, title: string): {
|
|
67
|
-
score: number;
|
|
68
|
-
strong: boolean;
|
|
69
|
-
} | undefined;
|
|
70
|
-
/** {@link matchTitle}'s score, `0` for no match. */
|
|
71
|
-
export declare function matchScore(query: string, title: string): number;
|
|
72
|
-
/** What a search reads of one list: its items and their ticks. */
|
|
73
|
-
export interface Searchable<L> {
|
|
74
|
-
list: L;
|
|
75
|
-
schema: QuillFieldSchema;
|
|
76
|
-
value: Record<string, unknown> | undefined;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* The items across `lists` that `query` names, best first; a tie keeps the order of
|
|
80
|
-
* `lists`, then of each list's items.
|
|
81
|
-
*/
|
|
82
|
-
export declare function searchChecklists<L>(query: string, lists: readonly Searchable<L>[], limit?: number): ChecklistHit<L>[];
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The document's checklists, and the search an open matrix's add box runs over them
|
|
3
|
-
* (VISUAL_EDITOR §"The matrix"). The editor root publishes every card-level matrix off
|
|
4
|
-
* the model it derives, so a list in a collapsed group or another card is searched
|
|
5
|
-
* as surely as the one the author is typing in. A matrix mounted off-tree, or nested
|
|
6
|
-
* in a container, is not indexed and searches its own items alone.
|
|
7
|
-
*/
|
|
8
|
-
import { getContext, setContext } from 'svelte';
|
|
9
|
-
import { addedItems, matrixHeld, matrixMembers, memberValue } from './structure.js';
|
|
10
|
-
const KEY = Symbol('qm-checklists');
|
|
11
|
-
export function setChecklists(index) {
|
|
12
|
-
setContext(KEY, index);
|
|
13
|
-
}
|
|
14
|
-
/** The index under a mounted editor; `undefined` off-tree. */
|
|
15
|
-
export function checklists() {
|
|
16
|
-
return getContext(KEY);
|
|
17
|
-
}
|
|
18
1
|
/** The value a matrix prints: the stored map, else a mapping `default:`. */
|
|
19
2
|
export function matrixPrinted(value, schema) {
|
|
20
3
|
const map = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
@@ -22,150 +5,3 @@ export function matrixPrinted(value, schema) {
|
|
|
22
5
|
: undefined;
|
|
23
6
|
return value == null ? map(schema.default) : map(value);
|
|
24
7
|
}
|
|
25
|
-
/** Every card-level matrix of the cards, in document order. */
|
|
26
|
-
export function cardChecklists(cards, cardLabel, path, leafKey, commit) {
|
|
27
|
-
const out = [];
|
|
28
|
-
for (const card of cards) {
|
|
29
|
-
if (card.unschemable)
|
|
30
|
-
continue;
|
|
31
|
-
for (const section of card.sections)
|
|
32
|
-
for (const f of section.fields) {
|
|
33
|
-
if (f.control !== 'matrix')
|
|
34
|
-
continue;
|
|
35
|
-
const at = path(card, f.name);
|
|
36
|
-
if (at == null)
|
|
37
|
-
continue;
|
|
38
|
-
out.push({
|
|
39
|
-
key: leafKey(card, f.name),
|
|
40
|
-
path: at,
|
|
41
|
-
label: card.isMain ? f.label : `${f.label} · ${cardLabel(card)}`,
|
|
42
|
-
schema: f.schema,
|
|
43
|
-
value: matrixPrinted(card.values[f.name], f.schema),
|
|
44
|
-
commit: (next) => commit(card, f.name, next)
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return out;
|
|
49
|
-
}
|
|
50
|
-
/** Every item a matrix can show: the roster in declaration order, then the items its
|
|
51
|
-
* map adds, in id order. */
|
|
52
|
-
export function checklistItems(schema, value) {
|
|
53
|
-
return [...matrixMembers(schema.members), ...addedItems(schema, value)];
|
|
54
|
-
}
|
|
55
|
-
/** Lowercase, unaccented words, any script: `Exec / Aide / CAG` → `exec aide cag`. */
|
|
56
|
-
function words(s) {
|
|
57
|
-
return s
|
|
58
|
-
.normalize('NFKD')
|
|
59
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
60
|
-
.toLowerCase()
|
|
61
|
-
.split(/[^\p{L}\p{N}]+/u)
|
|
62
|
-
.filter(Boolean);
|
|
63
|
-
}
|
|
64
|
-
/** `flt` in `flight`: every letter in order, the first letters equal. */
|
|
65
|
-
function abbreviates(token, word) {
|
|
66
|
-
if (token[0] !== word[0])
|
|
67
|
-
return false;
|
|
68
|
-
let i = 0;
|
|
69
|
-
for (const c of word)
|
|
70
|
-
if (c === token[i])
|
|
71
|
-
i++;
|
|
72
|
-
return i === token.length;
|
|
73
|
-
}
|
|
74
|
-
/** How a query word claims one title word: whole (3), as its opening (2), as an
|
|
75
|
-
* abbreviation (1), or not (0). */
|
|
76
|
-
function claim(token, word) {
|
|
77
|
-
return word === token ? 3 : word.startsWith(token) ? 2 : abbreviates(token, word) ? 1 : 0;
|
|
78
|
-
}
|
|
79
|
-
/** The end of the run of free words from `i` whose openings spell `token` in order —
|
|
80
|
-
* `fcc` over `flight cc`, `jqo` over `joint qualified officer` — or `-1`. */
|
|
81
|
-
function runFrom(token, w, free, i) {
|
|
82
|
-
if (i >= w.length || !free[i])
|
|
83
|
-
return -1;
|
|
84
|
-
for (let k = Math.min(token.length, w[i].length); k > 0; k--) {
|
|
85
|
-
if (!w[i].startsWith(token.slice(0, k)))
|
|
86
|
-
continue;
|
|
87
|
-
if (k === token.length)
|
|
88
|
-
return i + 1;
|
|
89
|
-
const end = runFrom(token.slice(k), w, free, i + 1);
|
|
90
|
-
if (end >= 0)
|
|
91
|
-
return end;
|
|
92
|
-
}
|
|
93
|
-
return -1;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* How well `query` names `title`, or `undefined` for not at all. Each query word claims
|
|
97
|
-
* a title word — whole, as its opening, or as an abbreviation of it (`flt cc` names
|
|
98
|
-
* `Flight CC`) — the stronger claims taken first, so `c cc` names `CC Candidate`; a
|
|
99
|
-
* word left over may claim a run of words whose openings it spells (`fcc`, `jqo`). The
|
|
100
|
-
* title the query spells outright ranks first, then one it opens.
|
|
101
|
-
*/
|
|
102
|
-
export function matchTitle(query, title) {
|
|
103
|
-
const q = words(query);
|
|
104
|
-
const w = words(title);
|
|
105
|
-
if (!q.length || !w.length)
|
|
106
|
-
return undefined;
|
|
107
|
-
const free = w.map(() => true);
|
|
108
|
-
const left = q.map(() => true);
|
|
109
|
-
let score = 0;
|
|
110
|
-
let strong = true;
|
|
111
|
-
for (const level of [3, 2, 1])
|
|
112
|
-
q.forEach((token, t) => {
|
|
113
|
-
if (!left[t])
|
|
114
|
-
return;
|
|
115
|
-
const at = w.findIndex((word, i) => free[i] && claim(token, word) === level);
|
|
116
|
-
if (at < 0)
|
|
117
|
-
return;
|
|
118
|
-
free[at] = left[t] = false;
|
|
119
|
-
score += level;
|
|
120
|
-
if (level === 1)
|
|
121
|
-
strong = false;
|
|
122
|
-
});
|
|
123
|
-
for (let t = 0; t < q.length; t++) {
|
|
124
|
-
if (!left[t])
|
|
125
|
-
continue;
|
|
126
|
-
let found = false;
|
|
127
|
-
for (let i = 0; i < w.length && !found; i++) {
|
|
128
|
-
const end = runFrom(q[t], w, free, i);
|
|
129
|
-
if (end - i < 2)
|
|
130
|
-
continue;
|
|
131
|
-
for (let k = i; k < end; k++)
|
|
132
|
-
free[k] = false;
|
|
133
|
-
found = true;
|
|
134
|
-
}
|
|
135
|
-
if (!found)
|
|
136
|
-
return undefined;
|
|
137
|
-
score += 1;
|
|
138
|
-
strong = false;
|
|
139
|
-
}
|
|
140
|
-
const typed = q.join(' ');
|
|
141
|
-
const whole = w.join(' ');
|
|
142
|
-
return { score: score + (whole === typed ? 3 : whole.startsWith(typed) ? 1 : 0), strong };
|
|
143
|
-
}
|
|
144
|
-
/** {@link matchTitle}'s score, `0` for no match. */
|
|
145
|
-
export function matchScore(query, title) {
|
|
146
|
-
return matchTitle(query, title)?.score ?? 0;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* The items across `lists` that `query` names, best first; a tie keeps the order of
|
|
150
|
-
* `lists`, then of each list's items.
|
|
151
|
-
*/
|
|
152
|
-
export function searchChecklists(query, lists, limit = 8) {
|
|
153
|
-
const hits = [];
|
|
154
|
-
for (const { list, schema, value } of lists)
|
|
155
|
-
for (const item of checklistItems(schema, value)) {
|
|
156
|
-
const match = matchTitle(query, item.title);
|
|
157
|
-
if (match)
|
|
158
|
-
hits.push({
|
|
159
|
-
list,
|
|
160
|
-
id: item.id,
|
|
161
|
-
title: item.title,
|
|
162
|
-
held: matrixHeld(memberValue(value, item.id)),
|
|
163
|
-
...match
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
return hits
|
|
167
|
-
.map((h, i) => [h, i])
|
|
168
|
-
.sort(([a, i], [b, j]) => b.score - a.score || i - j)
|
|
169
|
-
.slice(0, limit)
|
|
170
|
-
.map(([h]) => h);
|
|
171
|
-
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** `Mod-/`, read off `key` so a layout that shifts to reach `/` still matches; either
|
|
2
|
+
* modifier stands for `Mod`. */
|
|
3
|
+
export declare function isGuidanceKey(e: KeyboardEvent): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* The guidance trigger for the description nearest `from`: the innermost element
|
|
6
|
+
* carrying `aria-describedby` whose ids name a parked description with a trigger
|
|
7
|
+
* beside it. A trigger points at its own parked node through that same attribute,
|
|
8
|
+
* which is the relation read here; ids that are not a description (an array's count,
|
|
9
|
+
* a prose leaf's note) are skipped, and an undescribed property falls through to the
|
|
10
|
+
* group around it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function guidanceTrigger(from: Element, root: Element): HTMLElement | undefined;
|
|
13
|
+
/** Root keydown: the key raises (or lowers) the nearest guidance. A trigger's `click()`
|
|
14
|
+
* is an activation with no pointer behind it, which `FieldHint` toggles on. */
|
|
15
|
+
export declare function raiseGuidance(e: KeyboardEvent): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The guidance key: `Mod-/` on a control raises the description that control carries,
|
|
2
|
+
// the keyboard's way in to a `FieldHint` whose glyph is out of the tab order.
|
|
3
|
+
/** `Mod-/`, read off `key` so a layout that shifts to reach `/` still matches; either
|
|
4
|
+
* modifier stands for `Mod`. */
|
|
5
|
+
export function isGuidanceKey(e) {
|
|
6
|
+
return e.key === '/' && (e.ctrlKey || e.metaKey) && !e.altKey && !e.isComposing;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The guidance trigger for the description nearest `from`: the innermost element
|
|
10
|
+
* carrying `aria-describedby` whose ids name a parked description with a trigger
|
|
11
|
+
* beside it. A trigger points at its own parked node through that same attribute,
|
|
12
|
+
* which is the relation read here; ids that are not a description (an array's count,
|
|
13
|
+
* a prose leaf's note) are skipped, and an undescribed property falls through to the
|
|
14
|
+
* group around it.
|
|
15
|
+
*/
|
|
16
|
+
export function guidanceTrigger(from, root) {
|
|
17
|
+
for (let el = from.closest('[aria-describedby]'); el;) {
|
|
18
|
+
for (const id of el.getAttribute('aria-describedby').split(/\s+/)) {
|
|
19
|
+
const trigger = root.querySelector(`.qm-field-hint[aria-describedby="${id}"]`);
|
|
20
|
+
if (trigger)
|
|
21
|
+
return trigger;
|
|
22
|
+
}
|
|
23
|
+
el = el.parentElement?.closest('[aria-describedby]') ?? null;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/** Root keydown: the key raises (or lowers) the nearest guidance. A trigger's `click()`
|
|
28
|
+
* is an activation with no pointer behind it, which `FieldHint` toggles on. */
|
|
29
|
+
export function raiseGuidance(e) {
|
|
30
|
+
if (!isGuidanceKey(e) || !(e.target instanceof Element))
|
|
31
|
+
return;
|
|
32
|
+
const trigger = guidanceTrigger(e.target, e.currentTarget);
|
|
33
|
+
if (!trigger)
|
|
34
|
+
return;
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
trigger.click();
|
|
37
|
+
}
|