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} 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 before/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();
70 function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event: KeyboardEvent|null, after: boolean = true): boolean {
71 const selectionNodes = getLastSelection(editor)?.getNodes() || [];
72 if (!isSingleSelectedNode(selectionNodes)) {
76 event?.preventDefault();
78 const node = selectionNodes[0];
79 const nearestBlock = $getNearestNodeBlockParent(node) || node;
80 let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling();
84 target = $createParagraphNode();
86 nearestBlock.insertAfter(target)
88 nearestBlock.insertBefore(target);
99 * Insert a new node after a details node, if inside a details node that's
100 * the last element, and if the cursor is at the last block within the details node.
102 function insertAfterDetails(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
103 const scenario = getDetailsScenario(editor);
104 if (scenario === null || scenario.detailsSibling) {
108 editor.update(() => {
109 const newParagraph = $createParagraphNode();
110 scenario.parentDetails.insertAfter(newParagraph);
111 newParagraph.select();
113 event?.preventDefault();
119 * If within a details block, move after it, creating a new node if required, if we're on
120 * the last empty block element within the details node.
122 function moveAfterDetailsOnEmptyLine(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
123 const scenario = getDetailsScenario(editor);
124 if (scenario === null) {
128 if (scenario.parentBlock.getTextContent() !== '') {
132 event?.preventDefault()
134 const nextSibling = scenario.parentDetails.getNextSibling();
135 editor.update(() => {
137 nextSibling.selectStart();
139 const newParagraph = $createParagraphNode();
140 scenario.parentDetails.insertAfter(newParagraph);
141 newParagraph.select();
143 scenario.parentBlock.remove();
150 * Get the common nodes used for a details node scenario, relative to current selection.
151 * Returns null if not found, or if the parent block is not the last in the parent details node.
153 function getDetailsScenario(editor: LexicalEditor): {
154 parentDetails: DetailsNode;
155 parentBlock: LexicalNode;
156 detailsSibling: LexicalNode | null
158 const selection = getLastSelection(editor);
159 const firstNode = selection?.getNodes()[0];
164 const block = $getNearestNodeBlockParent(firstNode);
165 const details = $getParentOfType(firstNode, $isDetailsNode);
166 if (!$isDetailsNode(details) || block === null) {
170 if (block.getKey() !== details.getLastChild()?.getKey()) {
174 const nextSibling = details.getNextSibling();
176 parentDetails: details,
178 detailsSibling: nextSibling,
182 function $isSingleListItem(nodes: LexicalNode[]): boolean {
183 if (nodes.length !== 1) {
187 const node = nodes[0];
188 return $isListItemNode(node) || $isListItemNode(node.getParent());
192 * Inset the nodes within selection when a range of nodes is selected
193 * or if a list node is selected.
195 function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
196 const change = event?.shiftKey ? -40 : 40;
197 const selection = $getSelection();
198 const nodes = selection?.getNodes() || [];
199 if (nodes.length > 1 || $isSingleListItem(nodes)) {
200 editor.update(() => {
201 $setInsetForSelection(editor, change);
203 event?.preventDefault();
210 export function registerKeyboardHandling(context: EditorUiContext): () => void {
211 const unregisterBackspace = context.editor.registerCommand(KEY_BACKSPACE_COMMAND, (): boolean => {
212 deleteSingleSelectedNode(context.editor);
214 }, COMMAND_PRIORITY_LOW);
216 const unregisterDelete = context.editor.registerCommand(KEY_DELETE_COMMAND, (): boolean => {
217 deleteSingleSelectedNode(context.editor);
219 }, COMMAND_PRIORITY_LOW);
221 const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
222 return insertAfterSingleSelectedNode(context.editor, event)
223 || moveAfterDetailsOnEmptyLine(context.editor, event);
224 }, COMMAND_PRIORITY_LOW);
226 const unregisterTab = context.editor.registerCommand(KEY_TAB_COMMAND, (event): boolean => {
227 return handleInsetOnTab(context.editor, event);
228 }, COMMAND_PRIORITY_LOW);
230 const unregisterUp = context.editor.registerCommand(KEY_ARROW_UP_COMMAND, (event): boolean => {
231 return focusAdjacentOrInsertForSingleSelectNode(context.editor, event, false);
232 }, COMMAND_PRIORITY_LOW);
234 const unregisterDown = context.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event): boolean => {
235 return insertAfterDetails(context.editor, event)
236 || focusAdjacentOrInsertForSingleSelectNode(context.editor, event, true)
237 }, COMMAND_PRIORITY_LOW);
240 unregisterBackspace();