]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/selection.ts
74dd94527433c155a2620107a7995f7ead508365
[bookstack] / resources / js / wysiwyg / utils / selection.ts
1 import {
2     $createNodeSelection,
3     $createParagraphNode,
4     $getRoot,
5     $getSelection,
6     $isElementNode,
7     $isTextNode,
8     $setSelection,
9     BaseSelection,
10     ElementFormatType,
11     ElementNode, LexicalEditor,
12     LexicalNode,
13     TextFormatType
14 } from "lexical";
15 import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
16 import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
17 import {$setBlocksType} from "@lexical/selection";
18
19 import {$getParentOfType} from "./nodes";
20 import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
21
22 const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
23
24 export function getLastSelection(editor: LexicalEditor): BaseSelection|null {
25     return lastSelectionByEditor.get(editor) || null;
26 }
27
28 export function setLastSelection(editor: LexicalEditor, selection: BaseSelection|null): void {
29     lastSelectionByEditor.set(editor, selection);
30 }
31
32 export function $selectionContainsNodeType(selection: BaseSelection | null, matcher: LexicalNodeMatcher): boolean {
33     return $getNodeFromSelection(selection, matcher) !== null;
34 }
35
36 export function $getNodeFromSelection(selection: BaseSelection | null, matcher: LexicalNodeMatcher): LexicalNode | null {
37     if (!selection) {
38         return null;
39     }
40
41     for (const node of selection.getNodes()) {
42         if (matcher(node)) {
43             return node;
44         }
45
46         const matchedParent = $getParentOfType(node, matcher);
47         if (matchedParent) {
48             return matchedParent;
49         }
50     }
51
52     return null;
53 }
54
55 export function $selectionContainsTextFormat(selection: BaseSelection | null, format: TextFormatType): boolean {
56     if (!selection) {
57         return false;
58     }
59
60     for (const node of selection.getNodes()) {
61         if ($isTextNode(node) && node.hasFormat(format)) {
62             return true;
63         }
64     }
65
66     return false;
67 }
68
69 export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) {
70     const selection = $getSelection();
71     const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
72     if (selection && matcher(blockElement)) {
73         $setBlocksType(selection, $createCustomParagraphNode);
74     } else {
75         $setBlocksType(selection, creator);
76     }
77 }
78
79 export function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) {
80     $insertNewBlockNodesAtSelection([node], insertAfter);
81 }
82
83 export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfter: boolean = true) {
84     const selection = $getSelection();
85     const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
86
87     if (blockElement) {
88         if (insertAfter) {
89             for (let i = nodes.length - 1; i >= 0; i--) {
90                 blockElement.insertAfter(nodes[i]);
91             }
92         } else {
93             for (const node of nodes) {
94                 blockElement.insertBefore(node);
95             }
96         }
97     } else {
98         $getRoot().append(...nodes);
99     }
100 }
101
102 export function $selectSingleNode(node: LexicalNode) {
103     const nodeSelection = $createNodeSelection();
104     nodeSelection.add(node.getKey());
105     $setSelection(nodeSelection);
106 }
107
108 export function $selectionContainsNode(selection: BaseSelection | null, node: LexicalNode): boolean {
109     if (!selection) {
110         return false;
111     }
112
113     const key = node.getKey();
114     for (const node of selection.getNodes()) {
115         if (node.getKey() === key) {
116             return true;
117         }
118     }
119
120     return false;
121 }
122
123 export function $selectionContainsElementFormat(selection: BaseSelection | null, format: ElementFormatType): boolean {
124     const nodes = $getBlockElementNodesInSelection(selection);
125     for (const node of nodes) {
126         if (node.getFormatType() === format) {
127             return true;
128         }
129     }
130
131     return false;
132 }
133
134 export function $getBlockElementNodesInSelection(selection: BaseSelection | null): ElementNode[] {
135     if (!selection) {
136         return [];
137     }
138
139     const blockNodes: Map<string, ElementNode> = new Map();
140     for (const node of selection.getNodes()) {
141         const blockElement = $findMatchingParent(node, (node) => {
142             return $isElementNode(node) && !node.isInline();
143         }) as ElementNode | null;
144
145         if (blockElement) {
146             blockNodes.set(blockElement.getKey(), blockElement);
147         }
148     }
149
150     return Array.from(blockNodes.values());
151 }