]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/utils/positionNodeOnRange.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / lexical / utils / positionNodeOnRange.ts
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import type {LexicalEditor} from 'lexical';
10
11 import {createRectsFromDOMRange} from '@lexical/selection';
12 import invariant from 'lexical/shared/invariant';
13
14 import px from './px';
15
16 const mutationObserverConfig = {
17   attributes: true,
18   characterData: true,
19   childList: true,
20   subtree: true,
21 };
22
23 export default function positionNodeOnRange(
24   editor: LexicalEditor,
25   range: Range,
26   onReposition: (node: Array<HTMLElement>) => void,
27 ): () => void {
28   let rootDOMNode: null | HTMLElement = null;
29   let parentDOMNode: null | HTMLElement = null;
30   let observer: null | MutationObserver = null;
31   let lastNodes: Array<HTMLElement> = [];
32   const wrapperNode = document.createElement('div');
33
34   function position(): void {
35     invariant(rootDOMNode !== null, 'Unexpected null rootDOMNode');
36     invariant(parentDOMNode !== null, 'Unexpected null parentDOMNode');
37     const {left: rootLeft, top: rootTop} = rootDOMNode.getBoundingClientRect();
38     const parentDOMNode_ = parentDOMNode;
39     const rects = createRectsFromDOMRange(editor, range);
40     if (!wrapperNode.isConnected) {
41       parentDOMNode_.append(wrapperNode);
42     }
43     let hasRepositioned = false;
44     for (let i = 0; i < rects.length; i++) {
45       const rect = rects[i];
46       // Try to reuse the previously created Node when possible, no need to
47       // remove/create on the most common case reposition case
48       const rectNode = lastNodes[i] || document.createElement('div');
49       const rectNodeStyle = rectNode.style;
50       if (rectNodeStyle.position !== 'absolute') {
51         rectNodeStyle.position = 'absolute';
52         hasRepositioned = true;
53       }
54       const left = px(rect.left - rootLeft);
55       if (rectNodeStyle.left !== left) {
56         rectNodeStyle.left = left;
57         hasRepositioned = true;
58       }
59       const top = px(rect.top - rootTop);
60       if (rectNodeStyle.top !== top) {
61         rectNode.style.top = top;
62         hasRepositioned = true;
63       }
64       const width = px(rect.width);
65       if (rectNodeStyle.width !== width) {
66         rectNode.style.width = width;
67         hasRepositioned = true;
68       }
69       const height = px(rect.height);
70       if (rectNodeStyle.height !== height) {
71         rectNode.style.height = height;
72         hasRepositioned = true;
73       }
74       if (rectNode.parentNode !== wrapperNode) {
75         wrapperNode.append(rectNode);
76         hasRepositioned = true;
77       }
78       lastNodes[i] = rectNode;
79     }
80     while (lastNodes.length > rects.length) {
81       lastNodes.pop();
82     }
83     if (hasRepositioned) {
84       onReposition(lastNodes);
85     }
86   }
87
88   function stop(): void {
89     parentDOMNode = null;
90     rootDOMNode = null;
91     if (observer !== null) {
92       observer.disconnect();
93     }
94     observer = null;
95     wrapperNode.remove();
96     for (const node of lastNodes) {
97       node.remove();
98     }
99     lastNodes = [];
100   }
101
102   function restart(): void {
103     const currentRootDOMNode = editor.getRootElement();
104     if (currentRootDOMNode === null) {
105       return stop();
106     }
107     const currentParentDOMNode = currentRootDOMNode.parentElement;
108     if (!(currentParentDOMNode instanceof HTMLElement)) {
109       return stop();
110     }
111     stop();
112     rootDOMNode = currentRootDOMNode;
113     parentDOMNode = currentParentDOMNode;
114     observer = new MutationObserver((mutations) => {
115       const nextRootDOMNode = editor.getRootElement();
116       const nextParentDOMNode =
117         nextRootDOMNode && nextRootDOMNode.parentElement;
118       if (
119         nextRootDOMNode !== rootDOMNode ||
120         nextParentDOMNode !== parentDOMNode
121       ) {
122         return restart();
123       }
124       for (const mutation of mutations) {
125         if (!wrapperNode.contains(mutation.target)) {
126           // TODO throttle
127           return position();
128         }
129       }
130     });
131     observer.observe(currentParentDOMNode, mutationObserverConfig);
132     position();
133   }
134
135   const removeRootListener = editor.registerRootListener(restart);
136
137   return () => {
138     removeRootListener();
139     stop();
140   };
141 }