]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/helpers.ts
Lexical: Started on table actions
[bookstack] / resources / js / wysiwyg / helpers.ts
1 import {
2     $createNodeSelection,
3     $createParagraphNode, $getRoot,
4     $getSelection, $isElementNode,
5     $isTextNode, $setSelection,
6     BaseSelection, ElementFormatType, ElementNode, LexicalEditor,
7     LexicalNode, TextFormatType
8 } from "lexical";
9 import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
10 import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
11 import {$setBlocksType} from "@lexical/selection";
12 import {$createCustomParagraphNode} from "./nodes/custom-paragraph";
13 import {$generateNodesFromDOM} from "@lexical/html";
14
15 export function el(tag: string, attrs: Record<string, string|null> = {}, children: (string|HTMLElement)[] = []): HTMLElement {
16     const el = document.createElement(tag);
17     const attrKeys = Object.keys(attrs);
18     for (const attr of attrKeys) {
19         if (attrs[attr] !== null) {
20             el.setAttribute(attr, attrs[attr] as string);
21         }
22     }
23
24     for (const child of children) {
25         if (typeof child === 'string') {
26             el.append(document.createTextNode(child));
27         } else {
28             el.append(child);
29         }
30     }
31
32     return el;
33 }
34
35 function htmlToDom(html: string): Document {
36     const parser = new DOMParser();
37     return parser.parseFromString(html, 'text/html');
38 }
39
40 function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
41     return nodes.map(node => {
42         if ($isTextNode(node)) {
43             const paragraph = $createCustomParagraphNode();
44             paragraph.append(node);
45             return paragraph;
46         }
47         return node;
48     });
49 }
50
51 export function $htmlToBlockNodes(editor: LexicalEditor, html: string): LexicalNode[] {
52     const dom = htmlToDom(html);
53     const nodes = $generateNodesFromDOM(editor, dom);
54     return wrapTextNodes(nodes);
55 }
56
57 export function $selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean {
58     return $getNodeFromSelection(selection, matcher) !== null;
59 }
60
61 export function $getNodeFromSelection(selection: BaseSelection|null, matcher: LexicalNodeMatcher): LexicalNode|null {
62     if (!selection) {
63         return null;
64     }
65
66     for (const node of selection.getNodes()) {
67         if (matcher(node)) {
68             return node;
69         }
70
71         const matchedParent = $getParentOfType(node, matcher);
72         if (matchedParent) {
73             return matchedParent;
74         }
75     }
76
77     return null;
78 }
79
80 export function $getParentOfType(node: LexicalNode, matcher: LexicalNodeMatcher): LexicalNode|null {
81     for (const parent of node.getParents()) {
82         if (matcher(parent)) {
83             return parent;
84         }
85     }
86
87     return null;
88 }
89
90 export function $selectionContainsTextFormat(selection: BaseSelection|null, format: TextFormatType): boolean {
91     if (!selection) {
92         return false;
93     }
94
95     for (const node of selection.getNodes()) {
96         if ($isTextNode(node) && node.hasFormat(format)) {
97             return true;
98         }
99     }
100
101     return false;
102 }
103
104 export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) {
105     const selection = $getSelection();
106     const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
107     if (selection && matcher(blockElement)) {
108         $setBlocksType(selection, $createParagraphNode);
109     } else {
110         $setBlocksType(selection, creator);
111     }
112 }
113
114 export function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) {
115     $insertNewBlockNodesAtSelection([node], insertAfter);
116 }
117
118 export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfter: boolean = true) {
119     const selection = $getSelection();
120     const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
121
122     if (blockElement) {
123         if (insertAfter) {
124             for (let i = nodes.length - 1; i >= 0; i--) {
125                 blockElement.insertAfter(nodes[i]);
126             }
127         } else {
128             for (const node of nodes) {
129                 blockElement.insertBefore(node);
130             }
131         }
132     } else {
133         $getRoot().append(...nodes);
134     }
135 }
136
137 export function $selectSingleNode(node: LexicalNode) {
138     const nodeSelection = $createNodeSelection();
139     nodeSelection.add(node.getKey());
140     $setSelection(nodeSelection);
141 }
142
143 export function $selectionContainsNode(selection: BaseSelection|null, node: LexicalNode): boolean {
144     if (!selection) {
145         return false;
146     }
147
148     const key = node.getKey();
149     for (const node of selection.getNodes()) {
150         if (node.getKey() === key) {
151             return true;
152         }
153     }
154
155     return false;
156 }
157
158 export function $selectionContainsElementFormat(selection: BaseSelection|null, format: ElementFormatType): boolean {
159     const nodes = $getBlockElementNodesInSelection(selection);
160     for (const node of nodes) {
161         if (node.getFormatType() === format) {
162             return true;
163         }
164     }
165
166     return false;
167 }
168
169 export function $getBlockElementNodesInSelection(selection: BaseSelection|null): ElementNode[] {
170     if (!selection) {
171         return [];
172     }
173
174     const blockNodes: Map<string, ElementNode> = new Map();
175     for (const node of selection.getNodes()) {
176         const blockElement = $findMatchingParent(node, (node) => {
177             return $isElementNode(node) && !node.isInline();
178         }) as ElementNode|null;
179
180         if (blockElement) {
181             blockNodes.set(blockElement.getKey(), blockElement);
182         }
183     }
184
185     return Array.from(blockNodes.values());
186 }
187
188 /**
189  * Get the nearest root/block level node for the given position.
190  */
191 export function $getNearestBlockNodeForCoords(editor: LexicalEditor, x: number, y: number): LexicalNode|null {
192     // TODO - Take into account x for floated blocks?
193     const rootNodes = $getRoot().getChildren();
194     for (const node of rootNodes) {
195         const nodeDom = editor.getElementByKey(node.__key);
196         if (!nodeDom) {
197             continue;
198         }
199
200         const bounds = nodeDom.getBoundingClientRect();
201         if (y <= bounds.bottom) {
202             return node;
203         }
204     }
205
206     return null;
207 }