@quillmark/svelte 0.20.1 → 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/THEMING.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 +26 -11
- package/dist/core/codec/blocks.d.ts +5 -5
- package/dist/core/codec/blocks.js +27 -8
- package/dist/core/codec/code.d.ts +4 -4
- package/dist/core/codec/code.js +52 -22
- 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 -15
- package/dist/core/codec/field.js +66 -26
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +2 -2
- package/dist/core/codec/inputrules.d.ts +4 -0
- package/dist/core/codec/inputrules.js +44 -6
- package/dist/core/codec/link-paste.d.ts +11 -0
- package/dist/core/codec/link-paste.js +32 -0
- package/dist/core/codec/lists.d.ts +5 -4
- package/dist/core/codec/lists.js +87 -31
- package/dist/core/codec/nesting.d.ts +20 -0
- package/dist/core/codec/nesting.js +62 -0
- package/dist/core/codec/prose.css +145 -63
- package/dist/core/codec/schema.d.ts +2 -2
- package/dist/core/codec/schema.js +71 -18
- package/dist/core/codec/slash.js +0 -0
- 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 +6 -7
- package/dist/core/codec/table-view.js +281 -201
- 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/core/rungs.d.ts +6 -0
- package/dist/core/rungs.js +11 -0
- package/dist/core/theme.css +24 -12
- package/dist/visual/ArrayField.svelte +236 -78
- package/dist/visual/ArrayField.svelte.d.ts +0 -2
- package/dist/visual/BooleanField.svelte +12 -1
- package/dist/visual/Card.svelte +41 -23
- package/dist/visual/Card.svelte.d.ts +3 -2
- package/dist/visual/CardControls.svelte +15 -15
- package/dist/visual/DateField.svelte +93 -64
- package/dist/visual/EnumField.svelte +9 -4
- package/dist/visual/Field.svelte +20 -16
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldHint.svelte +22 -5
- package/dist/visual/FieldLabel.svelte +5 -33
- package/dist/visual/FieldLabel.svelte.d.ts +0 -2
- package/dist/visual/FormatPopover.svelte +78 -81
- package/dist/visual/MatrixField.svelte +28 -19
- package/dist/visual/NumberField.svelte +32 -3
- package/dist/visual/ObjectField.svelte +40 -24
- 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 +7 -7
- package/dist/visual/VariantField.svelte +1 -1
- package/dist/visual/VariantField.svelte.d.ts +1 -1
- package/dist/visual/VisualEditorInner.svelte +121 -39
- package/dist/visual/controls.css +21 -2
- package/dist/visual/icons/Icon.svelte +3 -2
- package/dist/visual/icons/Icon.svelte.d.ts +3 -2
- package/dist/visual/icons/nodes.d.ts +25 -21
- package/dist/visual/icons/nodes.js +9 -7
- package/dist/visual/leaves.d.ts +12 -9
- package/dist/visual/leaves.js +19 -0
- package/dist/visual/motion.d.ts +25 -3
- package/dist/visual/motion.js +58 -9
- package/dist/visual/strings.d.ts +15 -9
- package/dist/visual/strings.js +19 -12
- package/dist/visual/structure.d.ts +21 -31
- package/dist/visual/structure.js +54 -51
- package/package.json +3 -3
|
@@ -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
|
|
@@ -193,15 +198,17 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
193
198
|
* never fork the keymap/plugin ordering. History first, then any leaf-specific
|
|
194
199
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
195
200
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
196
|
-
* the rest of the stack leaves, then the markdown-shorthand input rules
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* the
|
|
201
|
+
* the rest of the stack leaves, then the markdown-shorthand input rules and the
|
|
202
|
+
* Backspace that takes one back, the field keymap over the base keymap, the URL paste
|
|
203
|
+
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
204
|
+
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
205
|
+
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
206
|
+
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
|
|
200
207
|
*
|
|
201
|
-
* Every mark-shaped plugin reads the schema rather than a flag: over either
|
|
202
|
-
* schema the shorthand rules build nothing (each is guarded on its mark type)
|
|
203
|
-
* toggles bind no key, so a `**bold**` keeps the delimiters
|
|
204
|
-
* a second rule saying so.
|
|
208
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
209
|
+
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
210
|
+
* toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
|
|
211
|
+
* its author typed without a second rule saying so.
|
|
205
212
|
*/
|
|
206
213
|
export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
207
214
|
inline: boolean;
|
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
|
|
@@ -27,12 +27,16 @@ import { lower, pmToContent, scanContent, scanDoc, contentEdit } from './encode.
|
|
|
27
27
|
import { islandPastePlugin } from './islands.js';
|
|
28
28
|
import { anchorsFromContent } from './marks.js';
|
|
29
29
|
import { contentEqual, createReconciler } from './reconcile.js';
|
|
30
|
-
import { inputRulesPlugin } from './inputrules.js';
|
|
30
|
+
import { inputRulesPlugin, undoRuleKeymap } from './inputrules.js';
|
|
31
31
|
import { linebreakPlugin } from './breaks.js';
|
|
32
32
|
import { bodyKeymap } from './keymap.js';
|
|
33
33
|
import { blockSchema, isBlockSchema, leafSchema, plainSchema } from './schema.js';
|
|
34
|
+
import { linkPastePlugin } from './link-paste.js';
|
|
34
35
|
import { markdownPastePlugin } from './markdown-paste.js';
|
|
36
|
+
import { nestingPlugin } from './nesting.js';
|
|
35
37
|
import { tablePastePlugin } from './table-paste.js';
|
|
38
|
+
import { tableCopyPlugin } from './table-copy.js';
|
|
39
|
+
import { taskPlugin } from './tasks.js';
|
|
36
40
|
import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
|
|
37
41
|
import { DEFAULT_SLASH_STRINGS, focusSlashItem, runSlashItem, slashPlugin } from './slash.js';
|
|
38
42
|
const anchorKey = new PluginKey('quill-anchors');
|
|
@@ -74,8 +78,8 @@ function anchorPlugin(seed) {
|
|
|
74
78
|
* and the verbatim read cannot answer it alone. A field the editor committed rests
|
|
75
79
|
* as `Content`; a field a markdown parse left rests as the authored string, and
|
|
76
80
|
* what that string means is the declared type's to say: `richtext` is markdown,
|
|
77
|
-
* `
|
|
78
|
-
*
|
|
81
|
+
* `text` is literal text, and reading one as the other silently eats every `*` the
|
|
82
|
+
* author typed. */
|
|
79
83
|
function readLeaf(reader, addr) {
|
|
80
84
|
return reader.getContent(addr) ?? emptyContent();
|
|
81
85
|
}
|
|
@@ -84,14 +88,18 @@ function readLeaf(reader, addr) {
|
|
|
84
88
|
export function emptyContent() {
|
|
85
89
|
return { text: '', lines: [{ containers: [], kind: 'para' }], marks: [], islands: [] };
|
|
86
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
|
+
}
|
|
87
95
|
/** Whether ops may commit here: the stored value is a `Content` object, so
|
|
88
96
|
* `applyChange` splices exactly the content the leaf read and PM is showing.
|
|
89
97
|
*
|
|
90
98
|
* The two other rest forms both take `overwrite` instead. An unset field (`undefined`;
|
|
91
99
|
* a default-only richtext field before its first edit) has nothing to splice, and an
|
|
92
100
|
* authored string nothing to lose: content-only marks do not survive markdown, so a
|
|
93
|
-
* string carries no anchors to pay. A body always rests as `Content`. A `
|
|
94
|
-
*
|
|
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}). */
|
|
95
103
|
function opsCommittable(doc, addr) {
|
|
96
104
|
const stored = doc.getStored(addr);
|
|
97
105
|
return stored !== null && typeof stored === 'object';
|
|
@@ -161,7 +169,7 @@ export function createField(opts) {
|
|
|
161
169
|
const slash = block ? opts.onSlash : undefined;
|
|
162
170
|
// Judged of the value being mounted rather than read off a diagnostic: a routed set
|
|
163
171
|
// can predate the value it arrives with, and `validation::not_plain` stands in for
|
|
164
|
-
// `
|
|
172
|
+
// `not_single_line` on a one-line `text` field.
|
|
165
173
|
const holds = (rt) => !fitsLeaf(rt, { inline, plaintext, islands: true });
|
|
166
174
|
let held = holds(reconciler.last);
|
|
167
175
|
const named = proseAttributes(opts) ?? {};
|
|
@@ -209,6 +217,9 @@ export function createField(opts) {
|
|
|
209
217
|
dispatchTransaction: (tr) => {
|
|
210
218
|
const oldRt = reconciler.last;
|
|
211
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;
|
|
212
223
|
// An edit takes a held default: the commit below writes the whole leaf, so the
|
|
213
224
|
// mark goes in the same update that draws the edit.
|
|
214
225
|
if (!held && (tr.docChanged || tr.getMeta(anchorKey)))
|
|
@@ -281,6 +292,8 @@ export function createField(opts) {
|
|
|
281
292
|
// Returns whether anything landed: the caller's change signal, which must not
|
|
282
293
|
// be a property of which branch the commit took.
|
|
283
294
|
function commitEdit(oldRt, newRt) {
|
|
295
|
+
if (opts.emptyUnsets && heldAnchors().length === 0 && isEmptyContent(newRt))
|
|
296
|
+
return commitUnset();
|
|
284
297
|
if (plaintext)
|
|
285
298
|
return commitPlain(newRt.text);
|
|
286
299
|
// The text splice, computed once per keystroke: the gate below, both overwrite
|
|
@@ -334,8 +347,8 @@ export function createField(opts) {
|
|
|
334
347
|
}
|
|
335
348
|
}
|
|
336
349
|
/**
|
|
337
|
-
* A `
|
|
338
|
-
*
|
|
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
|
|
339
352
|
* other rest form, and `toMarkdown` emits one as markdown: a break spelled as a
|
|
340
353
|
* backslash, a `*` and a trailing space escaped, a blank line doubled, so the value
|
|
341
354
|
* reads back changed. The leaf carries no anchors, so the op path has none to keep.
|
|
@@ -347,7 +360,7 @@ export function createField(opts) {
|
|
|
347
360
|
try {
|
|
348
361
|
const name = addr.field;
|
|
349
362
|
if (name === undefined)
|
|
350
|
-
throw new Error('a
|
|
363
|
+
throw new Error('a text leaf names a field');
|
|
351
364
|
(addr.card === undefined ? writer : writer.card(addr.card)).set(name, text);
|
|
352
365
|
reconciler.commit(readLeaf(reader, addr));
|
|
353
366
|
return true;
|
|
@@ -362,6 +375,23 @@ export function createField(opts) {
|
|
|
362
375
|
return false;
|
|
363
376
|
}
|
|
364
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
|
+
}
|
|
365
395
|
/** The anchors the plugin currently holds (PM coords); `[]` before the first apply. */
|
|
366
396
|
const heldAnchors = () => anchorKey.getState(view.state) ?? [];
|
|
367
397
|
/** The plugin's anchor positions as USV, against `index` — which `dispatchTransaction`
|
|
@@ -436,7 +466,7 @@ export function createField(opts) {
|
|
|
436
466
|
return readLeaf(reader, addr);
|
|
437
467
|
},
|
|
438
468
|
insertAnchor(id, pos) {
|
|
439
|
-
// A
|
|
469
|
+
// A text field carries no marks (§Inline mode), and a held one commits nothing.
|
|
440
470
|
if (plaintext || held)
|
|
441
471
|
return;
|
|
442
472
|
if (heldAnchors().some((a) => a.id === id))
|
|
@@ -504,28 +534,32 @@ export function createField(opts) {
|
|
|
504
534
|
* never fork the keymap/plugin ordering. History first, then any leaf-specific
|
|
505
535
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
506
536
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
507
|
-
* the rest of the stack leaves, then the markdown-shorthand input rules
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
* the
|
|
537
|
+
* the rest of the stack leaves, then the markdown-shorthand input rules and the
|
|
538
|
+
* Backspace that takes one back, the field keymap over the base keymap, the URL paste
|
|
539
|
+
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
540
|
+
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
541
|
+
* (`islands.ts`), the table paste (`table-paste.ts`), the markdown paste
|
|
542
|
+
* (`markdown-paste.ts`) and the task item's checkbox (`tasks.ts`).
|
|
511
543
|
*
|
|
512
|
-
* Every mark-shaped plugin reads the schema rather than a flag: over either
|
|
513
|
-
* schema the shorthand rules build nothing (each is guarded on its mark type)
|
|
514
|
-
* toggles bind no key, so a `**bold**` keeps the delimiters
|
|
515
|
-
* a second rule saying so.
|
|
544
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over either mark-free
|
|
545
|
+
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
546
|
+
* toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
|
|
547
|
+
* its author typed without a second rule saying so.
|
|
516
548
|
*/
|
|
517
549
|
export function proseLeafPlugins(schema, opts) {
|
|
518
550
|
const list = [history(), ...(opts.afterHistory ?? []), linebreakPlugin(schema)];
|
|
519
551
|
if (opts.slash)
|
|
520
552
|
list.push(slashPlugin(opts.slash, opts.slashHint));
|
|
521
553
|
if (!opts.noInputRules)
|
|
522
|
-
list.push(inputRulesPlugin(schema));
|
|
554
|
+
list.push(inputRulesPlugin(schema), undoRuleKeymap());
|
|
523
555
|
list.push(keymap(editorKeymap(schema, opts.inline, !!opts.slash)));
|
|
524
556
|
list.push(keymap(baseKeymap));
|
|
557
|
+
if (schema.marks.link)
|
|
558
|
+
list.push(linkPastePlugin(schema.marks.link));
|
|
525
559
|
// Each answers to something only the block schema holds: any other leaf is
|
|
526
560
|
// textblocks alone, with no island and no gap to put a cursor in.
|
|
527
561
|
if (isBlockSchema(schema))
|
|
528
|
-
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), markdownPastePlugin());
|
|
562
|
+
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), taskPlugin(), nestingPlugin());
|
|
529
563
|
if (schema === plainSchema)
|
|
530
564
|
list.push(plainClipboardPlugin());
|
|
531
565
|
if (opts.placeholder)
|
|
@@ -553,8 +587,10 @@ function plainClipboardPlugin() {
|
|
|
553
587
|
* A printable key over a selected atom writes past it. A selection is the subject
|
|
554
588
|
* of the next command, never a thing armed for
|
|
555
589
|
* replacement: Backspace deletes it, Mod-C copies it, and a character lands beside
|
|
556
|
-
* it. A block island takes
|
|
557
|
-
* the image in the same line: one rule, both node
|
|
590
|
+
* it. A block island takes the empty paragraph after it, or a new one where there is
|
|
591
|
+
* none; an inline one the caret after the image in the same line: one rule, both node
|
|
592
|
+
* types. Only a paragraph is reused, since an empty heading or fence would set the
|
|
593
|
+
* text in a shape the key never asked for.
|
|
558
594
|
*
|
|
559
595
|
* PM routes text input over a non-`TextSelection` through `handleTextInput` before
|
|
560
596
|
* falling back to replacing the selection, so this one prop is the whole of it.
|
|
@@ -570,9 +606,13 @@ function pastAtomPlugin() {
|
|
|
570
606
|
return false;
|
|
571
607
|
const tr = view.state.tr;
|
|
572
608
|
const at = selection.to; // just past the node, in both coordinate senses
|
|
609
|
+
const after = tr.doc.resolve(at).nodeAfter;
|
|
573
610
|
if (selection.node.isInline) {
|
|
574
611
|
tr.setSelection(TextSelection.create(tr.doc, at)).insertText(text);
|
|
575
612
|
}
|
|
613
|
+
else if (after?.type === schema.nodes.paragraph && after.content.size === 0) {
|
|
614
|
+
tr.setSelection(TextSelection.create(tr.doc, at + 1)).insertText(text);
|
|
615
|
+
}
|
|
576
616
|
else {
|
|
577
617
|
const para = schema.nodes.paragraph.create(null, schema.text(text));
|
|
578
618
|
tr.insert(at, para);
|
|
@@ -630,9 +670,9 @@ function ghostPlugin(opts) {
|
|
|
630
670
|
/**
|
|
631
671
|
* The field's keymap: history, mark toggles, and the body's structural keys; Enter
|
|
632
672
|
* suppressed inline. The toggles are bound per mark type the schema declares, so a
|
|
633
|
-
*
|
|
673
|
+
* `text` leaf leaves Mod-b/i/u to the browser.
|
|
634
674
|
*
|
|
635
|
-
* 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`
|
|
636
676
|
* leaf is a form field: Tab stays unbound, so field navigation is open for a shell
|
|
637
677
|
* keymap (VISUAL_EDITOR §Settled and open). A block-schema body is a document: Tab
|
|
638
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';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -13,7 +13,7 @@ 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
17
|
// The table island's chrome vocabulary: the visual tier's `strings` set extends it,
|
|
18
18
|
// so the island's wording is overridden beside every other key.
|
|
19
19
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
@@ -37,7 +37,7 @@ export { pmToContent, contentEdit, lower } from './encode.js';
|
|
|
37
37
|
export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
|
|
38
38
|
// Reconciliation gate.
|
|
39
39
|
export { createReconciler, contentEqual, valueEqual } from './reconcile.js';
|
|
40
|
-
// Input rules (`createField` mounts them unless `noInputRules
|
|
40
|
+
// Input rules (`createField` mounts them unless `noInputRules`, or over a mark-free schema).
|
|
41
41
|
export { inputRulesPlugin } from './inputrules.js';
|
|
42
42
|
// The body leaf's structural keys: `createField` binds the composed chains
|
|
43
43
|
// (`bodyKeymap`); the suite drives both it and the list link directly.
|
|
@@ -5,3 +5,7 @@ import type { Plugin } from 'prosemirror-state';
|
|
|
5
5
|
export declare function markdownInputRules(schema: Schema): InputRule[];
|
|
6
6
|
/** The input-rules plugin the field mounts by default (opt-out via createField). */
|
|
7
7
|
export declare function inputRulesPlugin(schema: Schema): Plugin;
|
|
8
|
+
/** Backspace on the press right after a rule fired takes the rule back and leaves the
|
|
9
|
+
* text it consumed, the way a writer who meant the literal `# ` says so; and Enter fires
|
|
10
|
+
* the fence. Mounted ahead of every other keymap: each declines on any other press. */
|
|
11
|
+
export declare function undoRuleKeymap(): Plugin;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Input rules: the markdown-shorthand plugin the codec owns (`**`→strong, `*`→em,
|
|
2
2
|
// `~~`→strike, `` ` ``→code, `# `→heading, `- `/`1. `→lists, `> `→blockquote,
|
|
3
|
-
// `---`→divider,
|
|
3
|
+
// `---`→divider), and the fence, which fires on Enter rather than as typed.
|
|
4
4
|
// Mark rules are hand-rolled (prosemirror-inputrules ships only node rules); every
|
|
5
5
|
// block rule is one regexp over a command in `blocks.ts`, which is the slash menu's
|
|
6
6
|
// implementation too. These produce transactions the codec already lowers: they add no
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// sets"), and a construct that arrives through `importMarkdown` must stay authorable or
|
|
13
13
|
// a document opens carrying a shape the editor refuses to make. So one guard survives:
|
|
14
14
|
// `- ` / `1. ` at the head of an existing item, where Tab owns the nesting.
|
|
15
|
-
import { InputRule, inputRules } from 'prosemirror-inputrules';
|
|
15
|
+
import { InputRule, inputRules, undoInputRule } from 'prosemirror-inputrules';
|
|
16
|
+
import { keymap } from 'prosemirror-keymap';
|
|
16
17
|
import { consuming, insertDivider, toCodeBlock, toHeading, wrapInList, wrapInQuote } from './blocks.js';
|
|
17
18
|
/**
|
|
18
19
|
* A mark input rule: when `regexp` (ending in the closing delimiter) matches
|
|
@@ -47,16 +48,17 @@ function markInputRule(regexp, markType, delimLen) {
|
|
|
47
48
|
}
|
|
48
49
|
/** A block shorthand: the regexp that fires it, and the command it runs in the match's
|
|
49
50
|
* place. Every one is `^`-anchored, so the command's own head-of-a-textblock guard is
|
|
50
|
-
* the same position the regexp already stands at.
|
|
51
|
+
* the same position the regexp already stands at. The caret it places is scrolled to
|
|
52
|
+
* here: `consuming` rebuilds the transaction, so a flag the command set is not on it. */
|
|
51
53
|
function blockRule(regexp, command) {
|
|
52
|
-
return new InputRule(regexp, (state, match, start, end) => consuming(state, start, end, command(match))?.tr ?? null);
|
|
54
|
+
return new InputRule(regexp, (state, match, start, end) => consuming(state, start, end, command(match))?.tr.scrollIntoView() ?? null);
|
|
53
55
|
}
|
|
54
56
|
/** The full markdown-shorthand rule set for a block schema. */
|
|
55
57
|
export function markdownInputRules(schema) {
|
|
56
58
|
const rules = [];
|
|
57
59
|
const m = schema.marks;
|
|
58
60
|
// Inline marks. Non-greedy capture between matching delimiters. Built only where the
|
|
59
|
-
// 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
|
|
60
62
|
// at all.
|
|
61
63
|
if (m.strong)
|
|
62
64
|
rules.push(markInputRule(/\*\*([^*]+)\*\*$/, m.strong, 2));
|
|
@@ -69,7 +71,6 @@ export function markdownInputRules(schema) {
|
|
|
69
71
|
// Block shorthands. Each command answers for the schema it is run against, so a
|
|
70
72
|
// schema without the node declines rather than being guarded here.
|
|
71
73
|
rules.push(blockRule(/^(#{1,6})\s$/, (match) => toHeading(match[1].length)));
|
|
72
|
-
rules.push(blockRule(/^```$/, toCodeBlock));
|
|
73
74
|
rules.push(blockRule(/^---$/, insertDivider));
|
|
74
75
|
// `- ` / `1. ` at the start of a block, here or nested inside an item — but not at
|
|
75
76
|
// the start of an item's own first block, where the command declines.
|
|
@@ -82,3 +83,40 @@ export function markdownInputRules(schema) {
|
|
|
82
83
|
export function inputRulesPlugin(schema) {
|
|
83
84
|
return inputRules({ rules: markdownInputRules(schema) });
|
|
84
85
|
}
|
|
86
|
+
/** A fence's opening line: three backticks, then the language, if one is typed. */
|
|
87
|
+
const FENCE = /^```([^\s`]*)$/;
|
|
88
|
+
/**
|
|
89
|
+
* Enter at the end of a paragraph that is a fence's opening line → a code block carrying
|
|
90
|
+
* the language typed after the backticks. The one shorthand Enter fires rather than its
|
|
91
|
+
* last character: on the third backtick the language is not typed yet. It records itself
|
|
92
|
+
* as the input-rules plugin's last rule, so the Backspace after takes it back like any
|
|
93
|
+
* other shorthand.
|
|
94
|
+
*/
|
|
95
|
+
const fenceOnEnter = (state, dispatch) => {
|
|
96
|
+
const { $from, empty } = state.selection;
|
|
97
|
+
const line = $from.parent;
|
|
98
|
+
if (!empty || line.type !== state.schema.nodes.paragraph)
|
|
99
|
+
return false;
|
|
100
|
+
if ($from.parentOffset !== line.content.size || line.childCount !== 1 || !line.firstChild.isText)
|
|
101
|
+
return false;
|
|
102
|
+
const match = FENCE.exec(line.textContent);
|
|
103
|
+
if (!match)
|
|
104
|
+
return false;
|
|
105
|
+
const done = consuming(state, $from.start(), $from.pos, toCodeBlock({ lang: match[1] || null }));
|
|
106
|
+
if (!done)
|
|
107
|
+
return false;
|
|
108
|
+
if (dispatch) {
|
|
109
|
+
const tr = done.tr.scrollIntoView();
|
|
110
|
+
const rules = state.plugins.find((plugin) => plugin.spec.isInputRules);
|
|
111
|
+
if (rules)
|
|
112
|
+
tr.setMeta(rules, { transform: tr, from: $from.pos, to: $from.pos, text: '' });
|
|
113
|
+
dispatch(tr);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
/** Backspace on the press right after a rule fired takes the rule back and leaves the
|
|
118
|
+
* text it consumed, the way a writer who meant the literal `# ` says so; and Enter fires
|
|
119
|
+
* the fence. Mounted ahead of every other keymap: each declines on any other press. */
|
|
120
|
+
export function undoRuleKeymap() {
|
|
121
|
+
return keymap({ Backspace: undoInputRule, Enter: fenceOnEnter });
|
|
122
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MarkType } from 'prosemirror-model';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
/**
|
|
4
|
+
* Links a selection within one block where the clipboard's text is one URL naming a
|
|
5
|
+
* scheme the mark renders. A selection crossing blocks, or held in one taking no link
|
|
6
|
+
* (a code block), keeps the default paste, and so does a URL nothing would follow.
|
|
7
|
+
*
|
|
8
|
+
* The ops are `setLink`'s pair (`visual/links.ts`), so the mark diff lowers the paste
|
|
9
|
+
* as one link exchanged for another.
|
|
10
|
+
*/
|
|
11
|
+
export declare function linkPastePlugin(link: MarkType): Plugin;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Plugin, TextSelection } from 'prosemirror-state';
|
|
2
|
+
import { rendersHref } from './urls.js';
|
|
3
|
+
/** One URL and nothing else: a scheme, then no whitespace. */
|
|
4
|
+
const URL_ALONE = /^[a-z][a-z0-9+.-]*:\S+$/i;
|
|
5
|
+
/**
|
|
6
|
+
* Links a selection within one block where the clipboard's text is one URL naming a
|
|
7
|
+
* scheme the mark renders. A selection crossing blocks, or held in one taking no link
|
|
8
|
+
* (a code block), keeps the default paste, and so does a URL nothing would follow.
|
|
9
|
+
*
|
|
10
|
+
* The ops are `setLink`'s pair (`visual/links.ts`), so the mark diff lowers the paste
|
|
11
|
+
* as one link exchanged for another.
|
|
12
|
+
*/
|
|
13
|
+
export function linkPastePlugin(link) {
|
|
14
|
+
return new Plugin({
|
|
15
|
+
props: {
|
|
16
|
+
handlePaste(view, event) {
|
|
17
|
+
const { selection } = view.state;
|
|
18
|
+
if (!(selection instanceof TextSelection) || selection.empty)
|
|
19
|
+
return false;
|
|
20
|
+
const { $from, $to } = selection;
|
|
21
|
+
if (!$from.sameParent($to) || !$from.parent.type.allowsMarkType(link))
|
|
22
|
+
return false;
|
|
23
|
+
const href = event.clipboardData?.getData('text/plain').trim() ?? '';
|
|
24
|
+
if (!URL_ALONE.test(href) || !rendersHref(href))
|
|
25
|
+
return false;
|
|
26
|
+
const { from, to } = selection;
|
|
27
|
+
view.dispatch(view.state.tr.removeMark(from, to, link).addMark(from, to, link.create({ href })));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -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,11 +13,12 @@ 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
|
-
* so the third link lifts a top-level empty item out to a paragraph. `Backspace`
|
|
20
|
-
*
|
|
20
|
+
* so the third link lifts a top-level empty item out to a paragraph. `Backspace` lifts
|
|
21
|
+
* an item at its start, and joins a block onto the list it stands after. `Delete` is
|
|
21
22
|
* that same chain at the seam below rather than a second table of cases beside it, so
|
|
22
23
|
* the two keys cannot come to differ over a seam this link reaches. An empty line is
|
|
23
24
|
* not one: the block link takes it ahead of this chain (`blocks.ts`), the caret's own
|