1 import {EditorUiContext} from "../ui/framework/core";
6 COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_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, $selectOrCreateAdjacent} from "../utils/nodes";
17 import {$setInsetForSelection} from "../utils/lists";
18 import {$isListItemNode} from "@lexical/list";
19 import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
20 import {$isDiagramNode} from "../utils/diagrams";
22 function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
23 if (nodes.length === 1) {
24 const node = nodes[0];
25 if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node)) {
34 * Delete the current node in the selection if the selection contains a single
35 * selected node (like image, media etc...).
37 function deleteSingleSelectedNode(editor: LexicalEditor) {
38 const selectionNodes = getLastSelection(editor)?.getNodes() || [];
39 if (isSingleSelectedNode(selectionNodes)) {
41 selectionNodes[0].remove();
47 * Insert a new empty node before/after the selection if the selection contains a single
48 * selected node (like image, media etc...).
50 function insertAdjacentToSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
51 const selectionNodes = getLastSelection(editor)?.getNodes() || [];
52 if (isSingleSelectedNode(selectionNodes)) {
53 const node = selectionNodes[0];
54 const nearestBlock = $getNearestNodeBlockParent(node) || node;
55 const insertBefore = event?.shiftKey === true;
57 requestAnimationFrame(() => {
59 const newParagraph = $createParagraphNode();
61 nearestBlock.insertBefore(newParagraph);
63 nearestBlock.insertAfter(newParagraph);
65 newParagraph.select();
68 event?.preventDefault();
76 function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event: KeyboardEvent|null, after: boolean = true): boolean {
77 const selectionNodes = getLastSelection(editor)?.getNodes() || [];
78 if (!isSingleSelectedNode(selectionNodes)) {
82 event?.preventDefault();
83 const node = selectionNodes[0];
85 $selectOrCreateAdjacent(node, after);
92 * Insert a new node after a details node, if inside a details node that's
93 * the last element, and if the cursor is at the last block within the details node.
95 function insertAfterDetails(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
96 const scenario = getDetailsScenario(editor);
97 if (scenario === null || scenario.detailsSibling) {
101 editor.update(() => {
102 const newParagraph = $createParagraphNode();
103 scenario.parentDetails.insertAfter(newParagraph);
104 newParagraph.select();
106 event?.preventDefault();
112 * If within a details block, move after it, creating a new node if required, if we're on
113 * the last empty block element within the details node.
115 function moveAfterDetailsOnEmptyLine(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
116 const scenario = getDetailsScenario(editor);
117 if (scenario === null) {
121 if (scenario.parentBlock.getTextContent() !== '') {
125 event?.preventDefault()
127 const nextSibling = scenario.parentDetails.getNextSibling();
128 editor.update(() => {
130 nextSibling.selectStart();
132 const newParagraph = $createParagraphNode();
133 scenario.parentDetails.insertAfter(newParagraph);
134 newParagraph.select();
136 scenario.parentBlock.remove();
143 * Get the common nodes used for a details node scenario, relative to current selection.
144 * Returns null if not found, or if the parent block is not the last in the parent details node.
146 function getDetailsScenario(editor: LexicalEditor): {
147 parentDetails: DetailsNode;
148 parentBlock: LexicalNode;
149 detailsSibling: LexicalNode | null
151 const selection = getLastSelection(editor);
152 const firstNode = selection?.getNodes()[0];
157 const block = $getNearestNodeBlockParent(firstNode);
158 const details = $getParentOfType(firstNode, $isDetailsNode);
159 if (!$isDetailsNode(details) || block === null) {
163 if (block.getKey() !== details.getLastChild()?.getKey()) {
167 const nextSibling = details.getNextSibling();
169 parentDetails: details,
171 detailsSibling: nextSibling,
175 function $isSingleListItem(nodes: LexicalNode[]): boolean {
176 if (nodes.length !== 1) {
180 const node = nodes[0];
181 return $isListItemNode(node) || $isListItemNode(node.getParent());
185 * Inset the nodes within selection when a range of nodes is selected
186 * or if a list node is selected.
188 function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
189 const change = event?.shiftKey ? -40 : 40;
190 const selection = $getSelection();
191 const nodes = selection?.getNodes() || [];
192 if (nodes.length > 1 || $isSingleListItem(nodes)) {
193 editor.update(() => {
194 $setInsetForSelection(editor, change);
196 event?.preventDefault();
203 export function registerKeyboardHandling(context: EditorUiContext): () => void {
204 const unregisterBackspace = context.editor.registerCommand(KEY_BACKSPACE_COMMAND, (): boolean => {
205 deleteSingleSelectedNode(context.editor);
207 }, COMMAND_PRIORITY_LOW);
209 const unregisterDelete = context.editor.registerCommand(KEY_DELETE_COMMAND, (): boolean => {
210 deleteSingleSelectedNode(context.editor);
212 }, COMMAND_PRIORITY_LOW);
214 const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
215 return insertAdjacentToSingleSelectedNode(context.editor, event)
216 || moveAfterDetailsOnEmptyLine(context.editor, event);
217 }, COMMAND_PRIORITY_LOW);
219 const unregisterTab = context.editor.registerCommand(KEY_TAB_COMMAND, (event): boolean => {
220 return handleInsetOnTab(context.editor, event);
221 }, COMMAND_PRIORITY_LOW);
223 const unregisterUp = context.editor.registerCommand(KEY_ARROW_UP_COMMAND, (event): boolean => {
224 return focusAdjacentOrInsertForSingleSelectNode(context.editor, event, false);
225 }, COMMAND_PRIORITY_LOW);
227 const unregisterDown = context.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event): boolean => {
228 return insertAfterDetails(context.editor, event)
229 || focusAdjacentOrInsertForSingleSelectNode(context.editor, event, true)
230 }, COMMAND_PRIORITY_LOW);
233 unregisterBackspace();