]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/selection.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / utils / selection.ts
1 import {
2     $createNodeSelection,
3     $createParagraphNode, $createRangeSelection,
4     $getRoot,
5     $getSelection, $isBlockElementNode, $isDecoratorNode,
6     $isElementNode, $isParagraphNode,
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 "lexical/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 $getTextNodeFromSelection(selection: BaseSelection | null): TextNode|null {
55     return $getNodeFromSelection(selection, $isTextNode) as TextNode|null;
56 }
57
58 export function $selectionContainsTextFormat(selection: BaseSelection | null, format: TextFormatType): boolean {
59     if (!selection) {
60         return false;
61     }
62
63     // Check text nodes
64     const nodes = selection.getNodes();
65     for (const node of nodes) {
66         if ($isTextNode(node) && node.hasFormat(format)) {
67             return true;
68         }
69     }
70
71     // If we're in an empty paragraph, check the paragraph format
72     if (nodes.length === 1 && $isParagraphNode(nodes[0]) && nodes[0].hasTextFormat(format)) {
73         return true;
74     }
75
76     return false;
77 }
78
79 export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) {
80     const selection = $getSelection();
81     const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
82     if (selection && matcher(blockElement)) {
83         $setBlocksType(selection, $createParagraphNode);
84     } else {
85         $setBlocksType(selection, creator);
86     }
87 }
88
89 export function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) {
90     $insertNewBlockNodesAtSelection([node], insertAfter);
91 }
92
93 export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfter: boolean = true) {
94     const selectionNodes = $getSelection()?.getNodes() || [];
95     const blockElement = selectionNodes.length > 0 ? $getNearestNodeBlockParent(selectionNodes[0]) : null;
96
97     if (blockElement) {
98         if (insertAfter) {
99             for (let i = nodes.length - 1; i >= 0; i--) {
100                 blockElement.insertAfter(nodes[i]);
101             }
102         } else {
103             for (const node of nodes) {
104                 blockElement.insertBefore(node);
105             }
106         }
107     } else {
108         $getRoot().append(...nodes);
109     }
110 }
111
112 export function $selectSingleNode(node: LexicalNode) {
113     const nodeSelection = $createNodeSelection();
114     nodeSelection.add(node.getKey());
115     $setSelection(nodeSelection);
116 }
117
118 function getFirstTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {
119     for (const node of nodes) {
120         if ($isTextNode(node)) {
121             return node;
122         }
123
124         if ($isElementNode(node)) {
125             const children = node.getChildren();
126             const textNode = getFirstTextNodeInNodes(children);
127             if (textNode !== null) {
128                 return textNode;
129             }
130         }
131     }
132
133     return null;
134 }
135
136 function getLastTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {
137     const revNodes = [...nodes].reverse();
138     for (const node of revNodes) {
139         if ($isTextNode(node)) {
140             return node;
141         }
142
143         if ($isElementNode(node)) {
144             const children = [...node.getChildren()].reverse();
145             const textNode = getLastTextNodeInNodes(children);
146             if (textNode !== null) {
147                 return textNode;
148             }
149         }
150     }
151
152     return null;
153 }
154
155 export function $selectNodes(nodes: LexicalNode[]) {
156     if (nodes.length === 0) {
157         return;
158     }
159
160     const selection = $createRangeSelection();
161     const firstText = getFirstTextNodeInNodes(nodes);
162     const lastText = getLastTextNodeInNodes(nodes);
163     if (firstText && lastText) {
164         selection.setTextNodeRange(firstText, 0, lastText, lastText.getTextContentSize() || 0)
165         $setSelection(selection);
166     }
167 }
168
169 export function $toggleSelection(editor: LexicalEditor) {
170     const lastSelection = getLastSelection(editor);
171
172     if (lastSelection) {
173         window.requestAnimationFrame(() => {
174             editor.update(() => {
175                 $setSelection(lastSelection.clone());
176             })
177         });
178     }
179 }
180
181 export function $selectionContainsNode(selection: BaseSelection | null, node: LexicalNode): boolean {
182     if (!selection) {
183         return false;
184     }
185
186     const key = node.getKey();
187     for (const node of selection.getNodes()) {
188         if (node.getKey() === key) {
189             return true;
190         }
191     }
192
193     return false;
194 }
195
196 export function $selectionContainsAlignment(selection: BaseSelection | null, alignment: CommonBlockAlignment): boolean {
197
198     const nodes = [
199         ...(selection?.getNodes() || []),
200         ...$getBlockElementNodesInSelection(selection)
201     ];
202     for (const node of nodes) {
203         if (nodeHasAlignment(node) && node.getAlignment() === alignment) {
204             return true;
205         }
206     }
207
208     return false;
209 }
210
211 export function $selectionContainsDirection(selection: BaseSelection | null, direction: 'rtl'|'ltr'): boolean {
212
213     const nodes = [
214         ...(selection?.getNodes() || []),
215         ...$getBlockElementNodesInSelection(selection)
216     ];
217
218     for (const node of nodes) {
219         if ($isBlockElementNode(node) && node.getDirection() === direction) {
220             return true;
221         }
222     }
223
224     return false;
225 }
226
227 export function $getBlockElementNodesInSelection(selection: BaseSelection | null): ElementNode[] {
228     if (!selection) {
229         return [];
230     }
231
232     const blockNodes: Map<string, ElementNode> = new Map();
233     for (const node of selection.getNodes()) {
234         const blockElement = $getNearestNodeBlockParent(node);
235         if ($isElementNode(blockElement)) {
236             blockNodes.set(blockElement.getKey(), blockElement);
237         }
238     }
239
240     return Array.from(blockNodes.values());
241 }
242
243 export function $getDecoratorNodesInSelection(selection: BaseSelection | null): DecoratorNode<any>[] {
244     if (!selection) {
245         return [];
246     }
247
248     return selection.getNodes().filter(node => $isDecoratorNode(node));
249 }