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