@lexical/list 0.13.1 → 0.14.1
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/LexicalList.dev.esm.js +1195 -0
- package/LexicalList.dev.js +107 -129
- package/LexicalList.esm.js +23 -0
- package/LexicalList.js +1 -1
- package/LexicalList.prod.esm.js +7 -0
- package/LexicalList.prod.js +30 -28
- package/LexicalListItemNode.d.ts +0 -1
- package/LexicalListNode.d.ts +1 -0
- package/README.md +3 -1
- package/formatList.d.ts +4 -5
- package/package.json +6 -4
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { $getSelection, $isRangeSelection, $isRootOrShadowRoot, $isElementNode, $isLeafNode, $createParagraphNode, $isParagraphNode, $applyNodeReplacement, ElementNode, $createTextNode, createCommand } from 'lexical';
|
|
8
|
+
import { $getNearestNodeOfType, removeClassNamesFromElement, addClassNamesToElement, isHTMLElement } from '@lexical/utils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
12
|
+
*
|
|
13
|
+
* This source code is licensed under the MIT license found in the
|
|
14
|
+
* LICENSE file in the root directory of this source tree.
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Checks the depth of listNode from the root node.
|
|
20
|
+
* @param listNode - The ListNode to be checked.
|
|
21
|
+
* @returns The depth of the ListNode.
|
|
22
|
+
*/
|
|
23
|
+
function $getListDepth(listNode) {
|
|
24
|
+
let depth = 1;
|
|
25
|
+
let parent = listNode.getParent();
|
|
26
|
+
while (parent != null) {
|
|
27
|
+
if ($isListItemNode(parent)) {
|
|
28
|
+
const parentList = parent.getParent();
|
|
29
|
+
if ($isListNode(parentList)) {
|
|
30
|
+
depth++;
|
|
31
|
+
parent = parentList.getParent();
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
{
|
|
35
|
+
throw Error(`A ListItemNode must have a ListNode for a parent.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return depth;
|
|
39
|
+
}
|
|
40
|
+
return depth;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Finds the nearest ancestral ListNode and returns it, throws an invariant if listItem is not a ListItemNode.
|
|
45
|
+
* @param listItem - The node to be checked.
|
|
46
|
+
* @returns The ListNode found.
|
|
47
|
+
*/
|
|
48
|
+
function $getTopListNode(listItem) {
|
|
49
|
+
let list = listItem.getParent();
|
|
50
|
+
if (!$isListNode(list)) {
|
|
51
|
+
{
|
|
52
|
+
throw Error(`A ListItemNode must have a ListNode for a parent.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let parent = list;
|
|
56
|
+
while (parent !== null) {
|
|
57
|
+
parent = parent.getParent();
|
|
58
|
+
if ($isListNode(parent)) {
|
|
59
|
+
list = parent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return list;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A recursive Depth-First Search (Postorder Traversal) that finds all of a node's children
|
|
67
|
+
* that are of type ListItemNode and returns them in an array.
|
|
68
|
+
* @param node - The ListNode to start the search.
|
|
69
|
+
* @returns An array containing all nodes of type ListItemNode found.
|
|
70
|
+
*/
|
|
71
|
+
// This should probably be $getAllChildrenOfType
|
|
72
|
+
function $getAllListItems(node) {
|
|
73
|
+
let listItemNodes = [];
|
|
74
|
+
const listChildren = node.getChildren().filter($isListItemNode);
|
|
75
|
+
for (let i = 0; i < listChildren.length; i++) {
|
|
76
|
+
const listItemNode = listChildren[i];
|
|
77
|
+
const firstChild = listItemNode.getFirstChild();
|
|
78
|
+
if ($isListNode(firstChild)) {
|
|
79
|
+
listItemNodes = listItemNodes.concat($getAllListItems(firstChild));
|
|
80
|
+
} else {
|
|
81
|
+
listItemNodes.push(listItemNode);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return listItemNodes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Checks to see if the passed node is a ListItemNode and has a ListNode as a child.
|
|
89
|
+
* @param node - The node to be checked.
|
|
90
|
+
* @returns true if the node is a ListItemNode and has a ListNode child, false otherwise.
|
|
91
|
+
*/
|
|
92
|
+
function isNestedListNode(node) {
|
|
93
|
+
return $isListItemNode(node) && $isListNode(node.getFirstChild());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Takes a deeply nested ListNode or ListItemNode and traverses up the branch to delete the first
|
|
98
|
+
* ancestral ListNode (which could be the root ListNode) or ListItemNode with siblings, essentially
|
|
99
|
+
* bringing the deeply nested node up the branch once. Would remove sublist if it has siblings.
|
|
100
|
+
* Should not break ListItem -> List -> ListItem chain as empty List/ItemNodes should be removed on .remove().
|
|
101
|
+
* @param sublist - The nested ListNode or ListItemNode to be brought up the branch.
|
|
102
|
+
*/
|
|
103
|
+
function $removeHighestEmptyListParent(sublist) {
|
|
104
|
+
// Nodes may be repeatedly indented, to create deeply nested lists that each
|
|
105
|
+
// contain just one bullet.
|
|
106
|
+
// Our goal is to remove these (empty) deeply nested lists. The easiest
|
|
107
|
+
// way to do that is crawl back up the tree until we find a node that has siblings
|
|
108
|
+
// (e.g. is actually part of the list contents) and delete that, or delete
|
|
109
|
+
// the root of the list (if no list nodes have siblings.)
|
|
110
|
+
let emptyListPtr = sublist;
|
|
111
|
+
while (emptyListPtr.getNextSibling() == null && emptyListPtr.getPreviousSibling() == null) {
|
|
112
|
+
const parent = emptyListPtr.getParent();
|
|
113
|
+
if (parent == null || !($isListItemNode(emptyListPtr) || $isListNode(emptyListPtr))) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
emptyListPtr = parent;
|
|
117
|
+
}
|
|
118
|
+
emptyListPtr.remove();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Wraps a node into a ListItemNode.
|
|
123
|
+
* @param node - The node to be wrapped into a ListItemNode
|
|
124
|
+
* @returns The ListItemNode which the passed node is wrapped in.
|
|
125
|
+
*/
|
|
126
|
+
function wrapInListItem(node) {
|
|
127
|
+
const listItemWrapper = $createListItemNode();
|
|
128
|
+
return listItemWrapper.append(node);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
133
|
+
*
|
|
134
|
+
* This source code is licensed under the MIT license found in the
|
|
135
|
+
* LICENSE file in the root directory of this source tree.
|
|
136
|
+
*
|
|
137
|
+
*/
|
|
138
|
+
function $isSelectingEmptyListItem(anchorNode, nodes) {
|
|
139
|
+
return $isListItemNode(anchorNode) && (nodes.length === 0 || nodes.length === 1 && anchorNode.is(nodes[0]) && anchorNode.getChildrenSize() === 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Inserts a new ListNode. If the selection's anchor node is an empty ListItemNode and is a child of
|
|
144
|
+
* the root/shadow root, it will replace the ListItemNode with a ListNode and the old ListItemNode.
|
|
145
|
+
* Otherwise it will replace its parent with a new ListNode and re-insert the ListItemNode and any previous children.
|
|
146
|
+
* If the selection's anchor node is not an empty ListItemNode, it will add a new ListNode or merge an existing ListNode,
|
|
147
|
+
* unless the the node is a leaf node, in which case it will attempt to find a ListNode up the branch and replace it with
|
|
148
|
+
* a new ListNode, or create a new ListNode at the nearest root/shadow root.
|
|
149
|
+
* @param editor - The lexical editor.
|
|
150
|
+
* @param listType - The type of list, "number" | "bullet" | "check".
|
|
151
|
+
*/
|
|
152
|
+
function insertList(editor, listType) {
|
|
153
|
+
editor.update(() => {
|
|
154
|
+
const selection = $getSelection();
|
|
155
|
+
if (selection !== null) {
|
|
156
|
+
const nodes = selection.getNodes();
|
|
157
|
+
if ($isRangeSelection(selection)) {
|
|
158
|
+
const anchorAndFocus = selection.getStartEndPoints();
|
|
159
|
+
if (!(anchorAndFocus !== null)) {
|
|
160
|
+
throw Error(`insertList: anchor should be defined`);
|
|
161
|
+
}
|
|
162
|
+
const [anchor] = anchorAndFocus;
|
|
163
|
+
const anchorNode = anchor.getNode();
|
|
164
|
+
const anchorNodeParent = anchorNode.getParent();
|
|
165
|
+
if ($isSelectingEmptyListItem(anchorNode, nodes)) {
|
|
166
|
+
const list = $createListNode(listType);
|
|
167
|
+
if ($isRootOrShadowRoot(anchorNodeParent)) {
|
|
168
|
+
anchorNode.replace(list);
|
|
169
|
+
const listItem = $createListItemNode();
|
|
170
|
+
if ($isElementNode(anchorNode)) {
|
|
171
|
+
listItem.setFormat(anchorNode.getFormatType());
|
|
172
|
+
listItem.setIndent(anchorNode.getIndent());
|
|
173
|
+
}
|
|
174
|
+
list.append(listItem);
|
|
175
|
+
} else if ($isListItemNode(anchorNode)) {
|
|
176
|
+
const parent = anchorNode.getParentOrThrow();
|
|
177
|
+
append(list, parent.getChildren());
|
|
178
|
+
parent.replace(list);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const handled = new Set();
|
|
184
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
185
|
+
const node = nodes[i];
|
|
186
|
+
if ($isElementNode(node) && node.isEmpty() && !$isListItemNode(node) && !handled.has(node.getKey())) {
|
|
187
|
+
createListOrMerge(node, listType);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if ($isLeafNode(node)) {
|
|
191
|
+
let parent = node.getParent();
|
|
192
|
+
while (parent != null) {
|
|
193
|
+
const parentKey = parent.getKey();
|
|
194
|
+
if ($isListNode(parent)) {
|
|
195
|
+
if (!handled.has(parentKey)) {
|
|
196
|
+
const newListNode = $createListNode(listType);
|
|
197
|
+
append(newListNode, parent.getChildren());
|
|
198
|
+
parent.replace(newListNode);
|
|
199
|
+
handled.add(parentKey);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
} else {
|
|
203
|
+
const nextParent = parent.getParent();
|
|
204
|
+
if ($isRootOrShadowRoot(nextParent) && !handled.has(parentKey)) {
|
|
205
|
+
handled.add(parentKey);
|
|
206
|
+
createListOrMerge(parent, listType);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
parent = nextParent;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function append(node, nodesToAppend) {
|
|
218
|
+
node.splice(node.getChildrenSize(), 0, nodesToAppend);
|
|
219
|
+
}
|
|
220
|
+
function createListOrMerge(node, listType) {
|
|
221
|
+
if ($isListNode(node)) {
|
|
222
|
+
return node;
|
|
223
|
+
}
|
|
224
|
+
const previousSibling = node.getPreviousSibling();
|
|
225
|
+
const nextSibling = node.getNextSibling();
|
|
226
|
+
const listItem = $createListItemNode();
|
|
227
|
+
listItem.setFormat(node.getFormatType());
|
|
228
|
+
listItem.setIndent(node.getIndent());
|
|
229
|
+
append(listItem, node.getChildren());
|
|
230
|
+
if ($isListNode(previousSibling) && listType === previousSibling.getListType()) {
|
|
231
|
+
previousSibling.append(listItem);
|
|
232
|
+
node.remove();
|
|
233
|
+
// if the same type of list is on both sides, merge them.
|
|
234
|
+
|
|
235
|
+
if ($isListNode(nextSibling) && listType === nextSibling.getListType()) {
|
|
236
|
+
append(previousSibling, nextSibling.getChildren());
|
|
237
|
+
nextSibling.remove();
|
|
238
|
+
}
|
|
239
|
+
return previousSibling;
|
|
240
|
+
} else if ($isListNode(nextSibling) && listType === nextSibling.getListType()) {
|
|
241
|
+
nextSibling.getFirstChildOrThrow().insertBefore(listItem);
|
|
242
|
+
node.remove();
|
|
243
|
+
return nextSibling;
|
|
244
|
+
} else {
|
|
245
|
+
const list = $createListNode(listType);
|
|
246
|
+
list.append(listItem);
|
|
247
|
+
node.replace(list);
|
|
248
|
+
return list;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A recursive function that goes through each list and their children, including nested lists,
|
|
254
|
+
* appending list2 children after list1 children and updating ListItemNode values.
|
|
255
|
+
* @param list1 - The first list to be merged.
|
|
256
|
+
* @param list2 - The second list to be merged.
|
|
257
|
+
*/
|
|
258
|
+
function mergeLists(list1, list2) {
|
|
259
|
+
const listItem1 = list1.getLastChild();
|
|
260
|
+
const listItem2 = list2.getFirstChild();
|
|
261
|
+
if (listItem1 && listItem2 && isNestedListNode(listItem1) && isNestedListNode(listItem2)) {
|
|
262
|
+
mergeLists(listItem1.getFirstChild(), listItem2.getFirstChild());
|
|
263
|
+
listItem2.remove();
|
|
264
|
+
}
|
|
265
|
+
const toMerge = list2.getChildren();
|
|
266
|
+
if (toMerge.length > 0) {
|
|
267
|
+
list1.append(...toMerge);
|
|
268
|
+
}
|
|
269
|
+
list2.remove();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Searches for the nearest ancestral ListNode and removes it. If selection is an empty ListItemNode
|
|
274
|
+
* it will remove the whole list, including the ListItemNode. For each ListItemNode in the ListNode,
|
|
275
|
+
* removeList will also generate new ParagraphNodes in the removed ListNode's place. Any child node
|
|
276
|
+
* inside a ListItemNode will be appended to the new ParagraphNodes.
|
|
277
|
+
* @param editor - The lexical editor.
|
|
278
|
+
*/
|
|
279
|
+
function removeList(editor) {
|
|
280
|
+
editor.update(() => {
|
|
281
|
+
const selection = $getSelection();
|
|
282
|
+
if ($isRangeSelection(selection)) {
|
|
283
|
+
const listNodes = new Set();
|
|
284
|
+
const nodes = selection.getNodes();
|
|
285
|
+
const anchorNode = selection.anchor.getNode();
|
|
286
|
+
if ($isSelectingEmptyListItem(anchorNode, nodes)) {
|
|
287
|
+
listNodes.add($getTopListNode(anchorNode));
|
|
288
|
+
} else {
|
|
289
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
290
|
+
const node = nodes[i];
|
|
291
|
+
if ($isLeafNode(node)) {
|
|
292
|
+
const listItemNode = $getNearestNodeOfType(node, ListItemNode);
|
|
293
|
+
if (listItemNode != null) {
|
|
294
|
+
listNodes.add($getTopListNode(listItemNode));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const listNode of listNodes) {
|
|
300
|
+
let insertionPoint = listNode;
|
|
301
|
+
const listItems = $getAllListItems(listNode);
|
|
302
|
+
for (const listItemNode of listItems) {
|
|
303
|
+
const paragraph = $createParagraphNode();
|
|
304
|
+
append(paragraph, listItemNode.getChildren());
|
|
305
|
+
insertionPoint.insertAfter(paragraph);
|
|
306
|
+
insertionPoint = paragraph;
|
|
307
|
+
|
|
308
|
+
// When the anchor and focus fall on the textNode
|
|
309
|
+
// we don't have to change the selection because the textNode will be appended to
|
|
310
|
+
// the newly generated paragraph.
|
|
311
|
+
// When selection is in empty nested list item, selection is actually on the listItemNode.
|
|
312
|
+
// When the corresponding listItemNode is deleted and replaced by the newly generated paragraph
|
|
313
|
+
// we should manually set the selection's focus and anchor to the newly generated paragraph.
|
|
314
|
+
if (listItemNode.__key === selection.anchor.key) {
|
|
315
|
+
selection.anchor.set(paragraph.getKey(), 0, 'element');
|
|
316
|
+
}
|
|
317
|
+
if (listItemNode.__key === selection.focus.key) {
|
|
318
|
+
selection.focus.set(paragraph.getKey(), 0, 'element');
|
|
319
|
+
}
|
|
320
|
+
listItemNode.remove();
|
|
321
|
+
}
|
|
322
|
+
listNode.remove();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Takes the value of a child ListItemNode and makes it the value the ListItemNode
|
|
330
|
+
* should be if it isn't already. Also ensures that checked is undefined if the
|
|
331
|
+
* parent does not have a list type of 'check'.
|
|
332
|
+
* @param list - The list whose children are updated.
|
|
333
|
+
*/
|
|
334
|
+
function updateChildrenListItemValue(list) {
|
|
335
|
+
const isNotChecklist = list.getListType() !== 'check';
|
|
336
|
+
let value = list.getStart();
|
|
337
|
+
for (const child of list.getChildren()) {
|
|
338
|
+
if ($isListItemNode(child)) {
|
|
339
|
+
if (child.getValue() !== value) {
|
|
340
|
+
child.setValue(value);
|
|
341
|
+
}
|
|
342
|
+
if (isNotChecklist && child.getChecked() != null) {
|
|
343
|
+
child.setChecked(undefined);
|
|
344
|
+
}
|
|
345
|
+
if (!$isListNode(child.getFirstChild())) {
|
|
346
|
+
value++;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Adds an empty ListNode/ListItemNode chain at listItemNode, so as to
|
|
354
|
+
* create an indent effect. Won't indent ListItemNodes that have a ListNode as
|
|
355
|
+
* a child, but does merge sibling ListItemNodes if one has a nested ListNode.
|
|
356
|
+
* @param listItemNode - The ListItemNode to be indented.
|
|
357
|
+
*/
|
|
358
|
+
function $handleIndent(listItemNode) {
|
|
359
|
+
// go through each node and decide where to move it.
|
|
360
|
+
const removed = new Set();
|
|
361
|
+
if (isNestedListNode(listItemNode) || removed.has(listItemNode.getKey())) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const parent = listItemNode.getParent();
|
|
365
|
+
|
|
366
|
+
// We can cast both of the below `isNestedListNode` only returns a boolean type instead of a user-defined type guards
|
|
367
|
+
const nextSibling = listItemNode.getNextSibling();
|
|
368
|
+
const previousSibling = listItemNode.getPreviousSibling();
|
|
369
|
+
// if there are nested lists on either side, merge them all together.
|
|
370
|
+
|
|
371
|
+
if (isNestedListNode(nextSibling) && isNestedListNode(previousSibling)) {
|
|
372
|
+
const innerList = previousSibling.getFirstChild();
|
|
373
|
+
if ($isListNode(innerList)) {
|
|
374
|
+
innerList.append(listItemNode);
|
|
375
|
+
const nextInnerList = nextSibling.getFirstChild();
|
|
376
|
+
if ($isListNode(nextInnerList)) {
|
|
377
|
+
const children = nextInnerList.getChildren();
|
|
378
|
+
append(innerList, children);
|
|
379
|
+
nextSibling.remove();
|
|
380
|
+
removed.add(nextSibling.getKey());
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} else if (isNestedListNode(nextSibling)) {
|
|
384
|
+
// if the ListItemNode is next to a nested ListNode, merge them
|
|
385
|
+
const innerList = nextSibling.getFirstChild();
|
|
386
|
+
if ($isListNode(innerList)) {
|
|
387
|
+
const firstChild = innerList.getFirstChild();
|
|
388
|
+
if (firstChild !== null) {
|
|
389
|
+
firstChild.insertBefore(listItemNode);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} else if (isNestedListNode(previousSibling)) {
|
|
393
|
+
const innerList = previousSibling.getFirstChild();
|
|
394
|
+
if ($isListNode(innerList)) {
|
|
395
|
+
innerList.append(listItemNode);
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
// otherwise, we need to create a new nested ListNode
|
|
399
|
+
|
|
400
|
+
if ($isListNode(parent)) {
|
|
401
|
+
const newListItem = $createListItemNode();
|
|
402
|
+
const newList = $createListNode(parent.getListType());
|
|
403
|
+
newListItem.append(newList);
|
|
404
|
+
newList.append(listItemNode);
|
|
405
|
+
if (previousSibling) {
|
|
406
|
+
previousSibling.insertAfter(newListItem);
|
|
407
|
+
} else if (nextSibling) {
|
|
408
|
+
nextSibling.insertBefore(newListItem);
|
|
409
|
+
} else {
|
|
410
|
+
parent.append(newListItem);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Removes an indent by removing an empty ListNode/ListItemNode chain. An indented ListItemNode
|
|
418
|
+
* has a great grandparent node of type ListNode, which is where the ListItemNode will reside
|
|
419
|
+
* within as a child.
|
|
420
|
+
* @param listItemNode - The ListItemNode to remove the indent (outdent).
|
|
421
|
+
*/
|
|
422
|
+
function $handleOutdent(listItemNode) {
|
|
423
|
+
// go through each node and decide where to move it.
|
|
424
|
+
|
|
425
|
+
if (isNestedListNode(listItemNode)) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const parentList = listItemNode.getParent();
|
|
429
|
+
const grandparentListItem = parentList ? parentList.getParent() : undefined;
|
|
430
|
+
const greatGrandparentList = grandparentListItem ? grandparentListItem.getParent() : undefined;
|
|
431
|
+
// If it doesn't have these ancestors, it's not indented.
|
|
432
|
+
|
|
433
|
+
if ($isListNode(greatGrandparentList) && $isListItemNode(grandparentListItem) && $isListNode(parentList)) {
|
|
434
|
+
// if it's the first child in it's parent list, insert it into the
|
|
435
|
+
// great grandparent list before the grandparent
|
|
436
|
+
const firstChild = parentList ? parentList.getFirstChild() : undefined;
|
|
437
|
+
const lastChild = parentList ? parentList.getLastChild() : undefined;
|
|
438
|
+
if (listItemNode.is(firstChild)) {
|
|
439
|
+
grandparentListItem.insertBefore(listItemNode);
|
|
440
|
+
if (parentList.isEmpty()) {
|
|
441
|
+
grandparentListItem.remove();
|
|
442
|
+
}
|
|
443
|
+
// if it's the last child in it's parent list, insert it into the
|
|
444
|
+
// great grandparent list after the grandparent.
|
|
445
|
+
} else if (listItemNode.is(lastChild)) {
|
|
446
|
+
grandparentListItem.insertAfter(listItemNode);
|
|
447
|
+
if (parentList.isEmpty()) {
|
|
448
|
+
grandparentListItem.remove();
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
// otherwise, we need to split the siblings into two new nested lists
|
|
452
|
+
const listType = parentList.getListType();
|
|
453
|
+
const previousSiblingsListItem = $createListItemNode();
|
|
454
|
+
const previousSiblingsList = $createListNode(listType);
|
|
455
|
+
previousSiblingsListItem.append(previousSiblingsList);
|
|
456
|
+
listItemNode.getPreviousSiblings().forEach(sibling => previousSiblingsList.append(sibling));
|
|
457
|
+
const nextSiblingsListItem = $createListItemNode();
|
|
458
|
+
const nextSiblingsList = $createListNode(listType);
|
|
459
|
+
nextSiblingsListItem.append(nextSiblingsList);
|
|
460
|
+
append(nextSiblingsList, listItemNode.getNextSiblings());
|
|
461
|
+
// put the sibling nested lists on either side of the grandparent list item in the great grandparent.
|
|
462
|
+
grandparentListItem.insertBefore(previousSiblingsListItem);
|
|
463
|
+
grandparentListItem.insertAfter(nextSiblingsListItem);
|
|
464
|
+
// replace the grandparent list item (now between the siblings) with the outdented list item.
|
|
465
|
+
grandparentListItem.replace(listItemNode);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Attempts to insert a ParagraphNode at selection and selects the new node. The selection must contain a ListItemNode
|
|
472
|
+
* or a node that does not already contain text. If its grandparent is the root/shadow root, it will get the ListNode
|
|
473
|
+
* (which should be the parent node) and insert the ParagraphNode as a sibling to the ListNode. If the ListNode is
|
|
474
|
+
* nested in a ListItemNode instead, it will add the ParagraphNode after the grandparent ListItemNode.
|
|
475
|
+
* Throws an invariant if the selection is not a child of a ListNode.
|
|
476
|
+
* @returns true if a ParagraphNode was inserted succesfully, false if there is no selection
|
|
477
|
+
* or the selection does not contain a ListItemNode or the node already holds text.
|
|
478
|
+
*/
|
|
479
|
+
function $handleListInsertParagraph() {
|
|
480
|
+
const selection = $getSelection();
|
|
481
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
// Only run this code on empty list items
|
|
485
|
+
const anchor = selection.anchor.getNode();
|
|
486
|
+
if (!$isListItemNode(anchor) || anchor.getChildrenSize() !== 0) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
const topListNode = $getTopListNode(anchor);
|
|
490
|
+
const parent = anchor.getParent();
|
|
491
|
+
if (!$isListNode(parent)) {
|
|
492
|
+
throw Error(`A ListItemNode must have a ListNode for a parent.`);
|
|
493
|
+
}
|
|
494
|
+
const grandparent = parent.getParent();
|
|
495
|
+
let replacementNode;
|
|
496
|
+
if ($isRootOrShadowRoot(grandparent)) {
|
|
497
|
+
replacementNode = $createParagraphNode();
|
|
498
|
+
topListNode.insertAfter(replacementNode);
|
|
499
|
+
} else if ($isListItemNode(grandparent)) {
|
|
500
|
+
replacementNode = $createListItemNode();
|
|
501
|
+
grandparent.insertAfter(replacementNode);
|
|
502
|
+
} else {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
replacementNode.select();
|
|
506
|
+
const nextSiblings = anchor.getNextSiblings();
|
|
507
|
+
if (nextSiblings.length > 0) {
|
|
508
|
+
const newList = $createListNode(parent.getListType());
|
|
509
|
+
if ($isParagraphNode(replacementNode)) {
|
|
510
|
+
replacementNode.insertAfter(newList);
|
|
511
|
+
} else {
|
|
512
|
+
const newListItem = $createListItemNode();
|
|
513
|
+
newListItem.append(newList);
|
|
514
|
+
replacementNode.insertAfter(newListItem);
|
|
515
|
+
}
|
|
516
|
+
nextSiblings.forEach(sibling => {
|
|
517
|
+
sibling.remove();
|
|
518
|
+
newList.append(sibling);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Don't leave hanging nested empty lists
|
|
523
|
+
$removeHighestEmptyListParent(anchor);
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
529
|
+
*
|
|
530
|
+
* This source code is licensed under the MIT license found in the
|
|
531
|
+
* LICENSE file in the root directory of this source tree.
|
|
532
|
+
*
|
|
533
|
+
*/
|
|
534
|
+
|
|
535
|
+
function normalizeClassNames(...classNames) {
|
|
536
|
+
const rval = [];
|
|
537
|
+
for (const className of classNames) {
|
|
538
|
+
if (className && typeof className === 'string') {
|
|
539
|
+
for (const [s] of className.matchAll(/\S+/g)) {
|
|
540
|
+
rval.push(s);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return rval;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
549
|
+
*
|
|
550
|
+
* This source code is licensed under the MIT license found in the
|
|
551
|
+
* LICENSE file in the root directory of this source tree.
|
|
552
|
+
*
|
|
553
|
+
*/
|
|
554
|
+
/** @noInheritDoc */
|
|
555
|
+
class ListItemNode extends ElementNode {
|
|
556
|
+
/** @internal */
|
|
557
|
+
|
|
558
|
+
/** @internal */
|
|
559
|
+
|
|
560
|
+
static getType() {
|
|
561
|
+
return 'listitem';
|
|
562
|
+
}
|
|
563
|
+
static clone(node) {
|
|
564
|
+
return new ListItemNode(node.__value, node.__checked, node.__key);
|
|
565
|
+
}
|
|
566
|
+
constructor(value, checked, key) {
|
|
567
|
+
super(key);
|
|
568
|
+
this.__value = value === undefined ? 1 : value;
|
|
569
|
+
this.__checked = checked;
|
|
570
|
+
}
|
|
571
|
+
createDOM(config) {
|
|
572
|
+
const element = document.createElement('li');
|
|
573
|
+
const parent = this.getParent();
|
|
574
|
+
if ($isListNode(parent) && parent.getListType() === 'check') {
|
|
575
|
+
updateListItemChecked(element, this, null);
|
|
576
|
+
}
|
|
577
|
+
element.value = this.__value;
|
|
578
|
+
$setListItemThemeClassNames(element, config.theme, this);
|
|
579
|
+
return element;
|
|
580
|
+
}
|
|
581
|
+
updateDOM(prevNode, dom, config) {
|
|
582
|
+
const parent = this.getParent();
|
|
583
|
+
if ($isListNode(parent) && parent.getListType() === 'check') {
|
|
584
|
+
updateListItemChecked(dom, this, prevNode);
|
|
585
|
+
}
|
|
586
|
+
// @ts-expect-error - this is always HTMLListItemElement
|
|
587
|
+
dom.value = this.__value;
|
|
588
|
+
$setListItemThemeClassNames(dom, config.theme, this);
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
static transform() {
|
|
592
|
+
return node => {
|
|
593
|
+
if (!$isListItemNode(node)) {
|
|
594
|
+
throw Error(`node is not a ListItemNode`);
|
|
595
|
+
}
|
|
596
|
+
if (node.__checked == null) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const parent = node.getParent();
|
|
600
|
+
if ($isListNode(parent)) {
|
|
601
|
+
if (parent.getListType() !== 'check' && node.getChecked() != null) {
|
|
602
|
+
node.setChecked(undefined);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
static importDOM() {
|
|
608
|
+
return {
|
|
609
|
+
li: node => ({
|
|
610
|
+
conversion: convertListItemElement,
|
|
611
|
+
priority: 0
|
|
612
|
+
})
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
static importJSON(serializedNode) {
|
|
616
|
+
const node = $createListItemNode();
|
|
617
|
+
node.setChecked(serializedNode.checked);
|
|
618
|
+
node.setValue(serializedNode.value);
|
|
619
|
+
node.setFormat(serializedNode.format);
|
|
620
|
+
node.setDirection(serializedNode.direction);
|
|
621
|
+
return node;
|
|
622
|
+
}
|
|
623
|
+
exportDOM(editor) {
|
|
624
|
+
const element = this.createDOM(editor._config);
|
|
625
|
+
element.style.textAlign = this.getFormatType();
|
|
626
|
+
return {
|
|
627
|
+
element
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
exportJSON() {
|
|
631
|
+
return {
|
|
632
|
+
...super.exportJSON(),
|
|
633
|
+
checked: this.getChecked(),
|
|
634
|
+
type: 'listitem',
|
|
635
|
+
value: this.getValue(),
|
|
636
|
+
version: 1
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
append(...nodes) {
|
|
640
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
641
|
+
const node = nodes[i];
|
|
642
|
+
if ($isElementNode(node) && this.canMergeWith(node)) {
|
|
643
|
+
const children = node.getChildren();
|
|
644
|
+
this.append(...children);
|
|
645
|
+
node.remove();
|
|
646
|
+
} else {
|
|
647
|
+
super.append(node);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return this;
|
|
651
|
+
}
|
|
652
|
+
replace(replaceWithNode, includeChildren) {
|
|
653
|
+
if ($isListItemNode(replaceWithNode)) {
|
|
654
|
+
return super.replace(replaceWithNode);
|
|
655
|
+
}
|
|
656
|
+
this.setIndent(0);
|
|
657
|
+
const list = this.getParentOrThrow();
|
|
658
|
+
if (!$isListNode(list)) {
|
|
659
|
+
return replaceWithNode;
|
|
660
|
+
}
|
|
661
|
+
if (list.__first === this.getKey()) {
|
|
662
|
+
list.insertBefore(replaceWithNode);
|
|
663
|
+
} else if (list.__last === this.getKey()) {
|
|
664
|
+
list.insertAfter(replaceWithNode);
|
|
665
|
+
} else {
|
|
666
|
+
// Split the list
|
|
667
|
+
const newList = $createListNode(list.getListType());
|
|
668
|
+
let nextSibling = this.getNextSibling();
|
|
669
|
+
while (nextSibling) {
|
|
670
|
+
const nodeToAppend = nextSibling;
|
|
671
|
+
nextSibling = nextSibling.getNextSibling();
|
|
672
|
+
newList.append(nodeToAppend);
|
|
673
|
+
}
|
|
674
|
+
list.insertAfter(replaceWithNode);
|
|
675
|
+
replaceWithNode.insertAfter(newList);
|
|
676
|
+
}
|
|
677
|
+
if (includeChildren) {
|
|
678
|
+
if (!$isElementNode(replaceWithNode)) {
|
|
679
|
+
throw Error(`includeChildren should only be true for ElementNodes`);
|
|
680
|
+
}
|
|
681
|
+
this.getChildren().forEach(child => {
|
|
682
|
+
replaceWithNode.append(child);
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
this.remove();
|
|
686
|
+
if (list.getChildrenSize() === 0) {
|
|
687
|
+
list.remove();
|
|
688
|
+
}
|
|
689
|
+
return replaceWithNode;
|
|
690
|
+
}
|
|
691
|
+
insertAfter(node, restoreSelection = true) {
|
|
692
|
+
const listNode = this.getParentOrThrow();
|
|
693
|
+
if (!$isListNode(listNode)) {
|
|
694
|
+
{
|
|
695
|
+
throw Error(`insertAfter: list node is not parent of list item node`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if ($isListItemNode(node)) {
|
|
699
|
+
return super.insertAfter(node, restoreSelection);
|
|
700
|
+
}
|
|
701
|
+
const siblings = this.getNextSiblings();
|
|
702
|
+
|
|
703
|
+
// Attempt to merge if the list is of the same type.
|
|
704
|
+
|
|
705
|
+
if ($isListNode(node)) {
|
|
706
|
+
let child = node;
|
|
707
|
+
const children = node.getChildren();
|
|
708
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
709
|
+
child = children[i];
|
|
710
|
+
this.insertAfter(child, restoreSelection);
|
|
711
|
+
}
|
|
712
|
+
return child;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Otherwise, split the list
|
|
716
|
+
// Split the lists and insert the node in between them
|
|
717
|
+
listNode.insertAfter(node, restoreSelection);
|
|
718
|
+
if (siblings.length !== 0) {
|
|
719
|
+
const newListNode = $createListNode(listNode.getListType());
|
|
720
|
+
siblings.forEach(sibling => newListNode.append(sibling));
|
|
721
|
+
node.insertAfter(newListNode, restoreSelection);
|
|
722
|
+
}
|
|
723
|
+
return node;
|
|
724
|
+
}
|
|
725
|
+
remove(preserveEmptyParent) {
|
|
726
|
+
const prevSibling = this.getPreviousSibling();
|
|
727
|
+
const nextSibling = this.getNextSibling();
|
|
728
|
+
super.remove(preserveEmptyParent);
|
|
729
|
+
if (prevSibling && nextSibling && isNestedListNode(prevSibling) && isNestedListNode(nextSibling)) {
|
|
730
|
+
mergeLists(prevSibling.getFirstChild(), nextSibling.getFirstChild());
|
|
731
|
+
nextSibling.remove();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
insertNewAfter(_, restoreSelection = true) {
|
|
735
|
+
const newElement = $createListItemNode(this.__checked == null ? undefined : false);
|
|
736
|
+
this.insertAfter(newElement, restoreSelection);
|
|
737
|
+
return newElement;
|
|
738
|
+
}
|
|
739
|
+
collapseAtStart(selection) {
|
|
740
|
+
const paragraph = $createParagraphNode();
|
|
741
|
+
const children = this.getChildren();
|
|
742
|
+
children.forEach(child => paragraph.append(child));
|
|
743
|
+
const listNode = this.getParentOrThrow();
|
|
744
|
+
const listNodeParent = listNode.getParentOrThrow();
|
|
745
|
+
const isIndented = $isListItemNode(listNodeParent);
|
|
746
|
+
if (listNode.getChildrenSize() === 1) {
|
|
747
|
+
if (isIndented) {
|
|
748
|
+
// if the list node is nested, we just want to remove it,
|
|
749
|
+
// effectively unindenting it.
|
|
750
|
+
listNode.remove();
|
|
751
|
+
listNodeParent.select();
|
|
752
|
+
} else {
|
|
753
|
+
listNode.insertBefore(paragraph);
|
|
754
|
+
listNode.remove();
|
|
755
|
+
// If we have selection on the list item, we'll need to move it
|
|
756
|
+
// to the paragraph
|
|
757
|
+
const anchor = selection.anchor;
|
|
758
|
+
const focus = selection.focus;
|
|
759
|
+
const key = paragraph.getKey();
|
|
760
|
+
if (anchor.type === 'element' && anchor.getNode().is(this)) {
|
|
761
|
+
anchor.set(key, anchor.offset, 'element');
|
|
762
|
+
}
|
|
763
|
+
if (focus.type === 'element' && focus.getNode().is(this)) {
|
|
764
|
+
focus.set(key, focus.offset, 'element');
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
listNode.insertBefore(paragraph);
|
|
769
|
+
this.remove();
|
|
770
|
+
}
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
getValue() {
|
|
774
|
+
const self = this.getLatest();
|
|
775
|
+
return self.__value;
|
|
776
|
+
}
|
|
777
|
+
setValue(value) {
|
|
778
|
+
const self = this.getWritable();
|
|
779
|
+
self.__value = value;
|
|
780
|
+
}
|
|
781
|
+
getChecked() {
|
|
782
|
+
const self = this.getLatest();
|
|
783
|
+
return self.__checked;
|
|
784
|
+
}
|
|
785
|
+
setChecked(checked) {
|
|
786
|
+
const self = this.getWritable();
|
|
787
|
+
self.__checked = checked;
|
|
788
|
+
}
|
|
789
|
+
toggleChecked() {
|
|
790
|
+
this.setChecked(!this.__checked);
|
|
791
|
+
}
|
|
792
|
+
getIndent() {
|
|
793
|
+
// If we don't have a parent, we are likely serializing
|
|
794
|
+
const parent = this.getParent();
|
|
795
|
+
if (parent === null) {
|
|
796
|
+
return this.getLatest().__indent;
|
|
797
|
+
}
|
|
798
|
+
// ListItemNode should always have a ListNode for a parent.
|
|
799
|
+
let listNodeParent = parent.getParentOrThrow();
|
|
800
|
+
let indentLevel = 0;
|
|
801
|
+
while ($isListItemNode(listNodeParent)) {
|
|
802
|
+
listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow();
|
|
803
|
+
indentLevel++;
|
|
804
|
+
}
|
|
805
|
+
return indentLevel;
|
|
806
|
+
}
|
|
807
|
+
setIndent(indent) {
|
|
808
|
+
if (!(typeof indent === 'number' && indent > -1)) {
|
|
809
|
+
throw Error(`Invalid indent value.`);
|
|
810
|
+
}
|
|
811
|
+
let currentIndent = this.getIndent();
|
|
812
|
+
while (currentIndent !== indent) {
|
|
813
|
+
if (currentIndent < indent) {
|
|
814
|
+
$handleIndent(this);
|
|
815
|
+
currentIndent++;
|
|
816
|
+
} else {
|
|
817
|
+
$handleOutdent(this);
|
|
818
|
+
currentIndent--;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return this;
|
|
822
|
+
}
|
|
823
|
+
canInsertAfter(node) {
|
|
824
|
+
return $isListItemNode(node);
|
|
825
|
+
}
|
|
826
|
+
canReplaceWith(replacement) {
|
|
827
|
+
return $isListItemNode(replacement);
|
|
828
|
+
}
|
|
829
|
+
canMergeWith(node) {
|
|
830
|
+
return $isParagraphNode(node) || $isListItemNode(node);
|
|
831
|
+
}
|
|
832
|
+
extractWithChild(child, selection) {
|
|
833
|
+
if (!$isRangeSelection(selection)) {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
const anchorNode = selection.anchor.getNode();
|
|
837
|
+
const focusNode = selection.focus.getNode();
|
|
838
|
+
return this.isParentOf(anchorNode) && this.isParentOf(focusNode) && this.getTextContent().length === selection.getTextContent().length;
|
|
839
|
+
}
|
|
840
|
+
isParentRequired() {
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
createParentElementNode() {
|
|
844
|
+
return $createListNode('bullet');
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function $setListItemThemeClassNames(dom, editorThemeClasses, node) {
|
|
848
|
+
const classesToAdd = [];
|
|
849
|
+
const classesToRemove = [];
|
|
850
|
+
const listTheme = editorThemeClasses.list;
|
|
851
|
+
const listItemClassName = listTheme ? listTheme.listitem : undefined;
|
|
852
|
+
let nestedListItemClassName;
|
|
853
|
+
if (listTheme && listTheme.nested) {
|
|
854
|
+
nestedListItemClassName = listTheme.nested.listitem;
|
|
855
|
+
}
|
|
856
|
+
if (listItemClassName !== undefined) {
|
|
857
|
+
classesToAdd.push(...normalizeClassNames(listItemClassName));
|
|
858
|
+
}
|
|
859
|
+
if (listTheme) {
|
|
860
|
+
const parentNode = node.getParent();
|
|
861
|
+
const isCheckList = $isListNode(parentNode) && parentNode.getListType() === 'check';
|
|
862
|
+
const checked = node.getChecked();
|
|
863
|
+
if (!isCheckList || checked) {
|
|
864
|
+
classesToRemove.push(listTheme.listitemUnchecked);
|
|
865
|
+
}
|
|
866
|
+
if (!isCheckList || !checked) {
|
|
867
|
+
classesToRemove.push(listTheme.listitemChecked);
|
|
868
|
+
}
|
|
869
|
+
if (isCheckList) {
|
|
870
|
+
classesToAdd.push(checked ? listTheme.listitemChecked : listTheme.listitemUnchecked);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (nestedListItemClassName !== undefined) {
|
|
874
|
+
const nestedListItemClasses = normalizeClassNames(nestedListItemClassName);
|
|
875
|
+
if (node.getChildren().some(child => $isListNode(child))) {
|
|
876
|
+
classesToAdd.push(...nestedListItemClasses);
|
|
877
|
+
} else {
|
|
878
|
+
classesToRemove.push(...nestedListItemClasses);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (classesToRemove.length > 0) {
|
|
882
|
+
removeClassNamesFromElement(dom, ...classesToRemove);
|
|
883
|
+
}
|
|
884
|
+
if (classesToAdd.length > 0) {
|
|
885
|
+
addClassNamesToElement(dom, ...classesToAdd);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function updateListItemChecked(dom, listItemNode, prevListItemNode, listNode) {
|
|
889
|
+
// Only add attributes for leaf list items
|
|
890
|
+
if ($isListNode(listItemNode.getFirstChild())) {
|
|
891
|
+
dom.removeAttribute('role');
|
|
892
|
+
dom.removeAttribute('tabIndex');
|
|
893
|
+
dom.removeAttribute('aria-checked');
|
|
894
|
+
} else {
|
|
895
|
+
dom.setAttribute('role', 'checkbox');
|
|
896
|
+
dom.setAttribute('tabIndex', '-1');
|
|
897
|
+
if (!prevListItemNode || listItemNode.__checked !== prevListItemNode.__checked) {
|
|
898
|
+
dom.setAttribute('aria-checked', listItemNode.getChecked() ? 'true' : 'false');
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function convertListItemElement(domNode) {
|
|
903
|
+
const checked = isHTMLElement(domNode) && domNode.getAttribute('aria-checked') === 'true';
|
|
904
|
+
return {
|
|
905
|
+
node: $createListItemNode(checked)
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Creates a new List Item node, passing true/false will convert it to a checkbox input.
|
|
911
|
+
* @param checked - Is the List Item a checkbox and, if so, is it checked? undefined/null: not a checkbox, true/false is a checkbox and checked/unchecked, respectively.
|
|
912
|
+
* @returns The new List Item.
|
|
913
|
+
*/
|
|
914
|
+
function $createListItemNode(checked) {
|
|
915
|
+
return $applyNodeReplacement(new ListItemNode(undefined, checked));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Checks to see if the node is a ListItemNode.
|
|
920
|
+
* @param node - The node to be checked.
|
|
921
|
+
* @returns true if the node is a ListItemNode, false otherwise.
|
|
922
|
+
*/
|
|
923
|
+
function $isListItemNode(node) {
|
|
924
|
+
return node instanceof ListItemNode;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
929
|
+
*
|
|
930
|
+
* This source code is licensed under the MIT license found in the
|
|
931
|
+
* LICENSE file in the root directory of this source tree.
|
|
932
|
+
*
|
|
933
|
+
*/
|
|
934
|
+
/** @noInheritDoc */
|
|
935
|
+
class ListNode extends ElementNode {
|
|
936
|
+
/** @internal */
|
|
937
|
+
|
|
938
|
+
/** @internal */
|
|
939
|
+
|
|
940
|
+
/** @internal */
|
|
941
|
+
|
|
942
|
+
static getType() {
|
|
943
|
+
return 'list';
|
|
944
|
+
}
|
|
945
|
+
static clone(node) {
|
|
946
|
+
const listType = node.__listType || TAG_TO_LIST_TYPE[node.__tag];
|
|
947
|
+
return new ListNode(listType, node.__start, node.__key);
|
|
948
|
+
}
|
|
949
|
+
constructor(listType, start, key) {
|
|
950
|
+
super(key);
|
|
951
|
+
const _listType = TAG_TO_LIST_TYPE[listType] || listType;
|
|
952
|
+
this.__listType = _listType;
|
|
953
|
+
this.__tag = _listType === 'number' ? 'ol' : 'ul';
|
|
954
|
+
this.__start = start;
|
|
955
|
+
}
|
|
956
|
+
getTag() {
|
|
957
|
+
return this.__tag;
|
|
958
|
+
}
|
|
959
|
+
setListType(type) {
|
|
960
|
+
const writable = this.getWritable();
|
|
961
|
+
writable.__listType = type;
|
|
962
|
+
writable.__tag = type === 'number' ? 'ol' : 'ul';
|
|
963
|
+
}
|
|
964
|
+
getListType() {
|
|
965
|
+
return this.__listType;
|
|
966
|
+
}
|
|
967
|
+
getStart() {
|
|
968
|
+
return this.__start;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// View
|
|
972
|
+
|
|
973
|
+
createDOM(config, _editor) {
|
|
974
|
+
const tag = this.__tag;
|
|
975
|
+
const dom = document.createElement(tag);
|
|
976
|
+
if (this.__start !== 1) {
|
|
977
|
+
dom.setAttribute('start', String(this.__start));
|
|
978
|
+
}
|
|
979
|
+
// @ts-expect-error Internal field.
|
|
980
|
+
dom.__lexicalListType = this.__listType;
|
|
981
|
+
setListThemeClassNames(dom, config.theme, this);
|
|
982
|
+
return dom;
|
|
983
|
+
}
|
|
984
|
+
updateDOM(prevNode, dom, config) {
|
|
985
|
+
if (prevNode.__tag !== this.__tag) {
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
setListThemeClassNames(dom, config.theme, this);
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
static transform() {
|
|
992
|
+
return node => {
|
|
993
|
+
if (!$isListNode(node)) {
|
|
994
|
+
throw Error(`node is not a ListNode`);
|
|
995
|
+
}
|
|
996
|
+
updateChildrenListItemValue(node);
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
static importDOM() {
|
|
1000
|
+
return {
|
|
1001
|
+
ol: node => ({
|
|
1002
|
+
conversion: convertListNode,
|
|
1003
|
+
priority: 0
|
|
1004
|
+
}),
|
|
1005
|
+
ul: node => ({
|
|
1006
|
+
conversion: convertListNode,
|
|
1007
|
+
priority: 0
|
|
1008
|
+
})
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
static importJSON(serializedNode) {
|
|
1012
|
+
const node = $createListNode(serializedNode.listType, serializedNode.start);
|
|
1013
|
+
node.setFormat(serializedNode.format);
|
|
1014
|
+
node.setIndent(serializedNode.indent);
|
|
1015
|
+
node.setDirection(serializedNode.direction);
|
|
1016
|
+
return node;
|
|
1017
|
+
}
|
|
1018
|
+
exportDOM(editor) {
|
|
1019
|
+
const {
|
|
1020
|
+
element
|
|
1021
|
+
} = super.exportDOM(editor);
|
|
1022
|
+
if (element && isHTMLElement(element)) {
|
|
1023
|
+
if (this.__start !== 1) {
|
|
1024
|
+
element.setAttribute('start', String(this.__start));
|
|
1025
|
+
}
|
|
1026
|
+
if (this.__listType === 'check') {
|
|
1027
|
+
element.setAttribute('__lexicalListType', 'check');
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
element
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
exportJSON() {
|
|
1035
|
+
return {
|
|
1036
|
+
...super.exportJSON(),
|
|
1037
|
+
listType: this.getListType(),
|
|
1038
|
+
start: this.getStart(),
|
|
1039
|
+
tag: this.getTag(),
|
|
1040
|
+
type: 'list',
|
|
1041
|
+
version: 1
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
canBeEmpty() {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
canIndent() {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
append(...nodesToAppend) {
|
|
1051
|
+
for (let i = 0; i < nodesToAppend.length; i++) {
|
|
1052
|
+
const currentNode = nodesToAppend[i];
|
|
1053
|
+
if ($isListItemNode(currentNode)) {
|
|
1054
|
+
super.append(currentNode);
|
|
1055
|
+
} else {
|
|
1056
|
+
const listItemNode = $createListItemNode();
|
|
1057
|
+
if ($isListNode(currentNode)) {
|
|
1058
|
+
listItemNode.append(currentNode);
|
|
1059
|
+
} else if ($isElementNode(currentNode)) {
|
|
1060
|
+
const textNode = $createTextNode(currentNode.getTextContent());
|
|
1061
|
+
listItemNode.append(textNode);
|
|
1062
|
+
} else {
|
|
1063
|
+
listItemNode.append(currentNode);
|
|
1064
|
+
}
|
|
1065
|
+
super.append(listItemNode);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return this;
|
|
1069
|
+
}
|
|
1070
|
+
extractWithChild(child) {
|
|
1071
|
+
return $isListItemNode(child);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function setListThemeClassNames(dom, editorThemeClasses, node) {
|
|
1075
|
+
const classesToAdd = [];
|
|
1076
|
+
const classesToRemove = [];
|
|
1077
|
+
const listTheme = editorThemeClasses.list;
|
|
1078
|
+
if (listTheme !== undefined) {
|
|
1079
|
+
const listLevelsClassNames = listTheme[`${node.__tag}Depth`] || [];
|
|
1080
|
+
const listDepth = $getListDepth(node) - 1;
|
|
1081
|
+
const normalizedListDepth = listDepth % listLevelsClassNames.length;
|
|
1082
|
+
const listLevelClassName = listLevelsClassNames[normalizedListDepth];
|
|
1083
|
+
const listClassName = listTheme[node.__tag];
|
|
1084
|
+
let nestedListClassName;
|
|
1085
|
+
const nestedListTheme = listTheme.nested;
|
|
1086
|
+
const checklistClassName = listTheme.checklist;
|
|
1087
|
+
if (nestedListTheme !== undefined && nestedListTheme.list) {
|
|
1088
|
+
nestedListClassName = nestedListTheme.list;
|
|
1089
|
+
}
|
|
1090
|
+
if (listClassName !== undefined) {
|
|
1091
|
+
classesToAdd.push(listClassName);
|
|
1092
|
+
}
|
|
1093
|
+
if (checklistClassName !== undefined && node.__listType === 'check') {
|
|
1094
|
+
classesToAdd.push(checklistClassName);
|
|
1095
|
+
}
|
|
1096
|
+
if (listLevelClassName !== undefined) {
|
|
1097
|
+
classesToAdd.push(...normalizeClassNames(listLevelClassName));
|
|
1098
|
+
for (let i = 0; i < listLevelsClassNames.length; i++) {
|
|
1099
|
+
if (i !== normalizedListDepth) {
|
|
1100
|
+
classesToRemove.push(node.__tag + i);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (nestedListClassName !== undefined) {
|
|
1105
|
+
const nestedListItemClasses = normalizeClassNames(nestedListClassName);
|
|
1106
|
+
if (listDepth > 1) {
|
|
1107
|
+
classesToAdd.push(...nestedListItemClasses);
|
|
1108
|
+
} else {
|
|
1109
|
+
classesToRemove.push(...nestedListItemClasses);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (classesToRemove.length > 0) {
|
|
1114
|
+
removeClassNamesFromElement(dom, ...classesToRemove);
|
|
1115
|
+
}
|
|
1116
|
+
if (classesToAdd.length > 0) {
|
|
1117
|
+
addClassNamesToElement(dom, ...classesToAdd);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/*
|
|
1122
|
+
* This function normalizes the children of a ListNode after the conversion from HTML,
|
|
1123
|
+
* ensuring that they are all ListItemNodes and contain either a single nested ListNode
|
|
1124
|
+
* or some other inline content.
|
|
1125
|
+
*/
|
|
1126
|
+
function normalizeChildren(nodes) {
|
|
1127
|
+
const normalizedListItems = [];
|
|
1128
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1129
|
+
const node = nodes[i];
|
|
1130
|
+
if ($isListItemNode(node)) {
|
|
1131
|
+
normalizedListItems.push(node);
|
|
1132
|
+
const children = node.getChildren();
|
|
1133
|
+
if (children.length > 1) {
|
|
1134
|
+
children.forEach(child => {
|
|
1135
|
+
if ($isListNode(child)) {
|
|
1136
|
+
normalizedListItems.push(wrapInListItem(child));
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
} else {
|
|
1141
|
+
normalizedListItems.push(wrapInListItem(node));
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return normalizedListItems;
|
|
1145
|
+
}
|
|
1146
|
+
function convertListNode(domNode) {
|
|
1147
|
+
const nodeName = domNode.nodeName.toLowerCase();
|
|
1148
|
+
let node = null;
|
|
1149
|
+
if (nodeName === 'ol') {
|
|
1150
|
+
// @ts-ignore
|
|
1151
|
+
const start = domNode.start;
|
|
1152
|
+
node = $createListNode('number', start);
|
|
1153
|
+
} else if (nodeName === 'ul') {
|
|
1154
|
+
if (isHTMLElement(domNode) && domNode.getAttribute('__lexicallisttype') === 'check') {
|
|
1155
|
+
node = $createListNode('check');
|
|
1156
|
+
} else {
|
|
1157
|
+
node = $createListNode('bullet');
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return {
|
|
1161
|
+
after: normalizeChildren,
|
|
1162
|
+
node
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
const TAG_TO_LIST_TYPE = {
|
|
1166
|
+
ol: 'number',
|
|
1167
|
+
ul: 'bullet'
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Creates a ListNode of listType.
|
|
1172
|
+
* @param listType - The type of list to be created. Can be 'number', 'bullet', or 'check'.
|
|
1173
|
+
* @param start - Where an ordered list starts its count, start = 1 if left undefined.
|
|
1174
|
+
* @returns The new ListNode
|
|
1175
|
+
*/
|
|
1176
|
+
function $createListNode(listType, start = 1) {
|
|
1177
|
+
return $applyNodeReplacement(new ListNode(listType, start));
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Checks to see if the node is a ListNode.
|
|
1182
|
+
* @param node - The node to be checked.
|
|
1183
|
+
* @returns true if the node is a ListNode, false otherwise.
|
|
1184
|
+
*/
|
|
1185
|
+
function $isListNode(node) {
|
|
1186
|
+
return node instanceof ListNode;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/** @module @lexical/list */
|
|
1190
|
+
const INSERT_UNORDERED_LIST_COMMAND = createCommand('INSERT_UNORDERED_LIST_COMMAND');
|
|
1191
|
+
const INSERT_ORDERED_LIST_COMMAND = createCommand('INSERT_ORDERED_LIST_COMMAND');
|
|
1192
|
+
const INSERT_CHECK_LIST_COMMAND = createCommand('INSERT_CHECK_LIST_COMMAND');
|
|
1193
|
+
const REMOVE_LIST_COMMAND = createCommand('REMOVE_LIST_COMMAND');
|
|
1194
|
+
|
|
1195
|
+
export { $createListItemNode, $createListNode, $getListDepth, $handleListInsertParagraph, $isListItemNode, $isListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND, insertList, removeList };
|