mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-07-15 21:31:36 +03:00
Lexical: Improved ability to break out of lists
Updates list handling so that you can break out of a list (or move down a level) via enter on an existing empty list item at any point in the list, not just the end. Added test to cover.
This commit is contained in:
@@ -36,6 +36,7 @@ import {$createListNode, $isListNode} from './LexicalListNode';
|
||||
import {mergeLists} from './formatList';
|
||||
import {isNestedListNode} from './utils';
|
||||
import {el} from "../../utils/dom";
|
||||
import {$escapeListAtItem} from "../../utils/lists";
|
||||
|
||||
export type SerializedListItemNode = Spread<
|
||||
{
|
||||
@@ -273,21 +274,13 @@ export class ListItemNode extends ElementNode {
|
||||
restoreSelection = true,
|
||||
): ListItemNode | ParagraphNode | null {
|
||||
|
||||
if (this.getTextContent().trim() === '' && this.isLastChild()) {
|
||||
const list = this.getParentOrThrow<ListNode>();
|
||||
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();
|
||||
return paragraph;
|
||||
}
|
||||
// If we're adding a new empty item, and coming from an empty item,
|
||||
// take that as a desire to break from the current list level.
|
||||
// Intended to be a bit more lenient on the last list item hence ignores whitespace,
|
||||
// which allows empty items to be listed with just a space (except for last).
|
||||
const textContent = this.getTextContent();
|
||||
if (textContent === '' || (textContent.trim() === '' && this.isLastChild())) {
|
||||
return $escapeListAtItem(this);
|
||||
}
|
||||
|
||||
const newElement = $createListItemNode(
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$createRangeSelection,
|
||||
$getRoot, LexicalEditor,
|
||||
$getRoot, LexicalEditor, LexicalNode, ParagraphNode,
|
||||
TextNode,
|
||||
} from 'lexical';
|
||||
import {
|
||||
createTestContext, destroyFromContext,
|
||||
expectHtmlToBeEqual,
|
||||
expectHtmlToBeEqual, expectNodeShapeToMatch,
|
||||
html,
|
||||
} from 'lexical/__tests__/utils';
|
||||
|
||||
@@ -1200,6 +1200,70 @@ describe('LexicalListItemNode tests', () => {
|
||||
</ul>`,
|
||||
);
|
||||
});
|
||||
|
||||
test('new items after empty top-level items splits the list in two, and inserts a paragraph inbetween', () => {
|
||||
let newItem: LexicalNode|null = null;
|
||||
const input = `<ul>
|
||||
<li>Item A</li>
|
||||
<li></li>
|
||||
<li>Item C</li>
|
||||
</ul>`;
|
||||
|
||||
editor.updateAndCommit(() => {
|
||||
const root = $getRoot();
|
||||
root.append(...$htmlToBlockNodes(editor, input));
|
||||
const list = root.getFirstChild() as ListNode;
|
||||
const itemB = list.getChildAtIndex(1) as ListItemNode;
|
||||
|
||||
newItem = itemB.insertNewAfter($createRangeSelection());
|
||||
});
|
||||
|
||||
expect(newItem).toBeInstanceOf(ParagraphNode);
|
||||
|
||||
expectNodeShapeToMatch(editor, [
|
||||
{
|
||||
type: 'list',
|
||||
children: [{type: 'listitem', children: [{text: 'Item A'}]}],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
children: [{type: 'listitem', children: [{text: 'Item C'}]}],
|
||||
}
|
||||
])
|
||||
});
|
||||
|
||||
test('new items after last empty top-level inserts a new paragraph below, and removes the list item', () => {
|
||||
let newItem: LexicalNode|null = null;
|
||||
const input = `<ul>
|
||||
<li>Item A</li>
|
||||
<li>Item B</li>
|
||||
<li></li>
|
||||
</ul>`;
|
||||
|
||||
editor.updateAndCommit(() => {
|
||||
const root = $getRoot();
|
||||
root.append(...$htmlToBlockNodes(editor, input));
|
||||
const list = root.getFirstChild() as ListNode;
|
||||
const itemC = list.getChildAtIndex(2) as ListItemNode;
|
||||
|
||||
newItem = itemC.insertNewAfter($createRangeSelection());
|
||||
});
|
||||
|
||||
expect(newItem).toBeInstanceOf(ParagraphNode);
|
||||
|
||||
expectNodeShapeToMatch(editor, [
|
||||
{
|
||||
type: 'list',
|
||||
children: [{type: 'listitem', children: [{text: 'Item A'}]}, {type: 'listitem', children: [{text: 'Item B'}]}],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
},
|
||||
])
|
||||
});
|
||||
});
|
||||
|
||||
test('$createListItemNode()', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical";
|
||||
import {$createParagraphNode, $createTextNode, $getSelection, BaseSelection, LexicalEditor, ParagraphNode, TextNode} from "lexical";
|
||||
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
|
||||
import {$sortNodes, nodeHasInset} from "./nodes";
|
||||
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
|
||||
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode, ListNode} from "@lexical/list";
|
||||
|
||||
|
||||
export function $nestListItem(node: ListItemNode): ListItemNode {
|
||||
@@ -186,4 +186,31 @@ export function $setInsetForSelection(editor: LexicalEditor, change: number): vo
|
||||
}
|
||||
|
||||
$toggleSelection(editor);
|
||||
}
|
||||
}
|
||||
|
||||
export function $escapeListAtItem(item: ListItemNode): ParagraphNode|null {
|
||||
const list = item.getParentOrThrow<ListNode>();
|
||||
const parentListItem = list.getParent();
|
||||
|
||||
// Un-nest list item if it's an empty nested item
|
||||
if ($isListItemNode(parentListItem)) {
|
||||
parentListItem.insertAfter(item);
|
||||
item.selectStart();
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we're anywhere but at the end, split the list, with following items
|
||||
// moved into their own new list.
|
||||
if (!item.isLastChild()) {
|
||||
const afterSiblings = item.getNextSiblings();
|
||||
const newList = $createListNode(list.getListType());
|
||||
newList.append(...afterSiblings);
|
||||
list.insertAfter(newList);
|
||||
}
|
||||
|
||||
// Insert a new empty paragraph to land on after our list
|
||||
const paragraph = $createParagraphNode();
|
||||
list.insertAfter(paragraph, true);
|
||||
item.remove();
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user