Lexical: Made a range of selection improvements

Updated up/down handling to create where a selection candidate does not
exist, to apply to a wider scenario via the selectPrevious/Next methods.

Updated DOM selection change handling to identify single selections
within decorated nodes to select them in full, instead of losing
selection due to partial selection of their contents.

Updated table selection handling so that our colgroups are ignored for
internal selection focus handling.
This commit is contained in:
Dan Brown 2025-05-26 14:48:13 +01:00
parent 1243108e0f
commit 2a32475541
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 46 additions and 19 deletions

View File

@ -48,6 +48,7 @@ import {
internalMarkNodeAsDirty,
removeFromParent,
} from './LexicalUtils';
import {$insertAndSelectNewEmptyAdjacentNode} from "../../utils/nodes";
export type NodeMap = Map<NodeKey, LexicalNode>;
@ -1130,7 +1131,7 @@ export class LexicalNode {
const prevSibling = this.getPreviousSibling();
const parent = this.getParentOrThrow();
if (prevSibling === null) {
return parent.select(0, 0);
return $insertAndSelectNewEmptyAdjacentNode(this, false);
}
if ($isElementNode(prevSibling)) {
return prevSibling.select();
@ -1152,7 +1153,7 @@ export class LexicalNode {
const nextSibling = this.getNextSibling();
const parent = this.getParentOrThrow();
if (nextSibling === null) {
return parent.select();
return $insertAndSelectNewEmptyAdjacentNode(this, true);
}
if ($isElementNode(nextSibling)) {
return nextSibling.select(0, 0);

View File

@ -17,7 +17,7 @@ import invariant from 'lexical/shared/invariant';
import {
$createLineBreakNode,
$createParagraphNode,
$createTextNode,
$createTextNode, $getNearestNodeFromDOMNode,
$isDecoratorNode,
$isElementNode,
$isLineBreakNode,
@ -63,6 +63,7 @@ import {
toggleTextFormatType,
} from './LexicalUtils';
import {$createTabNode, $isTabNode} from './nodes/LexicalTabNode';
import {$selectSingleNode} from "../../utils/selection";
export type TextPointType = {
_selection: BaseSelection;
@ -2568,6 +2569,17 @@ export function updateDOMSelection(
}
if (!$isRangeSelection(nextSelection)) {
// If the DOM selection enters a decorator node update the selection to a single node selection
if (activeElement !== null && domSelection.isCollapsed && focusDOMNode instanceof Node) {
const node = $getNearestNodeFromDOMNode(focusDOMNode);
if ($isDecoratorNode(node)) {
domSelection.removeAllRanges();
$selectSingleNode(node);
return;
}
}
// We don't remove selection if the prevSelection is null because
// of editor.setRootElement(). If this occurs on init when the
// editor is already focused, then this can cause the editor to

View File

@ -917,6 +917,11 @@ export function getTable(tableElement: HTMLElement): TableDOMTable {
while (currentNode != null) {
const nodeMame = currentNode.nodeName;
if (nodeMame === 'COLGROUP') {
currentNode = currentNode.nextSibling;
continue;
}
if (nodeMame === 'TD' || nodeMame === 'TH') {
const elem = currentNode as HTMLElement;
const cell = {

View File

@ -47,16 +47,21 @@ function deleteSingleSelectedNode(editor: LexicalEditor) {
* Insert a new empty node before/after the selection if the selection contains a single
* selected node (like image, media etc...).
*/
function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
function insertAdjacentToSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
if (isSingleSelectedNode(selectionNodes)) {
const node = selectionNodes[0];
const nearestBlock = $getNearestNodeBlockParent(node) || node;
const insertBefore = event?.shiftKey === true;
if (nearestBlock) {
requestAnimationFrame(() => {
editor.update(() => {
const newParagraph = $createParagraphNode();
nearestBlock.insertAfter(newParagraph);
if (insertBefore) {
nearestBlock.insertBefore(newParagraph);
} else {
nearestBlock.insertAfter(newParagraph);
}
newParagraph.select();
});
});
@ -75,22 +80,14 @@ function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event:
}
event?.preventDefault();
const node = selectionNodes[0];
const nearestBlock = $getNearestNodeBlockParent(node) || node;
let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling();
editor.update(() => {
if (!target) {
target = $createParagraphNode();
if (after) {
nearestBlock.insertAfter(target)
} else {
nearestBlock.insertBefore(target);
}
if (after) {
node.selectNext();
} else {
node.selectPrevious();
}
target.selectStart();
});
return true;
@ -220,7 +217,7 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void {
}, COMMAND_PRIORITY_LOW);
const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
return insertAfterSingleSelectedNode(context.editor, event)
return insertAdjacentToSingleSelectedNode(context.editor, event)
|| moveAfterDetailsOnEmptyLine(context.editor, event);
}, COMMAND_PRIORITY_LOW);

View File

@ -244,6 +244,7 @@ export class EditorUIManager {
if (selectionChange) {
editor.update(() => {
const selection = $getSelection();
// console.log('manager::selection', selection);
this.triggerStateUpdate({
editor, selection,
});

View File

@ -6,7 +6,7 @@ import {
$isTextNode,
ElementNode,
LexicalEditor,
LexicalNode
LexicalNode, RangeSelection
} from "lexical";
import {LexicalNodeMatcher} from "../nodes";
import {$generateNodesFromDOM} from "@lexical/html";
@ -118,6 +118,17 @@ export function $sortNodes(nodes: LexicalNode[]): LexicalNode[] {
return sorted;
}
export function $insertAndSelectNewEmptyAdjacentNode(node: LexicalNode, after: boolean): RangeSelection {
const target = $createParagraphNode();
if (after) {
node.insertAfter(target)
} else {
node.insertBefore(target);
}
return target.select();
}
export function nodeHasAlignment(node: object): node is NodeHasAlignment {
return '__alignment' in node;
}