]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/nodes.ts
Lexical: Changed table esacpe handling
[bookstack] / resources / js / wysiwyg / utils / nodes.ts
1 import {
2     $createParagraphNode,
3     $getRoot,
4     $isDecoratorNode,
5     $isElementNode, $isRootNode,
6     $isTextNode,
7     ElementNode,
8     LexicalEditor,
9     LexicalNode, RangeSelection
10 } from "lexical";
11 import {LexicalNodeMatcher} from "../nodes";
12 import {$generateNodesFromDOM} from "@lexical/html";
13 import {htmlToDom} from "./dom";
14 import {NodeHasAlignment, NodeHasInset} from "lexical/nodes/common";
15 import {$findMatchingParent} from "@lexical/utils";
16
17 function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
18     return nodes.map(node => {
19         if ($isTextNode(node)) {
20             const paragraph = $createParagraphNode();
21             paragraph.append(node);
22             return paragraph;
23         }
24         return node;
25     });
26 }
27
28 export function $htmlToBlockNodes(editor: LexicalEditor, html: string): LexicalNode[] {
29     const dom = htmlToDom(html);
30     const nodes = $generateNodesFromDOM(editor, dom);
31     return wrapTextNodes(nodes);
32 }
33
34 export function $getParentOfType(node: LexicalNode, matcher: LexicalNodeMatcher): LexicalNode | null {
35     for (const parent of node.getParents()) {
36         if (matcher(parent)) {
37             return parent;
38         }
39     }
40
41     return null;
42 }
43
44 export function $getAllNodesOfType(matcher: LexicalNodeMatcher, root?: ElementNode): LexicalNode[] {
45     if (!root) {
46         root = $getRoot();
47     }
48
49     const matches = [];
50
51     for (const child of root.getChildren()) {
52         if (matcher(child)) {
53             matches.push(child);
54         }
55
56         if ($isElementNode(child)) {
57             matches.push(...$getAllNodesOfType(matcher, child));
58         }
59     }
60
61     return matches;
62 }
63
64 /**
65  * Get the nearest root/block level node for the given position.
66  */
67 export function $getNearestBlockNodeForCoords(editor: LexicalEditor, x: number, y: number): LexicalNode | null {
68     // TODO - Take into account x for floated blocks?
69     const rootNodes = $getRoot().getChildren();
70     for (const node of rootNodes) {
71         const nodeDom = editor.getElementByKey(node.__key);
72         if (!nodeDom) {
73             continue;
74         }
75
76         const bounds = nodeDom.getBoundingClientRect();
77         if (y <= bounds.bottom) {
78             return node;
79         }
80     }
81
82     return null;
83 }
84
85 export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null {
86     const isBlockNode = (node: LexicalNode): boolean => {
87         return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline() && !$isRootNode(node);
88     };
89
90     if (isBlockNode(node)) {
91         return node;
92     }
93
94     return $findMatchingParent(node, isBlockNode);
95 }
96
97 export function $sortNodes(nodes: LexicalNode[]): LexicalNode[] {
98     const idChain: string[] = [];
99     const addIds = (n: ElementNode) => {
100         for (const child of n.getChildren()) {
101             idChain.push(child.getKey())
102             if ($isElementNode(child)) {
103                 addIds(child)
104             }
105         }
106     };
107
108     const root = $getRoot();
109     addIds(root);
110
111     const sorted = Array.from(nodes);
112     sorted.sort((a, b) => {
113         const aIndex = idChain.indexOf(a.getKey());
114         const bIndex = idChain.indexOf(b.getKey());
115         return aIndex - bIndex;
116     });
117
118     return sorted;
119 }
120
121 export function $selectOrCreateAdjacent(node: LexicalNode, after: boolean): RangeSelection {
122     const nearestBlock = $getNearestNodeBlockParent(node) || node;
123     let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling()
124
125     if (!target) {
126         target = $createParagraphNode();
127         if (after) {
128             node.insertAfter(target)
129         } else {
130             node.insertBefore(target);
131         }
132     }
133
134     return after ? target.selectStart() : target.selectEnd();
135 }
136
137 export function nodeHasAlignment(node: object): node is NodeHasAlignment {
138     return '__alignment' in node;
139 }
140
141 export function nodeHasInset(node: object): node is NodeHasInset {
142     return '__inset' in node;
143 }