@quillmark/svelte 0.21.0 → 0.23.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 +6 -7
- package/README.md +1 -1
- package/dist/core/address.d.ts +6 -1
- package/dist/core/address.js +12 -3
- package/dist/core/bloom.d.ts +9 -4
- package/dist/core/bloom.js +18 -49
- package/dist/core/codec/atoms.d.ts +1 -1
- package/dist/core/codec/atoms.js +1 -1
- package/dist/core/codec/blocks.js +4 -2
- package/dist/core/codec/cap.d.ts +21 -0
- package/dist/core/codec/cap.js +63 -0
- package/dist/core/codec/code.d.ts +1 -1
- package/dist/core/codec/code.js +1 -1
- package/dist/core/codec/decode.d.ts +5 -5
- package/dist/core/codec/decode.js +18 -12
- package/dist/core/codec/encode.js +6 -1
- package/dist/core/codec/field.d.ts +22 -10
- package/dist/core/codec/field.js +54 -18
- package/dist/core/codec/index.d.ts +3 -1
- package/dist/core/codec/index.js +3 -2
- package/dist/core/codec/inputrules.js +1 -1
- package/dist/core/codec/lists.d.ts +3 -2
- package/dist/core/codec/lists.js +21 -5
- package/dist/core/codec/nesting.d.ts +20 -0
- package/dist/core/codec/nesting.js +62 -0
- package/dist/core/codec/prose.css +74 -20
- package/dist/core/codec/schema.d.ts +2 -2
- package/dist/core/codec/schema.js +16 -7
- package/dist/core/codec/table-copy.d.ts +17 -0
- package/dist/core/codec/table-copy.js +84 -0
- package/dist/core/codec/table-view.d.ts +1 -7
- package/dist/core/codec/table-view.js +70 -119
- package/dist/core/codec/table.d.ts +2 -9
- package/dist/core/codec/table.js +10 -19
- package/dist/core/codec/tasks.d.ts +19 -0
- package/dist/core/codec/tasks.js +140 -0
- package/dist/core/errors.d.ts +2 -2
- package/dist/visual/ArrayField.svelte +103 -63
- package/dist/visual/ArrayField.svelte.d.ts +0 -2
- package/dist/visual/Card.svelte +9 -3
- package/dist/visual/Card.svelte.d.ts +2 -2
- package/dist/visual/Field.svelte +33 -17
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldLabel.svelte +19 -33
- package/dist/visual/FieldLabel.svelte.d.ts +4 -2
- package/dist/visual/FormatPopover.svelte +54 -73
- package/dist/visual/MatrixField.svelte +18 -12
- package/dist/visual/NumberField.svelte +32 -3
- package/dist/visual/ObjectField.svelte +50 -22
- package/dist/visual/ObjectField.svelte.d.ts +6 -0
- package/dist/visual/ProseField.svelte +20 -14
- package/dist/visual/ProseField.svelte.d.ts +8 -2
- package/dist/visual/ProseValue.svelte +30 -19
- package/dist/visual/ProseValue.svelte.d.ts +7 -4
- package/dist/visual/TextField.svelte +3 -2
- package/dist/visual/TipsCard.svelte +5 -2
- package/dist/visual/VariantField.svelte +1 -1
- package/dist/visual/VariantField.svelte.d.ts +1 -1
- package/dist/visual/VisualEditorInner.svelte +13 -13
- package/dist/visual/controls.css +19 -1
- package/dist/visual/icons/nodes.d.ts +0 -21
- package/dist/visual/icons/nodes.js +1 -7
- package/dist/visual/leaves.d.ts +12 -9
- package/dist/visual/leaves.js +19 -0
- package/dist/visual/strings.d.ts +16 -5
- package/dist/visual/strings.js +17 -9
- package/dist/visual/structure.d.ts +26 -32
- package/dist/visual/structure.js +71 -53
- package/package.json +3 -3
package/dist/core/codec/field.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// (a) apply optimistically to the view,
|
|
6
6
|
// (b) lower the tr to a `ChangeBundle` (or `overwrite` for a field not yet at
|
|
7
7
|
// content rest, which has nothing to splice),
|
|
8
|
-
// (c) commit via `doc.applyChange(addr, bundle)`, or a `
|
|
9
|
-
//
|
|
8
|
+
// (c) commit via `doc.applyChange(addr, bundle)`, or a `text` leaf's whole text
|
|
9
|
+
// through the typed writer,
|
|
10
10
|
// (d) fire `onChange` when the transaction committed, then `onCaretMove` with
|
|
11
11
|
// the new USV caret, which a bare selection move fires on its own.
|
|
12
12
|
// On an `applyChange` throw the optimistic PM state stays and the failure reports
|
|
@@ -24,6 +24,7 @@ import { reportError, errorMessage } from '../errors.js';
|
|
|
24
24
|
import { decode, fitsLeaf } from './decode.js';
|
|
25
25
|
import { usvToPM, pmToUsv, buildLineIndex, lineIndexOf } from './positions.js';
|
|
26
26
|
import { lower, pmToContent, scanContent, scanDoc, contentEdit } from './encode.js';
|
|
27
|
+
import { capPlugin } from './cap.js';
|
|
27
28
|
import { islandPastePlugin } from './islands.js';
|
|
28
29
|
import { anchorsFromContent } from './marks.js';
|
|
29
30
|
import { contentEqual, createReconciler } from './reconcile.js';
|
|
@@ -33,7 +34,10 @@ import { bodyKeymap } from './keymap.js';
|
|
|
33
34
|
import { blockSchema, isBlockSchema, leafSchema, plainSchema } from './schema.js';
|
|
34
35
|
import { linkPastePlugin } from './link-paste.js';
|
|
35
36
|
import { markdownPastePlugin } from './markdown-paste.js';
|
|
37
|
+
import { nestingPlugin } from './nesting.js';
|
|
36
38
|
import { tablePastePlugin } from './table-paste.js';
|
|
39
|
+
import { tableCopyPlugin } from './table-copy.js';
|
|
40
|
+
import { taskPlugin } from './tasks.js';
|
|
37
41
|
import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
|
|
38
42
|
import { DEFAULT_SLASH_STRINGS, focusSlashItem, runSlashItem, slashPlugin } from './slash.js';
|
|
39
43
|
const anchorKey = new PluginKey('quill-anchors');
|
|
@@ -75,8 +79,8 @@ function anchorPlugin(seed) {
|
|
|
75
79
|
* and the verbatim read cannot answer it alone. A field the editor committed rests
|
|
76
80
|
* as `Content`; a field a markdown parse left rests as the authored string, and
|
|
77
81
|
* what that string means is the declared type's to say: `richtext` is markdown,
|
|
78
|
-
* `
|
|
79
|
-
*
|
|
82
|
+
* `text` is literal text, and reading one as the other silently eats every `*` the
|
|
83
|
+
* author typed. */
|
|
80
84
|
function readLeaf(reader, addr) {
|
|
81
85
|
return reader.getContent(addr) ?? emptyContent();
|
|
82
86
|
}
|
|
@@ -85,14 +89,18 @@ function readLeaf(reader, addr) {
|
|
|
85
89
|
export function emptyContent() {
|
|
86
90
|
return { text: '', lines: [{ containers: [], kind: 'para' }], marks: [], islands: [] };
|
|
87
91
|
}
|
|
92
|
+
/** Whether `rt` is the empty leaf: {@link emptyContent}, nothing typed and no shape left. */
|
|
93
|
+
export function isEmptyContent(rt) {
|
|
94
|
+
return contentEqual(rt, emptyContent());
|
|
95
|
+
}
|
|
88
96
|
/** Whether ops may commit here: the stored value is a `Content` object, so
|
|
89
97
|
* `applyChange` splices exactly the content the leaf read and PM is showing.
|
|
90
98
|
*
|
|
91
99
|
* The two other rest forms both take `overwrite` instead. An unset field (`undefined`;
|
|
92
100
|
* a default-only richtext field before its first edit) has nothing to splice, and an
|
|
93
101
|
* authored string nothing to lose: content-only marks do not survive markdown, so a
|
|
94
|
-
* string carries no anchors to pay. A body always rests as `Content`. A `
|
|
95
|
-
*
|
|
102
|
+
* string carries no anchors to pay. A body always rests as `Content`. A `text` leaf
|
|
103
|
+
* never asks: it commits through the writer ({@link createField}). */
|
|
96
104
|
function opsCommittable(doc, addr) {
|
|
97
105
|
const stored = doc.getStored(addr);
|
|
98
106
|
return stored !== null && typeof stored === 'object';
|
|
@@ -162,7 +170,7 @@ export function createField(opts) {
|
|
|
162
170
|
const slash = block ? opts.onSlash : undefined;
|
|
163
171
|
// Judged of the value being mounted rather than read off a diagnostic: a routed set
|
|
164
172
|
// can predate the value it arrives with, and `validation::not_plain` stands in for
|
|
165
|
-
// `
|
|
173
|
+
// `not_single_line` on a one-line `text` field.
|
|
166
174
|
const holds = (rt) => !fitsLeaf(rt, { inline, plaintext, islands: true });
|
|
167
175
|
let held = holds(reconciler.last);
|
|
168
176
|
const named = proseAttributes(opts) ?? {};
|
|
@@ -210,6 +218,9 @@ export function createField(opts) {
|
|
|
210
218
|
dispatchTransaction: (tr) => {
|
|
211
219
|
const oldRt = reconciler.last;
|
|
212
220
|
const next = view.state.apply(tr);
|
|
221
|
+
// A filtered transaction (`nesting.ts`) leaves the state as it was.
|
|
222
|
+
if (next === view.state)
|
|
223
|
+
return;
|
|
213
224
|
// An edit takes a held default: the commit below writes the whole leaf, so the
|
|
214
225
|
// mark goes in the same update that draws the edit.
|
|
215
226
|
if (!held && (tr.docChanged || tr.getMeta(anchorKey)))
|
|
@@ -272,6 +283,7 @@ export function createField(opts) {
|
|
|
272
283
|
// given one later; the plugin draws nothing while the text is empty.
|
|
273
284
|
placeholder: () => placeholderText,
|
|
274
285
|
placeholderUntilEdit: opts.placeholderUntilEdit,
|
|
286
|
+
cap: opts.cap,
|
|
275
287
|
afterHistory: [anchorPlugin(seededAnchors)]
|
|
276
288
|
})
|
|
277
289
|
});
|
|
@@ -282,6 +294,8 @@ export function createField(opts) {
|
|
|
282
294
|
// Returns whether anything landed: the caller's change signal, which must not
|
|
283
295
|
// be a property of which branch the commit took.
|
|
284
296
|
function commitEdit(oldRt, newRt) {
|
|
297
|
+
if (opts.emptyUnsets && heldAnchors().length === 0 && isEmptyContent(newRt))
|
|
298
|
+
return commitUnset();
|
|
285
299
|
if (plaintext)
|
|
286
300
|
return commitPlain(newRt.text);
|
|
287
301
|
// The text splice, computed once per keystroke: the gate below, both overwrite
|
|
@@ -335,8 +349,8 @@ export function createField(opts) {
|
|
|
335
349
|
}
|
|
336
350
|
}
|
|
337
351
|
/**
|
|
338
|
-
* A `
|
|
339
|
-
*
|
|
352
|
+
* A `text` leaf's commit: its whole text through the typed writer, which rests the
|
|
353
|
+
* field as that literal string (CODEC §Inline mode). A content object is the
|
|
340
354
|
* other rest form, and `toMarkdown` emits one as markdown: a break spelled as a
|
|
341
355
|
* backslash, a `*` and a trailing space escaped, a blank line doubled, so the value
|
|
342
356
|
* reads back changed. The leaf carries no anchors, so the op path has none to keep.
|
|
@@ -348,7 +362,7 @@ export function createField(opts) {
|
|
|
348
362
|
try {
|
|
349
363
|
const name = addr.field;
|
|
350
364
|
if (name === undefined)
|
|
351
|
-
throw new Error('a
|
|
365
|
+
throw new Error('a text leaf names a field');
|
|
352
366
|
(addr.card === undefined ? writer : writer.card(addr.card)).set(name, text);
|
|
353
367
|
reconciler.commit(readLeaf(reader, addr));
|
|
354
368
|
return true;
|
|
@@ -363,6 +377,23 @@ export function createField(opts) {
|
|
|
363
377
|
return false;
|
|
364
378
|
}
|
|
365
379
|
}
|
|
380
|
+
/** An emptied optional leaf's commit: the field removed, back to unanswered. */
|
|
381
|
+
function commitUnset() {
|
|
382
|
+
try {
|
|
383
|
+
doc.removeField(addr);
|
|
384
|
+
reconciler.commit(readLeaf(reader, addr));
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
catch (e) {
|
|
388
|
+
reportError(opts.onError, {
|
|
389
|
+
code: 'commit-lost',
|
|
390
|
+
severity: 'error',
|
|
391
|
+
message: `the field could not be removed; the stored value is stale while the editor keeps the edit: ${errorMessage(e)}`,
|
|
392
|
+
cause: e
|
|
393
|
+
});
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
366
397
|
/** The anchors the plugin currently holds (PM coords); `[]` before the first apply. */
|
|
367
398
|
const heldAnchors = () => anchorKey.getState(view.state) ?? [];
|
|
368
399
|
/** The plugin's anchor positions as USV, against `index` — which `dispatchTransaction`
|
|
@@ -437,7 +468,7 @@ export function createField(opts) {
|
|
|
437
468
|
return readLeaf(reader, addr);
|
|
438
469
|
},
|
|
439
470
|
insertAnchor(id, pos) {
|
|
440
|
-
// A
|
|
471
|
+
// A text field carries no marks (§Inline mode), and a held one commits nothing.
|
|
441
472
|
if (plaintext || held)
|
|
442
473
|
return;
|
|
443
474
|
if (heldAnchors().some((a) => a.id === id))
|
|
@@ -509,16 +540,21 @@ export function createField(opts) {
|
|
|
509
540
|
* Backspace that takes one back, the field keymap over the base keymap, the URL paste
|
|
510
541
|
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
511
542
|
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
512
|
-
* (`islands.ts`), the table paste (`table-paste.ts`)
|
|
513
|
-
* (`markdown-paste.ts`).
|
|
543
|
+
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
544
|
+
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
|
|
514
545
|
*
|
|
515
|
-
* Every mark-shaped plugin reads the schema rather than a flag: over either
|
|
546
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
516
547
|
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
517
548
|
* toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
|
|
518
549
|
* its author typed without a second rule saying so.
|
|
519
550
|
*/
|
|
520
551
|
export function proseLeafPlugins(schema, opts) {
|
|
521
|
-
const list = [
|
|
552
|
+
const list = [
|
|
553
|
+
history(),
|
|
554
|
+
...(opts.cap ? [capPlugin(opts.cap)] : []),
|
|
555
|
+
...(opts.afterHistory ?? []),
|
|
556
|
+
linebreakPlugin(schema)
|
|
557
|
+
];
|
|
522
558
|
if (opts.slash)
|
|
523
559
|
list.push(slashPlugin(opts.slash, opts.slashHint));
|
|
524
560
|
if (!opts.noInputRules)
|
|
@@ -530,7 +566,7 @@ export function proseLeafPlugins(schema, opts) {
|
|
|
530
566
|
// Each answers to something only the block schema holds: any other leaf is
|
|
531
567
|
// textblocks alone, with no island and no gap to put a cursor in.
|
|
532
568
|
if (isBlockSchema(schema))
|
|
533
|
-
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), markdownPastePlugin());
|
|
569
|
+
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), taskPlugin(), nestingPlugin());
|
|
534
570
|
if (schema === plainSchema)
|
|
535
571
|
list.push(plainClipboardPlugin());
|
|
536
572
|
if (opts.placeholder)
|
|
@@ -641,9 +677,9 @@ function ghostPlugin(opts) {
|
|
|
641
677
|
/**
|
|
642
678
|
* The field's keymap: history, mark toggles, and the body's structural keys; Enter
|
|
643
679
|
* suppressed inline. The toggles are bound per mark type the schema declares, so a
|
|
644
|
-
*
|
|
680
|
+
* `text` leaf leaves Mod-b/i/u to the browser.
|
|
645
681
|
*
|
|
646
|
-
* Tab forks on the leaf's role, not on the caret's position.
|
|
682
|
+
* Tab forks on the leaf's role, not on the caret's position. A one-line or `text`
|
|
647
683
|
* leaf is a form field: Tab stays unbound, so field navigation is open for a shell
|
|
648
684
|
* keymap (VISUAL_EDITOR §Settled and open). A block-schema body is a document: Tab
|
|
649
685
|
* is structural, a chain each nested surface prepends to (`keymap.ts`). One key
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { rangeAnchor } from './anchor.js';
|
|
2
2
|
export type { RangeAnchor } from './anchor.js';
|
|
3
|
-
export { createField, emptyContent, heldAttributes, proseAttributes, proseLeafPlugins } from './field.js';
|
|
3
|
+
export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
4
4
|
export type { CreateFieldOpts, FieldController, LeafViews } from './field.js';
|
|
5
|
+
export { capCount } from './cap.js';
|
|
6
|
+
export type { TextCap } from './cap.js';
|
|
5
7
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
6
8
|
export type { TableChromeStrings } from './table-view.js';
|
|
7
9
|
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -13,7 +13,8 @@ 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, heldAttributes, proseAttributes, proseLeafPlugins } from './field.js';
|
|
16
|
+
export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
17
|
+
export { capCount } from './cap.js';
|
|
17
18
|
// The table island's chrome vocabulary: the visual tier's `strings` set extends it,
|
|
18
19
|
// so the island's wording is overridden beside every other key.
|
|
19
20
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
@@ -37,7 +38,7 @@ export { pmToContent, contentEdit, lower } from './encode.js';
|
|
|
37
38
|
export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
|
|
38
39
|
// Reconciliation gate.
|
|
39
40
|
export { createReconciler, contentEqual, valueEqual } from './reconcile.js';
|
|
40
|
-
// Input rules (`createField` mounts them unless `noInputRules
|
|
41
|
+
// Input rules (`createField` mounts them unless `noInputRules`, or over a mark-free schema).
|
|
41
42
|
export { inputRulesPlugin } from './inputrules.js';
|
|
42
43
|
// The body leaf's structural keys: `createField` binds the composed chains
|
|
43
44
|
// (`bodyKeymap`); the suite drives both it and the list link directly.
|
|
@@ -58,7 +58,7 @@ export function markdownInputRules(schema) {
|
|
|
58
58
|
const rules = [];
|
|
59
59
|
const m = schema.marks;
|
|
60
60
|
// Inline marks. Non-greedy capture between matching delimiters. Built only where the
|
|
61
|
-
// schema has the mark: the inline schema omits them, and a
|
|
61
|
+
// schema has the mark: the inline schema omits them, and a `text` one declares none
|
|
62
62
|
// at all.
|
|
63
63
|
if (m.strong)
|
|
64
64
|
rules.push(markInputRule(/\*\*([^*]+)\*\*$/, m.strong, 2));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Schema } from 'prosemirror-model';
|
|
2
2
|
import type { Command } from 'prosemirror-state';
|
|
3
3
|
/**
|
|
4
|
-
* The list bindings for a block-schema leaf: `{}` for the
|
|
4
|
+
* The list bindings for a block-schema leaf: `{}` for the one-line and plain
|
|
5
5
|
* schemas, which declare no list nodes.
|
|
6
6
|
*
|
|
7
7
|
* `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
|
|
@@ -13,7 +13,8 @@ import type { Command } from 'prosemirror-state';
|
|
|
13
13
|
* shell structural keymap (VISUAL_EDITOR §Settled and open).
|
|
14
14
|
*
|
|
15
15
|
* `Enter` is a chain in precedence order: the escape-above gesture, then
|
|
16
|
-
* `splitListItem`,
|
|
16
|
+
* `splitListItem` (`splitTaskItem`, which leaves a task item's new half an open task),
|
|
17
|
+
* then `liftListItem`. The middle link carries two behaviors of
|
|
17
18
|
* its own: it splits a non-empty item, and on an empty item it either splits the
|
|
18
19
|
* wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
|
|
19
20
|
* so the third link lifts a top-level empty item out to a paragraph. `Backspace` lifts
|
package/dist/core/codec/lists.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { liftListItem, sinkListItem
|
|
1
|
+
import { liftListItem, sinkListItem } from 'prosemirror-schema-list';
|
|
2
2
|
import { EditorState, Selection, TextSelection } from 'prosemirror-state';
|
|
3
3
|
import { chainCommands, joinTextblockBackward, joinTextblockForward } from 'prosemirror-commands';
|
|
4
|
+
import { withinNesting } from './nesting.js';
|
|
5
|
+
import { splitTaskItem } from './tasks.js';
|
|
4
6
|
/** The caret's enclosing `list_item` when it sits at the very start of that item's
|
|
5
7
|
* first block: the position both structural keys branch on. `null` otherwise
|
|
6
8
|
* (mid-text, a non-first block of a multi-paragraph item, a non-empty selection),
|
|
@@ -187,8 +189,21 @@ function backspaceBelow(itemType, press) {
|
|
|
187
189
|
return press(EditorState.create({ doc: state.doc, selection: below }), dispatch && keepCaret);
|
|
188
190
|
};
|
|
189
191
|
}
|
|
192
|
+
/** Tab → `sinkListItem`, declined where the sink would nest past `MAX_NESTING_DEPTH`
|
|
193
|
+
* (`nesting.ts`), so Tab there leaves the body as it does on an item with nothing to
|
|
194
|
+
* sink under. */
|
|
195
|
+
function sinkWithin(itemType) {
|
|
196
|
+
const sink = sinkListItem(itemType);
|
|
197
|
+
return (state, dispatch) => {
|
|
198
|
+
let sunk;
|
|
199
|
+
if (!sink(state, (tr) => (sunk = tr)) || !sunk || !withinNesting(sunk.doc))
|
|
200
|
+
return false;
|
|
201
|
+
dispatch?.(sunk);
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
190
205
|
/**
|
|
191
|
-
* The list bindings for a block-schema leaf: `{}` for the
|
|
206
|
+
* The list bindings for a block-schema leaf: `{}` for the one-line and plain
|
|
192
207
|
* schemas, which declare no list nodes.
|
|
193
208
|
*
|
|
194
209
|
* `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
|
|
@@ -200,7 +215,8 @@ function backspaceBelow(itemType, press) {
|
|
|
200
215
|
* shell structural keymap (VISUAL_EDITOR §Settled and open).
|
|
201
216
|
*
|
|
202
217
|
* `Enter` is a chain in precedence order: the escape-above gesture, then
|
|
203
|
-
* `splitListItem`,
|
|
218
|
+
* `splitListItem` (`splitTaskItem`, which leaves a task item's new half an open task),
|
|
219
|
+
* then `liftListItem`. The middle link carries two behaviors of
|
|
204
220
|
* its own: it splits a non-empty item, and on an empty item it either splits the
|
|
205
221
|
* wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
|
|
206
222
|
* so the third link lifts a top-level empty item out to a paragraph. `Backspace` lifts
|
|
@@ -217,9 +233,9 @@ export function listKeymap(schema) {
|
|
|
217
233
|
return {};
|
|
218
234
|
const backspace = chainCommands(liftAtItemStart(itemType), joinOntoList(itemType));
|
|
219
235
|
return {
|
|
220
|
-
Tab:
|
|
236
|
+
Tab: sinkWithin(itemType),
|
|
221
237
|
'Shift-Tab': liftListItem(itemType),
|
|
222
|
-
Enter: chainCommands(paragraphAboveList(itemType, paragraph),
|
|
238
|
+
Enter: chainCommands(paragraphAboveList(itemType, paragraph), splitTaskItem(itemType), liftListItem(itemType)),
|
|
223
239
|
Backspace: backspace,
|
|
224
240
|
Delete: backspaceBelow(itemType, backspace)
|
|
225
241
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Node as PMNode } from 'prosemirror-model';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
/** The store's bound on the containers around one line: a commit nesting past it is
|
|
4
|
+
* refused, and `@quillmark/wasm` exports no constant to read it from. */
|
|
5
|
+
export declare const MAX_NESTING_DEPTH = 12;
|
|
6
|
+
/** The most containers any block in `doc` stands inside, counted as the store counts
|
|
7
|
+
* them: a `list_item` or a `blockquote` is one, the list around an item none, and no
|
|
8
|
+
* node holds an element (`decode.ts`). */
|
|
9
|
+
export declare function nestingDepth(doc: PMNode): number;
|
|
10
|
+
/** Whether `doc` nests within {@link MAX_NESTING_DEPTH}: what a command that nests asks
|
|
11
|
+
* of the doc it would dispatch. */
|
|
12
|
+
export declare function withinNesting(doc: PMNode): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Refuses a transaction leaving `doc` past {@link MAX_NESTING_DEPTH}, which the commit
|
|
15
|
+
* would lose: a paste or a drop into a deep item, whose slice the import bounded alone.
|
|
16
|
+
* The nesting commands ask first and keep their key; this is what reaches the rest, and
|
|
17
|
+
* a refused paste lands nothing. Past the bound, a transaction passes where it leaves the
|
|
18
|
+
* deepest level no deeper, so a body that arrived that deep keeps its Enter.
|
|
19
|
+
*/
|
|
20
|
+
export declare function nestingPlugin(): Plugin;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import { ReplaceAroundStep, ReplaceStep } from 'prosemirror-transform';
|
|
3
|
+
/** The store's bound on the containers around one line: a commit nesting past it is
|
|
4
|
+
* refused, and `@quillmark/wasm` exports no constant to read it from. */
|
|
5
|
+
export const MAX_NESTING_DEPTH = 12;
|
|
6
|
+
/** The most containers any block in `doc` stands inside, counted as the store counts
|
|
7
|
+
* them: a `list_item` or a `blockquote` is one, the list around an item none, and no
|
|
8
|
+
* node holds an element (`decode.ts`). */
|
|
9
|
+
export function nestingDepth(doc) {
|
|
10
|
+
const walk = (node, depth) => {
|
|
11
|
+
let deepest = depth;
|
|
12
|
+
node.forEach((child) => {
|
|
13
|
+
if (child.isTextblock || child.isAtom)
|
|
14
|
+
return;
|
|
15
|
+
const name = child.type.name;
|
|
16
|
+
const d = depth + (name === 'list_item' || name === 'blockquote' ? 1 : 0);
|
|
17
|
+
deepest = Math.max(deepest, walk(child, d));
|
|
18
|
+
});
|
|
19
|
+
return deepest;
|
|
20
|
+
};
|
|
21
|
+
return walk(doc, 0);
|
|
22
|
+
}
|
|
23
|
+
/** Whether `doc` nests within {@link MAX_NESTING_DEPTH}: what a command that nests asks
|
|
24
|
+
* of the doc it would dispatch. */
|
|
25
|
+
export function withinNesting(doc) {
|
|
26
|
+
return nestingDepth(doc) <= MAX_NESTING_DEPTH;
|
|
27
|
+
}
|
|
28
|
+
/** Whether a step of `tr` can move a block under another container: any replace but
|
|
29
|
+
* inline content inside one textblock, which is every keystroke's. Mark and attribute
|
|
30
|
+
* steps move no node. */
|
|
31
|
+
function mayNest(tr) {
|
|
32
|
+
return tr.steps.some((step, i) => {
|
|
33
|
+
if (step instanceof ReplaceAroundStep)
|
|
34
|
+
return true;
|
|
35
|
+
if (!(step instanceof ReplaceStep))
|
|
36
|
+
return false;
|
|
37
|
+
const doc = tr.docs[i];
|
|
38
|
+
const $from = doc.resolve(step.from);
|
|
39
|
+
if (!$from.parent.isTextblock || !$from.sameParent(doc.resolve(step.to)))
|
|
40
|
+
return true;
|
|
41
|
+
let block = false;
|
|
42
|
+
step.slice.content.forEach((node) => (block ||= node.isBlock));
|
|
43
|
+
return block;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Refuses a transaction leaving `doc` past {@link MAX_NESTING_DEPTH}, which the commit
|
|
48
|
+
* would lose: a paste or a drop into a deep item, whose slice the import bounded alone.
|
|
49
|
+
* The nesting commands ask first and keep their key; this is what reaches the rest, and
|
|
50
|
+
* a refused paste lands nothing. Past the bound, a transaction passes where it leaves the
|
|
51
|
+
* deepest level no deeper, so a body that arrived that deep keeps its Enter.
|
|
52
|
+
*/
|
|
53
|
+
export function nestingPlugin() {
|
|
54
|
+
return new Plugin({
|
|
55
|
+
filterTransaction: (tr) => {
|
|
56
|
+
if (!tr.docChanged || !mayNest(tr))
|
|
57
|
+
return true;
|
|
58
|
+
const after = nestingDepth(tr.doc);
|
|
59
|
+
return after <= MAX_NESTING_DEPTH || after <= nestingDepth(tr.before);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -148,6 +148,20 @@ li.ProseMirror-selectednode::after {
|
|
|
148
148
|
font-size: inherit;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/* A fence's language at the label rung in its top corner, read off the `data-lang` the
|
|
152
|
+
node writes (`schema.ts`). Generated content, so it is never text the caret reaches
|
|
153
|
+
or a copy takes; the code's lines wrap around it. Read-only: the language is changed
|
|
154
|
+
in source mode. */
|
|
155
|
+
.ProseMirror pre[data-lang]::before {
|
|
156
|
+
content: attr(data-lang);
|
|
157
|
+
float: inline-end;
|
|
158
|
+
margin-inline-start: var(--_qm-space-3);
|
|
159
|
+
font-size: var(--_qm-text-label);
|
|
160
|
+
color: var(--_qm-ink-label);
|
|
161
|
+
user-select: none;
|
|
162
|
+
pointer-events: none;
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
/* A link takes the surface's ink (the UA's blue is the `#8cf` defect again) and a
|
|
152
166
|
dotted underline. Dotted rather than solid because `underline` is a mark in the
|
|
153
167
|
same schema, and a solid rule renders an authored `<u>` and a link identically in a
|
|
@@ -191,6 +205,59 @@ li.ProseMirror-selectednode::after {
|
|
|
191
205
|
list-style: revert;
|
|
192
206
|
}
|
|
193
207
|
|
|
208
|
+
/* A task item's checkbox (`tasks.ts`) stands after the item's marker and ahead of its
|
|
209
|
+
first line, the text wrapping back under it, as the paper sets the box. Its face is a
|
|
210
|
+
matrix tick's: the input drawn `appearance: none` as an edged well, held at the
|
|
211
|
+
accent, the check glyph over it in the surface ink. A disabled box is a held leaf's
|
|
212
|
+
or a copy's, and takes no press. */
|
|
213
|
+
.ProseMirror .qm-task {
|
|
214
|
+
position: relative;
|
|
215
|
+
float: left;
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
height: 1lh;
|
|
219
|
+
margin-right: var(--_qm-space-2);
|
|
220
|
+
}
|
|
221
|
+
.ProseMirror .qm-task-box {
|
|
222
|
+
appearance: none;
|
|
223
|
+
margin: 0;
|
|
224
|
+
width: var(--_qm-glyph-control);
|
|
225
|
+
height: var(--_qm-glyph-control);
|
|
226
|
+
border: var(--_qm-border-width) solid var(--_qm-border);
|
|
227
|
+
border-radius: calc(var(--_qm-radius-inner) / 2);
|
|
228
|
+
background: var(--_qm-surface-well);
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
transition:
|
|
231
|
+
background-color var(--_qm-duration-fast) var(--_qm-ease-reverse),
|
|
232
|
+
border-color var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
233
|
+
}
|
|
234
|
+
.ProseMirror .qm-task-box:checked {
|
|
235
|
+
background: var(--_qm-accent);
|
|
236
|
+
border-color: var(--_qm-accent);
|
|
237
|
+
}
|
|
238
|
+
.ProseMirror .qm-task-box:disabled {
|
|
239
|
+
cursor: default;
|
|
240
|
+
}
|
|
241
|
+
.ProseMirror .qm-task-box:focus-visible {
|
|
242
|
+
outline: var(--_qm-ring);
|
|
243
|
+
outline-offset: var(--_qm-ring-offset);
|
|
244
|
+
}
|
|
245
|
+
.ProseMirror .qm-task-mark {
|
|
246
|
+
position: absolute;
|
|
247
|
+
top: 50%;
|
|
248
|
+
left: 0;
|
|
249
|
+
width: var(--_qm-glyph-control);
|
|
250
|
+
height: var(--_qm-glyph-control);
|
|
251
|
+
translate: 0 -50%;
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
color: var(--_qm-surface);
|
|
254
|
+
opacity: 0;
|
|
255
|
+
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
256
|
+
}
|
|
257
|
+
.ProseMirror .qm-task:has(:checked) .qm-task-mark {
|
|
258
|
+
opacity: 1;
|
|
259
|
+
}
|
|
260
|
+
|
|
194
261
|
/* Six levels, one weight, no size step. The quill reads no level at all, so a size
|
|
195
262
|
ramp here would be the editor predicting a hierarchy the paper does not set; the
|
|
196
263
|
level stays in the model, which is where the codec round-trips it. It is also all
|
|
@@ -516,37 +583,24 @@ li.ProseMirror-selectednode::after {
|
|
|
516
583
|
height: var(--_qm-tap-min);
|
|
517
584
|
}
|
|
518
585
|
/* The reveal is the control's own (CODEC §"The table island"): one comes up while the
|
|
519
|
-
pointer is on that control, while it
|
|
520
|
-
about — the selected line's grip, and the lifted one, which is drawn through a drag the
|
|
586
|
+
pointer is on that control, or while it is what a gesture is about — the selected line's grip, and the lifted one, which is drawn through a drag the
|
|
521
587
|
pointer has travelled off. A cell under the pointer draws its row's and its column's
|
|
522
588
|
grips (`table-view.ts` §`onHover`), one pill on each axis and none anywhere else.
|
|
523
589
|
|
|
524
590
|
Opacity rather than absence, so every control keeps its box and is hit-tested wherever
|
|
525
|
-
it sits. A grip's reach
|
|
526
|
-
focus rung: what a keyboard draws a line with is the cell the
|
|
527
|
-
(`table-view.ts` §`cellKeys`), and the wash and the selected
|
|
528
|
-
|
|
529
|
-
on them, so the rung there draws one a keyboard has reached. */
|
|
591
|
+
it sits. A grip's reach and a bar's are the pointer's alone and neither holds focus, so
|
|
592
|
+
neither carries a focus rung: what a keyboard draws a line with is the cell the
|
|
593
|
+
selection runs through (`table-view.ts` §`cellKeys`), and the wash and the selected
|
|
594
|
+
grip's tint are what say so. */
|
|
530
595
|
:is(.qm-table-grip, .qm-table-add) {
|
|
531
596
|
opacity: 0;
|
|
532
597
|
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
533
598
|
}
|
|
534
|
-
.qm-table-add:
|
|
599
|
+
.qm-table-add:hover,
|
|
535
600
|
.qm-table-grip:is([data-near], :hover, [aria-pressed='true'], .qm-table-lifted) {
|
|
536
601
|
opacity: 1;
|
|
537
602
|
}
|
|
538
603
|
|
|
539
|
-
/* The ring, on the two add bars and nowhere else in this file. A prose leaf's focus
|
|
540
|
-
indicator is its caret and a cell's is too, so the outline is dropped there; a control
|
|
541
|
-
that comes up out of nothing has the reveal, and a ring under a keyboard is what says
|
|
542
|
-
the focus is there rather than on the paper behind it. Declared here rather than
|
|
543
|
-
reached for as `.qm-focus-ring`: that recipe is the visual tier's stylesheet, and the
|
|
544
|
-
island is inside the leaf, which is the codec's. */
|
|
545
|
-
.qm-table-add:focus-visible {
|
|
546
|
-
outline: var(--_qm-ring);
|
|
547
|
-
outline-offset: var(--_qm-ring-offset);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
604
|
/* A weighted table spans the leaf, laid out fixed, so the stored weights are the
|
|
551
605
|
proportions its columns draw, as the page's fractional columns span the text block
|
|
552
606
|
(`table-view.ts` §`weigh`). An auto-fit table keeps its content's width. */
|
|
@@ -610,7 +664,7 @@ li.ProseMirror-selectednode::after {
|
|
|
610
664
|
display: none;
|
|
611
665
|
}
|
|
612
666
|
/* The header toggle hangs under the first row's last cell, flush with the grid's
|
|
613
|
-
trailing edge
|
|
667
|
+
trailing edge. */
|
|
614
668
|
.qm-table-align[data-header] {
|
|
615
669
|
margin-inline: auto 0;
|
|
616
670
|
}
|
|
@@ -23,7 +23,7 @@ export declare const plaintextSchema: Schema<string, never>;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare const cellSchema: Schema<"hard_break", string>;
|
|
25
25
|
/**
|
|
26
|
-
* A `
|
|
26
|
+
* A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, no
|
|
27
27
|
* islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
|
|
28
28
|
* for the reason {@link plaintextSchema} is.
|
|
29
29
|
*
|
|
@@ -38,7 +38,7 @@ export declare function leafSchema(opts: {
|
|
|
38
38
|
plaintext?: boolean;
|
|
39
39
|
inline?: boolean;
|
|
40
40
|
}): Schema;
|
|
41
|
-
/** Whether `schema` can carry formatting at all: false for the two
|
|
41
|
+
/** Whether `schema` can carry formatting at all: false for the two mark-free schemas.
|
|
42
42
|
* What a mark command asks before it offers itself. */
|
|
43
43
|
export declare function hasMarks(schema: Schema): boolean;
|
|
44
44
|
/** True for the one-line schemas, which declare no `hard_break`; decode branches on it. */
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// (list_item/quote → lists/blockquote); marks mirror the content formatting set.
|
|
4
4
|
// Each set is closed upstream, so the schema names its whole vocabulary and carries
|
|
5
5
|
// nothing inert. `blockSchema` is the full field; `inlineSchema` is the constrained
|
|
6
|
-
// single-textblock form for `richtext
|
|
7
|
-
// containers, no islands) and `plaintextSchema` is that one without marks, for
|
|
8
|
-
// `
|
|
6
|
+
// single-textblock form for a one-line `richtext` (one paragraph, no block split, no
|
|
7
|
+
// containers, no islands) and `plaintextSchema` is that one without marks, for a
|
|
8
|
+
// one-line `text`; `plainSchema` is a `text` field declaring `multiline: true`,
|
|
9
9
|
// paragraphs and hard breaks and nothing else; `cellSchema` is the inline form with
|
|
10
10
|
// hard breaks, for a table cell. Same decode/lower/position machinery,
|
|
11
11
|
// narrower shapes. Anchors are not marks here (decorations).
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// marks, a `<table>` being the block leaf's clipboard parser's (`table-paste.ts`).
|
|
19
19
|
import { Schema } from 'prosemirror-model';
|
|
20
20
|
import { islandBlockSpec, islandInlineSpec } from './islands.js';
|
|
21
|
+
import { taskBody, taskChecked, taskDOM } from './tasks.js';
|
|
21
22
|
import { rendersHref, storableUrl } from './urls.js';
|
|
22
23
|
// ── Marks spelled as style ──────────────────────────────────────────────────
|
|
23
24
|
// A word processor writes formatting as a span's inline style rather than as a tag:
|
|
@@ -193,11 +194,19 @@ const blockNodes = {
|
|
|
193
194
|
parseDOM: [{ tag: 'ul' }],
|
|
194
195
|
toDOM: () => ['ul', 0]
|
|
195
196
|
},
|
|
197
|
+
// `checked` is a task item's, `null` on every other item (`tasks.ts`).
|
|
196
198
|
list_item: {
|
|
197
199
|
content: 'block+',
|
|
198
200
|
defining: true,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
attrs: { checked: { default: null } },
|
|
202
|
+
parseDOM: [
|
|
203
|
+
{
|
|
204
|
+
tag: 'li',
|
|
205
|
+
getAttrs: (el) => ({ checked: taskChecked(el) }),
|
|
206
|
+
contentElement: taskBody
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
toDOM: (node) => node.attrs.checked === null ? ['li', 0] : taskDOM(node.attrs.checked)
|
|
201
210
|
},
|
|
202
211
|
island_block: islandBlockSpec
|
|
203
212
|
};
|
|
@@ -254,7 +263,7 @@ export const cellSchema = new Schema({
|
|
|
254
263
|
marks
|
|
255
264
|
});
|
|
256
265
|
/**
|
|
257
|
-
* A `
|
|
266
|
+
* A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, no
|
|
258
267
|
* islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
|
|
259
268
|
* for the reason {@link plaintextSchema} is.
|
|
260
269
|
*
|
|
@@ -278,7 +287,7 @@ export function leafSchema(opts) {
|
|
|
278
287
|
return opts.inline ? plaintextSchema : plainSchema;
|
|
279
288
|
return opts.inline ? inlineSchema : blockSchema;
|
|
280
289
|
}
|
|
281
|
-
/** Whether `schema` can carry formatting at all: false for the two
|
|
290
|
+
/** Whether `schema` can carry formatting at all: false for the two mark-free schemas.
|
|
282
291
|
* What a mark command asks before it offers itself. */
|
|
283
292
|
export function hasMarks(schema) {
|
|
284
293
|
return Object.keys(schema.marks).length > 0;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import type { TableProps } from '@quillmark/wasm';
|
|
3
|
+
/** A block of cells, both corners inclusive, in the row space `table.ts` names. */
|
|
4
|
+
export interface CellRange {
|
|
5
|
+
r0: number;
|
|
6
|
+
c0: number;
|
|
7
|
+
r1: number;
|
|
8
|
+
c1: number;
|
|
9
|
+
}
|
|
10
|
+
/** The cells of `range` as a `<table>`, the header row in a `<thead>` where the range
|
|
11
|
+
* holds it and the table has one. The cells carry their marks and breaks, and nothing
|
|
12
|
+
* of the table's layout. */
|
|
13
|
+
export declare function tableElement(props: TableProps, range?: CellRange): HTMLTableElement;
|
|
14
|
+
/** The cells of `range` as tab-separated text, a line a row. */
|
|
15
|
+
export declare function tableText(props: TableProps, range?: CellRange): string;
|
|
16
|
+
/** A block leaf's copy: a table in the slice writes as a table in both spellings. */
|
|
17
|
+
export declare function tableCopyPlugin(): Plugin;
|