Skip to content

Commit

Permalink
Lexical: Adjusted handling of child/sibling list items on nesting
Browse files Browse the repository at this point in the history
Sibling/child items will now remain at the same visual level during
nesting/un-nested, so only the selected item level is visually altered.

Also added new model-based editor content matching system for tests.
  • Loading branch information
ssddanbrown committed Dec 17, 2024
1 parent fca8f92 commit f4005a1
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 6 deletions.
43 changes: 37 additions & 6 deletions resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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+</g, '><').replace(/\s*\n\s*/g, ' ').trim();
}
Expand Down
124 changes: 124 additions & 0 deletions resources/js/wysiwyg/utils/__tests__/lists.test.ts
Original file line number Diff line number Diff line change
@@ -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 = `<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'}]},
],
}
],
}
]
}
]);
});
});
});
22 changes: 22 additions & 0 deletions resources/js/wysiwyg/utils/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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();
Expand All @@ -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;
}

Expand Down

0 comments on commit f4005a1

Please sign in to comment.