From 2a324755414e18d96a472425533e0c033ffebe98 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 26 May 2025 14:48:13 +0100 Subject: [PATCH] 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. --- .../js/wysiwyg/lexical/core/LexicalNode.ts | 5 ++-- .../wysiwyg/lexical/core/LexicalSelection.ts | 14 +++++++++- .../table/LexicalTableSelectionHelpers.ts | 5 ++++ .../js/wysiwyg/services/keyboard-handling.ts | 27 +++++++++---------- resources/js/wysiwyg/ui/framework/manager.ts | 1 + resources/js/wysiwyg/utils/nodes.ts | 13 ++++++++- 6 files changed, 46 insertions(+), 19 deletions(-) diff --git a/resources/js/wysiwyg/lexical/core/LexicalNode.ts b/resources/js/wysiwyg/lexical/core/LexicalNode.ts index 7306e6bca..e54cd1066 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalNode.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalNode.ts @@ -48,6 +48,7 @@ import { internalMarkNodeAsDirty, removeFromParent, } from './LexicalUtils'; +import {$insertAndSelectNewEmptyAdjacentNode} from "../../utils/nodes"; export type NodeMap = Map; @@ -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); diff --git a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts index db18cfc4a..297286a4b 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts @@ -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 diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts b/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts index e098a21e4..d9164a778 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts @@ -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 = { diff --git a/resources/js/wysiwyg/services/keyboard-handling.ts b/resources/js/wysiwyg/services/keyboard-handling.ts index 41a917ecb..39818acb0 100644 --- a/resources/js/wysiwyg/services/keyboard-handling.ts +++ b/resources/js/wysiwyg/services/keyboard-handling.ts @@ -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); diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index c80291fb7..2d15b341b 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -244,6 +244,7 @@ export class EditorUIManager { if (selectionChange) { editor.update(() => { const selection = $getSelection(); + // console.log('manager::selection', selection); this.triggerStateUpdate({ editor, selection, }); diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index 591232ea3..ebf01e39d 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -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; }