]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/selection.ts
02838eba034e7fdb9adec28db8b8a0b52301f60b
[bookstack] / resources / js / wysiwyg / utils / selection.ts
1 import {
2     $createNodeSelection,
3     $createParagraphNode, $createRangeSelection,
4     $getRoot,
5     $getSelection, $isBlockElementNode, $isDecoratorNode,
6     $isElementNode,
7     $isTextNode,
8     $setSelection,
9     BaseSelection, DecoratorNode,
10     ElementNode, LexicalEditor,
11     LexicalNode,
12     TextFormatType, TextNode
13 } from "lexical";
14 import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
15 import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
16 import {$setBlocksType} from "@lexical/selection";
17
18 import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes";
19 import {CommonBlockAlignment} from "../nodes/_common";
20
21 const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
22
23 export function getLastSelection(editor: LexicalEditor): BaseSelection|null {
24     return lastSelectionByEditor.get(editor) || null;
25 }
26
27 export function setLastSelection(editor: LexicalEditor, selection: BaseSelection|null): void {
28     lastSelectionByEditor.set(editor, selection);
29 }
30
31 export function $selectionContainsNodeType(selection: BaseSelection | null, matcher: LexicalNodeMatcher): boolean {
32     return $getNodeFromSelection(selection, matcher) !== null;
33 }
34
35 export function $getNodeFromSelection(selection: BaseSelection | null, matcher: LexicalNodeMatcher): LexicalNode | null {
36     if (!selection) {
37         return null;
38     }
39
40     for (const node of selection.getNodes()) {
41         if (matcher(node)) {
42             return node;
43         }
44
45         const matchedParent = $getParentOfType(node, matcher);
46         if (matchedParent) {
47             return matchedParent;
48         }
49     }
50
51     return null;
52 }
53
54 export function $selectionContainsTextFormat(selection: BaseSelection | null, format: TextFormatType): boolean {
55     if (!selection) {
56         return false;
57     }
58
59     for (const node of selection.getNodes()) {
60         if ($isTextNode(node) && node.hasFormat(format)) {
61             return true;
62         }
63     }
64
65     return false;
66 }
67
68 export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) {
69     const selection = $getSelection();
70     const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
71     if (selection && matcher(blockElement)) {
72         $setBlocksType(selection, $createParagraphNode);
73     } else {
74         $setBlocksType(selection, creator);
75     }
76 }
77
78 export function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) {
79     $insertNewBlockNodesAtSelection([node], insertAfter);
80 }
81
82 export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfter: boolean = true) {
83     const selectionNodes = $getSelection()?.getNodes() || [];
84     const blockElement = selectionNodes.length > 0 ? $getNearestNodeBlockParent(selectionNodes[0]) : null;
85
86     if (blockElement) {
87         if (insertAfter) {
88             for (let i = nodes.length - 1; i >= 0; i--) {
89                 blockElement.insertAfter(nodes[i]);
90             }
91         } else {
92             for (const node of nodes) {
93                 blockElement.insertBefore(node);
94             }
95         }
96     } else {
97         $getRoot().append(...nodes);
98     }
99 }
100
101 export function $selectSingleNode(node: LexicalNode) {
102     const nodeSelection = $createNodeSelection();
103     nodeSelection.add(node.getKey());
104     $setSelection(nodeSelection);
105 }
106
107 function getFirstTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {
108     for (const node of nodes) {
109         if ($isTextNode(node)) {
110             return node;
111         }
112
113         if ($isElementNode(node)) {
114             const children = node.getChildren();
115             const textNode = getFirstTextNodeInNodes(children);
116             if (textNode !== null) {
117                 return textNode;
118             }
119         }
120     }
121
122     return null;
123 }
124
125 function getLastTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {
126     const revNodes = [...nodes].reverse();
127     for (const node of revNodes) {
128         if ($isTextNode(node)) {
129             return node;
130         }
131
132         if ($isElementNode(node)) {
133             const children = [...node.getChildren()].reverse();
134             const textNode = getLastTextNodeInNodes(children);
135             if (textNode !== null) {
136                 return textNode;
137             }
138         }
139     }
140
141     return null;
142 }
143
144 export function $selectNodes(nodes: LexicalNode[]) {
145     if (nodes.length === 0) {
146         return;
147     }
148
149     const selection = $createRangeSelection();
150     const firstText = getFirstTextNodeInNodes(nodes);
151     const lastText = getLastTextNodeInNodes(nodes);
152     if (firstText && lastText) {
153         selection.setTextNodeRange(firstText, 0, lastText, lastText.getTextContentSize() || 0)
154         $setSelection(selection);
155     }
156 }
157
158 export function $toggleSelection(editor: LexicalEditor) {
159     const lastSelection = getLastSelection(editor);
160
161     if (lastSelection) {
162         window.requestAnimationFrame(() => {
163             editor.update(() => {
164                 $setSelection(lastSelection.clone());
165             })
166         });
167     }
168 }
169
170 export function $selectionContainsNode(selection: BaseSelection | null, node: LexicalNode): boolean {
171     if (!selection) {
172         return false;
173     }
174
175     const key = node.getKey();
176     for (const node of selection.getNodes()) {
177         if (node.getKey() === key) {
178             return true;
179         }
180     }
181
182     return false;
183 }
184
185 export function $selectionContainsAlignment(selection: BaseSelection | null, alignment: CommonBlockAlignment): boolean {
186
187     const nodes = [
188         ...(selection?.getNodes() || []),
189         ...$getBlockElementNodesInSelection(selection)
190     ];
191     for (const node of nodes) {
192         if (nodeHasAlignment(node) && node.getAlignment() === alignment) {
193             return true;
194         }
195     }
196
197     return false;
198 }
199
200 export function $selectionContainsDirection(selection: BaseSelection | null, direction: 'rtl'|'ltr'): boolean {
201
202     const nodes = [
203         ...(selection?.getNodes() || []),
204         ...$getBlockElementNodesInSelection(selection)
205     ];
206
207     for (const node of nodes) {
208         if ($isBlockElementNode(node) && node.getDirection() === direction) {
209             return true;
210         }
211     }
212
213     return false;
214 }
215
216 export function $getBlockElementNodesInSelection(selection: BaseSelection | null): ElementNode[] {
217     if (!selection) {
218         return [];
219     }
220
221     const blockNodes: Map<string, ElementNode> = new Map();
222     for (const node of selection.getNodes()) {
223         const blockElement = $getNearestNodeBlockParent(node);
224         if ($isElementNode(blockElement)) {
225             blockNodes.set(blockElement.getKey(), blockElement);
226         }
227     }
228
229     return Array.from(blockNodes.values());
230 }
231
232 export function $getDecoratorNodesInSelection(selection: BaseSelection | null): DecoratorNode<any>[] {
233     if (!selection) {
234         return [];
235     }
236
237     return selection.getNodes().filter(node => $isDecoratorNode(node));
238 }