@quillmark/svelte 0.21.0 → 0.22.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/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 +15 -10
- package/dist/core/codec/field.js +46 -17
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +2 -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 +101 -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 +20 -16
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldLabel.svelte +5 -33
- package/dist/visual/FieldLabel.svelte.d.ts +0 -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 +38 -20
- package/dist/visual/ObjectField.svelte.d.ts +6 -0
- package/dist/visual/ProseField.svelte +10 -13
- package/dist/visual/ProseField.svelte.d.ts +5 -2
- package/dist/visual/ProseValue.svelte +25 -19
- package/dist/visual/ProseValue.svelte.d.ts +4 -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 +10 -5
- package/dist/visual/strings.js +16 -9
- package/dist/visual/structure.d.ts +21 -31
- package/dist/visual/structure.js +54 -51
- package/package.json +3 -3
package/NOTICE
CHANGED
|
@@ -6,14 +6,13 @@ Copyright 2026 Nibs
|
|
|
6
6
|
This package contains SVG glyph geometry derived from Lucide, carried in source
|
|
7
7
|
rather than installed.
|
|
8
8
|
|
|
9
|
-
`visual/icons/nodes.ts` holds
|
|
9
|
+
`visual/icons/nodes.ts` holds eighteen verbatim from @lucide/svelte 1.28.0:
|
|
10
10
|
chevron-right, arrow-right, chevron-down, chevron-up, bold, italic, underline,
|
|
11
|
-
strikethrough, code, link,
|
|
12
|
-
|
|
11
|
+
strikethrough, code, link, info, plus, x, trash-2, minus, check, calendar and
|
|
12
|
+
calendar-check.
|
|
13
13
|
|
|
14
14
|
`core/codec/table-view.ts` holds grip-horizontal, grip-vertical, align-left,
|
|
15
|
-
align-center, align-right
|
|
16
|
-
align-end-horizontal and panel-top, derived from an earlier release of the same
|
|
15
|
+
align-center, align-right and panel-top, derived from an earlier release of the same
|
|
17
16
|
project.
|
|
18
17
|
|
|
19
18
|
ISC License
|
|
@@ -34,8 +33,8 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
34
33
|
|
|
35
34
|
--------------------------------------------------------------------------------
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
code,
|
|
36
|
+
Twelve of those glyphs — arrow-right, chevron-down, chevron-right, chevron-up,
|
|
37
|
+
code, info, italic, link, minus, plus, trash-2 and x — are drawn exactly as
|
|
39
38
|
the Feather project draws them and carry its license as well.
|
|
40
39
|
|
|
41
40
|
The MIT License (MIT)
|
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ Hand it a different `doc` and the editor **re-keys itself**: every leaf remounts
|
|
|
149
149
|
|
|
150
150
|
### Wording
|
|
151
151
|
|
|
152
|
-
The package ships English and a seam to replace it. `strings` is keyed and **partial**: set what you have translations for, and the rest stay the package's. Several keys are **accessible names** rather than decoration (the card controls, the add trigger
|
|
152
|
+
The package ships English and a seam to replace it. `strings` is keyed and **partial**: set what you have translations for, and the rest stay the package's. Several keys are **accessible names** rather than decoration (the card controls, the add trigger), so an untranslated surface reads the wrong language to a screen reader. `DEFAULT_VISUAL_STRINGS` is the English, exported so you can compose against it. `<Preview>` takes its own three-key `strings` for the states it shows when there is nothing to paint.
|
|
153
153
|
|
|
154
154
|
```svelte
|
|
155
155
|
<VisualEditor
|
package/dist/core/address.d.ts
CHANGED
|
@@ -50,10 +50,15 @@ export interface Landing {
|
|
|
50
50
|
* - `{card: i}` → `"cards.<kind>[i].body"`
|
|
51
51
|
* - `{card: i, field}` → `"cards.<kind>[i].<field>"`
|
|
52
52
|
*
|
|
53
|
+
* `steps` descend from the field to a leaf inside it, the inverse of
|
|
54
|
+
* {@link nestedAddrForFieldPath}: `{field: 'contact'}` with `['email']` is
|
|
55
|
+
* `"main.contact.email"`, `{field: 'keywords'}` with `[0]` is `"main.keywords[0]"`. A body
|
|
56
|
+
* holds no cells, so a field-less address with steps is `undefined`.
|
|
57
|
+
*
|
|
53
58
|
* `cards[i]` stands in for an unknown or blank kind. The index is the addr's own: no
|
|
54
59
|
* per-kind counting, since `DocPath` addresses cards by document-array index.
|
|
55
60
|
*/
|
|
56
|
-
export declare function fieldPathForAddr(addr: Addr, kinds: readonly string[]): DocPath | undefined;
|
|
61
|
+
export declare function fieldPathForAddr(addr: Addr, kinds: readonly string[], steps?: readonly PathStep[]): DocPath | undefined;
|
|
57
62
|
/**
|
|
58
63
|
* A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
|
|
59
64
|
* where the change is the card rather than anything inside it. `undefined` for an
|
package/dist/core/address.js
CHANGED
|
@@ -15,15 +15,24 @@ import { core } from './lifecycle.js';
|
|
|
15
15
|
* - `{card: i}` → `"cards.<kind>[i].body"`
|
|
16
16
|
* - `{card: i, field}` → `"cards.<kind>[i].<field>"`
|
|
17
17
|
*
|
|
18
|
+
* `steps` descend from the field to a leaf inside it, the inverse of
|
|
19
|
+
* {@link nestedAddrForFieldPath}: `{field: 'contact'}` with `['email']` is
|
|
20
|
+
* `"main.contact.email"`, `{field: 'keywords'}` with `[0]` is `"main.keywords[0]"`. A body
|
|
21
|
+
* holds no cells, so a field-less address with steps is `undefined`.
|
|
22
|
+
*
|
|
18
23
|
* `cards[i]` stands in for an unknown or blank kind. The index is the addr's own: no
|
|
19
24
|
* per-kind counting, since `DocPath` addresses cards by document-array index.
|
|
20
25
|
*/
|
|
21
|
-
export function fieldPathForAddr(addr, kinds) {
|
|
26
|
+
export function fieldPathForAddr(addr, kinds, steps = []) {
|
|
22
27
|
const head = cardHead(addr.card, kinds);
|
|
23
28
|
if (!head)
|
|
24
29
|
return undefined;
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
if (addr.field == null)
|
|
31
|
+
return steps.length ? undefined : core().formatDocPath([head, { seg: 'body' }]);
|
|
32
|
+
const segs = [head, { seg: 'field', name: addr.field }];
|
|
33
|
+
for (const step of steps)
|
|
34
|
+
segs.push(typeof step === 'number' ? { seg: 'index', index: step } : { seg: 'field', name: step });
|
|
35
|
+
return core().formatDocPath(segs);
|
|
27
36
|
}
|
|
28
37
|
/**
|
|
29
38
|
* A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
|
package/dist/core/bloom.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Run one wash over `host`, a `.qm-wash` box, resolving the dwell off its root so the
|
|
3
|
+
* number is single-sourced in CSS (`core/rungs.ts`).
|
|
4
4
|
*
|
|
5
|
-
* One
|
|
5
|
+
* One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
|
|
6
|
+
* `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
|
|
7
|
+
* the shape: a hold and a cut carry no ramps, which is a frame list rather than a
|
|
8
|
+
* number and the one half CSS cannot hand over.
|
|
9
|
+
*
|
|
10
|
+
* A run in flight on the same host is cancelled first: two quick landings on one leaf
|
|
6
11
|
* must not stack two alphas over it.
|
|
7
12
|
*/
|
|
8
|
-
export declare function
|
|
13
|
+
export declare function bloom(host: HTMLElement): void;
|
package/dist/core/bloom.js
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
// fixed by `--_qm-accent-wash`, so interpolating 0→1→0 keeps the scale in CSS where
|
|
7
7
|
// `color-mix` resolves, instead of in keyframe values a script would have to
|
|
8
8
|
// pre-resolve and re-resolve on a theme change.
|
|
9
|
+
//
|
|
10
|
+
// The wash is the host's `::after` (`.qm-wash`, controls.css), drawn from mount: this
|
|
11
|
+
// module plays it and adds nothing to the tree.
|
|
9
12
|
import { rungStyle } from './rungs.js';
|
|
10
|
-
const
|
|
11
|
-
const WASH = 'var(--_qm-accent-wash)';
|
|
13
|
+
const WASH = '::after';
|
|
12
14
|
/** The dwell used when the derivation is out of reach (an unstyled root, or jsdom,
|
|
13
15
|
* where `getComputedStyle` reports custom properties as empty). A fallback is the
|
|
14
16
|
* absence of the scale, not a copy of it. */
|
|
@@ -25,61 +27,28 @@ const FRAMES = [
|
|
|
25
27
|
];
|
|
26
28
|
const FRAMES_REDUCED = [{ opacity: 1 }, { opacity: 1 }];
|
|
27
29
|
/**
|
|
28
|
-
* Run one wash
|
|
29
|
-
* single-sourced in CSS (`core/rungs.ts`).
|
|
30
|
+
* Run one wash over `host`, a `.qm-wash` box, resolving the dwell off its root so the
|
|
31
|
+
* number is single-sourced in CSS (`core/rungs.ts`).
|
|
30
32
|
*
|
|
31
33
|
* One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
|
|
32
34
|
* `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
|
|
33
35
|
* the shape: a hold and a cut carry no ramps, which is a frame list rather than a
|
|
34
36
|
* number and the one half CSS cannot hand over.
|
|
35
|
-
*/
|
|
36
|
-
function bloom(el) {
|
|
37
|
-
if (typeof el.animate !== 'function')
|
|
38
|
-
return undefined;
|
|
39
|
-
const reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
40
|
-
const raw = rungStyle(el).getPropertyValue('--_qm-bloom-dwell').trim();
|
|
41
|
-
const n = Number.parseFloat(raw);
|
|
42
|
-
const duration = !Number.isFinite(n) ? FALLBACK_MS : raw.endsWith('ms') ? n : n * 1000;
|
|
43
|
-
return el.animate(reduced ? FRAMES_REDUCED : FRAMES, { duration });
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Bloom over `host`'s content: an inset child, so the wash neither tints the host's
|
|
47
|
-
* own background nor fades the text under it. `host` must be positioned.
|
|
48
37
|
*
|
|
49
|
-
*
|
|
38
|
+
* A run in flight on the same host is cancelled first: two quick landings on one leaf
|
|
50
39
|
* must not stack two alphas over it.
|
|
51
40
|
*/
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
if (el) {
|
|
55
|
-
// Drop the in-flight run's handlers before cancelling: its `oncancel` would
|
|
56
|
-
// otherwise remove the node the new run is about to animate.
|
|
57
|
-
for (const a of el.getAnimations()) {
|
|
58
|
-
a.onfinish = null;
|
|
59
|
-
a.oncancel = null;
|
|
60
|
-
a.cancel();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
el = document.createElement('div');
|
|
65
|
-
el.className = BLOOM_CLASS;
|
|
66
|
-
Object.assign(el.style, {
|
|
67
|
-
position: 'absolute',
|
|
68
|
-
inset: '0',
|
|
69
|
-
pointerEvents: 'none',
|
|
70
|
-
borderRadius: 'inherit',
|
|
71
|
-
background: WASH,
|
|
72
|
-
opacity: '0'
|
|
73
|
-
});
|
|
74
|
-
host.appendChild(el);
|
|
75
|
-
}
|
|
76
|
-
const node = el;
|
|
77
|
-
const anim = bloom(node);
|
|
78
|
-
if (!anim) {
|
|
79
|
-
node.remove();
|
|
41
|
+
export function bloom(host) {
|
|
42
|
+
if (typeof host.animate !== 'function')
|
|
80
43
|
return;
|
|
44
|
+
for (const a of host.getAnimations({ subtree: true })) {
|
|
45
|
+
const effect = a.effect;
|
|
46
|
+
if (effect?.target === host && effect.pseudoElement === WASH)
|
|
47
|
+
a.cancel();
|
|
81
48
|
}
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
49
|
+
const reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
50
|
+
const raw = rungStyle(host).getPropertyValue('--_qm-bloom-dwell').trim();
|
|
51
|
+
const n = Number.parseFloat(raw);
|
|
52
|
+
const duration = !Number.isFinite(n) ? FALLBACK_MS : raw.endsWith('ms') ? n : n * 1000;
|
|
53
|
+
host.animate(reduced ? FRAMES_REDUCED : FRAMES, { duration, pseudoElement: WASH });
|
|
85
54
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Command } from 'prosemirror-state';
|
|
2
2
|
import type { Schema } from 'prosemirror-model';
|
|
3
3
|
/**
|
|
4
|
-
* The block-atom bindings for a block-schema leaf: `{}` for the
|
|
4
|
+
* The block-atom bindings for a block-schema leaf: `{}` for the one-line and plain
|
|
5
5
|
* schemas, which hold no block to be against.
|
|
6
6
|
*
|
|
7
7
|
* Both keys are the same command on opposite sides, because what a delete against an
|
package/dist/core/codec/atoms.js
CHANGED
|
@@ -77,7 +77,7 @@ function againstAtom(side) {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
|
-
* The block-atom bindings for a block-schema leaf: `{}` for the
|
|
80
|
+
* The block-atom bindings for a block-schema leaf: `{}` for the one-line and plain
|
|
81
81
|
* schemas, which hold no block to be against.
|
|
82
82
|
*
|
|
83
83
|
* Both keys are the same command on opposite sides, because what a delete against an
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Selection } from 'prosemirror-state';
|
|
2
2
|
import { chainCommands, splitBlock } from 'prosemirror-commands';
|
|
3
3
|
import { canJoin, findWrapping } from 'prosemirror-transform';
|
|
4
|
+
import { withinNesting } from './nesting.js';
|
|
4
5
|
import { takesLineBreak } from './schema.js';
|
|
5
6
|
/** The head of a textblock: where a shorthand fires, and where a pick lands once its
|
|
6
7
|
* run is consumed. */
|
|
@@ -59,7 +60,8 @@ function spaceBreaks(tr, $from, type) {
|
|
|
59
60
|
}
|
|
60
61
|
/** `wrappingInputRule`'s body: wrap the block range at `pos`, then join a preceding
|
|
61
62
|
* sibling of the same type — the one boundary the wrap itself opens (`lists.ts`
|
|
62
|
-
* §cleanup: command-local, never a global pass).
|
|
63
|
+
* §cleanup: command-local, never a global pass). False where no wrap fits, or where
|
|
64
|
+
* the one that does nests past `MAX_NESTING_DEPTH` (`nesting.ts`). */
|
|
63
65
|
function wrapAt(tr, pos, type, attrs, joinBefore) {
|
|
64
66
|
const range = tr.doc.resolve(pos).blockRange();
|
|
65
67
|
const wrapping = range && findWrapping(range, type, attrs);
|
|
@@ -73,7 +75,7 @@ function wrapAt(tr, pos, type, attrs, joinBefore) {
|
|
|
73
75
|
(!joinBefore || joinBefore(before))) {
|
|
74
76
|
tr.join(pos - 1);
|
|
75
77
|
}
|
|
76
|
-
return
|
|
78
|
+
return withinNesting(tr.doc);
|
|
77
79
|
}
|
|
78
80
|
/** `# `: the heading `level` deep. `list_item` is `block+`, so one inside an item is a
|
|
79
81
|
* shape the content holds and `importMarkdown` produces from `- # title`. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Schema } from 'prosemirror-model';
|
|
2
2
|
import { type Command } from 'prosemirror-state';
|
|
3
3
|
/**
|
|
4
|
-
* The `code_block` link of the body's key chains: `{}` for the
|
|
4
|
+
* The `code_block` link of the body's key chains: `{}` for the one-line and plain
|
|
5
5
|
* schemas, which declare no code node.
|
|
6
6
|
*
|
|
7
7
|
* `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
|
package/dist/core/codec/code.js
CHANGED
|
@@ -182,7 +182,7 @@ const belowFinalFence = (state, dispatch) => {
|
|
|
182
182
|
return true;
|
|
183
183
|
};
|
|
184
184
|
/**
|
|
185
|
-
* The `code_block` link of the body's key chains: `{}` for the
|
|
185
|
+
* The `code_block` link of the body's key chains: `{}` for the one-line and plain
|
|
186
186
|
* schemas, which declare no code node.
|
|
187
187
|
*
|
|
188
188
|
* `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
|
|
@@ -18,10 +18,10 @@ export declare function renderContent(rt: Content, schema: Schema): Node;
|
|
|
18
18
|
/** Decode a `Content` to a PM document under `schema`. */
|
|
19
19
|
export declare function decode(rt: Content, schema: Schema): PMNode;
|
|
20
20
|
/** Whether `rt` decodes under an inline schema with nothing but whitespace dropped:
|
|
21
|
-
* upstream's
|
|
22
|
-
* opens. That newline is the one a YAML `|` scalar keeps in a `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* upstream's one-line rule, or one plain line and the empty one a trailing `\n`
|
|
22
|
+
* opens. That newline is the one a YAML `|` scalar keeps in a `text` value, which
|
|
23
|
+
* upstream ignores and the inline decode drops. Marks are the schema's own business
|
|
24
|
+
* (`plaintextSchema` declares none). */
|
|
25
25
|
export declare function fitsInline(rt: Content): boolean;
|
|
26
26
|
/** Whether `rt` decodes under `plainSchema` losing nothing but its marks, which the
|
|
27
27
|
* next commit heals (CODEC §Inline mode): upstream's `isPlain`, asked of the content
|
|
@@ -29,7 +29,7 @@ export declare function fitsInline(rt: Content): boolean;
|
|
|
29
29
|
export declare function fitsPlain(rt: Content): boolean;
|
|
30
30
|
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
31
31
|
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
32
|
-
* store; on the block schema anything but an element
|
|
32
|
+
* store; on the block schema anything but an element ({@link editable}),
|
|
33
33
|
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
34
34
|
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
35
35
|
* reads. */
|
|
@@ -47,9 +47,9 @@ export function decode(rt, schema) {
|
|
|
47
47
|
acc += usvLength(lineTexts[i]) + 1; // +1 for the `\n` boundary
|
|
48
48
|
}
|
|
49
49
|
// A mark-free schema (`plaintextSchema`) has nothing to apply them with, and a
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
50
|
+
// `text` field that acquired one anyway — an older build's popover, a hand-written
|
|
51
|
+
// document — is exactly the content that must open and then heal on the next
|
|
52
|
+
// commit, not throw here.
|
|
53
53
|
const marks = hasMarks(schema) ? rt.marks.filter((m) => m.type !== 'anchor') : [];
|
|
54
54
|
const cursor = { i: 0, rt };
|
|
55
55
|
if (isInlineSchema(schema)) {
|
|
@@ -74,10 +74,10 @@ export function decode(rt, schema) {
|
|
|
74
74
|
return schema.nodes.doc.create(null, blocks.length ? blocks : schema.nodes.paragraph.create());
|
|
75
75
|
}
|
|
76
76
|
/** Whether `rt` decodes under an inline schema with nothing but whitespace dropped:
|
|
77
|
-
* upstream's
|
|
78
|
-
* opens. That newline is the one a YAML `|` scalar keeps in a `
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* upstream's one-line rule, or one plain line and the empty one a trailing `\n`
|
|
78
|
+
* opens. That newline is the one a YAML `|` scalar keeps in a `text` value, which
|
|
79
|
+
* upstream ignores and the inline decode drops. Marks are the schema's own business
|
|
80
|
+
* (`plaintextSchema` declares none). */
|
|
81
81
|
export function fitsInline(rt) {
|
|
82
82
|
if (core().isInline(rt))
|
|
83
83
|
return true;
|
|
@@ -96,7 +96,7 @@ export function fitsPlain(rt) {
|
|
|
96
96
|
}
|
|
97
97
|
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
98
98
|
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
99
|
-
* store; on the block schema anything but an element
|
|
99
|
+
* store; on the block schema anything but an element ({@link editable}),
|
|
100
100
|
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
101
101
|
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
102
102
|
* reads. */
|
|
@@ -107,10 +107,10 @@ export function fitsLeaf(rt, leaf) {
|
|
|
107
107
|
return fitsPlain(rt);
|
|
108
108
|
return (leaf.islands || rt.islands.length === 0) && rt.lines.every(editable);
|
|
109
109
|
}
|
|
110
|
-
/** Whether every container on `line` is one the block schema edits: an element
|
|
111
|
-
*
|
|
110
|
+
/** Whether every container on `line` is one the block schema edits: an element decodes
|
|
111
|
+
* to no node, so the first commit would drop it. */
|
|
112
112
|
function editable(line) {
|
|
113
|
-
return line.containers.every((c) => c.container === 'quote' ||
|
|
113
|
+
return line.containers.every((c) => c.container === 'quote' || c.container === 'list_item');
|
|
114
114
|
}
|
|
115
115
|
/** Inline decode: one paragraph, containers and islands stripped. */
|
|
116
116
|
function decodeInline(rt, schema, lineTexts, starts, marks, cursor) {
|
|
@@ -184,7 +184,7 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
184
184
|
let l = k + 1;
|
|
185
185
|
while (l < run.length && ordinalAt(run[l], depth) === ord)
|
|
186
186
|
l++;
|
|
187
|
-
items.push(schema.nodes.list_item.create(
|
|
187
|
+
items.push(schema.nodes.list_item.create({ checked: checkedAt(run[k], depth) }, groupBlocks(schema, run.slice(k, l), depth + 1, marks, cursor)));
|
|
188
188
|
k = l;
|
|
189
189
|
}
|
|
190
190
|
const listType = ordered ? schema.nodes.ordered_list : schema.nodes.bullet_list;
|
|
@@ -235,6 +235,12 @@ function ordinalAt(leaf, depth) {
|
|
|
235
235
|
const c = atDepth(leaf, depth);
|
|
236
236
|
return c?.container === 'list_item' ? c.attrs.ordinal : -1;
|
|
237
237
|
}
|
|
238
|
+
/** A task item's `checked` at `depth`, `null` on any other item: what the item's lines
|
|
239
|
+
* carry alike, so its first line answers for it. */
|
|
240
|
+
function checkedAt(leaf, depth) {
|
|
241
|
+
const c = atDepth(leaf, depth);
|
|
242
|
+
return c?.container === 'list_item' ? (c.attrs.checked ?? null) : null;
|
|
243
|
+
}
|
|
238
244
|
/** A container's `instance`, absent meaning the zero a canonical read omits. It is what
|
|
239
245
|
* tells one container from an adjacent sibling of identical shape, which contiguity
|
|
240
246
|
* alone reads as one, so absent and `0` have to answer alike. */
|
|
@@ -122,7 +122,12 @@ function scanBlock(acc, node, nodePos, containers, instance) {
|
|
|
122
122
|
const start = ordered ? node.attrs.start : 1;
|
|
123
123
|
let itemPos = contentStart;
|
|
124
124
|
node.forEach((item, _off, index) => {
|
|
125
|
-
const
|
|
125
|
+
const checked = item.attrs.checked;
|
|
126
|
+
const attrs = { ordered, start, ordinal: index };
|
|
127
|
+
const container = withInstance({
|
|
128
|
+
container: 'list_item',
|
|
129
|
+
attrs: checked === null ? attrs : { ...attrs, checked }
|
|
130
|
+
}, instance);
|
|
126
131
|
scanBlocks(acc, item, itemPos + 1, [...containers, container]);
|
|
127
132
|
itemPos += item.nodeSize;
|
|
128
133
|
});
|
|
@@ -14,20 +14,20 @@ export interface CreateFieldOpts {
|
|
|
14
14
|
quill: Quill;
|
|
15
15
|
addr: Addr;
|
|
16
16
|
container: HTMLElement;
|
|
17
|
-
/** Constrained single-textblock schema (a field
|
|
17
|
+
/** Constrained single-textblock schema (a one-line field). */
|
|
18
18
|
inline?: boolean;
|
|
19
|
-
/** A mark-free schema (a `
|
|
19
|
+
/** A mark-free schema (a `text` field): literal text, no formatting and no
|
|
20
20
|
* anchors, in paragraphs and hard breaks or, with `inline`, one textblock. */
|
|
21
21
|
plaintext?: boolean;
|
|
22
22
|
/** The id of the note the caller draws inside `container` for a held leaf →
|
|
23
23
|
* `aria-describedby` while it holds. */
|
|
24
24
|
heldNoteId?: string;
|
|
25
25
|
/**
|
|
26
|
-
* Fired when the leaf holds or releases, and at mount when it mounts held. A
|
|
27
|
-
*
|
|
26
|
+
* Fired when the leaf holds or releases, and at mount when it mounts held. A one-line
|
|
27
|
+
* leaf, or a `text` one, is held over a value its schema cannot
|
|
28
28
|
* hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
|
|
29
29
|
* and drop containers and islands for the first commit to store. Any leaf is held
|
|
30
|
-
* over an element
|
|
30
|
+
* over an element, which no node holds. A held leaf draws
|
|
31
31
|
* its content on the block schema, read-only, and commits nothing. Judged of the value
|
|
32
32
|
* each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
|
|
33
33
|
* re-evaluates it.
|
|
@@ -79,6 +79,9 @@ export interface CreateFieldOpts {
|
|
|
79
79
|
* emptied it holds an empty answer, which prints empty. Absent, the placeholder
|
|
80
80
|
* returns whenever the leaf is empty. */
|
|
81
81
|
placeholderUntilEdit?: boolean;
|
|
82
|
+
/** An edit leaving the leaf empty removes the field: an optional cell (`t?`), whose
|
|
83
|
+
* unanswered `none` is what the empty leaf draws and an empty answer would hide. */
|
|
84
|
+
emptyUnsets?: boolean;
|
|
82
85
|
onFocus?(addr: Addr): void;
|
|
83
86
|
/** Fired with the new USV caret after an edit or a selection move. */
|
|
84
87
|
onCaretMove?(addr: Addr, pos: number): void;
|
|
@@ -88,8 +91,8 @@ export interface CreateFieldOpts {
|
|
|
88
91
|
* move, so a host driving a recompile off it recompiles on every arrow key.
|
|
89
92
|
*
|
|
90
93
|
* Fires for every commit regardless of which branch it took (`applyChange`, the
|
|
91
|
-
* first-edit `overwrite`, the fallback `overwrite`, a `
|
|
92
|
-
*
|
|
94
|
+
* first-edit `overwrite`, the fallback `overwrite`, a `text` leaf's typed write)
|
|
95
|
+
* and for an anchor mutation,
|
|
93
96
|
* which changes no text. A commit that failed outright (`commit-lost`) does not
|
|
94
97
|
* fire it: nothing landed.
|
|
95
98
|
*/
|
|
@@ -168,6 +171,8 @@ export interface LeafViews {
|
|
|
168
171
|
/** The canonical empty `Content`: one empty `para` line. The empty a prose
|
|
169
172
|
* leaf (or an array's prose element) seeds from. */
|
|
170
173
|
export declare function emptyContent(): Content;
|
|
174
|
+
/** Whether `rt` is the empty leaf: {@link emptyContent}, nothing typed and no shape left. */
|
|
175
|
+
export declare function isEmptyContent(rt: Content): boolean;
|
|
171
176
|
/** The naming attributes for the `contenteditable`, shared by every prose leaf
|
|
172
177
|
* (`ProseValue` takes it too, so an addressed leaf and a by-value one name their
|
|
173
178
|
* regions by one rule). `aria-labelledby` supersedes `aria-label`: a leaf carrying
|
|
@@ -197,10 +202,10 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
197
202
|
* Backspace that takes one back, the field keymap over the base keymap, the URL paste
|
|
198
203
|
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
199
204
|
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
200
|
-
* (`islands.ts`), the table paste (`table-paste.ts`)
|
|
201
|
-
* (`markdown-paste.ts`).
|
|
205
|
+
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
206
|
+
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
|
|
202
207
|
*
|
|
203
|
-
* Every mark-shaped plugin reads the schema rather than a flag: over either
|
|
208
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
204
209
|
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
205
210
|
* toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
|
|
206
211
|
* its author typed without a second rule saying so.
|
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
|
|
@@ -33,7 +33,10 @@ import { bodyKeymap } from './keymap.js';
|
|
|
33
33
|
import { blockSchema, isBlockSchema, leafSchema, plainSchema } from './schema.js';
|
|
34
34
|
import { linkPastePlugin } from './link-paste.js';
|
|
35
35
|
import { markdownPastePlugin } from './markdown-paste.js';
|
|
36
|
+
import { nestingPlugin } from './nesting.js';
|
|
36
37
|
import { tablePastePlugin } from './table-paste.js';
|
|
38
|
+
import { tableCopyPlugin } from './table-copy.js';
|
|
39
|
+
import { taskPlugin } from './tasks.js';
|
|
37
40
|
import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
|
|
38
41
|
import { DEFAULT_SLASH_STRINGS, focusSlashItem, runSlashItem, slashPlugin } from './slash.js';
|
|
39
42
|
const anchorKey = new PluginKey('quill-anchors');
|
|
@@ -75,8 +78,8 @@ function anchorPlugin(seed) {
|
|
|
75
78
|
* and the verbatim read cannot answer it alone. A field the editor committed rests
|
|
76
79
|
* as `Content`; a field a markdown parse left rests as the authored string, and
|
|
77
80
|
* what that string means is the declared type's to say: `richtext` is markdown,
|
|
78
|
-
* `
|
|
79
|
-
*
|
|
81
|
+
* `text` is literal text, and reading one as the other silently eats every `*` the
|
|
82
|
+
* author typed. */
|
|
80
83
|
function readLeaf(reader, addr) {
|
|
81
84
|
return reader.getContent(addr) ?? emptyContent();
|
|
82
85
|
}
|
|
@@ -85,14 +88,18 @@ function readLeaf(reader, addr) {
|
|
|
85
88
|
export function emptyContent() {
|
|
86
89
|
return { text: '', lines: [{ containers: [], kind: 'para' }], marks: [], islands: [] };
|
|
87
90
|
}
|
|
91
|
+
/** Whether `rt` is the empty leaf: {@link emptyContent}, nothing typed and no shape left. */
|
|
92
|
+
export function isEmptyContent(rt) {
|
|
93
|
+
return contentEqual(rt, emptyContent());
|
|
94
|
+
}
|
|
88
95
|
/** Whether ops may commit here: the stored value is a `Content` object, so
|
|
89
96
|
* `applyChange` splices exactly the content the leaf read and PM is showing.
|
|
90
97
|
*
|
|
91
98
|
* The two other rest forms both take `overwrite` instead. An unset field (`undefined`;
|
|
92
99
|
* a default-only richtext field before its first edit) has nothing to splice, and an
|
|
93
100
|
* 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
|
-
*
|
|
101
|
+
* string carries no anchors to pay. A body always rests as `Content`. A `text` leaf
|
|
102
|
+
* never asks: it commits through the writer ({@link createField}). */
|
|
96
103
|
function opsCommittable(doc, addr) {
|
|
97
104
|
const stored = doc.getStored(addr);
|
|
98
105
|
return stored !== null && typeof stored === 'object';
|
|
@@ -162,7 +169,7 @@ export function createField(opts) {
|
|
|
162
169
|
const slash = block ? opts.onSlash : undefined;
|
|
163
170
|
// Judged of the value being mounted rather than read off a diagnostic: a routed set
|
|
164
171
|
// can predate the value it arrives with, and `validation::not_plain` stands in for
|
|
165
|
-
// `
|
|
172
|
+
// `not_single_line` on a one-line `text` field.
|
|
166
173
|
const holds = (rt) => !fitsLeaf(rt, { inline, plaintext, islands: true });
|
|
167
174
|
let held = holds(reconciler.last);
|
|
168
175
|
const named = proseAttributes(opts) ?? {};
|
|
@@ -210,6 +217,9 @@ export function createField(opts) {
|
|
|
210
217
|
dispatchTransaction: (tr) => {
|
|
211
218
|
const oldRt = reconciler.last;
|
|
212
219
|
const next = view.state.apply(tr);
|
|
220
|
+
// A filtered transaction (`nesting.ts`) leaves the state as it was.
|
|
221
|
+
if (next === view.state)
|
|
222
|
+
return;
|
|
213
223
|
// An edit takes a held default: the commit below writes the whole leaf, so the
|
|
214
224
|
// mark goes in the same update that draws the edit.
|
|
215
225
|
if (!held && (tr.docChanged || tr.getMeta(anchorKey)))
|
|
@@ -282,6 +292,8 @@ export function createField(opts) {
|
|
|
282
292
|
// Returns whether anything landed: the caller's change signal, which must not
|
|
283
293
|
// be a property of which branch the commit took.
|
|
284
294
|
function commitEdit(oldRt, newRt) {
|
|
295
|
+
if (opts.emptyUnsets && heldAnchors().length === 0 && isEmptyContent(newRt))
|
|
296
|
+
return commitUnset();
|
|
285
297
|
if (plaintext)
|
|
286
298
|
return commitPlain(newRt.text);
|
|
287
299
|
// The text splice, computed once per keystroke: the gate below, both overwrite
|
|
@@ -335,8 +347,8 @@ export function createField(opts) {
|
|
|
335
347
|
}
|
|
336
348
|
}
|
|
337
349
|
/**
|
|
338
|
-
* A `
|
|
339
|
-
*
|
|
350
|
+
* A `text` leaf's commit: its whole text through the typed writer, which rests the
|
|
351
|
+
* field as that literal string (CODEC §Inline mode). A content object is the
|
|
340
352
|
* other rest form, and `toMarkdown` emits one as markdown: a break spelled as a
|
|
341
353
|
* backslash, a `*` and a trailing space escaped, a blank line doubled, so the value
|
|
342
354
|
* reads back changed. The leaf carries no anchors, so the op path has none to keep.
|
|
@@ -348,7 +360,7 @@ export function createField(opts) {
|
|
|
348
360
|
try {
|
|
349
361
|
const name = addr.field;
|
|
350
362
|
if (name === undefined)
|
|
351
|
-
throw new Error('a
|
|
363
|
+
throw new Error('a text leaf names a field');
|
|
352
364
|
(addr.card === undefined ? writer : writer.card(addr.card)).set(name, text);
|
|
353
365
|
reconciler.commit(readLeaf(reader, addr));
|
|
354
366
|
return true;
|
|
@@ -363,6 +375,23 @@ export function createField(opts) {
|
|
|
363
375
|
return false;
|
|
364
376
|
}
|
|
365
377
|
}
|
|
378
|
+
/** An emptied optional leaf's commit: the field removed, back to unanswered. */
|
|
379
|
+
function commitUnset() {
|
|
380
|
+
try {
|
|
381
|
+
doc.removeField(addr);
|
|
382
|
+
reconciler.commit(readLeaf(reader, addr));
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
reportError(opts.onError, {
|
|
387
|
+
code: 'commit-lost',
|
|
388
|
+
severity: 'error',
|
|
389
|
+
message: `the field could not be removed; the stored value is stale while the editor keeps the edit: ${errorMessage(e)}`,
|
|
390
|
+
cause: e
|
|
391
|
+
});
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
366
395
|
/** The anchors the plugin currently holds (PM coords); `[]` before the first apply. */
|
|
367
396
|
const heldAnchors = () => anchorKey.getState(view.state) ?? [];
|
|
368
397
|
/** The plugin's anchor positions as USV, against `index` — which `dispatchTransaction`
|
|
@@ -437,7 +466,7 @@ export function createField(opts) {
|
|
|
437
466
|
return readLeaf(reader, addr);
|
|
438
467
|
},
|
|
439
468
|
insertAnchor(id, pos) {
|
|
440
|
-
// A
|
|
469
|
+
// A text field carries no marks (§Inline mode), and a held one commits nothing.
|
|
441
470
|
if (plaintext || held)
|
|
442
471
|
return;
|
|
443
472
|
if (heldAnchors().some((a) => a.id === id))
|
|
@@ -509,10 +538,10 @@ export function createField(opts) {
|
|
|
509
538
|
* Backspace that takes one back, the field keymap over the base keymap, the URL paste
|
|
510
539
|
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
511
540
|
* 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`).
|
|
541
|
+
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
542
|
+
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
|
|
514
543
|
*
|
|
515
|
-
* Every mark-shaped plugin reads the schema rather than a flag: over either
|
|
544
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
516
545
|
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
517
546
|
* toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
|
|
518
547
|
* its author typed without a second rule saying so.
|
|
@@ -530,7 +559,7 @@ export function proseLeafPlugins(schema, opts) {
|
|
|
530
559
|
// Each answers to something only the block schema holds: any other leaf is
|
|
531
560
|
// textblocks alone, with no island and no gap to put a cursor in.
|
|
532
561
|
if (isBlockSchema(schema))
|
|
533
|
-
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), markdownPastePlugin());
|
|
562
|
+
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), taskPlugin(), nestingPlugin());
|
|
534
563
|
if (schema === plainSchema)
|
|
535
564
|
list.push(plainClipboardPlugin());
|
|
536
565
|
if (opts.placeholder)
|
|
@@ -641,9 +670,9 @@ function ghostPlugin(opts) {
|
|
|
641
670
|
/**
|
|
642
671
|
* The field's keymap: history, mark toggles, and the body's structural keys; Enter
|
|
643
672
|
* suppressed inline. The toggles are bound per mark type the schema declares, so a
|
|
644
|
-
*
|
|
673
|
+
* `text` leaf leaves Mod-b/i/u to the browser.
|
|
645
674
|
*
|
|
646
|
-
* Tab forks on the leaf's role, not on the caret's position.
|
|
675
|
+
* Tab forks on the leaf's role, not on the caret's position. A one-line or `text`
|
|
647
676
|
* leaf is a form field: Tab stays unbound, so field navigation is open for a shell
|
|
648
677
|
* keymap (VISUAL_EDITOR §Settled and open). A block-schema body is a document: Tab
|
|
649
678
|
* is structural, a chain each nested surface prepends to (`keymap.ts`). One key
|
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
6
6
|
export type { TableChromeStrings } from './table-view.js';
|