@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/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`. */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Node as PMNode } from 'prosemirror-model';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
/** A leaf's character cap: the schema's `max:`, and whether the count drops trailing
|
|
4
|
+
* whitespace, which a one-line `text` does and nothing else. */
|
|
5
|
+
export interface TextCap {
|
|
6
|
+
max: number;
|
|
7
|
+
trimEnd?: boolean;
|
|
8
|
+
/** The count, at mount and after every change to the document. */
|
|
9
|
+
onCount?: (count: number) => void;
|
|
10
|
+
}
|
|
11
|
+
/** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
|
|
12
|
+
* values of the plain projection, islands counting nothing and trailing line breaks
|
|
13
|
+
* none. */
|
|
14
|
+
export declare function capCount(doc: PMNode, trimEnd?: boolean): number;
|
|
15
|
+
/**
|
|
16
|
+
* Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
|
|
17
|
+
* nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
|
|
18
|
+
* leaves the count no higher, so a value stored over the cap still edits down; undo and
|
|
19
|
+
* redo pass, each restoring a state the leaf already held.
|
|
20
|
+
*/
|
|
21
|
+
export declare function capPlugin(cap: TextCap): Plugin;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { isHistoryTransaction } from 'prosemirror-history';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
import { usvLength } from './decode.js';
|
|
4
|
+
import { scanDoc } from './encode.js';
|
|
5
|
+
import { ISLAND_SLOT } from './islands.js';
|
|
6
|
+
/** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
|
|
7
|
+
* values of the plain projection, islands counting nothing and trailing line breaks
|
|
8
|
+
* none. */
|
|
9
|
+
export function capCount(doc, trimEnd = false) {
|
|
10
|
+
const text = scanDoc(doc).text.replaceAll(ISLAND_SLOT, '');
|
|
11
|
+
return usvLength(trimEnd ? text.trimEnd() : text.replace(/[\n\r]+$/, ''));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
|
|
15
|
+
* nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
|
|
16
|
+
* leaves the count no higher, so a value stored over the cap still edits down; undo and
|
|
17
|
+
* redo pass, each restoring a state the leaf already held.
|
|
18
|
+
*/
|
|
19
|
+
export function capPlugin(cap) {
|
|
20
|
+
const count = (doc) => capCount(doc, cap.trimEnd);
|
|
21
|
+
return new Plugin({
|
|
22
|
+
filterTransaction: (tr) => {
|
|
23
|
+
if (!tr.docChanged || isHistoryTransaction(tr))
|
|
24
|
+
return true;
|
|
25
|
+
const after = count(tr.doc);
|
|
26
|
+
return after <= cap.max || after <= count(tr.before);
|
|
27
|
+
},
|
|
28
|
+
props: {
|
|
29
|
+
handlePaste: (view, _event, slice) => {
|
|
30
|
+
const { state } = view;
|
|
31
|
+
const pasted = () => state.tr.replaceSelection(slice);
|
|
32
|
+
if (count(pasted().doc) <= cap.max)
|
|
33
|
+
return false;
|
|
34
|
+
const tr = pasted();
|
|
35
|
+
const start = tr.mapping.map(state.selection.from, -1);
|
|
36
|
+
const end = tr.mapping.map(state.selection.to, 1);
|
|
37
|
+
const fits = (cut) => count(pasted().delete(cut, end).doc) <= cap.max;
|
|
38
|
+
if (!fits(start))
|
|
39
|
+
return true;
|
|
40
|
+
let lo = start;
|
|
41
|
+
let hi = end;
|
|
42
|
+
while (hi - lo > 1) {
|
|
43
|
+
const mid = (lo + hi) >> 1;
|
|
44
|
+
if (fits(mid))
|
|
45
|
+
lo = mid;
|
|
46
|
+
else
|
|
47
|
+
hi = mid;
|
|
48
|
+
}
|
|
49
|
+
view.dispatch(tr.delete(lo, end).scrollIntoView().setMeta('uiEvent', 'paste'));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
view: (v) => {
|
|
54
|
+
cap.onCount?.(count(v.state.doc));
|
|
55
|
+
return {
|
|
56
|
+
update: (next, prev) => {
|
|
57
|
+
if (next.state.doc !== prev.doc)
|
|
58
|
+
cap.onCount?.(count(next.state.doc));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -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
|
});
|
|
@@ -3,6 +3,7 @@ import { Plugin } from 'prosemirror-state';
|
|
|
3
3
|
import { EditorView } from 'prosemirror-view';
|
|
4
4
|
import type { Document, Content, Addr, Quill } from '@quillmark/wasm';
|
|
5
5
|
import type { EditorErrorHandler } from '../errors.js';
|
|
6
|
+
import { type TextCap } from './cap.js';
|
|
6
7
|
import { type TableChromeStrings } from './table-view.js';
|
|
7
8
|
import { type SlashState, type SlashStrings } from './slash.js';
|
|
8
9
|
/** Options for {@link createField}. */
|
|
@@ -14,20 +15,20 @@ export interface CreateFieldOpts {
|
|
|
14
15
|
quill: Quill;
|
|
15
16
|
addr: Addr;
|
|
16
17
|
container: HTMLElement;
|
|
17
|
-
/** Constrained single-textblock schema (a field
|
|
18
|
+
/** Constrained single-textblock schema (a one-line field). */
|
|
18
19
|
inline?: boolean;
|
|
19
|
-
/** A mark-free schema (a `
|
|
20
|
+
/** A mark-free schema (a `text` field): literal text, no formatting and no
|
|
20
21
|
* anchors, in paragraphs and hard breaks or, with `inline`, one textblock. */
|
|
21
22
|
plaintext?: boolean;
|
|
22
23
|
/** The id of the note the caller draws inside `container` for a held leaf →
|
|
23
24
|
* `aria-describedby` while it holds. */
|
|
24
25
|
heldNoteId?: string;
|
|
25
26
|
/**
|
|
26
|
-
* Fired when the leaf holds or releases, and at mount when it mounts held. A
|
|
27
|
-
*
|
|
27
|
+
* Fired when the leaf holds or releases, and at mount when it mounts held. A one-line
|
|
28
|
+
* leaf, or a `text` one, is held over a value its schema cannot
|
|
28
29
|
* hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
|
|
29
30
|
* and drop containers and islands for the first commit to store. Any leaf is held
|
|
30
|
-
* over an element
|
|
31
|
+
* over an element, which no node holds. A held leaf draws
|
|
31
32
|
* its content on the block schema, read-only, and commits nothing. Judged of the value
|
|
32
33
|
* each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
|
|
33
34
|
* re-evaluates it.
|
|
@@ -75,10 +76,16 @@ export interface CreateFieldOpts {
|
|
|
75
76
|
* a body's invitation. The initial value; {@link FieldController.setPlaceholder}
|
|
76
77
|
* moves it after mount. Empty/absent shows no ghost. */
|
|
77
78
|
placeholder?: string;
|
|
79
|
+
/** The schema's `max:` on the field: a character count no edit takes the leaf past
|
|
80
|
+
* (`cap.ts`). */
|
|
81
|
+
cap?: TextCap;
|
|
78
82
|
/** The placeholder goes at the leaf's first edit: the edit answers the leaf, and
|
|
79
83
|
* emptied it holds an empty answer, which prints empty. Absent, the placeholder
|
|
80
84
|
* returns whenever the leaf is empty. */
|
|
81
85
|
placeholderUntilEdit?: boolean;
|
|
86
|
+
/** An edit leaving the leaf empty removes the field: an optional cell (`t?`), whose
|
|
87
|
+
* unanswered `none` is what the empty leaf draws and an empty answer would hide. */
|
|
88
|
+
emptyUnsets?: boolean;
|
|
82
89
|
onFocus?(addr: Addr): void;
|
|
83
90
|
/** Fired with the new USV caret after an edit or a selection move. */
|
|
84
91
|
onCaretMove?(addr: Addr, pos: number): void;
|
|
@@ -88,8 +95,8 @@ export interface CreateFieldOpts {
|
|
|
88
95
|
* move, so a host driving a recompile off it recompiles on every arrow key.
|
|
89
96
|
*
|
|
90
97
|
* Fires for every commit regardless of which branch it took (`applyChange`, the
|
|
91
|
-
* first-edit `overwrite`, the fallback `overwrite`, a `
|
|
92
|
-
*
|
|
98
|
+
* first-edit `overwrite`, the fallback `overwrite`, a `text` leaf's typed write)
|
|
99
|
+
* and for an anchor mutation,
|
|
93
100
|
* which changes no text. A commit that failed outright (`commit-lost`) does not
|
|
94
101
|
* fire it: nothing landed.
|
|
95
102
|
*/
|
|
@@ -168,6 +175,8 @@ export interface LeafViews {
|
|
|
168
175
|
/** The canonical empty `Content`: one empty `para` line. The empty a prose
|
|
169
176
|
* leaf (or an array's prose element) seeds from. */
|
|
170
177
|
export declare function emptyContent(): Content;
|
|
178
|
+
/** Whether `rt` is the empty leaf: {@link emptyContent}, nothing typed and no shape left. */
|
|
179
|
+
export declare function isEmptyContent(rt: Content): boolean;
|
|
171
180
|
/** The naming attributes for the `contenteditable`, shared by every prose leaf
|
|
172
181
|
* (`ProseValue` takes it too, so an addressed leaf and a by-value one name their
|
|
173
182
|
* regions by one rule). `aria-labelledby` supersedes `aria-label`: a leaf carrying
|
|
@@ -197,10 +206,10 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
197
206
|
* Backspace that takes one back, the field keymap over the base keymap, the URL paste
|
|
198
207
|
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
199
208
|
* 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`).
|
|
209
|
+
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
210
|
+
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
|
|
202
211
|
*
|
|
203
|
-
* Every mark-shaped plugin reads the schema rather than a flag: over either
|
|
212
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
204
213
|
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
205
214
|
* toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
|
|
206
215
|
* its author typed without a second rule saying so.
|
|
@@ -219,5 +228,8 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
|
219
228
|
placeholder?: () => string | undefined;
|
|
220
229
|
/** {@link CreateFieldOpts.placeholderUntilEdit}. */
|
|
221
230
|
placeholderUntilEdit?: boolean;
|
|
231
|
+
/** {@link CreateFieldOpts.cap}. Ahead of every other paste handler, which a paste
|
|
232
|
+
* it cuts never reaches. */
|
|
233
|
+
cap?: TextCap;
|
|
222
234
|
afterHistory?: Plugin[];
|
|
223
235
|
}): Plugin[];
|