| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Copyright (c) Meta Platforms, Inc. and affiliates. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * This source code is licensed under the MIT license found in the | 
					
						
							|  |  |  |  * LICENSE file in the root directory of this source tree. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import type {ListNode, ListType} from './'; | 
					
						
							|  |  |  | import type { | 
					
						
							|  |  |  |   BaseSelection, | 
					
						
							|  |  |  |   DOMConversionMap, | 
					
						
							|  |  |  |   DOMConversionOutput, | 
					
						
							|  |  |  |   DOMExportOutput, | 
					
						
							|  |  |  |   EditorConfig, | 
					
						
							|  |  |  |   EditorThemeClasses, | 
					
						
							|  |  |  |   LexicalNode, | 
					
						
							|  |  |  |   NodeKey, | 
					
						
							|  |  |  |   ParagraphNode, | 
					
						
							|  |  |  |   RangeSelection, | 
					
						
							|  |  |  |   SerializedElementNode, | 
					
						
							|  |  |  |   Spread, | 
					
						
							|  |  |  | } from 'lexical'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import { | 
					
						
							|  |  |  |   addClassNamesToElement, | 
					
						
							|  |  |  |   removeClassNamesFromElement, | 
					
						
							|  |  |  | } from '@lexical/utils'; | 
					
						
							|  |  |  | import { | 
					
						
							|  |  |  |   $applyNodeReplacement, | 
					
						
							|  |  |  |   $createParagraphNode, | 
					
						
							|  |  |  |   $isElementNode, | 
					
						
							|  |  |  |   $isParagraphNode, | 
					
						
							|  |  |  |   $isRangeSelection, | 
					
						
							|  |  |  |   ElementNode, | 
					
						
							|  |  |  |   LexicalEditor, | 
					
						
							|  |  |  | } from 'lexical'; | 
					
						
							|  |  |  | import invariant from 'lexical/shared/invariant'; | 
					
						
							|  |  |  | import normalizeClassNames from 'lexical/shared/normalizeClassNames'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {$createListNode, $isListNode} from './'; | 
					
						
							|  |  |  | import {$handleIndent, $handleOutdent, mergeLists} from './formatList'; | 
					
						
							|  |  |  | import {isNestedListNode} from './utils'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export type SerializedListItemNode = Spread< | 
					
						
							|  |  |  |   { | 
					
						
							|  |  |  |     checked: boolean | undefined; | 
					
						
							|  |  |  |     value: number; | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  |   SerializedElementNode | 
					
						
							|  |  |  | >; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** @noInheritDoc */ | 
					
						
							|  |  |  | export class ListItemNode extends ElementNode { | 
					
						
							|  |  |  |   /** @internal */ | 
					
						
							|  |  |  |   __value: number; | 
					
						
							|  |  |  |   /** @internal */ | 
					
						
							|  |  |  |   __checked?: boolean; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static getType(): string { | 
					
						
							|  |  |  |     return 'listitem'; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static clone(node: ListItemNode): ListItemNode { | 
					
						
							|  |  |  |     return new ListItemNode(node.__value, node.__checked, node.__key); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor(value?: number, checked?: boolean, key?: NodeKey) { | 
					
						
							|  |  |  |     super(key); | 
					
						
							|  |  |  |     this.__value = value === undefined ? 1 : value; | 
					
						
							|  |  |  |     this.__checked = checked; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   createDOM(config: EditorConfig): HTMLElement { | 
					
						
							|  |  |  |     const element = document.createElement('li'); | 
					
						
							|  |  |  |     const parent = this.getParent(); | 
					
						
							|  |  |  |     if ($isListNode(parent) && parent.getListType() === 'check') { | 
					
						
							|  |  |  |       updateListItemChecked(element, this, null, parent); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     element.value = this.__value; | 
					
						
							|  |  |  |     $setListItemThemeClassNames(element, config.theme, this); | 
					
						
							|  |  |  |     return element; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   updateDOM( | 
					
						
							|  |  |  |     prevNode: ListItemNode, | 
					
						
							|  |  |  |     dom: HTMLElement, | 
					
						
							|  |  |  |     config: EditorConfig, | 
					
						
							|  |  |  |   ): boolean { | 
					
						
							|  |  |  |     const parent = this.getParent(); | 
					
						
							|  |  |  |     if ($isListNode(parent) && parent.getListType() === 'check') { | 
					
						
							|  |  |  |       updateListItemChecked(dom, this, prevNode, parent); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     // @ts-expect-error - this is always HTMLListItemElement
 | 
					
						
							|  |  |  |     dom.value = this.__value; | 
					
						
							|  |  |  |     $setListItemThemeClassNames(dom, config.theme, this); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static transform(): (node: LexicalNode) => void { | 
					
						
							|  |  |  |     return (node: LexicalNode) => { | 
					
						
							|  |  |  |       invariant($isListItemNode(node), 'node is not a ListItemNode'); | 
					
						
							|  |  |  |       if (node.__checked == null) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const parent = node.getParent(); | 
					
						
							|  |  |  |       if ($isListNode(parent)) { | 
					
						
							|  |  |  |         if (parent.getListType() !== 'check' && node.getChecked() != null) { | 
					
						
							|  |  |  |           node.setChecked(undefined); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static importDOM(): DOMConversionMap | null { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       li: () => ({ | 
					
						
							|  |  |  |         conversion: $convertListItemElement, | 
					
						
							|  |  |  |         priority: 0, | 
					
						
							|  |  |  |       }), | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static importJSON(serializedNode: SerializedListItemNode): ListItemNode { | 
					
						
							|  |  |  |     const node = $createListItemNode(); | 
					
						
							|  |  |  |     node.setChecked(serializedNode.checked); | 
					
						
							|  |  |  |     node.setValue(serializedNode.value); | 
					
						
							|  |  |  |     node.setFormat(serializedNode.format); | 
					
						
							|  |  |  |     node.setDirection(serializedNode.direction); | 
					
						
							|  |  |  |     return node; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   exportDOM(editor: LexicalEditor): DOMExportOutput { | 
					
						
							|  |  |  |     const element = this.createDOM(editor._config); | 
					
						
							|  |  |  |     element.style.textAlign = this.getFormatType(); | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       element, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   exportJSON(): SerializedListItemNode { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       ...super.exportJSON(), | 
					
						
							|  |  |  |       checked: this.getChecked(), | 
					
						
							|  |  |  |       type: 'listitem', | 
					
						
							|  |  |  |       value: this.getValue(), | 
					
						
							|  |  |  |       version: 1, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   append(...nodes: LexicalNode[]): this { | 
					
						
							|  |  |  |     for (let i = 0; i < nodes.length; i++) { | 
					
						
							|  |  |  |       const node = nodes[i]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if ($isElementNode(node) && this.canMergeWith(node)) { | 
					
						
							|  |  |  |         const children = node.getChildren(); | 
					
						
							|  |  |  |         this.append(...children); | 
					
						
							|  |  |  |         node.remove(); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         super.append(node); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return this; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   replace<N extends LexicalNode>( | 
					
						
							|  |  |  |     replaceWithNode: N, | 
					
						
							|  |  |  |     includeChildren?: boolean, | 
					
						
							|  |  |  |   ): N { | 
					
						
							|  |  |  |     if ($isListItemNode(replaceWithNode)) { | 
					
						
							|  |  |  |       return super.replace(replaceWithNode); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     this.setIndent(0); | 
					
						
							|  |  |  |     const list = this.getParentOrThrow(); | 
					
						
							|  |  |  |     if (!$isListNode(list)) { | 
					
						
							|  |  |  |       return replaceWithNode; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (list.__first === this.getKey()) { | 
					
						
							|  |  |  |       list.insertBefore(replaceWithNode); | 
					
						
							|  |  |  |     } else if (list.__last === this.getKey()) { | 
					
						
							|  |  |  |       list.insertAfter(replaceWithNode); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       // Split the list
 | 
					
						
							|  |  |  |       const newList = $createListNode(list.getListType()); | 
					
						
							|  |  |  |       let nextSibling = this.getNextSibling(); | 
					
						
							|  |  |  |       while (nextSibling) { | 
					
						
							|  |  |  |         const nodeToAppend = nextSibling; | 
					
						
							|  |  |  |         nextSibling = nextSibling.getNextSibling(); | 
					
						
							|  |  |  |         newList.append(nodeToAppend); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       list.insertAfter(replaceWithNode); | 
					
						
							|  |  |  |       replaceWithNode.insertAfter(newList); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (includeChildren) { | 
					
						
							|  |  |  |       invariant( | 
					
						
							|  |  |  |         $isElementNode(replaceWithNode), | 
					
						
							|  |  |  |         'includeChildren should only be true for ElementNodes', | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       this.getChildren().forEach((child: LexicalNode) => { | 
					
						
							|  |  |  |         replaceWithNode.append(child); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     this.remove(); | 
					
						
							|  |  |  |     if (list.getChildrenSize() === 0) { | 
					
						
							|  |  |  |       list.remove(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return replaceWithNode; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   insertAfter(node: LexicalNode, restoreSelection = true): LexicalNode { | 
					
						
							|  |  |  |     const listNode = this.getParentOrThrow(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!$isListNode(listNode)) { | 
					
						
							|  |  |  |       invariant( | 
					
						
							|  |  |  |         false, | 
					
						
							|  |  |  |         'insertAfter: list node is not parent of list item node', | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if ($isListItemNode(node)) { | 
					
						
							|  |  |  |       return super.insertAfter(node, restoreSelection); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const siblings = this.getNextSiblings(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Split the lists and insert the node in between them
 | 
					
						
							|  |  |  |     listNode.insertAfter(node, restoreSelection); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (siblings.length !== 0) { | 
					
						
							|  |  |  |       const newListNode = $createListNode(listNode.getListType()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       siblings.forEach((sibling) => newListNode.append(sibling)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       node.insertAfter(newListNode, restoreSelection); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return node; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   remove(preserveEmptyParent?: boolean): void { | 
					
						
							|  |  |  |     const prevSibling = this.getPreviousSibling(); | 
					
						
							|  |  |  |     const nextSibling = this.getNextSibling(); | 
					
						
							|  |  |  |     super.remove(preserveEmptyParent); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if ( | 
					
						
							|  |  |  |       prevSibling && | 
					
						
							|  |  |  |       nextSibling && | 
					
						
							|  |  |  |       isNestedListNode(prevSibling) && | 
					
						
							|  |  |  |       isNestedListNode(nextSibling) | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |       mergeLists(prevSibling.getFirstChild(), nextSibling.getFirstChild()); | 
					
						
							|  |  |  |       nextSibling.remove(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   insertNewAfter( | 
					
						
							|  |  |  |     _: RangeSelection, | 
					
						
							|  |  |  |     restoreSelection = true, | 
					
						
							|  |  |  |   ): ListItemNode | ParagraphNode { | 
					
						
							| 
									
										
										
										
											2024-09-22 23:15:02 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (this.getTextContent().trim() === '' && this.isLastChild()) { | 
					
						
							|  |  |  |       const list = this.getParentOrThrow<ListNode>(); | 
					
						
							|  |  |  |       if (!$isListItemNode(list.getParent())) { | 
					
						
							|  |  |  |         const paragraph = $createParagraphNode(); | 
					
						
							|  |  |  |         list.insertAfter(paragraph, restoreSelection); | 
					
						
							|  |  |  |         this.remove(); | 
					
						
							|  |  |  |         return paragraph; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  |     const newElement = $createListItemNode( | 
					
						
							|  |  |  |       this.__checked == null ? undefined : false, | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2024-09-22 23:15:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-18 20:43:39 +08:00
										 |  |  |     this.insertAfter(newElement, restoreSelection); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return newElement; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   collapseAtStart(selection: RangeSelection): true { | 
					
						
							|  |  |  |     const paragraph = $createParagraphNode(); | 
					
						
							|  |  |  |     const children = this.getChildren(); | 
					
						
							|  |  |  |     children.forEach((child) => paragraph.append(child)); | 
					
						
							|  |  |  |     const listNode = this.getParentOrThrow(); | 
					
						
							|  |  |  |     const listNodeParent = listNode.getParentOrThrow(); | 
					
						
							|  |  |  |     const isIndented = $isListItemNode(listNodeParent); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (listNode.getChildrenSize() === 1) { | 
					
						
							|  |  |  |       if (isIndented) { | 
					
						
							|  |  |  |         // if the list node is nested, we just want to remove it,
 | 
					
						
							|  |  |  |         // effectively unindenting it.
 | 
					
						
							|  |  |  |         listNode.remove(); | 
					
						
							|  |  |  |         listNodeParent.select(); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         listNode.insertBefore(paragraph); | 
					
						
							|  |  |  |         listNode.remove(); | 
					
						
							|  |  |  |         // If we have selection on the list item, we'll need to move it
 | 
					
						
							|  |  |  |         // to the paragraph
 | 
					
						
							|  |  |  |         const anchor = selection.anchor; | 
					
						
							|  |  |  |         const focus = selection.focus; | 
					
						
							|  |  |  |         const key = paragraph.getKey(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (anchor.type === 'element' && anchor.getNode().is(this)) { | 
					
						
							|  |  |  |           anchor.set(key, anchor.offset, 'element'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (focus.type === 'element' && focus.getNode().is(this)) { | 
					
						
							|  |  |  |           focus.set(key, focus.offset, 'element'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       listNode.insertBefore(paragraph); | 
					
						
							|  |  |  |       this.remove(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getValue(): number { | 
					
						
							|  |  |  |     const self = this.getLatest(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return self.__value; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   setValue(value: number): void { | 
					
						
							|  |  |  |     const self = this.getWritable(); | 
					
						
							|  |  |  |     self.__value = value; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getChecked(): boolean | undefined { | 
					
						
							|  |  |  |     const self = this.getLatest(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     let listType: ListType | undefined; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const parent = this.getParent(); | 
					
						
							|  |  |  |     if ($isListNode(parent)) { | 
					
						
							|  |  |  |       listType = parent.getListType(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return listType === 'check' ? Boolean(self.__checked) : undefined; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   setChecked(checked?: boolean): void { | 
					
						
							|  |  |  |     const self = this.getWritable(); | 
					
						
							|  |  |  |     self.__checked = checked; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   toggleChecked(): void { | 
					
						
							|  |  |  |     this.setChecked(!this.__checked); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getIndent(): number { | 
					
						
							|  |  |  |     // If we don't have a parent, we are likely serializing
 | 
					
						
							|  |  |  |     const parent = this.getParent(); | 
					
						
							|  |  |  |     if (parent === null) { | 
					
						
							|  |  |  |       return this.getLatest().__indent; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     // ListItemNode should always have a ListNode for a parent.
 | 
					
						
							|  |  |  |     let listNodeParent = parent.getParentOrThrow(); | 
					
						
							|  |  |  |     let indentLevel = 0; | 
					
						
							|  |  |  |     while ($isListItemNode(listNodeParent)) { | 
					
						
							|  |  |  |       listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow(); | 
					
						
							|  |  |  |       indentLevel++; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return indentLevel; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   setIndent(indent: number): this { | 
					
						
							|  |  |  |     invariant(typeof indent === 'number', 'Invalid indent value.'); | 
					
						
							|  |  |  |     indent = Math.floor(indent); | 
					
						
							|  |  |  |     invariant(indent >= 0, 'Indent value must be non-negative.'); | 
					
						
							|  |  |  |     let currentIndent = this.getIndent(); | 
					
						
							|  |  |  |     while (currentIndent !== indent) { | 
					
						
							|  |  |  |       if (currentIndent < indent) { | 
					
						
							|  |  |  |         $handleIndent(this); | 
					
						
							|  |  |  |         currentIndent++; | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         $handleOutdent(this); | 
					
						
							|  |  |  |         currentIndent--; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return this; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** @deprecated @internal */ | 
					
						
							|  |  |  |   canInsertAfter(node: LexicalNode): boolean { | 
					
						
							|  |  |  |     return $isListItemNode(node); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** @deprecated @internal */ | 
					
						
							|  |  |  |   canReplaceWith(replacement: LexicalNode): boolean { | 
					
						
							|  |  |  |     return $isListItemNode(replacement); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   canMergeWith(node: LexicalNode): boolean { | 
					
						
							|  |  |  |     return $isParagraphNode(node) || $isListItemNode(node); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   extractWithChild(child: LexicalNode, selection: BaseSelection): boolean { | 
					
						
							|  |  |  |     if (!$isRangeSelection(selection)) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const anchorNode = selection.anchor.getNode(); | 
					
						
							|  |  |  |     const focusNode = selection.focus.getNode(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return ( | 
					
						
							|  |  |  |       this.isParentOf(anchorNode) && | 
					
						
							|  |  |  |       this.isParentOf(focusNode) && | 
					
						
							|  |  |  |       this.getTextContent().length === selection.getTextContent().length | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   isParentRequired(): true { | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   createParentElementNode(): ElementNode { | 
					
						
							|  |  |  |     return $createListNode('bullet'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   canMergeWhenEmpty(): true { | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function $setListItemThemeClassNames( | 
					
						
							|  |  |  |   dom: HTMLElement, | 
					
						
							|  |  |  |   editorThemeClasses: EditorThemeClasses, | 
					
						
							|  |  |  |   node: ListItemNode, | 
					
						
							|  |  |  | ): void { | 
					
						
							|  |  |  |   const classesToAdd = []; | 
					
						
							|  |  |  |   const classesToRemove = []; | 
					
						
							|  |  |  |   const listTheme = editorThemeClasses.list; | 
					
						
							|  |  |  |   const listItemClassName = listTheme ? listTheme.listitem : undefined; | 
					
						
							|  |  |  |   let nestedListItemClassName; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (listTheme && listTheme.nested) { | 
					
						
							|  |  |  |     nestedListItemClassName = listTheme.nested.listitem; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (listItemClassName !== undefined) { | 
					
						
							|  |  |  |     classesToAdd.push(...normalizeClassNames(listItemClassName)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (listTheme) { | 
					
						
							|  |  |  |     const parentNode = node.getParent(); | 
					
						
							|  |  |  |     const isCheckList = | 
					
						
							|  |  |  |       $isListNode(parentNode) && parentNode.getListType() === 'check'; | 
					
						
							|  |  |  |     const checked = node.getChecked(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!isCheckList || checked) { | 
					
						
							|  |  |  |       classesToRemove.push(listTheme.listitemUnchecked); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!isCheckList || !checked) { | 
					
						
							|  |  |  |       classesToRemove.push(listTheme.listitemChecked); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (isCheckList) { | 
					
						
							|  |  |  |       classesToAdd.push( | 
					
						
							|  |  |  |         checked ? listTheme.listitemChecked : listTheme.listitemUnchecked, | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (nestedListItemClassName !== undefined) { | 
					
						
							|  |  |  |     const nestedListItemClasses = normalizeClassNames(nestedListItemClassName); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (node.getChildren().some((child) => $isListNode(child))) { | 
					
						
							|  |  |  |       classesToAdd.push(...nestedListItemClasses); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       classesToRemove.push(...nestedListItemClasses); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (classesToRemove.length > 0) { | 
					
						
							|  |  |  |     removeClassNamesFromElement(dom, ...classesToRemove); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (classesToAdd.length > 0) { | 
					
						
							|  |  |  |     addClassNamesToElement(dom, ...classesToAdd); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function updateListItemChecked( | 
					
						
							|  |  |  |   dom: HTMLElement, | 
					
						
							|  |  |  |   listItemNode: ListItemNode, | 
					
						
							|  |  |  |   prevListItemNode: ListItemNode | null, | 
					
						
							|  |  |  |   listNode: ListNode, | 
					
						
							|  |  |  | ): void { | 
					
						
							|  |  |  |   // Only add attributes for leaf list items
 | 
					
						
							|  |  |  |   if ($isListNode(listItemNode.getFirstChild())) { | 
					
						
							|  |  |  |     dom.removeAttribute('role'); | 
					
						
							|  |  |  |     dom.removeAttribute('tabIndex'); | 
					
						
							|  |  |  |     dom.removeAttribute('aria-checked'); | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     dom.setAttribute('role', 'checkbox'); | 
					
						
							|  |  |  |     dom.setAttribute('tabIndex', '-1'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if ( | 
					
						
							|  |  |  |       !prevListItemNode || | 
					
						
							|  |  |  |       listItemNode.__checked !== prevListItemNode.__checked | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |       dom.setAttribute( | 
					
						
							|  |  |  |         'aria-checked', | 
					
						
							|  |  |  |         listItemNode.getChecked() ? 'true' : 'false', | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function $convertListItemElement(domNode: HTMLElement): DOMConversionOutput { | 
					
						
							|  |  |  |   const isGitHubCheckList = domNode.classList.contains('task-list-item'); | 
					
						
							|  |  |  |   if (isGitHubCheckList) { | 
					
						
							|  |  |  |     for (const child of domNode.children) { | 
					
						
							|  |  |  |       if (child.tagName === 'INPUT') { | 
					
						
							|  |  |  |         return $convertCheckboxInput(child); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const ariaCheckedAttr = domNode.getAttribute('aria-checked'); | 
					
						
							|  |  |  |   const checked = | 
					
						
							|  |  |  |     ariaCheckedAttr === 'true' | 
					
						
							|  |  |  |       ? true | 
					
						
							|  |  |  |       : ariaCheckedAttr === 'false' | 
					
						
							|  |  |  |       ? false | 
					
						
							|  |  |  |       : undefined; | 
					
						
							|  |  |  |   return {node: $createListItemNode(checked)}; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function $convertCheckboxInput(domNode: Element): DOMConversionOutput { | 
					
						
							|  |  |  |   const isCheckboxInput = domNode.getAttribute('type') === 'checkbox'; | 
					
						
							|  |  |  |   if (!isCheckboxInput) { | 
					
						
							|  |  |  |     return {node: null}; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const checked = domNode.hasAttribute('checked'); | 
					
						
							|  |  |  |   return {node: $createListItemNode(checked)}; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Creates a new List Item node, passing true/false will convert it to a checkbox input. | 
					
						
							|  |  |  |  * @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. | 
					
						
							|  |  |  |  * @returns The new List Item. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export function $createListItemNode(checked?: boolean): ListItemNode { | 
					
						
							|  |  |  |   return $applyNodeReplacement(new ListItemNode(undefined, checked)); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Checks to see if the node is a ListItemNode. | 
					
						
							|  |  |  |  * @param node - The node to be checked. | 
					
						
							|  |  |  |  * @returns true if the node is a ListItemNode, false otherwise. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export function $isListItemNode( | 
					
						
							|  |  |  |   node: LexicalNode | null | undefined, | 
					
						
							|  |  |  | ): node is ListItemNode { | 
					
						
							|  |  |  |   return node instanceof ListItemNode; | 
					
						
							|  |  |  | } |