TextNode,
} from 'lexical';
-import {
- CreateEditorArgs,
- HTMLConfig,
- LexicalNodeReplacement,
-} from '../../LexicalEditor';
+import {CreateEditorArgs, HTMLConfig, LexicalNodeReplacement,} from '../../LexicalEditor';
import {resetRandomKey} from '../../LexicalUtils';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
import {EditorUiContext} from "../../../../ui/framework/core";
import {EditorUIManager} from "../../../../ui/framework/manager";
-import {registerRichText} from "@lexical/rich-text";
+import {turtle} from "@codemirror/legacy-modes/mode/turtle";
type TestEnv = {
expect(formatHtml(expected)).toBe(formatHtml(actual));
}
+type nodeTextShape = {
+ text: string;
+};
+
+type nodeShape = {
+ type: string;
+ children?: (nodeShape|nodeTextShape)[];
+}
+
+export function getNodeShape(node: SerializedLexicalNode): nodeShape|nodeTextShape {
+ // @ts-ignore
+ const children: SerializedLexicalNode[] = (node.children || []);
+
+ const shape: nodeShape = {
+ type: node.type,
+ };
+
+ if (shape.type === 'text') {
+ // @ts-ignore
+ return {text: node.text}
+ }
+
+ if (children.length > 0) {
+ shape.children = children.map(c => getNodeShape(c));
+ }
+
+ return shape;
+}
+
+export function expectNodeShapeToMatch(editor: LexicalEditor, expected: nodeShape[]) {
+ const json = editor.getEditorState().toJSON();
+ const shape = getNodeShape(json.root) as nodeShape;
+ expect(shape.children).toMatchObject(expected);
+}
+
function formatHtml(s: string): string {
return s.replace(/>\s+</g, '><').replace(/\s*\n\s*/g, ' ').trim();
}
--- /dev/null
+import {
+ createTestContext, destroyFromContext,
+ dispatchKeydownEventForNode, expectNodeShapeToMatch,
+} from "lexical/__tests__/utils";
+import {
+ $createParagraphNode, $getRoot, LexicalEditor, LexicalNode,
+ ParagraphNode,
+} from "lexical";
+import {$createDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
+import {EditorUiContext} from "../../ui/framework/core";
+import {$htmlToBlockNodes} from "../nodes";
+import {ListItemNode, ListNode} from "@lexical/list";
+import {$nestListItem, $unnestListItem} from "../lists";
+
+describe('List Utils', () => {
+
+ let context!: EditorUiContext;
+ let editor!: LexicalEditor;
+
+ beforeEach(() => {
+ context = createTestContext();
+ editor = context.editor;
+ });
+
+ afterEach(() => {
+ destroyFromContext(context);
+ });
+
+ describe('$nestListItem', () => {
+ test('nesting handles child items to leave at the same level', () => {
+ const input = `<ul>
+ <li>Inner A</li>
+ <li>Inner B <ul>
+ <li>Inner C</li>
+ </ul></li>
+</ul>`;
+ let list!: ListNode;
+
+ editor.updateAndCommit(() => {
+ $getRoot().append(...$htmlToBlockNodes(editor, input));
+ list = $getRoot().getFirstChild() as ListNode;
+ });
+
+ editor.updateAndCommit(() => {
+ $nestListItem(list.getChildren()[1] as ListItemNode);
+ });
+
+ expectNodeShapeToMatch(editor, [
+ {
+ type: 'list',
+ children: [
+ {
+ type: 'listitem',
+ children: [
+ {text: 'Inner A'},
+ {
+ type: 'list',
+ children: [
+ {type: 'listitem', children: [{text: 'Inner B'}]},
+ {type: 'listitem', children: [{text: 'Inner C'}]},
+ ]
+ }
+ ]
+ },
+ ]
+ }
+ ]);
+ });
+ });
+
+ describe('$unnestListItem', () => {
+ test('middle in nested list converts to new parent item at same place', () => {
+ const input = `<ul>
+<li>Nested list:<ul>
+ <li>Inner A</li>
+ <li>Inner B</li>
+ <li>Inner C</li>
+</ul></li>
+</ul>`;
+ let innerList!: ListNode;
+
+ editor.updateAndCommit(() => {
+ $getRoot().append(...$htmlToBlockNodes(editor, input));
+ innerList = (($getRoot().getFirstChild() as ListNode).getFirstChild() as ListItemNode).getLastChild() as ListNode;
+ });
+
+ editor.updateAndCommit(() => {
+ $unnestListItem(innerList.getChildren()[1] as ListItemNode);
+ });
+
+ expectNodeShapeToMatch(editor, [
+ {
+ type: 'list',
+ children: [
+ {
+ type: 'listitem',
+ children: [
+ {text: 'Nested list:'},
+ {
+ type: 'list',
+ children: [
+ {type: 'listitem', children: [{text: 'Inner A'}]},
+ ],
+ }
+ ],
+ },
+ {
+ type: 'listitem',
+ children: [
+ {text: 'Inner B'},
+ {
+ type: 'list',
+ children: [
+ {type: 'listitem', children: [{text: 'Inner C'}]},
+ ],
+ }
+ ],
+ }
+ ]
+ }
+ ]);
+ });
+ });
+});
\ No newline at end of file
return node;
}
+ const nodeChildList = node.getChildren().filter(n => $isListNode(n))[0] || null;
+ const nodeChildItems = nodeChildList?.getChildren() || [];
+
const listItems = list.getChildren() as ListItemNode[];
const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey());
const isFirst = nodeIndex === 0;
node.remove();
}
+ if (nodeChildList) {
+ for (const child of nodeChildItems) {
+ newListItem.insertAfter(child);
+ }
+ nodeChildList.remove();
+ }
+
return newListItem;
}
return node;
}
+ const laterSiblings = node.getNextSiblings();
+
parentListItem.insertAfter(node);
if (list.getChildren().length === 0) {
list.remove();
parentListItem.remove();
}
+ if (laterSiblings.length > 0) {
+ const childList = $createListNode(list.getListType());
+ childList.append(...laterSiblings);
+ node.append(childList);
+ }
+
+ if (list.getChildrenSize() === 0) {
+ list.remove();
+ }
+
return node;
}