Lexical: Merged list nodes

This commit is contained in:
Dan Brown 2024-12-03 19:03:52 +00:00
parent 36a4d79120
commit ebd4604f21
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 111 additions and 492 deletions

View File

@ -13,7 +13,6 @@ import type {
DOMConversionOutput, DOMConversionOutput,
DOMExportOutput, DOMExportOutput,
EditorConfig, EditorConfig,
EditorThemeClasses,
LexicalNode, LexicalNode,
NodeKey, NodeKey,
ParagraphNode, ParagraphNode,
@ -22,10 +21,6 @@ import type {
Spread, Spread,
} from 'lexical'; } from 'lexical';
import {
addClassNamesToElement,
removeClassNamesFromElement,
} from '@lexical/utils';
import { import {
$applyNodeReplacement, $applyNodeReplacement,
$createParagraphNode, $createParagraphNode,
@ -36,11 +31,11 @@ import {
LexicalEditor, LexicalEditor,
} from 'lexical'; } from 'lexical';
import invariant from 'lexical/shared/invariant'; import invariant from 'lexical/shared/invariant';
import normalizeClassNames from 'lexical/shared/normalizeClassNames';
import {$createListNode, $isListNode} from './'; import {$createListNode, $isListNode} from './';
import {$handleIndent, $handleOutdent, mergeLists} from './formatList'; import {mergeLists} from './formatList';
import {isNestedListNode} from './utils'; import {isNestedListNode} from './utils';
import {el} from "../../utils/dom";
export type SerializedListItemNode = Spread< export type SerializedListItemNode = Spread<
{ {
@ -74,11 +69,17 @@ export class ListItemNode extends ElementNode {
createDOM(config: EditorConfig): HTMLElement { createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('li'); const element = document.createElement('li');
const parent = this.getParent(); const parent = this.getParent();
if ($isListNode(parent) && parent.getListType() === 'check') { if ($isListNode(parent) && parent.getListType() === 'check') {
updateListItemChecked(element, this, null, parent); updateListItemChecked(element, this);
} }
element.value = this.__value; element.value = this.__value;
$setListItemThemeClassNames(element, config.theme, this);
if ($hasNestedListWithoutLabel(this)) {
element.style.listStyle = 'none';
}
return element; return element;
} }
@ -89,11 +90,12 @@ export class ListItemNode extends ElementNode {
): boolean { ): boolean {
const parent = this.getParent(); const parent = this.getParent();
if ($isListNode(parent) && parent.getListType() === 'check') { if ($isListNode(parent) && parent.getListType() === 'check') {
updateListItemChecked(dom, this, prevNode, parent); updateListItemChecked(dom, this);
} }
dom.style.listStyle = $hasNestedListWithoutLabel(this) ? 'none' : '';
// @ts-expect-error - this is always HTMLListItemElement // @ts-expect-error - this is always HTMLListItemElement
dom.value = this.__value; dom.value = this.__value;
$setListItemThemeClassNames(dom, config.theme, this);
return false; return false;
} }
@ -132,6 +134,20 @@ export class ListItemNode extends ElementNode {
exportDOM(editor: LexicalEditor): DOMExportOutput { exportDOM(editor: LexicalEditor): DOMExportOutput {
const element = this.createDOM(editor._config); const element = this.createDOM(editor._config);
if (element.classList.contains('task-list-item')) {
const input = el('input', {
type: 'checkbox',
disabled: 'disabled',
});
if (element.hasAttribute('checked')) {
input.setAttribute('checked', 'checked');
element.removeAttribute('checked');
}
element.prepend(input);
}
return { return {
element, element,
}; };
@ -390,89 +406,33 @@ export class ListItemNode extends ElementNode {
} }
} }
function $setListItemThemeClassNames( function $hasNestedListWithoutLabel(node: ListItemNode): boolean {
dom: HTMLElement, const children = node.getChildren();
editorThemeClasses: EditorThemeClasses, let hasLabel = false;
node: ListItemNode, let hasNestedList = false;
): void {
const classesToAdd = [];
const classesToRemove = [];
const listTheme = editorThemeClasses.list;
const listItemClassName = listTheme ? listTheme.listitem : undefined;
let nestedListItemClassName;
if (listTheme && listTheme.nested) { for (const child of children) {
nestedListItemClassName = listTheme.nested.listitem; if ($isListNode(child)) {
} hasNestedList = true;
} else if (child.getTextContent().trim().length > 0) {
if (listItemClassName !== undefined) { hasLabel = true;
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) { return hasNestedList && !hasLabel;
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( function updateListItemChecked(
dom: HTMLElement, dom: HTMLElement,
listItemNode: ListItemNode, listItemNode: ListItemNode,
prevListItemNode: ListItemNode | null,
listNode: ListNode,
): void { ): void {
// Only add attributes for leaf list items // Only set task list attrs for leaf list items
if ($isListNode(listItemNode.getFirstChild())) { const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild());
dom.removeAttribute('role'); dom.classList.toggle('task-list-item', shouldBeTaskItem);
dom.removeAttribute('tabIndex'); if (listItemNode.__checked) {
dom.removeAttribute('aria-checked'); dom.setAttribute('checked', 'checked');
} else { } else {
dom.setAttribute('role', 'checkbox'); dom.removeAttribute('checked');
dom.setAttribute('tabIndex', '-1');
if (
!prevListItemNode ||
listItemNode.__checked !== prevListItemNode.__checked
) {
dom.setAttribute(
'aria-checked',
listItemNode.getChecked() ? 'true' : 'false',
);
}
} }
} }

View File

@ -36,9 +36,11 @@ import {
updateChildrenListItemValue, updateChildrenListItemValue,
} from './formatList'; } from './formatList';
import {$getListDepth, $wrapInListItem} from './utils'; import {$getListDepth, $wrapInListItem} from './utils';
import {extractDirectionFromElement} from "../../nodes/_common";
export type SerializedListNode = Spread< export type SerializedListNode = Spread<
{ {
id: string;
listType: ListType; listType: ListType;
start: number; start: number;
tag: ListNodeTagType; tag: ListNodeTagType;
@ -58,15 +60,18 @@ export class ListNode extends ElementNode {
__start: number; __start: number;
/** @internal */ /** @internal */
__listType: ListType; __listType: ListType;
/** @internal */
__id: string = '';
static getType(): string { static getType(): string {
return 'list'; return 'list';
} }
static clone(node: ListNode): ListNode { static clone(node: ListNode): ListNode {
const listType = node.__listType || TAG_TO_LIST_TYPE[node.__tag]; const newNode = new ListNode(node.__listType, node.__start, node.__key);
newNode.__id = node.__id;
return new ListNode(listType, node.__start, node.__key); newNode.__dir = node.__dir;
return newNode;
} }
constructor(listType: ListType, start: number, key?: NodeKey) { constructor(listType: ListType, start: number, key?: NodeKey) {
@ -81,6 +86,16 @@ export class ListNode extends ElementNode {
return this.__tag; return this.__tag;
} }
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
setListType(type: ListType): void { setListType(type: ListType): void {
const writable = this.getWritable(); const writable = this.getWritable();
writable.__listType = type; writable.__listType = type;
@ -108,6 +123,14 @@ export class ListNode extends ElementNode {
dom.__lexicalListType = this.__listType; dom.__lexicalListType = this.__listType;
$setListThemeClassNames(dom, config.theme, this); $setListThemeClassNames(dom, config.theme, this);
if (this.__id) {
dom.setAttribute('id', this.__id);
}
if (this.__dir) {
dom.setAttribute('dir', this.__dir);
}
return dom; return dom;
} }
@ -116,7 +139,11 @@ export class ListNode extends ElementNode {
dom: HTMLElement, dom: HTMLElement,
config: EditorConfig, config: EditorConfig,
): boolean { ): boolean {
if (prevNode.__tag !== this.__tag) { if (
prevNode.__tag !== this.__tag
|| prevNode.__dir !== this.__dir
|| prevNode.__id !== this.__id
) {
return true; return true;
} }
@ -148,8 +175,7 @@ export class ListNode extends ElementNode {
static importJSON(serializedNode: SerializedListNode): ListNode { static importJSON(serializedNode: SerializedListNode): ListNode {
const node = $createListNode(serializedNode.listType, serializedNode.start); const node = $createListNode(serializedNode.listType, serializedNode.start);
node.setFormat(serializedNode.format); node.setId(serializedNode.id);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction); node.setDirection(serializedNode.direction);
return node; return node;
} }
@ -177,6 +203,7 @@ export class ListNode extends ElementNode {
tag: this.getTag(), tag: this.getTag(),
type: 'list', type: 'list',
version: 1, version: 1,
id: this.__id,
}; };
} }
@ -277,28 +304,21 @@ function $setListThemeClassNames(
} }
/* /*
* This function normalizes the children of a ListNode after the conversion from HTML, * This function is a custom normalization function to allow nested lists within list item elements.
* ensuring that they are all ListItemNodes and contain either a single nested ListNode * Original taken from https://github.com/facebook/lexical/blob/6e10210fd1e113ccfafdc999b1d896733c5c5bea/packages/lexical-list/src/LexicalListNode.ts#L284-L303
* or some other inline content. * With modifications made.
*/ */
function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> { function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> {
const normalizedListItems: Array<ListItemNode> = []; const normalizedListItems: Array<ListItemNode> = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]; for (const node of nodes) {
if ($isListItemNode(node)) { if ($isListItemNode(node)) {
normalizedListItems.push(node); normalizedListItems.push(node);
const children = node.getChildren();
if (children.length > 1) {
children.forEach((child) => {
if ($isListNode(child)) {
normalizedListItems.push($wrapInListItem(child));
}
});
}
} else { } else {
normalizedListItems.push($wrapInListItem(node)); normalizedListItems.push($wrapInListItem(node));
} }
} }
return normalizedListItems; return normalizedListItems;
} }
@ -334,6 +354,14 @@ function $convertListNode(domNode: HTMLElement): DOMConversionOutput {
} }
} }
if (domNode.id && node) {
node.setId(domNode.id);
}
if (domNode.dir && node) {
node.setDirection(extractDirectionFromElement(domNode));
}
return { return {
after: $normalizeChildren, after: $normalizeChildren,
node, node,

View File

@ -1265,99 +1265,5 @@ describe('LexicalListItemNode tests', () => {
expect($isListItemNode(listItemNode)).toBe(true); expect($isListItemNode(listItemNode)).toBe(true);
}); });
}); });
describe('ListItemNode.setIndent()', () => {
let listNode: ListNode;
let listItemNode1: ListItemNode;
let listItemNode2: ListItemNode;
beforeEach(async () => {
const {editor} = testEnv;
await editor.update(() => {
const root = $getRoot();
listNode = new ListNode('bullet', 1);
listItemNode1 = new ListItemNode();
listItemNode2 = new ListItemNode();
root.append(listNode);
listNode.append(listItemNode1, listItemNode2);
listItemNode1.append(new TextNode('one'));
listItemNode2.append(new TextNode('two'));
});
});
it('indents and outdents list item', async () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode1.setIndent(3);
});
await editor.update(() => {
expect(listItemNode1.getIndent()).toBe(3);
});
expectHtmlToBeEqual(
editor.getRootElement()!.innerHTML,
html`
<ul>
<li value="1">
<ul>
<li value="1">
<ul>
<li value="1">
<ul>
<li value="1">
<span data-lexical-text="true">one</span>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li value="1">
<span data-lexical-text="true">two</span>
</li>
</ul>
`,
);
await editor.update(() => {
listItemNode1.setIndent(0);
});
await editor.update(() => {
expect(listItemNode1.getIndent()).toBe(0);
});
expectHtmlToBeEqual(
editor.getRootElement()!.innerHTML,
html`
<ul>
<li value="1">
<span data-lexical-text="true">one</span>
</li>
<li value="2">
<span data-lexical-text="true">two</span>
</li>
</ul>
`,
);
});
it('handles fractional indent values', async () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode1.setIndent(0.5);
});
await editor.update(() => {
expect(listItemNode1.getIndent()).toBe(0);
});
});
});
}); });
}); });

View File

@ -1,120 +0,0 @@
import {$isListNode, ListItemNode, SerializedListItemNode} from "@lexical/list";
import {EditorConfig} from "lexical/LexicalEditor";
import {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical";
import {el} from "../utils/dom";
import {$isCustomListNode} from "./custom-list";
function updateListItemChecked(
dom: HTMLElement,
listItemNode: ListItemNode,
): void {
// Only set task list attrs for leaf list items
const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild());
dom.classList.toggle('task-list-item', shouldBeTaskItem);
if (listItemNode.__checked) {
dom.setAttribute('checked', 'checked');
} else {
dom.removeAttribute('checked');
}
}
export class CustomListItemNode extends ListItemNode {
static getType(): string {
return 'custom-list-item';
}
static clone(node: CustomListItemNode): CustomListItemNode {
return new CustomListItemNode(node.__value, node.__checked, node.__key);
}
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('li');
const parent = this.getParent();
if ($isListNode(parent) && parent.getListType() === 'check') {
updateListItemChecked(element, this);
}
element.value = this.__value;
if ($hasNestedListWithoutLabel(this)) {
element.style.listStyle = 'none';
}
return element;
}
updateDOM(
prevNode: ListItemNode,
dom: HTMLElement,
config: EditorConfig,
): boolean {
const parent = this.getParent();
if ($isListNode(parent) && parent.getListType() === 'check') {
updateListItemChecked(dom, this);
}
dom.style.listStyle = $hasNestedListWithoutLabel(this) ? 'none' : '';
// @ts-expect-error - this is always HTMLListItemElement
dom.value = this.__value;
return false;
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const element = this.createDOM(editor._config);
element.style.textAlign = this.getFormatType();
if (element.classList.contains('task-list-item')) {
const input = el('input', {
type: 'checkbox',
disabled: 'disabled',
});
if (element.hasAttribute('checked')) {
input.setAttribute('checked', 'checked');
element.removeAttribute('checked');
}
element.prepend(input);
}
return {
element,
};
}
exportJSON(): SerializedListItemNode {
return {
...super.exportJSON(),
type: 'custom-list-item',
};
}
}
function $hasNestedListWithoutLabel(node: CustomListItemNode): boolean {
const children = node.getChildren();
let hasLabel = false;
let hasNestedList = false;
for (const child of children) {
if ($isCustomListNode(child)) {
hasNestedList = true;
} else if (child.getTextContent().trim().length > 0) {
hasLabel = true;
}
}
return hasNestedList && !hasLabel;
}
export function $isCustomListItemNode(
node: LexicalNode | null | undefined,
): node is CustomListItemNode {
return node instanceof CustomListItemNode;
}
export function $createCustomListItemNode(): CustomListItemNode {
return new CustomListItemNode();
}

View File

@ -1,139 +0,0 @@
import {
DOMConversionFn,
DOMConversionMap, EditorConfig,
LexicalNode,
Spread
} from "lexical";
import {$isListItemNode, ListItemNode, ListNode, ListType, SerializedListNode} from "@lexical/list";
import {$createCustomListItemNode} from "./custom-list-item";
import {extractDirectionFromElement} from "./_common";
export type SerializedCustomListNode = Spread<{
id: string;
}, SerializedListNode>
export class CustomListNode extends ListNode {
__id: string = '';
static getType() {
return 'custom-list';
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
static clone(node: CustomListNode) {
const newNode = new CustomListNode(node.__listType, node.__start, node.__key);
newNode.__id = node.__id;
newNode.__dir = node.__dir;
return newNode;
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
if (this.__id) {
dom.setAttribute('id', this.__id);
}
if (this.__dir) {
dom.setAttribute('dir', this.__dir);
}
return dom;
}
updateDOM(prevNode: ListNode, dom: HTMLElement, config: EditorConfig): boolean {
return super.updateDOM(prevNode, dom, config) ||
prevNode.__dir !== this.__dir;
}
exportJSON(): SerializedCustomListNode {
return {
...super.exportJSON(),
type: 'custom-list',
version: 1,
id: this.__id,
};
}
static importJSON(serializedNode: SerializedCustomListNode): CustomListNode {
const node = $createCustomListNode(serializedNode.listType);
node.setId(serializedNode.id);
node.setDirection(serializedNode.direction);
return node;
}
static importDOM(): DOMConversionMap | null {
// @ts-ignore
const converter = super.importDOM().ol().conversion as DOMConversionFn<HTMLElement>;
const customConvertFunction = (element: HTMLElement) => {
const baseResult = converter(element);
if (element.id && baseResult?.node) {
(baseResult.node as CustomListNode).setId(element.id);
}
if (element.dir && baseResult?.node) {
(baseResult.node as CustomListNode).setDirection(extractDirectionFromElement(element));
}
if (baseResult) {
baseResult.after = $normalizeChildren;
}
return baseResult;
};
return {
ol: () => ({
conversion: customConvertFunction,
priority: 0,
}),
ul: () => ({
conversion: customConvertFunction,
priority: 0,
}),
};
}
}
/*
* This function is a custom normalization function to allow nested lists within list item elements.
* Original taken from https://github.com/facebook/lexical/blob/6e10210fd1e113ccfafdc999b1d896733c5c5bea/packages/lexical-list/src/LexicalListNode.ts#L284-L303
* With modifications made.
* Copyright (c) Meta Platforms, Inc. and affiliates.
* MIT license
*/
function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> {
const normalizedListItems: Array<ListItemNode> = [];
for (const node of nodes) {
if ($isListItemNode(node)) {
normalizedListItems.push(node);
} else {
normalizedListItems.push($wrapInListItem(node));
}
}
return normalizedListItems;
}
function $wrapInListItem(node: LexicalNode): ListItemNode {
const listItemWrapper = $createCustomListItemNode();
return listItemWrapper.append(node);
}
export function $createCustomListNode(type: ListType): CustomListNode {
return new CustomListNode(type, 1);
}
export function $isCustomListNode(node: LexicalNode | null | undefined): node is CustomListNode {
return node instanceof CustomListNode;
}

View File

@ -17,10 +17,8 @@ import {CodeBlockNode} from "./code-block";
import {DiagramNode} from "./diagram"; import {DiagramNode} from "./diagram";
import {EditorUiContext} from "../ui/framework/core"; import {EditorUiContext} from "../ui/framework/core";
import {MediaNode} from "./media"; import {MediaNode} from "./media";
import {CustomListItemNode} from "./custom-list-item";
import {CustomTableCellNode} from "./custom-table-cell"; import {CustomTableCellNode} from "./custom-table-cell";
import {CustomTableRowNode} from "./custom-table-row"; import {CustomTableRowNode} from "./custom-table-row";
import {CustomListNode} from "./custom-list";
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode"; import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode"; import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
@ -32,8 +30,8 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
CalloutNode, CalloutNode,
HeadingNode, HeadingNode,
QuoteNode, QuoteNode,
CustomListNode, ListNode,
CustomListItemNode, // TODO - Alignment? ListItemNode,
CustomTableNode, CustomTableNode,
CustomTableRowNode, CustomTableRowNode,
CustomTableCellNode, CustomTableCellNode,
@ -45,18 +43,6 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
MediaNode, // TODO - Alignment MediaNode, // TODO - Alignment
ParagraphNode, ParagraphNode,
LinkNode, LinkNode,
{
replace: ListNode,
with: (node: ListNode) => {
return new CustomListNode(node.getListType(), node.getStart());
}
},
{
replace: ListItemNode,
with: (node: ListItemNode) => {
return new CustomListItemNode(node.__value, node.__checked);
}
},
{ {
replace: TableNode, replace: TableNode,
with(node: TableNode) { with(node: TableNode) {

View File

@ -14,8 +14,8 @@ import {$isImageNode} from "../nodes/image";
import {$isMediaNode} from "../nodes/media"; import {$isMediaNode} from "../nodes/media";
import {getLastSelection} from "../utils/selection"; import {getLastSelection} from "../utils/selection";
import {$getNearestNodeBlockParent} from "../utils/nodes"; import {$getNearestNodeBlockParent} from "../utils/nodes";
import {$isCustomListItemNode} from "../nodes/custom-list-item";
import {$setInsetForSelection} from "../utils/lists"; import {$setInsetForSelection} from "../utils/lists";
import {$isListItemNode} from "@lexical/list";
function isSingleSelectedNode(nodes: LexicalNode[]): boolean { function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
if (nodes.length === 1) { if (nodes.length === 1) {
@ -62,7 +62,7 @@ function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boo
const change = event?.shiftKey ? -40 : 40; const change = event?.shiftKey ? -40 : 40;
const selection = $getSelection(); const selection = $getSelection();
const nodes = selection?.getNodes() || []; const nodes = selection?.getNodes() || [];
if (nodes.length > 1 || (nodes.length === 1 && $isCustomListItemNode(nodes[0].getParent()))) { if (nodes.length > 1 || (nodes.length === 1 && $isListItemNode(nodes[0].getParent()))) {
editor.update(() => { editor.update(() => {
$setInsetForSelection(editor, change); $setInsetForSelection(editor, change);
}); });

View File

@ -1,5 +1,5 @@
import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical"; import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
import {$isCustomListItemNode} from "../../../nodes/custom-list-item"; import {$isListItemNode} from "@lexical/list";
class TaskListHandler { class TaskListHandler {
protected editorContainer: HTMLElement; protected editorContainer: HTMLElement;
@ -38,7 +38,7 @@ class TaskListHandler {
this.editor.update(() => { this.editor.update(() => {
const node = $getNearestNodeFromDOMNode(listItem); const node = $getNearestNodeFromDOMNode(listItem);
if ($isCustomListItemNode(node)) { if ($isListItemNode(node)) {
node.setChecked(!node.getChecked()); node.setChecked(!node.getChecked());
} }
}); });

View File

@ -16,8 +16,7 @@ import {
} from "./selection"; } from "./selection";
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block"; import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout"; import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
import {insertList, ListNode, ListType, removeList} from "@lexical/list"; import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
import {$isCustomListNode} from "../nodes/custom-list";
import {$createLinkNode, $isLinkNode} from "@lexical/link"; import {$createLinkNode, $isLinkNode} from "@lexical/link";
import {$createHeadingNode, $isHeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode"; import {$createHeadingNode, $isHeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
import {$createQuoteNode, $isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode"; import {$createQuoteNode, $isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
@ -51,7 +50,7 @@ export function toggleSelectionAsList(editor: LexicalEditor, type: ListType) {
editor.getEditorState().read(() => { editor.getEditorState().read(() => {
const selection = $getSelection(); const selection = $getSelection();
const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => { const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
return $isCustomListNode(node) && (node as ListNode).getListType() === type; return $isListNode(node) && (node as ListNode).getListType() === type;
}); });
if (listSelected) { if (listSelected) {

View File

@ -1,22 +1,21 @@
import {$createCustomListItemNode, $isCustomListItemNode, CustomListItemNode} from "../nodes/custom-list-item";
import {$createCustomListNode, $isCustomListNode} from "../nodes/custom-list";
import {$getSelection, BaseSelection, LexicalEditor} from "lexical"; import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection"; import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
import {nodeHasInset} from "./nodes"; import {nodeHasInset} from "./nodes";
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
export function $nestListItem(node: CustomListItemNode): CustomListItemNode { export function $nestListItem(node: ListItemNode): ListItemNode {
const list = node.getParent(); const list = node.getParent();
if (!$isCustomListNode(list)) { if (!$isListNode(list)) {
return node; return node;
} }
const listItems = list.getChildren() as CustomListItemNode[]; const listItems = list.getChildren() as ListItemNode[];
const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey()); const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey());
const isFirst = nodeIndex === 0; const isFirst = nodeIndex === 0;
const newListItem = $createCustomListItemNode(); const newListItem = $createListItemNode();
const newList = $createCustomListNode(list.getListType()); const newList = $createListNode(list.getListType());
newList.append(newListItem); newList.append(newListItem);
newListItem.append(...node.getChildren()); newListItem.append(...node.getChildren());
@ -31,11 +30,11 @@ export function $nestListItem(node: CustomListItemNode): CustomListItemNode {
return newListItem; return newListItem;
} }
export function $unnestListItem(node: CustomListItemNode): CustomListItemNode { export function $unnestListItem(node: ListItemNode): ListItemNode {
const list = node.getParent(); const list = node.getParent();
const parentListItem = list?.getParent(); const parentListItem = list?.getParent();
const outerList = parentListItem?.getParent(); const outerList = parentListItem?.getParent();
if (!$isCustomListNode(list) || !$isCustomListNode(outerList) || !$isCustomListItemNode(parentListItem)) { if (!$isListNode(list) || !$isListNode(outerList) || !$isListItemNode(parentListItem)) {
return node; return node;
} }
@ -51,19 +50,19 @@ export function $unnestListItem(node: CustomListItemNode): CustomListItemNode {
return node; return node;
} }
function getListItemsForSelection(selection: BaseSelection|null): (CustomListItemNode|null)[] { function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] {
const nodes = selection?.getNodes() || []; const nodes = selection?.getNodes() || [];
const listItemNodes = []; const listItemNodes = [];
outer: for (const node of nodes) { outer: for (const node of nodes) {
if ($isCustomListItemNode(node)) { if ($isListItemNode(node)) {
listItemNodes.push(node); listItemNodes.push(node);
continue; continue;
} }
const parents = node.getParents(); const parents = node.getParents();
for (const parent of parents) { for (const parent of parents) {
if ($isCustomListItemNode(parent)) { if ($isListItemNode(parent)) {
listItemNodes.push(parent); listItemNodes.push(parent);
continue outer; continue outer;
} }
@ -75,8 +74,8 @@ function getListItemsForSelection(selection: BaseSelection|null): (CustomListIte
return listItemNodes; return listItemNodes;
} }
function $reduceDedupeListItems(listItems: (CustomListItemNode|null)[]): CustomListItemNode[] { function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[] {
const listItemMap: Record<string, CustomListItemNode> = {}; const listItemMap: Record<string, ListItemNode> = {};
for (const item of listItems) { for (const item of listItems) {
if (item === null) { if (item === null) {