@lexical/list 0.44.1-nightly.20260519.0 → 0.45.1-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{LexicalList.dev.js → dist/LexicalList.dev.js} +286 -31
- package/{LexicalList.dev.mjs → dist/LexicalList.dev.mjs} +285 -33
- package/{LexicalList.mjs → dist/LexicalList.mjs} +3 -0
- package/{LexicalList.node.mjs → dist/LexicalList.node.mjs} +3 -0
- package/dist/LexicalList.prod.js +9 -0
- package/dist/LexicalList.prod.mjs +9 -0
- package/dist/LexicalListExtension.d.ts +29 -0
- package/dist/ListImportExtension.d.ts +34 -0
- package/{index.d.ts → dist/index.d.ts} +4 -37
- package/dist/registerList.d.ts +20 -0
- package/package.json +33 -17
- package/src/LexicalListExtension.ts +76 -0
- package/src/LexicalListItemNode.ts +724 -0
- package/src/LexicalListNode.ts +393 -0
- package/src/ListImportExtension.ts +319 -0
- package/src/checkList.ts +469 -0
- package/src/formatList.ts +582 -0
- package/src/index.ts +98 -0
- package/src/registerList.ts +231 -0
- package/src/utils.ts +196 -0
- package/LexicalList.prod.js +0 -9
- package/LexicalList.prod.mjs +0 -9
- /package/{LexicalList.js → dist/LexicalList.js} +0 -0
- /package/{LexicalList.js.flow → dist/LexicalList.js.flow} +0 -0
- /package/{LexicalListItemNode.d.ts → dist/LexicalListItemNode.d.ts} +0 -0
- /package/{LexicalListNode.d.ts → dist/LexicalListNode.d.ts} +0 -0
- /package/{checkList.d.ts → dist/checkList.d.ts} +0 -0
- /package/{formatList.d.ts → dist/formatList.d.ts} +0 -0
- /package/{utils.d.ts → dist/utils.d.ts} +0 -0
|
@@ -0,0 +1,724 @@
|
|
|
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
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {ListNode, ListType} from './';
|
|
10
|
+
import type {
|
|
11
|
+
BaseSelection,
|
|
12
|
+
DOMConversionOutput,
|
|
13
|
+
DOMExportOutput,
|
|
14
|
+
EditorConfig,
|
|
15
|
+
EditorThemeClasses,
|
|
16
|
+
LexicalNode,
|
|
17
|
+
LexicalUpdateJSON,
|
|
18
|
+
NodeKey,
|
|
19
|
+
ParagraphNode,
|
|
20
|
+
RangeSelection,
|
|
21
|
+
SerializedElementNode,
|
|
22
|
+
Spread,
|
|
23
|
+
} from 'lexical';
|
|
24
|
+
|
|
25
|
+
import invariant from '@lexical/internal/invariant';
|
|
26
|
+
import {
|
|
27
|
+
$insertNodeToNearestRootAtCaret,
|
|
28
|
+
addClassNamesToElement,
|
|
29
|
+
removeClassNamesFromElement,
|
|
30
|
+
} from '@lexical/utils';
|
|
31
|
+
import {
|
|
32
|
+
$applyNodeReplacement,
|
|
33
|
+
$copyNode,
|
|
34
|
+
$createParagraphNode,
|
|
35
|
+
$getSelection,
|
|
36
|
+
$getSiblingCaret,
|
|
37
|
+
$isElementNode,
|
|
38
|
+
$isParagraphNode,
|
|
39
|
+
$isRangeSelection,
|
|
40
|
+
$isRootOrShadowRoot,
|
|
41
|
+
$rewindSiblingCaret,
|
|
42
|
+
$setDirectionFromDOM,
|
|
43
|
+
$setFormatFromDOM,
|
|
44
|
+
buildImportMap,
|
|
45
|
+
ElementNode,
|
|
46
|
+
getStyleObjectFromCSS,
|
|
47
|
+
isHTMLElement,
|
|
48
|
+
LexicalEditor,
|
|
49
|
+
normalizeClassNames,
|
|
50
|
+
setDOMStyleFromCSS,
|
|
51
|
+
} from 'lexical';
|
|
52
|
+
|
|
53
|
+
import {$createListNode, $isListNode} from './';
|
|
54
|
+
import {$handleIndent, $handleOutdent, mergeLists} from './formatList';
|
|
55
|
+
import {isNestedListNode} from './utils';
|
|
56
|
+
|
|
57
|
+
export type SerializedListItemNode = Spread<
|
|
58
|
+
{
|
|
59
|
+
checked: boolean | undefined;
|
|
60
|
+
value: number;
|
|
61
|
+
},
|
|
62
|
+
SerializedElementNode
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
function applyMarkerStyles(
|
|
66
|
+
dom: HTMLElement,
|
|
67
|
+
node: ListItemNode,
|
|
68
|
+
prevNode: ListItemNode | null,
|
|
69
|
+
): void {
|
|
70
|
+
const nextTextStyle = node.__textStyle;
|
|
71
|
+
const prevTextStyle = prevNode ? prevNode.__textStyle : '';
|
|
72
|
+
|
|
73
|
+
if (prevNode !== null && prevTextStyle === nextTextStyle) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const styles: Record<string, string> = getStyleObjectFromCSS(nextTextStyle);
|
|
78
|
+
for (const k in styles) {
|
|
79
|
+
dom.style.setProperty(`--listitem-marker-${k}`, styles[k]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (prevTextStyle !== '') {
|
|
83
|
+
for (const k in getStyleObjectFromCSS(prevTextStyle)) {
|
|
84
|
+
if (!(k in styles)) {
|
|
85
|
+
dom.style.removeProperty(`--listitem-marker-${k}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @noInheritDoc */
|
|
92
|
+
export class ListItemNode extends ElementNode {
|
|
93
|
+
/** @internal */
|
|
94
|
+
__value: number;
|
|
95
|
+
/** @internal */
|
|
96
|
+
__checked?: boolean;
|
|
97
|
+
|
|
98
|
+
/** @internal */
|
|
99
|
+
$config() {
|
|
100
|
+
return this.config('listitem', {
|
|
101
|
+
$transform: (node: ListItemNode): void => {
|
|
102
|
+
const parent = node.getParent();
|
|
103
|
+
if ($isListNode(parent)) {
|
|
104
|
+
if (parent.getListType() !== 'check' && node.getChecked() != null) {
|
|
105
|
+
node.setChecked(undefined);
|
|
106
|
+
}
|
|
107
|
+
} else if (parent) {
|
|
108
|
+
const newParent = node.createParentElementNode();
|
|
109
|
+
invariant(
|
|
110
|
+
$isListNode(newParent),
|
|
111
|
+
'ListItemNode.createParentElementNode() must return a ListNode',
|
|
112
|
+
);
|
|
113
|
+
// Insert an empty ListNode at the orphan's position, splitting
|
|
114
|
+
// any enclosing non-shadow-root blocks so the ListNode lifts to
|
|
115
|
+
// a valid container before we move the orphan in. The ListNode
|
|
116
|
+
// $transform merges adjacent same-type lists, so neighbouring
|
|
117
|
+
// orphans will coalesce once their own transforms run.
|
|
118
|
+
const children = [node];
|
|
119
|
+
for (const dir of ['previous', 'next'] as const) {
|
|
120
|
+
children.reverse();
|
|
121
|
+
for (const {origin} of $getSiblingCaret(node, dir)) {
|
|
122
|
+
if (!$isListItemNode(origin)) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
children.push(origin);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
node.insertBefore(newParent);
|
|
129
|
+
newParent.splice(0, 0, children);
|
|
130
|
+
if (!$isRootOrShadowRoot(parent)) {
|
|
131
|
+
$insertNodeToNearestRootAtCaret(
|
|
132
|
+
newParent,
|
|
133
|
+
$rewindSiblingCaret($getSiblingCaret(newParent, 'next')),
|
|
134
|
+
{$shouldSplit: () => false, removeEmptyDestination: true},
|
|
135
|
+
);
|
|
136
|
+
if (parent.isEmpty() && parent.isAttached()) {
|
|
137
|
+
parent.remove();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
extends: ElementNode,
|
|
143
|
+
importDOM: buildImportMap({
|
|
144
|
+
li: () => ({
|
|
145
|
+
conversion: $convertListItemElement,
|
|
146
|
+
priority: 0,
|
|
147
|
+
}),
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
constructor(
|
|
153
|
+
value: number = 1,
|
|
154
|
+
checked: undefined | boolean = undefined,
|
|
155
|
+
key?: NodeKey,
|
|
156
|
+
) {
|
|
157
|
+
super(key);
|
|
158
|
+
this.__value = value === undefined ? 1 : value;
|
|
159
|
+
this.__checked = checked;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
afterCloneFrom(prevNode: this): void {
|
|
163
|
+
super.afterCloneFrom(prevNode);
|
|
164
|
+
this.__value = prevNode.__value;
|
|
165
|
+
this.__checked = prevNode.__checked;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
createDOM(config: EditorConfig): HTMLElement {
|
|
169
|
+
const element = document.createElement('li');
|
|
170
|
+
this.updateListItemDOM(null, element, config);
|
|
171
|
+
|
|
172
|
+
return element;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
updateListItemDOM(
|
|
176
|
+
prevNode: ListItemNode | null,
|
|
177
|
+
dom: HTMLLIElement,
|
|
178
|
+
config: EditorConfig,
|
|
179
|
+
) {
|
|
180
|
+
updateListItemChecked(dom, this, prevNode);
|
|
181
|
+
|
|
182
|
+
dom.value = this.__value;
|
|
183
|
+
$setListItemThemeClassNames(dom, config.theme, this);
|
|
184
|
+
const prevStyle = prevNode ? prevNode.__style : '';
|
|
185
|
+
const nextStyle = this.__style;
|
|
186
|
+
|
|
187
|
+
if (prevStyle !== nextStyle) {
|
|
188
|
+
setDOMStyleFromCSS(dom.style, nextStyle, prevStyle);
|
|
189
|
+
}
|
|
190
|
+
applyMarkerStyles(dom, this, prevNode);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
updateDOM(
|
|
194
|
+
prevNode: ListItemNode,
|
|
195
|
+
dom: HTMLElement,
|
|
196
|
+
config: EditorConfig,
|
|
197
|
+
): boolean {
|
|
198
|
+
// @ts-expect-error - this is always HTMLListItemElement
|
|
199
|
+
const element: HTMLLIElement = dom;
|
|
200
|
+
this.updateListItemDOM(prevNode, element, config);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
updateFromJSON(
|
|
205
|
+
serializedNode: LexicalUpdateJSON<SerializedListItemNode>,
|
|
206
|
+
): this {
|
|
207
|
+
return super
|
|
208
|
+
.updateFromJSON(serializedNode)
|
|
209
|
+
.setValue(serializedNode.value)
|
|
210
|
+
.setChecked(serializedNode.checked);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
|
214
|
+
const element = this.createDOM(editor._config);
|
|
215
|
+
|
|
216
|
+
const formatType = this.getFormatType();
|
|
217
|
+
if (formatType) {
|
|
218
|
+
element.style.textAlign = formatType;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const direction = this.getDirection();
|
|
222
|
+
if (direction) {
|
|
223
|
+
element.dir = direction;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (isNestedListNode(this)) {
|
|
227
|
+
return {
|
|
228
|
+
after(containerElement) {
|
|
229
|
+
if (isHTMLElement(containerElement)) {
|
|
230
|
+
const prevSibling = containerElement.previousElementSibling;
|
|
231
|
+
if (isHTMLElement(prevSibling) && prevSibling.nodeName === 'LI') {
|
|
232
|
+
while (containerElement.firstChild) {
|
|
233
|
+
prevSibling.append(containerElement.firstChild);
|
|
234
|
+
}
|
|
235
|
+
containerElement.remove();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return containerElement;
|
|
239
|
+
},
|
|
240
|
+
element,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
element,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
exportJSON(): SerializedListItemNode {
|
|
250
|
+
return {
|
|
251
|
+
...super.exportJSON(),
|
|
252
|
+
checked: this.getChecked(),
|
|
253
|
+
value: this.getValue(),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
append(...nodes: LexicalNode[]): this {
|
|
258
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
259
|
+
const node = nodes[i];
|
|
260
|
+
|
|
261
|
+
if ($isElementNode(node) && this.canMergeWith(node)) {
|
|
262
|
+
const children = node.getChildren();
|
|
263
|
+
this.append(...children);
|
|
264
|
+
node.remove();
|
|
265
|
+
} else {
|
|
266
|
+
super.append(node);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
replace<N extends LexicalNode>(
|
|
274
|
+
replaceWithNode: N,
|
|
275
|
+
includeChildren?: boolean,
|
|
276
|
+
): N {
|
|
277
|
+
if ($isListItemNode(replaceWithNode)) {
|
|
278
|
+
return super.replace(replaceWithNode);
|
|
279
|
+
}
|
|
280
|
+
this.setIndent(0);
|
|
281
|
+
const list = this.getParentOrThrow();
|
|
282
|
+
if (!$isListNode(list)) {
|
|
283
|
+
return replaceWithNode;
|
|
284
|
+
}
|
|
285
|
+
if (list.__first === this.getKey()) {
|
|
286
|
+
list.insertBefore(replaceWithNode);
|
|
287
|
+
} else if (list.__last === this.getKey()) {
|
|
288
|
+
list.insertAfter(replaceWithNode);
|
|
289
|
+
} else {
|
|
290
|
+
// Split the list
|
|
291
|
+
const newList = $copyNode(list);
|
|
292
|
+
let nextSibling = this.getNextSibling();
|
|
293
|
+
while (nextSibling) {
|
|
294
|
+
const nodeToAppend = nextSibling;
|
|
295
|
+
nextSibling = nextSibling.getNextSibling();
|
|
296
|
+
newList.append(nodeToAppend);
|
|
297
|
+
}
|
|
298
|
+
list.insertAfter(replaceWithNode);
|
|
299
|
+
replaceWithNode.insertAfter(newList);
|
|
300
|
+
}
|
|
301
|
+
const toReplaceKey = this.__key;
|
|
302
|
+
let prevSizeBeforeChildrenTransfer = 0;
|
|
303
|
+
if (includeChildren) {
|
|
304
|
+
invariant(
|
|
305
|
+
$isElementNode(replaceWithNode),
|
|
306
|
+
'includeChildren should only be true for ElementNodes',
|
|
307
|
+
);
|
|
308
|
+
prevSizeBeforeChildrenTransfer = replaceWithNode.getChildrenSize();
|
|
309
|
+
replaceWithNode.splice(
|
|
310
|
+
prevSizeBeforeChildrenTransfer,
|
|
311
|
+
0,
|
|
312
|
+
this.getChildren(),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
// The base LexicalNode.replace remaps element-anchored selection points
|
|
316
|
+
// from the replaced node to the replacement, but this override skips
|
|
317
|
+
// super and the trailing this.remove() would otherwise drop selection
|
|
318
|
+
// onto a sibling list item via moveSelectionPointToSibling. Mirror the
|
|
319
|
+
// base behavior here for the element-anchored case.
|
|
320
|
+
if (includeChildren && $isElementNode(replaceWithNode)) {
|
|
321
|
+
const selection = $getSelection();
|
|
322
|
+
if ($isRangeSelection(selection)) {
|
|
323
|
+
for (const point of selection.getStartEndPoints()) {
|
|
324
|
+
if (point.key === toReplaceKey && point.type === 'element') {
|
|
325
|
+
point.set(
|
|
326
|
+
replaceWithNode.getKey(),
|
|
327
|
+
prevSizeBeforeChildrenTransfer + point.offset,
|
|
328
|
+
'element',
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
this.remove();
|
|
335
|
+
if (list.getChildrenSize() === 0) {
|
|
336
|
+
list.remove();
|
|
337
|
+
}
|
|
338
|
+
return replaceWithNode;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
insertAfter(node: LexicalNode, restoreSelection = true): LexicalNode {
|
|
342
|
+
const listNode = this.getParentOrThrow();
|
|
343
|
+
|
|
344
|
+
if (!$isListNode(listNode)) {
|
|
345
|
+
invariant(
|
|
346
|
+
false,
|
|
347
|
+
'insertAfter: list node is not parent of list item node',
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if ($isListItemNode(node)) {
|
|
352
|
+
return super.insertAfter(node, restoreSelection);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const siblings = this.getNextSiblings();
|
|
356
|
+
|
|
357
|
+
// Split the lists and insert the node in between them
|
|
358
|
+
listNode.insertAfter(node, restoreSelection);
|
|
359
|
+
|
|
360
|
+
if (siblings.length !== 0) {
|
|
361
|
+
const newListNode = $copyNode(listNode);
|
|
362
|
+
|
|
363
|
+
siblings.forEach(sibling => newListNode.append(sibling));
|
|
364
|
+
|
|
365
|
+
node.insertAfter(newListNode, restoreSelection);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return node;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
remove(preserveEmptyParent?: boolean): void {
|
|
372
|
+
const prevSibling = this.getPreviousSibling();
|
|
373
|
+
const nextSibling = this.getNextSibling();
|
|
374
|
+
super.remove(preserveEmptyParent);
|
|
375
|
+
|
|
376
|
+
if (
|
|
377
|
+
prevSibling &&
|
|
378
|
+
nextSibling &&
|
|
379
|
+
isNestedListNode(prevSibling) &&
|
|
380
|
+
isNestedListNode(nextSibling)
|
|
381
|
+
) {
|
|
382
|
+
mergeLists(prevSibling.getFirstChild(), nextSibling.getFirstChild());
|
|
383
|
+
nextSibling.remove();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
resetOnCopyNodeFrom(original: this): void {
|
|
388
|
+
super.resetOnCopyNodeFrom(original);
|
|
389
|
+
if (original.getChecked()) {
|
|
390
|
+
this.setChecked(false);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
insertNewAfter(
|
|
395
|
+
_: RangeSelection,
|
|
396
|
+
restoreSelection = true,
|
|
397
|
+
): ListItemNode | ParagraphNode {
|
|
398
|
+
const newElement = $copyNode(this);
|
|
399
|
+
|
|
400
|
+
this.insertAfter(newElement, restoreSelection);
|
|
401
|
+
|
|
402
|
+
return newElement;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
collapseAtStart(selection: RangeSelection): true {
|
|
406
|
+
const paragraph = $createParagraphNode();
|
|
407
|
+
const children = this.getChildren();
|
|
408
|
+
children.forEach(child => paragraph.append(child));
|
|
409
|
+
const listNode = this.getParentOrThrow();
|
|
410
|
+
const listNodeParent = listNode.getParentOrThrow();
|
|
411
|
+
const isIndented = $isListItemNode(listNodeParent);
|
|
412
|
+
|
|
413
|
+
if (listNode.getChildrenSize() === 1) {
|
|
414
|
+
if (isIndented) {
|
|
415
|
+
// if the list node is nested, we just want to remove it,
|
|
416
|
+
// effectively unindenting it.
|
|
417
|
+
listNode.remove();
|
|
418
|
+
listNodeParent.select();
|
|
419
|
+
} else {
|
|
420
|
+
listNode.insertBefore(paragraph);
|
|
421
|
+
listNode.remove();
|
|
422
|
+
// If we have selection on the list item, we'll need to move it
|
|
423
|
+
// to the paragraph
|
|
424
|
+
const anchor = selection.anchor;
|
|
425
|
+
const focus = selection.focus;
|
|
426
|
+
const key = paragraph.getKey();
|
|
427
|
+
|
|
428
|
+
if (anchor.type === 'element' && anchor.getNode().is(this)) {
|
|
429
|
+
anchor.set(key, anchor.offset, 'element');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (focus.type === 'element' && focus.getNode().is(this)) {
|
|
433
|
+
focus.set(key, focus.offset, 'element');
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
listNode.insertBefore(paragraph);
|
|
438
|
+
this.remove();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
getValue(): number {
|
|
445
|
+
const self = this.getLatest();
|
|
446
|
+
|
|
447
|
+
return self.__value;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
setValue(value: number): this {
|
|
451
|
+
const self = this.getWritable();
|
|
452
|
+
self.__value = value;
|
|
453
|
+
return self;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
getChecked(): boolean | undefined {
|
|
457
|
+
const self = this.getLatest();
|
|
458
|
+
|
|
459
|
+
let listType: ListType | undefined;
|
|
460
|
+
|
|
461
|
+
const parent = this.getParent();
|
|
462
|
+
if ($isListNode(parent)) {
|
|
463
|
+
listType = parent.getListType();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return listType === 'check' ? Boolean(self.__checked) : undefined;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
setChecked(checked?: boolean): this {
|
|
470
|
+
const self = this.getWritable();
|
|
471
|
+
self.__checked = checked;
|
|
472
|
+
return self;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
toggleChecked(): this {
|
|
476
|
+
const self = this.getWritable();
|
|
477
|
+
return self.setChecked(!self.__checked);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
getIndent(): number {
|
|
481
|
+
// If we don't have a parent, we are likely serializing
|
|
482
|
+
const parent = this.getParent();
|
|
483
|
+
if (parent === null || !this.isAttached()) {
|
|
484
|
+
return this.getLatest().__indent;
|
|
485
|
+
}
|
|
486
|
+
// ListItemNode should always have a ListNode for a parent.
|
|
487
|
+
let listNodeParent = parent.getParentOrThrow();
|
|
488
|
+
let indentLevel = 0;
|
|
489
|
+
while ($isListItemNode(listNodeParent)) {
|
|
490
|
+
listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow();
|
|
491
|
+
indentLevel++;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return indentLevel;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
setIndent(indent: number): this {
|
|
498
|
+
invariant(typeof indent === 'number', 'Invalid indent value.');
|
|
499
|
+
indent = Math.floor(indent);
|
|
500
|
+
invariant(indent >= 0, 'Indent value must be non-negative.');
|
|
501
|
+
let currentIndent = this.getIndent();
|
|
502
|
+
while (currentIndent !== indent) {
|
|
503
|
+
if (currentIndent < indent) {
|
|
504
|
+
$handleIndent(this);
|
|
505
|
+
currentIndent++;
|
|
506
|
+
} else {
|
|
507
|
+
$handleOutdent(this);
|
|
508
|
+
currentIndent--;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return this;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** @deprecated @internal */
|
|
516
|
+
canInsertAfter(node: LexicalNode): boolean {
|
|
517
|
+
return $isListItemNode(node);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** @deprecated @internal */
|
|
521
|
+
canReplaceWith(replacement: LexicalNode): boolean {
|
|
522
|
+
return $isListItemNode(replacement);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
canMergeWith(node: LexicalNode): boolean {
|
|
526
|
+
return $isListItemNode(node) || $isParagraphNode(node);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
extractWithChild(child: LexicalNode, selection: BaseSelection): boolean {
|
|
530
|
+
if (!$isRangeSelection(selection)) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const anchorNode = selection.anchor.getNode();
|
|
535
|
+
const focusNode = selection.focus.getNode();
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
this.isParentOf(anchorNode) &&
|
|
539
|
+
this.isParentOf(focusNode) &&
|
|
540
|
+
this.getTextContent().length === selection.getTextContent().length
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
isParentRequired(): true {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
createParentElementNode(): ListNode {
|
|
549
|
+
return $createListNode('bullet');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
canMergeWhenEmpty(): true {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function $setListItemThemeClassNames(
|
|
558
|
+
dom: HTMLElement,
|
|
559
|
+
editorThemeClasses: EditorThemeClasses,
|
|
560
|
+
node: ListItemNode,
|
|
561
|
+
): void {
|
|
562
|
+
const listTheme = editorThemeClasses.list;
|
|
563
|
+
if (!listTheme) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const listItemClassName = listTheme.listitem;
|
|
568
|
+
const nestedListItemClassName = listTheme.nested && listTheme.nested.listitem;
|
|
569
|
+
const parentNode = node.getParent();
|
|
570
|
+
const isCheckList =
|
|
571
|
+
$isListNode(parentNode) && parentNode.getListType() === 'check';
|
|
572
|
+
const checked = node.getChecked();
|
|
573
|
+
const isNested = node.getChildren().some(child => $isListNode(child));
|
|
574
|
+
|
|
575
|
+
// Always remove the variable theme classes first so that the className
|
|
576
|
+
// string stays in a canonical order regardless of how the dom got here
|
|
577
|
+
// (fresh create vs. cross-parent reuse). classList.remove on a missing
|
|
578
|
+
// class is a no-op, so this is safe even on a freshly-created element.
|
|
579
|
+
const classesToRemove: string[] = [];
|
|
580
|
+
if (listTheme.listitemChecked !== undefined) {
|
|
581
|
+
classesToRemove.push(listTheme.listitemChecked);
|
|
582
|
+
}
|
|
583
|
+
if (listTheme.listitemUnchecked !== undefined) {
|
|
584
|
+
classesToRemove.push(listTheme.listitemUnchecked);
|
|
585
|
+
}
|
|
586
|
+
if (nestedListItemClassName !== undefined) {
|
|
587
|
+
classesToRemove.push(...normalizeClassNames(nestedListItemClassName));
|
|
588
|
+
}
|
|
589
|
+
if (classesToRemove.length > 0) {
|
|
590
|
+
removeClassNamesFromElement(dom, ...classesToRemove);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const classesToAdd: string[] = [];
|
|
594
|
+
if (listItemClassName !== undefined) {
|
|
595
|
+
classesToAdd.push(...normalizeClassNames(listItemClassName));
|
|
596
|
+
}
|
|
597
|
+
if (isCheckList) {
|
|
598
|
+
const checkClassName = checked
|
|
599
|
+
? listTheme.listitemChecked
|
|
600
|
+
: listTheme.listitemUnchecked;
|
|
601
|
+
if (checkClassName !== undefined) {
|
|
602
|
+
classesToAdd.push(checkClassName);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (nestedListItemClassName !== undefined && isNested) {
|
|
606
|
+
classesToAdd.push(...normalizeClassNames(nestedListItemClassName));
|
|
607
|
+
}
|
|
608
|
+
if (classesToAdd.length > 0) {
|
|
609
|
+
addClassNamesToElement(dom, ...classesToAdd);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function updateListItemChecked(
|
|
614
|
+
dom: HTMLElement,
|
|
615
|
+
listItemNode: ListItemNode,
|
|
616
|
+
prevListItemNode: ListItemNode | null,
|
|
617
|
+
): void {
|
|
618
|
+
const parent = listItemNode.getParent();
|
|
619
|
+
const isCheckbox =
|
|
620
|
+
$isListNode(parent) &&
|
|
621
|
+
parent.getListType() === 'check' &&
|
|
622
|
+
// Only add attributes for leaf list items
|
|
623
|
+
!$isListNode(listItemNode.getFirstChild());
|
|
624
|
+
if (!isCheckbox) {
|
|
625
|
+
dom.removeAttribute('role');
|
|
626
|
+
dom.removeAttribute('tabIndex');
|
|
627
|
+
dom.removeAttribute('aria-checked');
|
|
628
|
+
} else {
|
|
629
|
+
dom.setAttribute('role', 'checkbox');
|
|
630
|
+
dom.setAttribute('tabIndex', '-1');
|
|
631
|
+
dom.setAttribute(
|
|
632
|
+
'aria-checked',
|
|
633
|
+
listItemNode.getChecked() ? 'true' : 'false',
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function $convertListItemElement(domNode: HTMLElement): DOMConversionOutput {
|
|
639
|
+
const isGitHubCheckList = domNode.classList.contains('task-list-item');
|
|
640
|
+
if (isGitHubCheckList) {
|
|
641
|
+
for (const child of domNode.children) {
|
|
642
|
+
if (child.tagName === 'INPUT') {
|
|
643
|
+
return $convertCheckboxInput(child);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const isJoplinCheckList = domNode.classList.contains('joplin-checkbox');
|
|
649
|
+
if (isJoplinCheckList) {
|
|
650
|
+
for (const child of domNode.children) {
|
|
651
|
+
if (
|
|
652
|
+
child.classList.contains('checkbox-wrapper') &&
|
|
653
|
+
child.children.length > 0 &&
|
|
654
|
+
child.children[0].tagName === 'INPUT'
|
|
655
|
+
) {
|
|
656
|
+
return $convertCheckboxInput(child.children[0]);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const ariaCheckedAttr = domNode.getAttribute('aria-checked');
|
|
662
|
+
const checked =
|
|
663
|
+
ariaCheckedAttr === 'true'
|
|
664
|
+
? true
|
|
665
|
+
: ariaCheckedAttr === 'false'
|
|
666
|
+
? false
|
|
667
|
+
: undefined;
|
|
668
|
+
|
|
669
|
+
const node = $createListItemNode(checked);
|
|
670
|
+
$setFormatFromDOM(node, domNode);
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
after: setFormatFromChildren.bind(null, node),
|
|
674
|
+
node: $setDirectionFromDOM(node, domNode),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function $convertCheckboxInput(domNode: Element): DOMConversionOutput {
|
|
679
|
+
const isCheckboxInput = domNode.getAttribute('type') === 'checkbox';
|
|
680
|
+
if (!isCheckboxInput) {
|
|
681
|
+
return {node: null};
|
|
682
|
+
}
|
|
683
|
+
const checked = domNode.hasAttribute('checked');
|
|
684
|
+
const node = $createListItemNode(checked);
|
|
685
|
+
return {after: setFormatFromChildren.bind(null, node), node};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function setFormatFromChildren(
|
|
689
|
+
listItemNode: ListItemNode,
|
|
690
|
+
children: LexicalNode[],
|
|
691
|
+
): LexicalNode[] {
|
|
692
|
+
const firstChild = children[0];
|
|
693
|
+
// google doc sets the alignment of the <p> tag inside the <li>
|
|
694
|
+
if (
|
|
695
|
+
children.length === 1 &&
|
|
696
|
+
$isParagraphNode(firstChild) &&
|
|
697
|
+
!listItemNode.getFormatType() &&
|
|
698
|
+
firstChild.getFormatType()
|
|
699
|
+
) {
|
|
700
|
+
listItemNode.setFormat(firstChild.getFormatType());
|
|
701
|
+
return firstChild.getChildren();
|
|
702
|
+
}
|
|
703
|
+
return children;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Creates a new List Item node, passing true/false will convert it to a checkbox input.
|
|
708
|
+
* @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.
|
|
709
|
+
* @returns The new List Item.
|
|
710
|
+
*/
|
|
711
|
+
export function $createListItemNode(checked?: boolean): ListItemNode {
|
|
712
|
+
return $applyNodeReplacement(new ListItemNode(undefined, checked));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Checks to see if the node is a ListItemNode.
|
|
717
|
+
* @param node - The node to be checked.
|
|
718
|
+
* @returns true if the node is a ListItemNode, false otherwise.
|
|
719
|
+
*/
|
|
720
|
+
export function $isListItemNode(
|
|
721
|
+
node: LexicalNode | null | undefined,
|
|
722
|
+
): node is ListItemNode {
|
|
723
|
+
return node instanceof ListItemNode;
|
|
724
|
+
}
|