BookStack/resources/js/wysiwyg/lexical/utils/positionNodeOnRange.ts

142 lines
4.1 KiB
TypeScript
Raw Normal View History

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {LexicalEditor} from 'lexical';
import {createRectsFromDOMRange} from '@lexical/selection';
import invariant from 'lexical/shared/invariant';
import px from './px';
const mutationObserverConfig = {
attributes: true,
characterData: true,
childList: true,
subtree: true,
};
export default function positionNodeOnRange(
editor: LexicalEditor,
range: Range,
onReposition: (node: Array<HTMLElement>) => void,
): () => void {
let rootDOMNode: null | HTMLElement = null;
let parentDOMNode: null | HTMLElement = null;
let observer: null | MutationObserver = null;
let lastNodes: Array<HTMLElement> = [];
const wrapperNode = document.createElement('div');
function position(): void {
invariant(rootDOMNode !== null, 'Unexpected null rootDOMNode');
invariant(parentDOMNode !== null, 'Unexpected null parentDOMNode');
const {left: rootLeft, top: rootTop} = rootDOMNode.getBoundingClientRect();
const parentDOMNode_ = parentDOMNode;
const rects = createRectsFromDOMRange(editor, range);
if (!wrapperNode.isConnected) {
parentDOMNode_.append(wrapperNode);
}
let hasRepositioned = false;
for (let i = 0; i < rects.length; i++) {
const rect = rects[i];
// Try to reuse the previously created Node when possible, no need to
// remove/create on the most common case reposition case
const rectNode = lastNodes[i] || document.createElement('div');
const rectNodeStyle = rectNode.style;
if (rectNodeStyle.position !== 'absolute') {
rectNodeStyle.position = 'absolute';
hasRepositioned = true;
}
const left = px(rect.left - rootLeft);
if (rectNodeStyle.left !== left) {
rectNodeStyle.left = left;
hasRepositioned = true;
}
const top = px(rect.top - rootTop);
if (rectNodeStyle.top !== top) {
rectNode.style.top = top;
hasRepositioned = true;
}
const width = px(rect.width);
if (rectNodeStyle.width !== width) {
rectNode.style.width = width;
hasRepositioned = true;
}
const height = px(rect.height);
if (rectNodeStyle.height !== height) {
rectNode.style.height = height;
hasRepositioned = true;
}
if (rectNode.parentNode !== wrapperNode) {
wrapperNode.append(rectNode);
hasRepositioned = true;
}
lastNodes[i] = rectNode;
}
while (lastNodes.length > rects.length) {
lastNodes.pop();
}
if (hasRepositioned) {
onReposition(lastNodes);
}
}
function stop(): void {
parentDOMNode = null;
rootDOMNode = null;
if (observer !== null) {
observer.disconnect();
}
observer = null;
wrapperNode.remove();
for (const node of lastNodes) {
node.remove();
}
lastNodes = [];
}
function restart(): void {
const currentRootDOMNode = editor.getRootElement();
if (currentRootDOMNode === null) {
return stop();
}
const currentParentDOMNode = currentRootDOMNode.parentElement;
if (!(currentParentDOMNode instanceof HTMLElement)) {
return stop();
}
stop();
rootDOMNode = currentRootDOMNode;
parentDOMNode = currentParentDOMNode;
observer = new MutationObserver((mutations) => {
const nextRootDOMNode = editor.getRootElement();
const nextParentDOMNode =
nextRootDOMNode && nextRootDOMNode.parentElement;
if (
nextRootDOMNode !== rootDOMNode ||
nextParentDOMNode !== parentDOMNode
) {
return restart();
}
for (const mutation of mutations) {
if (!wrapperNode.contains(mutation.target)) {
// TODO throttle
return position();
}
}
});
observer.observe(currentParentDOMNode, mutationObserverConfig);
position();
}
const removeRootListener = editor.registerRootListener(restart);
return () => {
removeRootListener();
stop();
};
}