Skip to content

Commit

Permalink
Lexical: Added mulitple methods to escape details block
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ssddanbrown committed Dec 16, 2024
1 parent 5887322 commit 8486775
Showing 1 changed file with 106 additions and 3 deletions.
109 changes: 106 additions & 3 deletions resources/js/wysiwyg/services/keyboard-handling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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)) {
Expand All @@ -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)) {
Expand All @@ -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();
Expand Down Expand Up @@ -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();
};
}

0 comments on commit 8486775

Please sign in to comment.