0
0
Fork 0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-05-04 16:20:26 +00:00

Lexical: Aligned new empty item behaviour for nested lists

- Makes enter on empty nested list item un-nest instead of just creating
  new list items.
- Also updated existing lists tests to use newer helper setup.
This commit is contained in:
Dan Brown 2024-12-17 16:50:03 +00:00
parent ace8af077d
commit fca8f928a3
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
4 changed files with 635 additions and 617 deletions
resources/js/wysiwyg
lexical
core/__tests__/utils
list
services/__tests__

View file

@ -776,6 +776,7 @@ export function dispatchKeydownEventForNode(node: LexicalNode, editor: LexicalEd
key,
});
nodeDomEl?.dispatchEvent(event);
editor.commitUpdates();
}
export function dispatchKeydownEventForSelectedNode(editor: LexicalEditor, key: string) {

View file

@ -271,11 +271,18 @@ export class ListItemNode extends ElementNode {
insertNewAfter(
_: RangeSelection,
restoreSelection = true,
): ListItemNode | ParagraphNode {
): ListItemNode | ParagraphNode | null {
if (this.getTextContent().trim() === '' && this.isLastChild()) {
const list = this.getParentOrThrow<ListNode>();
if (!$isListItemNode(list.getParent())) {
const parentListItem = list.getParent();
if ($isListItemNode(parentListItem)) {
// Un-nest list item if empty nested item
parentListItem.insertAfter(this);
this.selectStart();
return null;
} else {
// Insert empty paragraph after list if adding after last empty child
const paragraph = $createParagraphNode();
list.insertAfter(paragraph, restoreSelection);
this.remove();

View file

@ -9,13 +9,13 @@
import {
$createParagraphNode,
$createRangeSelection,
$getRoot,
$getRoot, LexicalEditor,
TextNode,
} from 'lexical';
import {
createTestContext, destroyFromContext,
expectHtmlToBeEqual,
html,
initializeUnitTest,
} from 'lexical/__tests__/utils';
import {
@ -24,23 +24,24 @@ import {
ListItemNode,
ListNode,
} from '../..';
const editorConfig = Object.freeze({
namespace: '',
theme: {
list: {
listitem: 'my-listItem-item-class',
nested: {
listitem: 'my-nested-list-listItem-class',
},
},
},
});
import {EditorUiContext} from "../../../../ui/framework/core";
import {$htmlToBlockNodes} from "../../../../utils/nodes";
describe('LexicalListItemNode tests', () => {
initializeUnitTest((testEnv) => {
let context!: EditorUiContext;
let editor!: LexicalEditor;
beforeEach(() => {
context = createTestContext();
editor = context.editor;
});
afterEach(() => {
destroyFromContext(context);
});
test('ListItemNode.constructor', async () => {
const {editor} = testEnv;
await editor.update(() => {
const listItemNode = new ListItemNode();
@ -54,13 +55,12 @@ describe('LexicalListItemNode tests', () => {
});
test('ListItemNode.createDOM()', async () => {
const {editor} = testEnv;
await editor.update(() => {
const listItemNode = new ListItemNode();
expectHtmlToBeEqual(
listItemNode.createDOM(editorConfig).outerHTML,
listItemNode.createDOM(editor._config).outerHTML,
html`
<li value="1"></li>
`,
@ -80,12 +80,11 @@ describe('LexicalListItemNode tests', () => {
describe('ListItemNode.updateDOM()', () => {
test('base', async () => {
const {editor} = testEnv;
await editor.update(() => {
const listItemNode = new ListItemNode();
const domElement = listItemNode.createDOM(editorConfig);
const domElement = listItemNode.createDOM(editor._config);
expectHtmlToBeEqual(
domElement.outerHTML,
@ -98,7 +97,7 @@ describe('LexicalListItemNode tests', () => {
const result = newListItemNode.updateDOM(
listItemNode,
domElement,
editorConfig,
editor._config,
);
expect(result).toBe(false);
@ -113,14 +112,13 @@ describe('LexicalListItemNode tests', () => {
});
test('nested list', async () => {
const {editor} = testEnv;
await editor.update(() => {
const parentListNode = new ListNode('bullet', 1);
const parentlistItemNode = new ListItemNode();
parentListNode.append(parentlistItemNode);
const domElement = parentlistItemNode.createDOM(editorConfig);
const domElement = parentlistItemNode.createDOM(editor._config);
expectHtmlToBeEqual(
domElement.outerHTML,
@ -134,7 +132,7 @@ describe('LexicalListItemNode tests', () => {
const result = parentlistItemNode.updateDOM(
parentlistItemNode,
domElement,
editorConfig,
editor._config,
);
expect(result).toBe(false);
@ -156,7 +154,6 @@ describe('LexicalListItemNode tests', () => {
let listItemNode3: ListItemNode;
beforeEach(async () => {
const {editor} = testEnv;
await editor.update(() => {
const root = $getRoot();
@ -175,7 +172,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -198,7 +195,6 @@ describe('LexicalListItemNode tests', () => {
});
test('another list item node', async () => {
const {editor} = testEnv;
await editor.update(() => {
const newListItemNode = new ListItemNode();
@ -208,7 +204,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -231,14 +227,13 @@ describe('LexicalListItemNode tests', () => {
});
test('first list item with a non list item node', async () => {
const {editor} = testEnv;
await editor.update(() => {
return;
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -265,7 +260,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -286,7 +281,6 @@ describe('LexicalListItemNode tests', () => {
});
test('last list item with a non list item node', async () => {
const {editor} = testEnv;
await editor.update(() => {
const paragraphNode = $createParagraphNode();
@ -294,7 +288,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -315,7 +309,6 @@ describe('LexicalListItemNode tests', () => {
});
test('middle list item with a non list item node', async () => {
const {editor} = testEnv;
await editor.update(() => {
const paragraphNode = $createParagraphNode();
@ -323,7 +316,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -346,7 +339,6 @@ describe('LexicalListItemNode tests', () => {
});
test('the only list item with a non list item node', async () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode2.remove();
@ -354,7 +346,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -375,7 +367,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -393,7 +385,6 @@ describe('LexicalListItemNode tests', () => {
// - x
// - B
test('siblings are not nested', async () => {
const {editor} = testEnv;
let x: ListItemNode;
await editor.update(() => {
@ -414,7 +405,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -438,7 +429,7 @@ describe('LexicalListItemNode tests', () => {
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -461,7 +452,6 @@ describe('LexicalListItemNode tests', () => {
// - x
// - B
test('the previous sibling is nested', async () => {
const {editor} = testEnv;
let x: ListItemNode;
await editor.update(() => {
@ -486,7 +476,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -509,7 +499,7 @@ describe('LexicalListItemNode tests', () => {
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -531,7 +521,6 @@ describe('LexicalListItemNode tests', () => {
// - x
// - B
test('the next sibling is nested', async () => {
const {editor} = testEnv;
let x: ListItemNode;
await editor.update(() => {
@ -556,7 +545,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1">
@ -579,7 +568,7 @@ describe('LexicalListItemNode tests', () => {
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1">
@ -601,7 +590,6 @@ describe('LexicalListItemNode tests', () => {
// - x
// - B
test('both siblings are nested', async () => {
const {editor} = testEnv;
let x: ListItemNode;
await editor.update(() => {
@ -630,7 +618,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -657,7 +645,7 @@ describe('LexicalListItemNode tests', () => {
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -680,7 +668,6 @@ describe('LexicalListItemNode tests', () => {
// - x
// - B
test('the previous sibling is nested deeper than the next sibling', async () => {
const {editor} = testEnv;
let x: ListItemNode;
await editor.update(() => {
@ -716,7 +703,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -750,7 +737,7 @@ describe('LexicalListItemNode tests', () => {
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -780,7 +767,6 @@ describe('LexicalListItemNode tests', () => {
// - B1
// - B2
test('the next sibling is nested deeper than the previous sibling', async () => {
const {editor} = testEnv;
let x: ListItemNode;
await editor.update(() => {
@ -816,7 +802,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -850,7 +836,7 @@ describe('LexicalListItemNode tests', () => {
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -881,7 +867,6 @@ describe('LexicalListItemNode tests', () => {
// - B1
// - B2
test('both siblings are deeply nested', async () => {
const {editor} = testEnv;
let x: ListItemNode;
await editor.update(() => {
@ -924,7 +909,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -965,7 +950,7 @@ describe('LexicalListItemNode tests', () => {
await editor.update(() => x.remove());
expectHtmlToBeEqual(
testEnv.innerHTML,
context.editorDOM.innerHTML,
html`
<ul>
<li value="1" style="list-style: none;">
@ -1001,7 +986,6 @@ describe('LexicalListItemNode tests', () => {
let listItemNode3: ListItemNode;
beforeEach(async () => {
const {editor} = testEnv;
await editor.update(() => {
const root = $getRoot();
@ -1020,7 +1004,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -1043,14 +1027,13 @@ describe('LexicalListItemNode tests', () => {
});
test('first list item', async () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode1.insertNewAfter($createRangeSelection());
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -1074,14 +1057,13 @@ describe('LexicalListItemNode tests', () => {
});
test('last list item', async () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode3.insertNewAfter($createRangeSelection());
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -1105,14 +1087,13 @@ describe('LexicalListItemNode tests', () => {
});
test('middle list item', async () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode3.insertNewAfter($createRangeSelection());
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -1136,15 +1117,13 @@ describe('LexicalListItemNode tests', () => {
});
test('the only list item', async () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode2.remove();
listItemNode3.remove();
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -1164,7 +1143,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
testEnv.outerHTML,
context.editorDOM.outerHTML,
html`
<div
contenteditable="true"
@ -1182,9 +1161,48 @@ describe('LexicalListItemNode tests', () => {
});
});
test('$createListItemNode()', async () => {
const {editor} = testEnv;
describe('ListItemNode.insertNewAfter()', () => {
test('new items after empty nested items un-nests the current item instead of creating new', () => {
let nestedItem!: ListItemNode;
const input = `<ul>
<li>
Item A
<ul><li>Nested item A</li></ul>
</li>
<li>Item B</li>
</ul>`;
editor.updateAndCommit(() => {
const root = $getRoot();
root.append(...$htmlToBlockNodes(editor, input));
const list = root.getFirstChild() as ListNode;
const itemA = list.getFirstChild() as ListItemNode;
const nestedList = itemA.getLastChild() as ListNode;
nestedItem = nestedList.getFirstChild() as ListItemNode;
nestedList.selectEnd();
});
editor.updateAndCommit(() => {
nestedItem.insertNewAfter($createRangeSelection());
const newItem = nestedItem.getNextSibling() as ListItemNode;
newItem.insertNewAfter($createRangeSelection());
});
expectHtmlToBeEqual(
context.editorDOM.innerHTML,
html`<ul>
<li value="1">
<span data-lexical-text="true">Item A</span>
<ul><li value="1"><span data-lexical-text="true">Nested item A</span></li></ul>
</li>
<li value="2"><br></li>
<li value="3"><span data-lexical-text="true">Item B</span></li>
</ul>`,
);
});
});
test('$createListItemNode()', async () => {
await editor.update(() => {
const listItemNode = new ListItemNode();
@ -1197,8 +1215,6 @@ describe('LexicalListItemNode tests', () => {
});
test('$isListItemNode()', async () => {
const {editor} = testEnv;
await editor.update(() => {
const listItemNode = new ListItemNode();
@ -1206,4 +1222,3 @@ describe('LexicalListItemNode tests', () => {
});
});
});
});

View file

@ -48,7 +48,6 @@ describe('Keyboard-handling service tests', () => {
expect(lastRootChild).toBeInstanceOf(DetailsNode);
dispatchKeydownEventForNode(detailsPara, editor, 'ArrowDown');
editor.commitUpdates();
editor.getEditorState().read(() => {
lastRootChild = $getRoot().getLastChild();
@ -79,10 +78,7 @@ describe('Keyboard-handling service tests', () => {
expect(lastRootChild).toBeInstanceOf(DetailsNode);
dispatchKeydownEventForNode(detailsPara, editor, 'Enter');
editor.commitUpdates();
dispatchKeydownEventForSelectedNode(editor, 'Enter');
editor.commitUpdates();
let detailsChildren!: LexicalNode[];
let lastDetailsText!: string;
@ -115,7 +111,6 @@ describe('Keyboard-handling service tests', () => {
});
dispatchKeydownEventForNode(listItemB, editor, 'Tab');
editor.commitUpdates();
editor.getEditorState().read(() => {
const list = $getRoot().getChildren()[0] as ListNode;