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