]> BookStack Code Mirror - bookstack/blobdiff - resources/js/wysiwyg/utils/selection.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / utils / selection.ts
index 4aa21045f3013641ac115c8ba336cf431de66729..e4b5bf2dce6fb589160b750d5cd885125c1fed11 100644 (file)
@@ -2,23 +2,21 @@ import {
     $createNodeSelection,
     $createParagraphNode, $createRangeSelection,
     $getRoot,
-    $getSelection, $isDecoratorNode,
-    $isElementNode,
+    $getSelection, $isBlockElementNode, $isDecoratorNode,
+    $isElementNode, $isParagraphNode,
     $isTextNode,
     $setSelection,
     BaseSelection, DecoratorNode,
-    ElementFormatType,
     ElementNode, LexicalEditor,
     LexicalNode,
-    TextFormatType
+    TextFormatType, TextNode
 } from "lexical";
-import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
+import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
 import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
 import {$setBlocksType} from "@lexical/selection";
 
 import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes";
-import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
-import {CommonBlockAlignment} from "../nodes/_common";
+import {CommonBlockAlignment} from "lexical/nodes/common";
 
 const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
 
@@ -53,17 +51,28 @@ export function $getNodeFromSelection(selection: BaseSelection | null, matcher:
     return null;
 }
 
+export function $getTextNodeFromSelection(selection: BaseSelection | null): TextNode|null {
+    return $getNodeFromSelection(selection, $isTextNode) as TextNode|null;
+}
+
 export function $selectionContainsTextFormat(selection: BaseSelection | null, format: TextFormatType): boolean {
     if (!selection) {
         return false;
     }
 
-    for (const node of selection.getNodes()) {
+    // Check text nodes
+    const nodes = selection.getNodes();
+    for (const node of nodes) {
         if ($isTextNode(node) && node.hasFormat(format)) {
             return true;
         }
     }
 
+    // If we're in an empty paragraph, check the paragraph format
+    if (nodes.length === 1 && $isParagraphNode(nodes[0]) && nodes[0].hasTextFormat(format)) {
+        return true;
+    }
+
     return false;
 }
 
@@ -71,7 +80,7 @@ export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creat
     const selection = $getSelection();
     const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
     if (selection && matcher(blockElement)) {
-        $setBlocksType(selection, $createCustomParagraphNode);
+        $setBlocksType(selection, $createParagraphNode);
     } else {
         $setBlocksType(selection, creator);
     }
@@ -82,8 +91,8 @@ export function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: b
 }
 
 export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfter: boolean = true) {
-    const selection = $getSelection();
-    const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
+    const selectionNodes = $getSelection()?.getNodes() || [];
+    const blockElement = selectionNodes.length > 0 ? $getNearestNodeBlockParent(selectionNodes[0]) : null;
 
     if (blockElement) {
         if (insertAfter) {
@@ -106,6 +115,57 @@ export function $selectSingleNode(node: LexicalNode) {
     $setSelection(nodeSelection);
 }
 
+function getFirstTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {
+    for (const node of nodes) {
+        if ($isTextNode(node)) {
+            return node;
+        }
+
+        if ($isElementNode(node)) {
+            const children = node.getChildren();
+            const textNode = getFirstTextNodeInNodes(children);
+            if (textNode !== null) {
+                return textNode;
+            }
+        }
+    }
+
+    return null;
+}
+
+function getLastTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {
+    const revNodes = [...nodes].reverse();
+    for (const node of revNodes) {
+        if ($isTextNode(node)) {
+            return node;
+        }
+
+        if ($isElementNode(node)) {
+            const children = [...node.getChildren()].reverse();
+            const textNode = getLastTextNodeInNodes(children);
+            if (textNode !== null) {
+                return textNode;
+            }
+        }
+    }
+
+    return null;
+}
+
+export function $selectNodes(nodes: LexicalNode[]) {
+    if (nodes.length === 0) {
+        return;
+    }
+
+    const selection = $createRangeSelection();
+    const firstText = getFirstTextNodeInNodes(nodes);
+    const lastText = getLastTextNodeInNodes(nodes);
+    if (firstText && lastText) {
+        selection.setTextNodeRange(firstText, 0, lastText, lastText.getTextContentSize() || 0)
+        $setSelection(selection);
+    }
+}
+
 export function $toggleSelection(editor: LexicalEditor) {
     const lastSelection = getLastSelection(editor);
 
@@ -148,6 +208,22 @@ export function $selectionContainsAlignment(selection: BaseSelection | null, ali
     return false;
 }
 
+export function $selectionContainsDirection(selection: BaseSelection | null, direction: 'rtl'|'ltr'): boolean {
+
+    const nodes = [
+        ...(selection?.getNodes() || []),
+        ...$getBlockElementNodesInSelection(selection)
+    ];
+
+    for (const node of nodes) {
+        if ($isBlockElementNode(node) && node.getDirection() === direction) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
 export function $getBlockElementNodesInSelection(selection: BaseSelection | null): ElementNode[] {
     if (!selection) {
         return [];