Skip to content

Commit 8486775

Browse files
committed
Lexical: Added mulitple methods to escape details block
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.
1 parent 5887322 commit 8486775

File tree

1 file changed

+106
-3
lines changed

1 file changed

+106
-3
lines changed

resources/js/wysiwyg/services/keyboard-handling.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
$createParagraphNode,
44
$getSelection,
55
$isDecoratorNode,
6-
COMMAND_PRIORITY_LOW,
6+
COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND,
77
KEY_BACKSPACE_COMMAND,
88
KEY_DELETE_COMMAND,
99
KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
@@ -13,9 +13,10 @@ import {
1313
import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
1414
import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
1515
import {getLastSelection} from "../utils/selection";
16-
import {$getNearestNodeBlockParent} from "../utils/nodes";
16+
import {$getNearestNodeBlockParent, $getParentOfType} from "../utils/nodes";
1717
import {$setInsetForSelection} from "../utils/lists";
1818
import {$isListItemNode} from "@lexical/list";
19+
import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
1920

2021
function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
2122
if (nodes.length === 1) {
@@ -28,6 +29,10 @@ function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
2829
return false;
2930
}
3031

32+
/**
33+
* Delete the current node in the selection if the selection contains a single
34+
* selected node (like image, media etc...).
35+
*/
3136
function deleteSingleSelectedNode(editor: LexicalEditor) {
3237
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
3338
if (isSingleSelectedNode(selectionNodes)) {
@@ -37,6 +42,10 @@ function deleteSingleSelectedNode(editor: LexicalEditor) {
3742
}
3843
}
3944

45+
/**
46+
* Insert a new empty node after the selection if the selection contains a single
47+
* selected node (like image, media etc...).
48+
*/
4049
function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
4150
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
4251
if (isSingleSelectedNode(selectionNodes)) {
@@ -58,6 +67,94 @@ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEve
5867
return false;
5968
}
6069

70+
/**
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.
73+
*/
74+
function insertAfterDetails(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
75+
const scenario = getDetailsScenario(editor);
76+
if (scenario === null || scenario.detailsSibling) {
77+
return false;
78+
}
79+
80+
editor.update(() => {
81+
const newParagraph = $createParagraphNode();
82+
scenario.parentDetails.insertAfter(newParagraph);
83+
newParagraph.select();
84+
});
85+
event?.preventDefault();
86+
87+
return true;
88+
}
89+
90+
/**
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.
93+
*/
94+
function moveAfterDetailsOnEmptyLine(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
95+
const scenario = getDetailsScenario(editor);
96+
if (scenario === null) {
97+
return false;
98+
}
99+
100+
if (scenario.parentBlock.getTextContent() !== '') {
101+
return false;
102+
}
103+
104+
event?.preventDefault()
105+
106+
const nextSibling = scenario.parentDetails.getNextSibling();
107+
editor.update(() => {
108+
if (nextSibling) {
109+
nextSibling.selectStart();
110+
} else {
111+
const newParagraph = $createParagraphNode();
112+
scenario.parentDetails.insertAfter(newParagraph);
113+
newParagraph.select();
114+
}
115+
scenario.parentBlock.remove();
116+
});
117+
118+
return true;
119+
}
120+
121+
/**
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.
124+
*/
125+
function getDetailsScenario(editor: LexicalEditor): {
126+
parentDetails: DetailsNode;
127+
parentBlock: LexicalNode;
128+
detailsSibling: LexicalNode | null
129+
} | null {
130+
const selection = getLastSelection(editor);
131+
const firstNode = selection?.getNodes()[0];
132+
if (!firstNode) {
133+
return null;
134+
}
135+
136+
const block = $getNearestNodeBlockParent(firstNode);
137+
const details = $getParentOfType(firstNode, $isDetailsNode);
138+
if (!$isDetailsNode(details) || block === null) {
139+
return null;
140+
}
141+
142+
if (block.getKey() !== details.getLastChild()?.getKey()) {
143+
return null;
144+
}
145+
146+
const nextSibling = details.getNextSibling();
147+
return {
148+
parentDetails: details,
149+
parentBlock: block,
150+
detailsSibling: nextSibling,
151+
}
152+
}
153+
154+
/**
155+
* Inset the nodes within selection when a range of nodes is selected
156+
* or if a list node is selected.
157+
*/
61158
function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
62159
const change = event?.shiftKey ? -40 : 40;
63160
const selection = $getSelection();
@@ -85,17 +182,23 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void {
85182
}, COMMAND_PRIORITY_LOW);
86183

87184
const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
88-
return insertAfterSingleSelectedNode(context.editor, event);
185+
return insertAfterSingleSelectedNode(context.editor, event)
186+
|| moveAfterDetailsOnEmptyLine(context.editor, event);
89187
}, COMMAND_PRIORITY_LOW);
90188

91189
const unregisterTab = context.editor.registerCommand(KEY_TAB_COMMAND, (event): boolean => {
92190
return handleInsetOnTab(context.editor, event);
93191
}, COMMAND_PRIORITY_LOW);
94192

193+
const unregisterDown = context.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event): boolean => {
194+
return insertAfterDetails(context.editor, event);
195+
}, COMMAND_PRIORITY_LOW);
196+
95197
return () => {
96198
unregisterBackspace();
97199
unregisterDelete();
98200
unregisterEnter();
99201
unregisterTab();
202+
unregisterDown();
100203
};
101204
}

0 commit comments

Comments
 (0)