mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-12-14 15:13:37 +08:00
142 lines
4.1 KiB
TypeScript
142 lines
4.1 KiB
TypeScript
|
/**
|
||
|
* 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();
|
||
|
};
|
||
|
}
|