Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/slate/format/indent.ts
1698 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Editor, Element, Location, Path, Transforms } from "slate";
7
import { isListElement } from "../elements/list";
8
import { emptyParagraph } from "../padding";
9
import { moveCursorToBeginningOfBlock } from "../control";
10
11
export function unindentListItem(editor: Editor): boolean {
12
const [item, path] = getNode(editor, (node) => node.type == "list_item");
13
if (item == null || path == null) {
14
// no list item containing cursor...
15
return false;
16
}
17
if (!item.children) {
18
// this shouldn't happen since all list_item's should
19
// have children
20
return false;
21
}
22
23
const [list, listPath] = getNode(editor, isListElement);
24
if (list == null || listPath == null) {
25
// shouldn't happen, since list_item should be inside of an actual list.
26
return false;
27
}
28
29
// Now the parent of that list itself has to be a list item
30
// to be able to unindent.
31
const parentOfListPath = Path.parent(listPath);
32
const [parentOfList] = Editor.node(editor, parentOfListPath);
33
34
if (!Element.isElement(parentOfList) || parentOfList.type != "list_item") {
35
// Top level list item. Remove bullet point and make it
36
// no longer a list item at all.
37
let to = Path.parent(path);
38
if (Path.hasPrevious(path)) {
39
to = Path.next(to);
40
}
41
try {
42
Editor.withoutNormalizing(editor, () => {
43
// First move the cursor, since cursor can't be in the middle
44
// of this list item, or we end up splitting
45
// the item itself, e.g.,
46
// - foo
47
// - bar[CURSOR]stuff
48
// Also, moving to beginning feels right for unindent. We do
49
// this in all cases for consistency.
50
moveCursorToBeginningOfBlock(editor);
51
if (path[path.length - 1] < list.children.length - 1) {
52
// not last child so split:
53
Transforms.splitNodes(editor, {
54
match: (node) => Element.isElement(node) && isListElement(node),
55
mode: "lowest",
56
});
57
}
58
Transforms.moveNodes(editor, {
59
match: (node) => node === item,
60
to,
61
});
62
Transforms.unwrapNodes(editor, {
63
match: (node) => node === item,
64
mode: "lowest",
65
at: to,
66
});
67
});
68
} catch (err) {
69
console.warn(`SLATE -- issue making list item ${err}`);
70
}
71
return true;
72
}
73
74
const to = Path.next(parentOfListPath);
75
76
try {
77
Editor.withoutNormalizing(editor, () => {
78
Transforms.moveNodes(editor, {
79
to,
80
match: (node) => node === list,
81
});
82
Transforms.unwrapNodes(editor, { at: to });
83
});
84
} catch (err) {
85
console.warn(`SLATE -- issue with unindentListItem ${err}`);
86
}
87
88
// re-indent any extra siblings that we just unintentionally un-indented
89
// Yes, I wish there was a simpler way than this, but fortunately this
90
// is not a speed critical path for anything.
91
const numBefore = path[path.length - 1];
92
const numAfter = list.children.length - numBefore - 1;
93
for (let i = 0; i < numBefore; i++) {
94
indentListItem(editor, to);
95
}
96
const after = Path.next(to);
97
for (let i = 0; i < numAfter; i++) {
98
indentListItem(editor, after);
99
}
100
101
return true;
102
}
103
104
export function getNode(
105
editor,
106
match,
107
at: Location | undefined = undefined,
108
): [Element, number[]] | [undefined, undefined] {
109
if (at != null) {
110
// First try the node at *specific* given position.
111
// For some reason there seems to be no mode
112
// with Editor.nodes that does this, but we use
113
// this for re-indenting in the unindent code above.
114
try {
115
const [elt, path] = Editor.node(editor, at);
116
if (Element.isElement(elt) && match(elt, path)) {
117
return [elt as Element, path];
118
}
119
} catch (_err) {
120
// no such element, so try search below...
121
}
122
}
123
try {
124
for (const elt of Editor.nodes(editor, {
125
match: (node, path) => Element.isElement(node) && match(node, path),
126
mode: "lowest",
127
at,
128
})) {
129
return [elt[0] as Element, elt[1]];
130
}
131
} catch (_err) {
132
// no such element
133
}
134
return [undefined, undefined];
135
}
136
137
export function indentListItem(
138
editor: Editor,
139
at: Location | undefined = undefined,
140
): boolean {
141
const [item, path] = getNode(editor, (node) => node.type == "list_item", at);
142
if (item == null || path == null) {
143
// console.log("no list item containing cursor...");
144
return false;
145
}
146
if (!item.children) {
147
// console.log("this shouldn't happen since all list_item's should have children");
148
return false;
149
}
150
151
const [list] = getNode(editor, isListElement, at);
152
if (list == null) {
153
// console.log("shouldn't happen, since list_item should be inside of an actual list.");
154
return false;
155
}
156
157
if (list.children[0] === item || path[path.length - 1] == 0) {
158
// console.log("can't indent the first item", { list, path, item });
159
return false;
160
}
161
162
const prevPath = Path.previous(path);
163
const [prevItem] = Editor.node(editor, prevPath);
164
if (!Element.isElement(prevItem)) {
165
// console.log("type issue -- should not happen");
166
return false;
167
}
168
if (
169
prevItem.children.length > 0 &&
170
!Element.isElement(prevItem.children[prevItem.children.length - 1])
171
) {
172
// we can't stick our list item adjacent to a leaf node (e.g.,
173
// not next to a text node). This naturally happens, since an
174
// empty list item is parsed without a block child in it.
175
Transforms.wrapNodes(editor, emptyParagraph() as any, {
176
at: prevPath.concat(0),
177
});
178
}
179
const to = prevPath.concat([prevItem.children.length]);
180
181
if (list.type != "bullet_list" && list.type != "ordered_list") {
182
// console.log("Type issue that should not happen.");
183
return false;
184
}
185
try {
186
Editor.withoutNormalizing(editor, () => {
187
Transforms.moveNodes(editor, {
188
to,
189
match: (node) => node === item,
190
at,
191
});
192
Transforms.wrapNodes(
193
editor,
194
{ type: list.type, tight: true, children: [] },
195
{ at: to },
196
);
197
});
198
} catch (err) {
199
console.warn(`SLATE -- issue with indentListItem ${err}`);
200
}
201
202
return true;
203
}
204
205