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