1 import {EditorUiContext} from "../ui/framework/core";
6 COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND,
9 KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
13 import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
14 import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
15 import {getLastSelection} from "../utils/selection";
16 import {$getNearestNodeBlockParent, $getParentOfType} from "../utils/nodes";
17 import {$setInsetForSelection} from "../utils/lists";
18 import {$isListItemNode} from "@lexical/list";
19 import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
21 function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
22 if (nodes.length === 1) {
23 const node = nodes[0];
24 if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) {
33 * Delete the current node in the selection if the selection contains a single
34 * selected node (like image, media etc...).
36 function deleteSingleSelectedNode(editor: LexicalEditor) {
37 const selectionNodes = getLastSelection(editor)?.getNodes() || [];
38 if (isSingleSelectedNode(selectionNodes)) {
40 selectionNodes[0].remove();
46 * Insert a new empty node after the selection if the selection contains a single
47 * selected node (like image, media etc...).
49 function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
50 const selectionNodes = getLastSelection(editor)?.getNodes() || [];
51 if (isSingleSelectedNode(selectionNodes)) {
52 const node = selectionNodes[0];
53 const nearestBlock = $getNearestNodeBlockParent(node) || node;
55 requestAnimationFrame(() => {
57 const newParagraph = $createParagraphNode();
58 nearestBlock.insertAfter(newParagraph);
59 newParagraph.select();
62 event?.preventDefault();
71 * Insert a new node after a details node, if inside a details node that's
72 * the last element, and if the cursor is at the last block within the details node.
74 function insertAfterDetails(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
75 const scenario = getDetailsScenario(editor);
76 if (scenario === null || scenario.detailsSibling) {
81 const newParagraph = $createParagraphNode();
82 scenario.parentDetails.insertAfter(newParagraph);
83 newParagraph.select();
85 event?.preventDefault();
91 * If within a details block, move after it, creating a new node if required, if we're on
92 * the last empty block element within the details node.
94 function moveAfterDetailsOnEmptyLine(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
95 const scenario = getDetailsScenario(editor);
96 if (scenario === null) {
100 if (scenario.parentBlock.getTextContent() !== '') {
104 event?.preventDefault()
106 const nextSibling = scenario.parentDetails.getNextSibling();
107 editor.update(() => {
109 nextSibling.selectStart();
111 const newParagraph = $createParagraphNode();
112 scenario.parentDetails.insertAfter(newParagraph);
113 newParagraph.select();
115 scenario.parentBlock.remove();
122 * Get the common nodes used for a details node scenario, relative to current selection.
123 * Returns null if not found, or if the parent block is not the last in the parent details node.
125 function getDetailsScenario(editor: LexicalEditor): {
126 parentDetails: DetailsNode;
127 parentBlock: LexicalNode;
128 detailsSibling: LexicalNode | null
130 const selection = getLastSelection(editor);
131 const firstNode = selection?.getNodes()[0];
136 const block = $getNearestNodeBlockParent(firstNode);
137 const details = $getParentOfType(firstNode, $isDetailsNode);
138 if (!$isDetailsNode(details) || block === null) {
142 if (block.getKey() !== details.getLastChild()?.getKey()) {
146 const nextSibling = details.getNextSibling();
148 parentDetails: details,
150 detailsSibling: nextSibling,
155 * Inset the nodes within selection when a range of nodes is selected
156 * or if a list node is selected.
158 function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
159 const change = event?.shiftKey ? -40 : 40;
160 const selection = $getSelection();
161 const nodes = selection?.getNodes() || [];
162 if (nodes.length > 1 || (nodes.length === 1 && $isListItemNode(nodes[0].getParent()))) {
163 editor.update(() => {
164 $setInsetForSelection(editor, change);
166 event?.preventDefault();
173 export function registerKeyboardHandling(context: EditorUiContext): () => void {
174 const unregisterBackspace = context.editor.registerCommand(KEY_BACKSPACE_COMMAND, (): boolean => {
175 deleteSingleSelectedNode(context.editor);
177 }, COMMAND_PRIORITY_LOW);
179 const unregisterDelete = context.editor.registerCommand(KEY_DELETE_COMMAND, (): boolean => {
180 deleteSingleSelectedNode(context.editor);
182 }, COMMAND_PRIORITY_LOW);
184 const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
185 return insertAfterSingleSelectedNode(context.editor, event)
186 || moveAfterDetailsOnEmptyLine(context.editor, event);
187 }, COMMAND_PRIORITY_LOW);
189 const unregisterTab = context.editor.registerCommand(KEY_TAB_COMMAND, (event): boolean => {
190 return handleInsetOnTab(context.editor, event);
191 }, COMMAND_PRIORITY_LOW);
193 const unregisterDown = context.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event): boolean => {
194 return insertAfterDetails(context.editor, event);
195 }, COMMAND_PRIORITY_LOW);
198 unregisterBackspace();