]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Made a range of selection improvements
authorDan Brown <redacted>
Mon, 26 May 2025 13:48:13 +0000 (14:48 +0100)
committerDan Brown <redacted>
Mon, 26 May 2025 13:51:03 +0000 (14:51 +0100)
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.

resources/js/wysiwyg/lexical/core/LexicalNode.ts
resources/js/wysiwyg/lexical/core/LexicalSelection.ts
resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts
resources/js/wysiwyg/services/keyboard-handling.ts
resources/js/wysiwyg/ui/framework/manager.ts
resources/js/wysiwyg/utils/nodes.ts

index 7306e6bca2755a578826f4bf87f37d24935ed465..e54cd1066c2050322750dd8a654aefbd5a1fb554 100644 (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);
index db18cfc4a779fe65dfbd7d7d679d03df05285172..297286a4b8ca6069a07b3a42a90c24fa1b9b25de 100644 (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
index e098a21e498a64988efaa5960c1ab8842e4662a3..d9164a778ce5094cf19285424dd7a25ec78dc7e1 100644 (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 = {
index 41a917ecb343b4bd2703f361d6f44233da945bde..39818acb09e167fccae454edccc27675298bf479 100644 (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);
 
index c80291fb707528583508bfe7fe2528ce7d391b49..2d15b341bdba6ae9a39e9236c77e5a6742854c4f 100644 (file)
@@ -244,6 +244,7 @@ export class EditorUIManager {
             if (selectionChange) {
                 editor.update(() => {
                     const selection = $getSelection();
+                    // console.log('manager::selection', selection);
                     this.triggerStateUpdate({
                         editor, selection,
                     });
index 591232ea385b994a9e4c9901ae2027a1a44afedf..ebf01e39ddef50a291a5b5b00558b7e7d1bf1d9b 100644 (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;
 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.