mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-01-31 20:21:36 +01:00
Lexical: Added mulitple methods to escape details block
Enter on empty last line, or down on last empty line, will focus on the next node after details, or created a new paragraph to focus on if needed.
This commit is contained in:
parent
5887322178
commit
8486775edf
@ -3,7 +3,7 @@ import {
|
||||
$createParagraphNode,
|
||||
$getSelection,
|
||||
$isDecoratorNode,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND,
|
||||
KEY_BACKSPACE_COMMAND,
|
||||
KEY_DELETE_COMMAND,
|
||||
KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
|
||||
@ -13,9 +13,10 @@ import {
|
||||
import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
|
||||
import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
|
||||
import {getLastSelection} from "../utils/selection";
|
||||
import {$getNearestNodeBlockParent} from "../utils/nodes";
|
||||
import {$getNearestNodeBlockParent, $getParentOfType} from "../utils/nodes";
|
||||
import {$setInsetForSelection} from "../utils/lists";
|
||||
import {$isListItemNode} from "@lexical/list";
|
||||
import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
|
||||
|
||||
function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
|
||||
if (nodes.length === 1) {
|
||||
@ -28,6 +29,10 @@ function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current node in the selection if the selection contains a single
|
||||
* selected node (like image, media etc...).
|
||||
*/
|
||||
function deleteSingleSelectedNode(editor: LexicalEditor) {
|
||||
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
|
||||
if (isSingleSelectedNode(selectionNodes)) {
|
||||
@ -37,6 +42,10 @@ function deleteSingleSelectedNode(editor: LexicalEditor) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new empty node after the selection if the selection contains a single
|
||||
* selected node (like image, media etc...).
|
||||
*/
|
||||
function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
|
||||
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
|
||||
if (isSingleSelectedNode(selectionNodes)) {
|
||||
@ -58,6 +67,94 @@ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEve
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new node after a details node, if inside a details node that's
|
||||
* the last element, and if the cursor is at the last block within the details node.
|
||||
*/
|
||||
function insertAfterDetails(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
|
||||
const scenario = getDetailsScenario(editor);
|
||||
if (scenario === null || scenario.detailsSibling) {
|
||||
return false;
|
||||
}
|
||||
|
||||
editor.update(() => {
|
||||
const newParagraph = $createParagraphNode();
|
||||
scenario.parentDetails.insertAfter(newParagraph);
|
||||
newParagraph.select();
|
||||
});
|
||||
event?.preventDefault();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If within a details block, move after it, creating a new node if required, if we're on
|
||||
* the last empty block element within the details node.
|
||||
*/
|
||||
function moveAfterDetailsOnEmptyLine(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
|
||||
const scenario = getDetailsScenario(editor);
|
||||
if (scenario === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scenario.parentBlock.getTextContent() !== '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
event?.preventDefault()
|
||||
|
||||
const nextSibling = scenario.parentDetails.getNextSibling();
|
||||
editor.update(() => {
|
||||
if (nextSibling) {
|
||||
nextSibling.selectStart();
|
||||
} else {
|
||||
const newParagraph = $createParagraphNode();
|
||||
scenario.parentDetails.insertAfter(newParagraph);
|
||||
newParagraph.select();
|
||||
}
|
||||
scenario.parentBlock.remove();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the common nodes used for a details node scenario, relative to current selection.
|
||||
* Returns null if not found, or if the parent block is not the last in the parent details node.
|
||||
*/
|
||||
function getDetailsScenario(editor: LexicalEditor): {
|
||||
parentDetails: DetailsNode;
|
||||
parentBlock: LexicalNode;
|
||||
detailsSibling: LexicalNode | null
|
||||
} | null {
|
||||
const selection = getLastSelection(editor);
|
||||
const firstNode = selection?.getNodes()[0];
|
||||
if (!firstNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const block = $getNearestNodeBlockParent(firstNode);
|
||||
const details = $getParentOfType(firstNode, $isDetailsNode);
|
||||
if (!$isDetailsNode(details) || block === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (block.getKey() !== details.getLastChild()?.getKey()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextSibling = details.getNextSibling();
|
||||
return {
|
||||
parentDetails: details,
|
||||
parentBlock: block,
|
||||
detailsSibling: nextSibling,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inset the nodes within selection when a range of nodes is selected
|
||||
* or if a list node is selected.
|
||||
*/
|
||||
function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
|
||||
const change = event?.shiftKey ? -40 : 40;
|
||||
const selection = $getSelection();
|
||||
@ -85,17 +182,23 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void {
|
||||
}, COMMAND_PRIORITY_LOW);
|
||||
|
||||
const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
|
||||
return insertAfterSingleSelectedNode(context.editor, event);
|
||||
return insertAfterSingleSelectedNode(context.editor, event)
|
||||
|| moveAfterDetailsOnEmptyLine(context.editor, event);
|
||||
}, COMMAND_PRIORITY_LOW);
|
||||
|
||||
const unregisterTab = context.editor.registerCommand(KEY_TAB_COMMAND, (event): boolean => {
|
||||
return handleInsetOnTab(context.editor, event);
|
||||
}, COMMAND_PRIORITY_LOW);
|
||||
|
||||
const unregisterDown = context.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event): boolean => {
|
||||
return insertAfterDetails(context.editor, event);
|
||||
}, COMMAND_PRIORITY_LOW);
|
||||
|
||||
return () => {
|
||||
unregisterBackspace();
|
||||
unregisterDelete();
|
||||
unregisterEnter();
|
||||
unregisterTab();
|
||||
unregisterDown();
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user