$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,
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) {
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)) {
}
}
+/**
+ * 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)) {
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();
}, 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();
};
}
\ No newline at end of file