diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts index d90853b7c34..b13bba6977e 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts @@ -30,18 +30,14 @@ import { 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 = { @@ -764,6 +760,41 @@ export function expectHtmlToBeEqual(expected: string, actual: string): void { 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+<').replace(/\s*\n\s*/g, ' ').trim(); } diff --git a/resources/js/wysiwyg/utils/__tests__/lists.test.ts b/resources/js/wysiwyg/utils/__tests__/lists.test.ts new file mode 100644 index 00000000000..20dcad24098 --- /dev/null +++ b/resources/js/wysiwyg/utils/__tests__/lists.test.ts @@ -0,0 +1,124 @@ +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 = ``; + 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 = ``; + 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 diff --git a/resources/js/wysiwyg/utils/lists.ts b/resources/js/wysiwyg/utils/lists.ts index 2fc1c5f6b4e..005b05f9816 100644 --- a/resources/js/wysiwyg/utils/lists.ts +++ b/resources/js/wysiwyg/utils/lists.ts @@ -10,6 +10,9 @@ export function $nestListItem(node: ListItemNode): ListItemNode { 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; @@ -27,6 +30,13 @@ export function $nestListItem(node: ListItemNode): ListItemNode { node.remove(); } + if (nodeChildList) { + for (const child of nodeChildItems) { + newListItem.insertAfter(child); + } + nodeChildList.remove(); + } + return newListItem; } @@ -38,6 +48,8 @@ export function $unnestListItem(node: ListItemNode): ListItemNode { return node; } + const laterSiblings = node.getNextSiblings(); + parentListItem.insertAfter(node); if (list.getChildren().length === 0) { list.remove(); @@ -47,6 +59,16 @@ export function $unnestListItem(node: ListItemNode): ListItemNode { 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; }