mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-05 21:35:24 +00:00
Lexical: Improved list tab handling, Improved test utils
- Made tab work on empty list items - Improved select preservation on single list item tab - Altered test context creation for more standard testing
This commit is contained in:
parent
e50cd33277
commit
ace8af077d
4 changed files with 154 additions and 70 deletions
resources/js/wysiwyg
lexical/core/__tests__/utils
services
utils
|
@ -472,16 +472,34 @@ export function createTestHeadlessEditor(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTestContext(env: TestEnv): EditorUiContext {
|
export function createTestContext(): EditorUiContext {
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
const scrollWrap = document.createElement('div');
|
||||||
|
const editorDOM = document.createElement('div');
|
||||||
|
editorDOM.setAttribute('contenteditable', 'true');
|
||||||
|
|
||||||
|
scrollWrap.append(editorDOM);
|
||||||
|
container.append(scrollWrap);
|
||||||
|
|
||||||
|
const editor = createTestEditor({
|
||||||
|
namespace: 'testing',
|
||||||
|
theme: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.setRootElement(editorDOM);
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
containerDOM: document.createElement('div'),
|
containerDOM: container,
|
||||||
editor: env.editor,
|
editor: editor,
|
||||||
editorDOM: document.createElement('div'),
|
editorDOM: editorDOM,
|
||||||
error(text: string | Error): void {
|
error(text: string | Error): void {
|
||||||
},
|
},
|
||||||
manager: new EditorUIManager(),
|
manager: new EditorUIManager(),
|
||||||
options: {},
|
options: {},
|
||||||
scrollDOM: document.createElement('div'),
|
scrollDOM: scrollWrap,
|
||||||
translate(text: string): string {
|
translate(text: string): string {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -492,6 +510,10 @@ export function createTestContext(env: TestEnv): EditorUiContext {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function destroyFromContext(context: EditorUiContext) {
|
||||||
|
context.containerDOM.remove();
|
||||||
|
}
|
||||||
|
|
||||||
export function $assertRangeSelection(selection: unknown): RangeSelection {
|
export function $assertRangeSelection(selection: unknown): RangeSelection {
|
||||||
if (!$isRangeSelection(selection)) {
|
if (!$isRangeSelection(selection)) {
|
||||||
throw new Error(`Expected RangeSelection, got ${selection}`);
|
throw new Error(`Expected RangeSelection, got ${selection}`);
|
||||||
|
|
|
@ -1,95 +1,135 @@
|
||||||
import {
|
import {
|
||||||
createTestContext,
|
createTestContext, destroyFromContext,
|
||||||
dispatchKeydownEventForNode,
|
dispatchKeydownEventForNode,
|
||||||
dispatchKeydownEventForSelectedNode,
|
dispatchKeydownEventForSelectedNode,
|
||||||
initializeUnitTest
|
|
||||||
} from "lexical/__tests__/utils";
|
} from "lexical/__tests__/utils";
|
||||||
import {
|
import {
|
||||||
$createParagraphNode, $createTextNode,
|
$createParagraphNode, $createTextNode,
|
||||||
$getRoot, LexicalNode,
|
$getRoot, $getSelection, LexicalEditor, LexicalNode,
|
||||||
ParagraphNode,
|
ParagraphNode, TextNode,
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import {$createDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
|
import {$createDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
|
||||||
import {registerKeyboardHandling} from "../keyboard-handling";
|
import {registerKeyboardHandling} from "../keyboard-handling";
|
||||||
import {registerRichText} from "@lexical/rich-text";
|
import {registerRichText} from "@lexical/rich-text";
|
||||||
|
import {EditorUiContext} from "../../ui/framework/core";
|
||||||
|
import {$createListItemNode, $createListNode, ListItemNode, ListNode} from "@lexical/list";
|
||||||
|
|
||||||
describe('Keyboard-handling service tests', () => {
|
describe('Keyboard-handling service tests', () => {
|
||||||
initializeUnitTest((testEnv) => {
|
|
||||||
|
|
||||||
test('Details: down key on last lines creates new sibling node', () => {
|
let context!: EditorUiContext;
|
||||||
const {editor} = testEnv;
|
let editor!: LexicalEditor;
|
||||||
|
|
||||||
registerRichText(editor);
|
beforeEach(() => {
|
||||||
registerKeyboardHandling(createTestContext(testEnv));
|
context = createTestContext();
|
||||||
|
editor = context.editor;
|
||||||
|
registerRichText(editor);
|
||||||
|
registerKeyboardHandling(context);
|
||||||
|
});
|
||||||
|
|
||||||
let lastRootChild!: LexicalNode|null;
|
afterEach(() => {
|
||||||
let detailsPara!: ParagraphNode;
|
destroyFromContext(context);
|
||||||
|
});
|
||||||
|
|
||||||
editor.updateAndCommit(() => {
|
test('Details: down key on last lines creates new sibling node', () => {
|
||||||
const root = $getRoot()
|
let lastRootChild!: LexicalNode|null;
|
||||||
const details = $createDetailsNode();
|
let detailsPara!: ParagraphNode;
|
||||||
detailsPara = $createParagraphNode();
|
|
||||||
details.append(detailsPara);
|
|
||||||
$getRoot().append(details);
|
|
||||||
detailsPara.select();
|
|
||||||
|
|
||||||
lastRootChild = root.getLastChild();
|
editor.updateAndCommit(() => {
|
||||||
});
|
const root = $getRoot()
|
||||||
|
const details = $createDetailsNode();
|
||||||
|
detailsPara = $createParagraphNode();
|
||||||
|
details.append(detailsPara);
|
||||||
|
$getRoot().append(details);
|
||||||
|
detailsPara.select();
|
||||||
|
|
||||||
expect(lastRootChild).toBeInstanceOf(DetailsNode);
|
lastRootChild = root.getLastChild();
|
||||||
|
|
||||||
dispatchKeydownEventForNode(detailsPara, editor, 'ArrowDown');
|
|
||||||
editor.commitUpdates();
|
|
||||||
|
|
||||||
editor.getEditorState().read(() => {
|
|
||||||
lastRootChild = $getRoot().getLastChild();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(lastRootChild).toBeInstanceOf(ParagraphNode);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Details: enter on last empy block creates new sibling node', () => {
|
expect(lastRootChild).toBeInstanceOf(DetailsNode);
|
||||||
const {editor} = testEnv;
|
|
||||||
|
|
||||||
registerRichText(editor);
|
dispatchKeydownEventForNode(detailsPara, editor, 'ArrowDown');
|
||||||
registerKeyboardHandling(createTestContext(testEnv));
|
editor.commitUpdates();
|
||||||
|
|
||||||
let lastRootChild!: LexicalNode|null;
|
editor.getEditorState().read(() => {
|
||||||
let detailsPara!: ParagraphNode;
|
lastRootChild = $getRoot().getLastChild();
|
||||||
|
});
|
||||||
|
|
||||||
editor.updateAndCommit(() => {
|
expect(lastRootChild).toBeInstanceOf(ParagraphNode);
|
||||||
const root = $getRoot()
|
});
|
||||||
const details = $createDetailsNode();
|
|
||||||
const text = $createTextNode('Hello!');
|
|
||||||
detailsPara = $createParagraphNode();
|
|
||||||
detailsPara.append(text);
|
|
||||||
details.append(detailsPara);
|
|
||||||
$getRoot().append(details);
|
|
||||||
text.selectEnd();
|
|
||||||
|
|
||||||
lastRootChild = root.getLastChild();
|
test('Details: enter on last empty block creates new sibling node', () => {
|
||||||
});
|
registerRichText(editor);
|
||||||
|
|
||||||
expect(lastRootChild).toBeInstanceOf(DetailsNode);
|
let lastRootChild!: LexicalNode|null;
|
||||||
|
let detailsPara!: ParagraphNode;
|
||||||
|
|
||||||
dispatchKeydownEventForNode(detailsPara, editor, 'Enter');
|
editor.updateAndCommit(() => {
|
||||||
editor.commitUpdates();
|
const root = $getRoot()
|
||||||
|
const details = $createDetailsNode();
|
||||||
|
const text = $createTextNode('Hello!');
|
||||||
|
detailsPara = $createParagraphNode();
|
||||||
|
detailsPara.append(text);
|
||||||
|
details.append(detailsPara);
|
||||||
|
$getRoot().append(details);
|
||||||
|
text.selectEnd();
|
||||||
|
|
||||||
dispatchKeydownEventForSelectedNode(editor, 'Enter');
|
lastRootChild = root.getLastChild();
|
||||||
editor.commitUpdates();
|
});
|
||||||
|
|
||||||
let detailsChildren!: LexicalNode[];
|
expect(lastRootChild).toBeInstanceOf(DetailsNode);
|
||||||
let lastDetailsText!: string;
|
|
||||||
|
|
||||||
editor.getEditorState().read(() => {
|
dispatchKeydownEventForNode(detailsPara, editor, 'Enter');
|
||||||
detailsChildren = (lastRootChild as DetailsNode).getChildren();
|
editor.commitUpdates();
|
||||||
lastRootChild = $getRoot().getLastChild();
|
|
||||||
lastDetailsText = detailsChildren[0].getTextContent();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(lastRootChild).toBeInstanceOf(ParagraphNode);
|
dispatchKeydownEventForSelectedNode(editor, 'Enter');
|
||||||
expect(detailsChildren).toHaveLength(1);
|
editor.commitUpdates();
|
||||||
expect(lastDetailsText).toBe('Hello!');
|
|
||||||
|
let detailsChildren!: LexicalNode[];
|
||||||
|
let lastDetailsText!: string;
|
||||||
|
|
||||||
|
editor.getEditorState().read(() => {
|
||||||
|
detailsChildren = (lastRootChild as DetailsNode).getChildren();
|
||||||
|
lastRootChild = $getRoot().getLastChild();
|
||||||
|
lastDetailsText = detailsChildren[0].getTextContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastRootChild).toBeInstanceOf(ParagraphNode);
|
||||||
|
expect(detailsChildren).toHaveLength(1);
|
||||||
|
expect(lastDetailsText).toBe('Hello!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Lists: tab on empty list item insets item', () => {
|
||||||
|
|
||||||
|
let list!: ListNode;
|
||||||
|
let listItemB!: ListItemNode;
|
||||||
|
|
||||||
|
editor.updateAndCommit(() => {
|
||||||
|
const root = $getRoot();
|
||||||
|
list = $createListNode('bullet');
|
||||||
|
const listItemA = $createListItemNode();
|
||||||
|
listItemA.append($createTextNode('Hello!'));
|
||||||
|
listItemB = $createListItemNode();
|
||||||
|
list.append(listItemA, listItemB);
|
||||||
|
root.append(list);
|
||||||
|
listItemB.selectStart();
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatchKeydownEventForNode(listItemB, editor, 'Tab');
|
||||||
|
editor.commitUpdates();
|
||||||
|
|
||||||
|
editor.getEditorState().read(() => {
|
||||||
|
const list = $getRoot().getChildren()[0] as ListNode;
|
||||||
|
const listChild = list.getChildren()[0] as ListItemNode;
|
||||||
|
const children = listChild.getChildren();
|
||||||
|
expect(children).toHaveLength(2);
|
||||||
|
expect(children[0]).toBeInstanceOf(TextNode);
|
||||||
|
expect(children[0].getTextContent()).toBe('Hello!');
|
||||||
|
expect(children[1]).toBeInstanceOf(ListNode);
|
||||||
|
|
||||||
|
const innerList = children[1] as ListNode;
|
||||||
|
const selectedNode = $getSelection()?.getNodes()[0];
|
||||||
|
expect(selectedNode).toBeInstanceOf(ListItemNode);
|
||||||
|
expect(selectedNode?.getKey()).toBe(innerList.getChildren()[0].getKey());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -151,6 +151,15 @@ function getDetailsScenario(editor: LexicalEditor): {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function $isSingleListItem(nodes: LexicalNode[]): boolean {
|
||||||
|
if (nodes.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = nodes[0];
|
||||||
|
return $isListItemNode(node) || $isListItemNode(node.getParent());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inset the nodes within selection when a range of nodes is selected
|
* Inset the nodes within selection when a range of nodes is selected
|
||||||
* or if a list node is selected.
|
* or if a list node is selected.
|
||||||
|
@ -159,7 +168,7 @@ function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boo
|
||||||
const change = event?.shiftKey ? -40 : 40;
|
const change = event?.shiftKey ? -40 : 40;
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
const nodes = selection?.getNodes() || [];
|
const nodes = selection?.getNodes() || [];
|
||||||
if (nodes.length > 1 || (nodes.length === 1 && $isListItemNode(nodes[0].getParent()))) {
|
if (nodes.length > 1 || $isSingleListItem(nodes)) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
$setInsetForSelection(editor, change);
|
$setInsetForSelection(editor, change);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
|
import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical";
|
||||||
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
|
import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
|
||||||
import {nodeHasInset} from "./nodes";
|
import {nodeHasInset} from "./nodes";
|
||||||
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
|
import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
|
||||||
|
@ -93,6 +93,7 @@ function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[
|
||||||
|
|
||||||
export function $setInsetForSelection(editor: LexicalEditor, change: number): void {
|
export function $setInsetForSelection(editor: LexicalEditor, change: number): void {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
|
const selectionBounds = selection?.getStartEndPoints();
|
||||||
const listItemsInSelection = getListItemsForSelection(selection);
|
const listItemsInSelection = getListItemsForSelection(selection);
|
||||||
const isListSelection = listItemsInSelection.length > 0 && !listItemsInSelection.includes(null);
|
const isListSelection = listItemsInSelection.length > 0 && !listItemsInSelection.includes(null);
|
||||||
|
|
||||||
|
@ -110,7 +111,19 @@ export function $setInsetForSelection(editor: LexicalEditor, change: number): vo
|
||||||
alteredListItems.reverse();
|
alteredListItems.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectNodes(alteredListItems);
|
if (alteredListItems.length === 1 && selectionBounds) {
|
||||||
|
// Retain selection range if moving just one item
|
||||||
|
const listItem = alteredListItems[0] as ListItemNode;
|
||||||
|
let child = listItem.getChildren()[0] as TextNode;
|
||||||
|
if (!child) {
|
||||||
|
child = $createTextNode('');
|
||||||
|
listItem.append(child);
|
||||||
|
}
|
||||||
|
child.select(selectionBounds[0].offset, selectionBounds[1].offset);
|
||||||
|
} else {
|
||||||
|
$selectNodes(alteredListItems);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue