2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import type {LexicalEditor} from 'lexical';
11 import {createRectsFromDOMRange} from '@lexical/selection';
12 import invariant from 'lexical/shared/invariant';
14 import px from './px';
16 const mutationObserverConfig = {
23 export default function positionNodeOnRange(
24 editor: LexicalEditor,
26 onReposition: (node: Array<HTMLElement>) => 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');
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);
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;
54 const left = px(rect.left - rootLeft);
55 if (rectNodeStyle.left !== left) {
56 rectNodeStyle.left = left;
57 hasRepositioned = true;
59 const top = px(rect.top - rootTop);
60 if (rectNodeStyle.top !== top) {
61 rectNode.style.top = top;
62 hasRepositioned = true;
64 const width = px(rect.width);
65 if (rectNodeStyle.width !== width) {
66 rectNode.style.width = width;
67 hasRepositioned = true;
69 const height = px(rect.height);
70 if (rectNodeStyle.height !== height) {
71 rectNode.style.height = height;
72 hasRepositioned = true;
74 if (rectNode.parentNode !== wrapperNode) {
75 wrapperNode.append(rectNode);
76 hasRepositioned = true;
78 lastNodes[i] = rectNode;
80 while (lastNodes.length > rects.length) {
83 if (hasRepositioned) {
84 onReposition(lastNodes);
88 function stop(): void {
91 if (observer !== null) {
92 observer.disconnect();
96 for (const node of lastNodes) {
102 function restart(): void {
103 const currentRootDOMNode = editor.getRootElement();
104 if (currentRootDOMNode === null) {
107 const currentParentDOMNode = currentRootDOMNode.parentElement;
108 if (!(currentParentDOMNode instanceof HTMLElement)) {
112 rootDOMNode = currentRootDOMNode;
113 parentDOMNode = currentParentDOMNode;
114 observer = new MutationObserver((mutations) => {
115 const nextRootDOMNode = editor.getRootElement();
116 const nextParentDOMNode =
117 nextRootDOMNode && nextRootDOMNode.parentElement;
119 nextRootDOMNode !== rootDOMNode ||
120 nextParentDOMNode !== parentDOMNode
124 for (const mutation of mutations) {
125 if (!wrapperNode.contains(mutation.target)) {
131 observer.observe(currentParentDOMNode, mutationObserverConfig);
135 const removeRootListener = editor.registerRootListener(restart);
138 removeRootListener();