@quillmark/svelte 0.24.0 → 0.26.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/THEMING.md +1 -1
- package/dist/core/codec/blocks.d.ts +23 -6
- package/dist/core/codec/blocks.js +163 -38
- package/dist/core/codec/code.js +3 -8
- package/dist/core/codec/field.d.ts +4 -2
- package/dist/core/codec/field.js +13 -4
- package/dist/core/codec/index.d.ts +2 -0
- package/dist/core/codec/index.js +4 -0
- package/dist/core/codec/inputrules.js +40 -10
- package/dist/core/codec/keymap.js +19 -1
- package/dist/core/codec/line-paste.d.ts +7 -0
- package/dist/core/codec/line-paste.js +36 -0
- package/dist/core/codec/list-move.d.ts +6 -0
- package/dist/core/codec/list-move.js +30 -0
- package/dist/core/codec/list-paste.d.ts +2 -0
- package/dist/core/codec/list-paste.js +142 -0
- package/dist/core/codec/list-toggle.d.ts +4 -0
- package/dist/core/codec/list-toggle.js +197 -0
- package/dist/core/codec/lists.d.ts +22 -15
- package/dist/core/codec/lists.js +404 -97
- package/dist/core/codec/markdown-paste.d.ts +9 -0
- package/dist/core/codec/markdown-paste.js +63 -2
- package/dist/core/codec/nesting.d.ts +5 -1
- package/dist/core/codec/nesting.js +1 -1
- package/dist/core/codec/prose.css +21 -20
- package/dist/core/codec/slash.d.ts +2 -1
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/touching.d.ts +12 -0
- package/dist/core/codec/touching.js +33 -0
- package/dist/core/codec/undo.d.ts +5 -0
- package/dist/core/codec/undo.js +21 -0
- package/dist/core/codec/word-paste.d.ts +5 -0
- package/dist/core/codec/word-paste.js +87 -0
- package/dist/preview/Preview.svelte +88 -13
- package/dist/preview/Preview.svelte.d.ts +2 -2
- package/dist/preview/bridge.js +8 -2
- package/dist/preview/controller.d.ts +8 -4
- package/dist/preview/controller.js +10 -2
- package/dist/preview/index.js +1 -1
- package/dist/preview/paint.d.ts +6 -2
- package/dist/preview/paint.js +67 -56
- package/dist/preview/strings.d.ts +2 -0
- package/dist/preview/strings.js +4 -2
- package/dist/preview/viewport.d.ts +10 -0
- package/dist/preview/viewport.js +271 -0
- package/dist/preview/zoom.d.ts +41 -0
- package/dist/preview/zoom.js +52 -0
- package/dist/visual/ArrayField.svelte +24 -4
- package/dist/visual/DateField.svelte +26 -13
- package/dist/visual/FormatPopover.svelte +41 -2
- package/dist/visual/ProseValue.svelte +35 -3
- package/dist/visual/ProseValue.svelte.d.ts +7 -0
- package/dist/visual/icons/nodes.d.ts +26 -0
- package/dist/visual/icons/nodes.js +16 -0
- package/dist/visual/strings.d.ts +4 -2
- package/dist/visual/strings.js +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// A one-line leaf holds one paragraph, so a paste of several lines lands as that one line:
|
|
2
|
+
// the lines joined by a space, as a browser's single-line input joins them, where the
|
|
3
|
+
// schema would keep the first and drop the rest. An element of an array of them is the
|
|
4
|
+
// exception the array asks for (`visual/ProseValue.svelte`): there each line is a row.
|
|
5
|
+
import { Fragment, Slice } from 'prosemirror-model';
|
|
6
|
+
import { Plugin } from 'prosemirror-state';
|
|
7
|
+
/** The lines a slice holds that hold anything, in order. */
|
|
8
|
+
export function pastedLines(slice) {
|
|
9
|
+
const lines = [];
|
|
10
|
+
slice.content.descendants((node) => {
|
|
11
|
+
if (node.isTextblock && node.content.size)
|
|
12
|
+
lines.push(node);
|
|
13
|
+
return !node.isTextblock;
|
|
14
|
+
});
|
|
15
|
+
return lines;
|
|
16
|
+
}
|
|
17
|
+
/** `lines` as the content of one line, a space between each two. */
|
|
18
|
+
export function joinLines(lines, schema) {
|
|
19
|
+
const parts = [];
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
if (parts.length)
|
|
22
|
+
parts.push(schema.text(' '));
|
|
23
|
+
line.content.forEach((node) => parts.push(node));
|
|
24
|
+
}
|
|
25
|
+
return Fragment.from(parts);
|
|
26
|
+
}
|
|
27
|
+
export function linePastePlugin() {
|
|
28
|
+
return new Plugin({
|
|
29
|
+
props: {
|
|
30
|
+
transformPasted(slice, view) {
|
|
31
|
+
const lines = pastedLines(slice);
|
|
32
|
+
return lines.length > 1 ? new Slice(joinLines(lines, view.state.schema), 0, 0) : slice;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Schema } from 'prosemirror-model';
|
|
2
|
+
import type { Command } from 'prosemirror-state';
|
|
3
|
+
/** The items the selection stands in, in the innermost list holding all of them, moved
|
|
4
|
+
* one place up (`-1`) or down (`1`). */
|
|
5
|
+
export declare function moveItems(dir: -1 | 1): Command;
|
|
6
|
+
export declare function listMoveKeymap(schema: Schema): Record<string, Command>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const isList = (node) => node.type.name === 'ordered_list' || node.type.name === 'bullet_list';
|
|
2
|
+
/** The items the selection stands in, in the innermost list holding all of them, moved
|
|
3
|
+
* one place up (`-1`) or down (`1`). */
|
|
4
|
+
export function moveItems(dir) {
|
|
5
|
+
return (state, dispatch) => {
|
|
6
|
+
const { $from, $to } = state.selection;
|
|
7
|
+
const range = $from.blockRange($to, isList);
|
|
8
|
+
if (!range)
|
|
9
|
+
return false;
|
|
10
|
+
const index = dir < 0 ? range.startIndex - 1 : range.endIndex;
|
|
11
|
+
if (index < 0 || index >= range.parent.childCount)
|
|
12
|
+
return false;
|
|
13
|
+
if (!dispatch)
|
|
14
|
+
return true;
|
|
15
|
+
const sibling = range.parent.child(index);
|
|
16
|
+
const size = sibling.nodeSize;
|
|
17
|
+
const tr = state.tr;
|
|
18
|
+
if (dir < 0)
|
|
19
|
+
tr.delete(range.start - size, range.start).insert(range.end - size, sibling);
|
|
20
|
+
else
|
|
21
|
+
tr.delete(range.end, range.end + size).insert(range.start, sibling);
|
|
22
|
+
dispatch(tr.scrollIntoView());
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function listMoveKeymap(schema) {
|
|
27
|
+
if (!schema.nodes.list_item)
|
|
28
|
+
return {};
|
|
29
|
+
return { 'Alt-ArrowUp': moveItems(-1), 'Alt-ArrowDown': moveItems(1) };
|
|
30
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// A pasted list takes the numbering of where it lands (CODEC §"Markdown at the edges").
|
|
2
|
+
// An item's number is its position, so a paste counts where it stands: items pasted into
|
|
3
|
+
// a list are that list's next items, of its kind and in its count, and items pasted on
|
|
4
|
+
// the empty line a list was just left by are the same list's. A list pasted on a line of
|
|
5
|
+
// its own keeps the `start` it was copied with, which a writer typed (`lists.ts` writes
|
|
6
|
+
// none) — but a run of items copied from inside a list is copied counting from 1: their
|
|
7
|
+
// numbers were positions in the list they came out of.
|
|
8
|
+
//
|
|
9
|
+
// A line's shape travels with its end, as a word processor's paragraph mark carries its
|
|
10
|
+
// style: text copied from inside one line pastes as text, whatever block it was copied
|
|
11
|
+
// out of, and paragraphs pasted into a numbered line are numbered lines.
|
|
12
|
+
import { Fragment, Slice } from 'prosemirror-model';
|
|
13
|
+
import { Plugin, Selection } from 'prosemirror-state';
|
|
14
|
+
const isList = (node) => !!node && (node.type.name === 'ordered_list' || node.type.name === 'bullet_list');
|
|
15
|
+
/** The one list a slice is, if it is one: whole, or open where a run of its items was
|
|
16
|
+
* copied. */
|
|
17
|
+
function soleList(slice) {
|
|
18
|
+
return slice.content.childCount === 1 && isList(slice.content.firstChild)
|
|
19
|
+
? slice.content.firstChild
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
/** The textblock a slice is the inside of: text copied from within one line, the slice
|
|
23
|
+
* open to that line at both ends down a chain of only children. */
|
|
24
|
+
function lineInside(slice) {
|
|
25
|
+
if (slice.openStart === 0 || slice.openStart !== slice.openEnd)
|
|
26
|
+
return null;
|
|
27
|
+
let node = null;
|
|
28
|
+
for (let d = 0, content = slice.content; d < slice.openStart; d++, content = node.content) {
|
|
29
|
+
if (content.childCount !== 1)
|
|
30
|
+
return null;
|
|
31
|
+
node = content.firstChild;
|
|
32
|
+
}
|
|
33
|
+
return node.isTextblock ? node : null;
|
|
34
|
+
}
|
|
35
|
+
/** `content` with the node `depth` levels down its leading edge counting from 1. */
|
|
36
|
+
function fromOne(content, depth) {
|
|
37
|
+
const first = content.firstChild;
|
|
38
|
+
const node = depth === 1
|
|
39
|
+
? first.type.create({ ...first.attrs, start: 1 }, first.content, first.marks)
|
|
40
|
+
: first.copy(fromOne(first.content, depth - 1));
|
|
41
|
+
return content.replaceChild(0, node);
|
|
42
|
+
}
|
|
43
|
+
/** A new item beside `item`: a task's sibling is a task, and unchecked. */
|
|
44
|
+
const itemAttrs = (item) => ({ checked: item.attrs.checked === null ? null : false });
|
|
45
|
+
/** HTML a ProseMirror view wrote, which states how deep the copy was open at each end. */
|
|
46
|
+
const PM_SLICE = /\sdata-pm-slice="\d+ \d+/;
|
|
47
|
+
export function listPastePlugin() {
|
|
48
|
+
// Whether the paste in flight states its own open depths: a ProseMirror copy does, and
|
|
49
|
+
// a drag inside the view carries its slice whole. Any other source is parsed open as
|
|
50
|
+
// deep as it goes, so a whole item copied off a web page reads as the inside of one
|
|
51
|
+
// line, and only a slice that says so is taken for one. Read once, by the transform.
|
|
52
|
+
let exact = false;
|
|
53
|
+
return new Plugin({
|
|
54
|
+
props: {
|
|
55
|
+
handleDOMEvents: {
|
|
56
|
+
drop: (view) => {
|
|
57
|
+
exact = !!view.dragging;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
transformPastedHTML(html) {
|
|
62
|
+
exact = PM_SLICE.test(html);
|
|
63
|
+
return html;
|
|
64
|
+
},
|
|
65
|
+
// A copy whose leading edge stands past an ordered list's first item is a run
|
|
66
|
+
// out of it, and goes out counting from 1; one from the first item keeps the
|
|
67
|
+
// list's `start`, however the selection ends. The copy is the selection's own
|
|
68
|
+
// slice, its leading edge the caret's ancestors from the top, so the list at
|
|
69
|
+
// depth `d` stands `d` levels down it.
|
|
70
|
+
transformCopied(slice, view) {
|
|
71
|
+
const { $from } = view.state.selection;
|
|
72
|
+
let content = slice.content;
|
|
73
|
+
for (let d = 1; d < $from.depth; d++) {
|
|
74
|
+
const list = $from.node(d);
|
|
75
|
+
if (list.type.name === 'ordered_list' && $from.index(d) > 0 && list.attrs.start !== 1)
|
|
76
|
+
content = fromOne(content, d);
|
|
77
|
+
}
|
|
78
|
+
return content === slice.content
|
|
79
|
+
? slice
|
|
80
|
+
: new Slice(content, slice.openStart, slice.openEnd);
|
|
81
|
+
},
|
|
82
|
+
// The caret's line keeps its shape under text from inside another line: no
|
|
83
|
+
// list, heading or fence comes along to wrap it. Into a line of an item, a list
|
|
84
|
+
// is its items, which join the list the caret is in where a whole list would
|
|
85
|
+
// nest under that line's item. Paragraphs into an item's first line are items
|
|
86
|
+
// beside it, as a word processor numbers each pasted paragraph; a later line of
|
|
87
|
+
// an item is unnumbered, and so are the paragraphs pasted into it.
|
|
88
|
+
transformPasted(slice, view) {
|
|
89
|
+
const line = exact && lineInside(slice);
|
|
90
|
+
exact = false;
|
|
91
|
+
if (line)
|
|
92
|
+
return new Slice(line.content, 0, 0);
|
|
93
|
+
const { $from, $to } = view.state.selection;
|
|
94
|
+
if (!$from.sameParent($to) || !$from.parent.isTextblock || $from.depth < 2)
|
|
95
|
+
return slice;
|
|
96
|
+
if ($from.parent.type.spec.code)
|
|
97
|
+
return slice;
|
|
98
|
+
const item = $from.node(-1);
|
|
99
|
+
if (item.type.name !== 'list_item')
|
|
100
|
+
return slice;
|
|
101
|
+
const list = soleList(slice);
|
|
102
|
+
if (list)
|
|
103
|
+
return new Slice(list.content, Math.max(slice.openStart - 1, 0), Math.max(slice.openEnd - 1, 0));
|
|
104
|
+
const { content } = slice;
|
|
105
|
+
if ($from.index(-1) !== 0 || content.childCount < 2)
|
|
106
|
+
return slice;
|
|
107
|
+
const items = [];
|
|
108
|
+
for (let i = 0; i < content.childCount; i++) {
|
|
109
|
+
const block = content.child(i);
|
|
110
|
+
if (block.type.name !== 'paragraph')
|
|
111
|
+
return slice;
|
|
112
|
+
items.push(item.type.create(itemAttrs(item), block));
|
|
113
|
+
}
|
|
114
|
+
return new Slice(Fragment.from(items), slice.openStart && slice.openStart + 1, slice.openEnd && slice.openEnd + 1);
|
|
115
|
+
},
|
|
116
|
+
// On the empty line right under a list of the same kind, the line Enter on an
|
|
117
|
+
// empty last item leaves, the items are that list's next ones.
|
|
118
|
+
handlePaste(view, _event, slice) {
|
|
119
|
+
const list = soleList(slice);
|
|
120
|
+
const { $from, empty } = view.state.selection;
|
|
121
|
+
if (!list || !empty || !$from.parent.isTextblock || $from.parent.content.size !== 0)
|
|
122
|
+
return false;
|
|
123
|
+
if ($from.parent.type.spec.code)
|
|
124
|
+
return false;
|
|
125
|
+
const above = view.state.doc.resolve($from.before()).nodeBefore;
|
|
126
|
+
if (!isList(above) || above.type !== list.type)
|
|
127
|
+
return false;
|
|
128
|
+
const end = $from.before() - 1;
|
|
129
|
+
const tr = view.state.tr.delete($from.before(), $from.after()).insert(end, list.content);
|
|
130
|
+
try {
|
|
131
|
+
tr.doc.check();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
tr.setSelection(Selection.near(tr.doc.resolve(end + list.content.size), -1));
|
|
137
|
+
view.dispatch(tr.scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Command } from 'prosemirror-state';
|
|
2
|
+
/** Number (`ordered`) or bullet every line of the selection, or, where every one already
|
|
3
|
+
* carries the kind, take those markers off. Declines where the selection holds no line. */
|
|
4
|
+
export declare function toggleList(ordered: boolean): Command;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// The list toggle: every line of a selection numbered or bulleted as one gesture, or
|
|
2
|
+
// every one's marker taken off. Mod-Shift-7 and Mod-Shift-8 bind it, the Docs chords
|
|
3
|
+
// (`lists.ts`), and the selection toolbar offers it (`visual/FormatPopover.svelte`).
|
|
4
|
+
//
|
|
5
|
+
// A line is a textblock the selection touches; a fence is no line here, a list item
|
|
6
|
+
// being no place for one to land unasked. A line carries a marker where it opens its
|
|
7
|
+
// item. An item's later line, a continuation, goes with its item where the item's
|
|
8
|
+
// opening line is selected too, and otherwise carries no marker of its own. Where every
|
|
9
|
+
// line carries one of the kind, the toggle takes those markers off, and only those.
|
|
10
|
+
// Otherwise each line becomes the kind: a run of plain lines is wrapped, one item a
|
|
11
|
+
// line, and a run of continuation lines the same way where it stands, the first items
|
|
12
|
+
// of a sub-list under their item, as `- ` or `1. ` at a continuation's head opens one
|
|
13
|
+
// (`blocks.ts` §`openingAnItem`); the selected items of a list of the other kind split
|
|
14
|
+
// out of it as a list of the kind, a list being one kind (`blocks.ts` §`listAtItem`),
|
|
15
|
+
// its tail counting from 1 as `unlist` leaves one; a line already in the kind stays.
|
|
16
|
+
// What it wraps or retypes joins the lists of its kind it leaves touching
|
|
17
|
+
// (`touching.ts`), so the lines of one selection count as one list.
|
|
18
|
+
import { Fragment, NodeRange, Slice } from 'prosemirror-model';
|
|
19
|
+
import { wrapRangeInList } from 'prosemirror-schema-list';
|
|
20
|
+
import { ReplaceAroundStep } from 'prosemirror-transform';
|
|
21
|
+
import { withinNesting } from './nesting.js';
|
|
22
|
+
import { joinAll } from './touching.js';
|
|
23
|
+
/** The textblocks `[from, to]` touches, by position, fences left out. A selection that
|
|
24
|
+
* ends at the very head of a block has selected nothing in it. */
|
|
25
|
+
function linesIn(doc, from, to) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
doc.nodesBetween(from, to, (node, pos) => {
|
|
28
|
+
if (!node.isTextblock)
|
|
29
|
+
return true;
|
|
30
|
+
if (!node.type.spec.code && (from === to || pos + 1 < to))
|
|
31
|
+
lines.push(pos);
|
|
32
|
+
return false;
|
|
33
|
+
});
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
function lineAt(doc, pos, itemType) {
|
|
37
|
+
const $at = doc.resolve(pos + 1);
|
|
38
|
+
let item = -1;
|
|
39
|
+
for (let depth = $at.depth - 1; depth > 0 && item < 0; depth--)
|
|
40
|
+
if ($at.node(depth).type === itemType)
|
|
41
|
+
item = depth;
|
|
42
|
+
let opens = item > 0;
|
|
43
|
+
for (let depth = item; opens && depth < $at.depth; depth++)
|
|
44
|
+
opens = $at.index(depth) === 0;
|
|
45
|
+
return { pos, $at, item, opens };
|
|
46
|
+
}
|
|
47
|
+
/** The selection's lines, each with whether its item's opening line is among them. */
|
|
48
|
+
function linesOf(doc, from, to, itemType) {
|
|
49
|
+
const lines = linesIn(doc, from, to).map((pos) => lineAt(doc, pos, itemType));
|
|
50
|
+
const opened = new Set(lines.filter((l) => l.opens).map((l) => l.$at.before(l.item)));
|
|
51
|
+
return lines.map((line) => ({
|
|
52
|
+
...line,
|
|
53
|
+
covered: line.item > 0 && !line.opens && opened.has(line.$at.before(line.item))
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `prosemirror-schema-list`'s `liftOutOfList`, over a transaction, which upstream does
|
|
58
|
+
* not export: the items of `range` out of their list, their blocks standing where the
|
|
59
|
+
* list stood, the list closed around them. In a sub-list the blocks land in the parent
|
|
60
|
+
* item, the continuation lines a marker coming off leaves (`lists.ts` §`markerOff`).
|
|
61
|
+
* Returns where the list's tail stands, `null` where the items were its last, and
|
|
62
|
+
* `false` where the parent cannot hold the items' blocks.
|
|
63
|
+
*/
|
|
64
|
+
function liftOut(tr, range) {
|
|
65
|
+
const list = range.parent;
|
|
66
|
+
for (let pos = range.end, i = range.endIndex - 1; i > range.startIndex; i--) {
|
|
67
|
+
pos -= list.child(i).nodeSize;
|
|
68
|
+
tr.delete(pos - 1, pos + 1);
|
|
69
|
+
}
|
|
70
|
+
const $start = tr.doc.resolve(range.start);
|
|
71
|
+
const item = $start.nodeAfter;
|
|
72
|
+
const atStart = range.startIndex === 0;
|
|
73
|
+
const atEnd = range.endIndex === list.childCount;
|
|
74
|
+
const start = $start.pos;
|
|
75
|
+
const end = start + item.nodeSize;
|
|
76
|
+
const shell = Fragment.from(list.copy(Fragment.empty));
|
|
77
|
+
const step = new ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1, new Slice((atStart ? Fragment.empty : shell).append(atEnd ? Fragment.empty : shell), atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1);
|
|
78
|
+
if (tr.maybeStep(step).failed)
|
|
79
|
+
return false;
|
|
80
|
+
if (atEnd)
|
|
81
|
+
return null;
|
|
82
|
+
const $list = tr.doc.resolve(range.$from.before(range.depth));
|
|
83
|
+
return $list.posAtIndex($list.index() + (atStart ? 0 : 1) + item.childCount);
|
|
84
|
+
}
|
|
85
|
+
/** Take every marker the selection's lines carry off, the shallowest list first:
|
|
86
|
+
* lifting an item brings its sub-lists up a level, where the next pass reaches them. A
|
|
87
|
+
* list's tail counts from 1 again, a number that names its position in the list it now
|
|
88
|
+
* is (`lists.ts` §`lineOut`). */
|
|
89
|
+
function unlist(tr, type, itemType) {
|
|
90
|
+
for (;;) {
|
|
91
|
+
const { from, to } = tr.selection;
|
|
92
|
+
const marked = linesIn(tr.doc, from, to)
|
|
93
|
+
.map((pos) => lineAt(tr.doc, pos, itemType))
|
|
94
|
+
.filter((l) => l.opens && l.$at.node(l.item - 1).type === type);
|
|
95
|
+
if (!marked.length)
|
|
96
|
+
return true;
|
|
97
|
+
const depth = Math.min(...marked.map((l) => l.item - 1));
|
|
98
|
+
const listPos = marked.find((l) => l.item - 1 === depth).$at.before(depth);
|
|
99
|
+
const indices = marked
|
|
100
|
+
.filter((l) => l.item - 1 === depth && l.$at.before(depth) === listPos)
|
|
101
|
+
.map((l) => l.$at.index(depth));
|
|
102
|
+
const $list = tr.doc.resolve(listPos + 1);
|
|
103
|
+
const range = new NodeRange(tr.doc.resolve($list.posAtIndex(Math.min(...indices))), tr.doc.resolve($list.posAtIndex(Math.max(...indices) + 1)), depth);
|
|
104
|
+
const tail = liftOut(tr, range);
|
|
105
|
+
if (tail === false)
|
|
106
|
+
return false;
|
|
107
|
+
const rest = tail === null ? null : tr.doc.nodeAt(tail);
|
|
108
|
+
if (tail !== null && rest?.attrs.start != null && rest.attrs.start !== 1)
|
|
109
|
+
tr.setNodeMarkup(tail, null, { ...rest.attrs, start: 1 });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Items `first` to `last` of the list at `pos` split out as a list of their own,
|
|
113
|
+
* the tail after them counting from 1. Returns where that list stands. */
|
|
114
|
+
function splitOut(tr, pos, first, last) {
|
|
115
|
+
const list = tr.doc.nodeAt(pos);
|
|
116
|
+
const $inside = tr.doc.resolve(pos + 1);
|
|
117
|
+
if (last + 1 < list.childCount) {
|
|
118
|
+
tr.split($inside.posAtIndex(last + 1));
|
|
119
|
+
const tail = tr.doc.resolve(pos + 1).posAtIndex(last + 1) + 1;
|
|
120
|
+
if (list.attrs.start != null && list.attrs.start !== 1)
|
|
121
|
+
tr.setNodeMarkup(tail, null, { ...list.attrs, start: 1 });
|
|
122
|
+
}
|
|
123
|
+
if (first === 0)
|
|
124
|
+
return pos;
|
|
125
|
+
tr.split($inside.posAtIndex(first));
|
|
126
|
+
return $inside.posAtIndex(first) + 1;
|
|
127
|
+
}
|
|
128
|
+
/** Make every line the kind, and join what that leaves touching. */
|
|
129
|
+
function relist(tr, lines, type, paragraph) {
|
|
130
|
+
const changed = [];
|
|
131
|
+
const runs = [];
|
|
132
|
+
const others = new Map();
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const { $at, item } = line;
|
|
135
|
+
if (line.covered)
|
|
136
|
+
continue;
|
|
137
|
+
if (line.opens) {
|
|
138
|
+
if ($at.node(item - 1).type === type)
|
|
139
|
+
continue;
|
|
140
|
+
const pos = $at.before(item - 1);
|
|
141
|
+
const index = $at.index(item - 1);
|
|
142
|
+
const span = others.get(pos);
|
|
143
|
+
if (span)
|
|
144
|
+
span.last = index;
|
|
145
|
+
else
|
|
146
|
+
others.set(pos, { first: index, last: index });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const end = line.pos + $at.parent.nodeSize;
|
|
150
|
+
const run = runs[runs.length - 1];
|
|
151
|
+
if (run && run.to === line.pos && run.depth === $at.depth - 1)
|
|
152
|
+
run.to = end;
|
|
153
|
+
else
|
|
154
|
+
runs.push({ from: line.pos, to: end, depth: $at.depth - 1 });
|
|
155
|
+
}
|
|
156
|
+
for (const run of runs.reverse()) {
|
|
157
|
+
tr.setBlockType(run.from, run.to, paragraph);
|
|
158
|
+
const range = new NodeRange(tr.doc.resolve(run.from), tr.doc.resolve(run.to), run.depth);
|
|
159
|
+
if (!wrapRangeInList(tr, range, type))
|
|
160
|
+
return false;
|
|
161
|
+
changed.push({ pos: run.from, step: tr.steps.length });
|
|
162
|
+
}
|
|
163
|
+
for (const [pos, span] of [...others].reverse()) {
|
|
164
|
+
const at = splitOut(tr, tr.mapping.map(pos), span.first, span.last);
|
|
165
|
+
tr.setNodeMarkup(at, type, type.name === 'ordered_list' ? { start: 1 } : null);
|
|
166
|
+
changed.push({ pos: at, step: tr.steps.length });
|
|
167
|
+
}
|
|
168
|
+
const seams = [];
|
|
169
|
+
for (const { pos, step } of changed) {
|
|
170
|
+
const at = tr.mapping.slice(step).map(pos);
|
|
171
|
+
const node = tr.doc.nodeAt(at);
|
|
172
|
+
if (node)
|
|
173
|
+
seams.push(at, at + node.nodeSize);
|
|
174
|
+
}
|
|
175
|
+
joinAll(tr, seams);
|
|
176
|
+
return withinNesting(tr.doc);
|
|
177
|
+
}
|
|
178
|
+
/** Number (`ordered`) or bullet every line of the selection, or, where every one already
|
|
179
|
+
* carries the kind, take those markers off. Declines where the selection holds no line. */
|
|
180
|
+
export function toggleList(ordered) {
|
|
181
|
+
return (state, dispatch) => {
|
|
182
|
+
const { list_item: item, paragraph } = state.schema.nodes;
|
|
183
|
+
const type = state.schema.nodes[ordered ? 'ordered_list' : 'bullet_list'];
|
|
184
|
+
if (!item || !paragraph || !type)
|
|
185
|
+
return false;
|
|
186
|
+
const { from, to } = state.selection;
|
|
187
|
+
const lines = linesOf(state.doc, from, to, item);
|
|
188
|
+
if (!lines.length)
|
|
189
|
+
return false;
|
|
190
|
+
const tr = state.tr;
|
|
191
|
+
const off = lines.every((l) => l.covered || (l.opens && l.$at.node(l.item - 1).type === type));
|
|
192
|
+
if (!(off ? unlist(tr, type, item) : relist(tr, lines, type, paragraph)))
|
|
193
|
+
return false;
|
|
194
|
+
dispatch?.(tr.scrollIntoView());
|
|
195
|
+
return true;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -7,21 +7,28 @@ import type { Command } from 'prosemirror-state';
|
|
|
7
7
|
* `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
|
|
8
8
|
* Tab more locally under the same rule (`prose/canon/VISUAL_EDITOR.md` §Chrome)
|
|
9
9
|
* prepend a link ahead of these in `keymap.ts`: a `code_block` takes literal
|
|
10
|
-
* indentation (`code.ts`)
|
|
11
|
-
*
|
|
12
|
-
* default meaning, which is the body's only keyboard exit and the open seam for a
|
|
13
|
-
* shell structural keymap (VISUAL_EDITOR §Settled and open).
|
|
10
|
+
* indentation (`code.ts`). Where every link declines, the body holds the key
|
|
11
|
+
* (`keymap.ts`).
|
|
14
12
|
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
13
|
+
* `Mod-Shift-7` / `Mod-Shift-8` are the list toggle (`list-toggle.ts`), numbering or
|
|
14
|
+
* bulleting the selection's lines or taking their markers off.
|
|
15
|
+
*
|
|
16
|
+
* `Tab` sinks an unnumbered line alone into a sub-list, any other line its whole item,
|
|
17
|
+
* and a block right after a list into the list's last item.
|
|
18
|
+
*
|
|
19
|
+
* `Enter` is a chain in precedence order: the empty line above an unnumbered one, the
|
|
20
|
+
* marker off an empty middle item and back on an empty continuation line, then `splitListItem`
|
|
21
|
+
* (`splitTaskItem`, which leaves a task item's new half an open task), then
|
|
22
|
+
* `liftListItem`. The split link carries two behaviors of its own: it splits a
|
|
23
|
+
* non-empty item, and on an empty last item it either splits the wrapping item (a
|
|
24
|
+
* nested one, so Enter lifts exactly one level) or bails so the last link lifts a
|
|
25
|
+
* top-level one out to a paragraph. `Backspace` takes an item's marker off at its
|
|
26
|
+
* start, a top-level continuation line out to full width, and joins a block onto the
|
|
27
|
+
* list it stands after, and both keys over a selection spanning blocks join the lists
|
|
28
|
+
* the delete leaves touching. `Delete` is the marker off at the seam below with the two lines
|
|
29
|
+
* joined, never the line out: a forward delete edits the line after the caret, not its
|
|
30
|
+
* indent. An empty line is neither key's here:
|
|
31
|
+
* the block link takes it ahead of this chain (`blocks.ts`), the caret's own line
|
|
32
|
+
* being a nearer fact than the boundary under it.
|
|
26
33
|
*/
|
|
27
34
|
export declare function listKeymap(schema: Schema): Record<string, Command>;
|