rittenhop-ghost/versions/5.94.2/node_modules/@lexical/utils/LexicalUtils.dev.js

735 lines
26 KiB
JavaScript
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.
*/
'use strict';
var selection = require('@lexical/selection');
var lexical = require('lexical');
/**
* 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.
*
*/
/**
* Returns a function that will execute all functions passed when called. It is generally used
* to register multiple lexical listeners and then tear them down with a single function call, such
* as React's useEffect hook.
* @example
* ```ts
* useEffect(() => {
* return mergeRegister(
* editor.registerCommand(...registerCommand1 logic),
* editor.registerCommand(...registerCommand2 logic),
* editor.registerCommand(...registerCommand3 logic)
* )
* }, [editor])
* ```
* In this case, useEffect is returning the function returned by mergeRegister as a cleanup
* function to be executed after either the useEffect runs again (due to one of its dependencies
* updating) or the component it resides in unmounts.
* Note the functions don't neccesarily need to be in an array as all arguements
* are considered to be the func argument and spread from there.
* @param func - An array of functions meant to be executed by the returned function.
* @returns the function which executes all the passed register command functions.
*/
function mergeRegister(...func) {
return () => {
func.forEach(f => f());
};
}
/**
* 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.
*
*/
function px(value) {
return `${value}px`;
}
/**
* 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.
*
*/
const mutationObserverConfig = {
attributes: true,
characterData: true,
childList: true,
subtree: true
};
function positionNodeOnRange(editor, range, onReposition) {
let rootDOMNode = null;
let parentDOMNode = null;
let observer = null;
let lastNodes = [];
const wrapperNode = document.createElement('div');
function position() {
if (!(rootDOMNode !== null)) {
throw Error(`Unexpected null rootDOMNode`);
}
if (!(parentDOMNode !== null)) {
throw Error(`Unexpected null parentDOMNode`);
}
const {
left: rootLeft,
top: rootTop
} = rootDOMNode.getBoundingClientRect();
const parentDOMNode_ = parentDOMNode;
const rects = selection.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() {
parentDOMNode = null;
rootDOMNode = null;
if (observer !== null) {
observer.disconnect();
}
observer = null;
wrapperNode.remove();
for (const node of lastNodes) {
node.remove();
}
lastNodes = [];
}
function restart() {
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();
};
}
/**
* 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.
*
*/
function markSelection(editor, onReposition) {
let previousAnchorNode = null;
let previousAnchorOffset = null;
let previousFocusNode = null;
let previousFocusOffset = null;
let removeRangeListener = () => {};
function compute(editorState) {
editorState.read(() => {
const selection = lexical.$getSelection();
if (!lexical.$isRangeSelection(selection)) {
// TODO
previousAnchorNode = null;
previousAnchorOffset = null;
previousFocusNode = null;
previousFocusOffset = null;
removeRangeListener();
removeRangeListener = () => {};
return;
}
const {
anchor,
focus
} = selection;
const currentAnchorNode = anchor.getNode();
const currentAnchorNodeKey = currentAnchorNode.getKey();
const currentAnchorOffset = anchor.offset;
const currentFocusNode = focus.getNode();
const currentFocusNodeKey = currentFocusNode.getKey();
const currentFocusOffset = focus.offset;
const currentAnchorNodeDOM = editor.getElementByKey(currentAnchorNodeKey);
const currentFocusNodeDOM = editor.getElementByKey(currentFocusNodeKey);
const differentAnchorDOM = previousAnchorNode === null || currentAnchorNodeDOM === null || currentAnchorOffset !== previousAnchorOffset || currentAnchorNodeKey !== previousAnchorNode.getKey() || currentAnchorNode !== previousAnchorNode && (!(previousAnchorNode instanceof lexical.TextNode) || currentAnchorNode.updateDOM(previousAnchorNode, currentAnchorNodeDOM, editor._config));
const differentFocusDOM = previousFocusNode === null || currentFocusNodeDOM === null || currentFocusOffset !== previousFocusOffset || currentFocusNodeKey !== previousFocusNode.getKey() || currentFocusNode !== previousFocusNode && (!(previousFocusNode instanceof lexical.TextNode) || currentFocusNode.updateDOM(previousFocusNode, currentFocusNodeDOM, editor._config));
if (differentAnchorDOM || differentFocusDOM) {
const anchorHTMLElement = editor.getElementByKey(anchor.getNode().getKey());
const focusHTMLElement = editor.getElementByKey(focus.getNode().getKey());
// TODO handle selection beyond the common TextNode
if (anchorHTMLElement !== null && focusHTMLElement !== null && anchorHTMLElement.tagName === 'SPAN' && focusHTMLElement.tagName === 'SPAN') {
const range = document.createRange();
let firstHTMLElement;
let firstOffset;
let lastHTMLElement;
let lastOffset;
if (focus.isBefore(anchor)) {
firstHTMLElement = focusHTMLElement;
firstOffset = focus.offset;
lastHTMLElement = anchorHTMLElement;
lastOffset = anchor.offset;
} else {
firstHTMLElement = anchorHTMLElement;
firstOffset = anchor.offset;
lastHTMLElement = focusHTMLElement;
lastOffset = focus.offset;
}
const firstTextNode = firstHTMLElement.firstChild;
if (!(firstTextNode !== null)) {
throw Error(`Expected text node to be first child of span`);
}
const lastTextNode = lastHTMLElement.firstChild;
if (!(lastTextNode !== null)) {
throw Error(`Expected text node to be first child of span`);
}
range.setStart(firstTextNode, firstOffset);
range.setEnd(lastTextNode, lastOffset);
removeRangeListener();
removeRangeListener = positionNodeOnRange(editor, range, domNodes => {
for (const domNode of domNodes) {
const domNodeStyle = domNode.style;
if (domNodeStyle.background !== 'Highlight') {
domNodeStyle.background = 'Highlight';
}
if (domNodeStyle.color !== 'HighlightText') {
domNodeStyle.color = 'HighlightText';
}
if (domNodeStyle.zIndex !== '-1') {
domNodeStyle.zIndex = '-1';
}
if (domNodeStyle.pointerEvents !== 'none') {
domNodeStyle.pointerEvents = 'none';
}
if (domNodeStyle.marginTop !== px(-1.5)) {
domNodeStyle.marginTop = px(-1.5);
}
if (domNodeStyle.paddingTop !== px(4)) {
domNodeStyle.paddingTop = px(4);
}
if (domNodeStyle.paddingBottom !== px(0)) {
domNodeStyle.paddingBottom = px(0);
}
}
if (onReposition !== undefined) {
onReposition(domNodes);
}
});
}
}
previousAnchorNode = currentAnchorNode;
previousAnchorOffset = currentAnchorOffset;
previousFocusNode = currentFocusNode;
previousFocusOffset = currentFocusOffset;
});
}
compute(editor.getEditorState());
return mergeRegister(editor.registerUpdateListener(({
editorState
}) => compute(editorState)), removeRangeListener, () => {
removeRangeListener();
});
}
/** @module @lexical/utils */
/**
* Takes an HTML element and adds the classNames passed within an array,
* ignoring any non-string types. A space can be used to add multiple classes
* eg. addClassNamesToElement(element, ['element-inner active', true, null])
* will add both 'element-inner' and 'active' as classes to that element.
* @param element - The element in which the classes are added
* @param classNames - An array defining the class names to add to the element
*/
function addClassNamesToElement(element, ...classNames) {
classNames.forEach(className => {
if (typeof className === 'string') {
const classesToAdd = className.split(' ').filter(n => n !== '');
element.classList.add(...classesToAdd);
}
});
}
/**
* Takes an HTML element and removes the classNames passed within an array,
* ignoring any non-string types. A space can be used to remove multiple classes
* eg. removeClassNamesFromElement(element, ['active small', true, null])
* will remove both the 'active' and 'small' classes from that element.
* @param element - The element in which the classes are removed
* @param classNames - An array defining the class names to remove from the element
*/
function removeClassNamesFromElement(element, ...classNames) {
classNames.forEach(className => {
if (typeof className === 'string') {
element.classList.remove(...className.split(' '));
}
});
}
/**
* Returns true if the file type matches the types passed within the acceptableMimeTypes array, false otherwise.
* The types passed must be strings and are CASE-SENSITIVE.
* eg. if file is of type 'text' and acceptableMimeTypes = ['TEXT', 'IMAGE'] the function will return false.
* @param file - The file you want to type check.
* @param acceptableMimeTypes - An array of strings of types which the file is checked against.
* @returns true if the file is an acceptable mime type, false otherwise.
*/
function isMimeType(file, acceptableMimeTypes) {
for (const acceptableType of acceptableMimeTypes) {
if (file.type.startsWith(acceptableType)) {
return true;
}
}
return false;
}
/**
* Lexical File Reader with:
* 1. MIME type support
* 2. batched results (HistoryPlugin compatibility)
* 3. Order aware (respects the order when multiple Files are passed)
*
* const filesResult = await mediaFileReader(files, ['image/']);
* filesResult.forEach(file => editor.dispatchCommand('INSERT_IMAGE', {
* src: file.result,
* }));
*/
function mediaFileReader(files, acceptableMimeTypes) {
const filesIterator = files[Symbol.iterator]();
return new Promise((resolve, reject) => {
const processed = [];
const handleNextFile = () => {
const {
done,
value: file
} = filesIterator.next();
if (done) {
return resolve(processed);
}
const fileReader = new FileReader();
fileReader.addEventListener('error', reject);
fileReader.addEventListener('load', () => {
const result = fileReader.result;
if (typeof result === 'string') {
processed.push({
file,
result
});
}
handleNextFile();
});
if (isMimeType(file, acceptableMimeTypes)) {
fileReader.readAsDataURL(file);
} else {
handleNextFile();
}
};
handleNextFile();
});
}
/**
* "Depth-First Search" starts at the root/top node of a tree and goes as far as it can down a branch end
* before backtracking and finding a new path. Consider solving a maze by hugging either wall, moving down a
* branch until you hit a dead-end (leaf) and backtracking to find the nearest branching path and repeat.
* It will then return all the nodes found in the search in an array of objects.
* @param startingNode - The node to start the search, if ommitted, it will start at the root node.
* @param endingNode - The node to end the search, if ommitted, it will find all descendants of the startingNode.
* @returns An array of objects of all the nodes found by the search, including their depth into the tree.
* {depth: number, node: LexicalNode} It will always return at least 1 node (the ending node) so long as it exists
*/
function $dfs(startingNode, endingNode) {
const nodes = [];
const start = (startingNode || lexical.$getRoot()).getLatest();
const end = endingNode || (lexical.$isElementNode(start) ? start.getLastDescendant() : start);
let node = start;
let depth = $getDepth(node);
while (node !== null && !node.is(end)) {
nodes.push({
depth,
node
});
if (lexical.$isElementNode(node) && node.getChildrenSize() > 0) {
node = node.getFirstChild();
depth++;
} else {
// Find immediate sibling or nearest parent sibling
let sibling = null;
while (sibling === null && node !== null) {
sibling = node.getNextSibling();
if (sibling === null) {
node = node.getParent();
depth--;
} else {
node = sibling;
}
}
}
}
if (node !== null && node.is(end)) {
nodes.push({
depth,
node
});
}
return nodes;
}
function $getDepth(node) {
let innerNode = node;
let depth = 0;
while ((innerNode = innerNode.getParent()) !== null) {
depth++;
}
return depth;
}
/**
* Takes a node and traverses up its ancestors (toward the root node)
* in order to find a specific type of node.
* @param node - the node to begin searching.
* @param klass - an instance of the type of node to look for.
* @returns the node of type klass that was passed, or null if none exist.
*/
function $getNearestNodeOfType(node, klass) {
let parent = node;
while (parent != null) {
if (parent instanceof klass) {
return parent;
}
parent = parent.getParent();
}
return null;
}
/**
* Returns the element node of the nearest ancestor, otherwise throws an error.
* @param startNode - The starting node of the search
* @returns The ancestor node found
*/
function $getNearestBlockElementAncestorOrThrow(startNode) {
const blockNode = $findMatchingParent(startNode, node => lexical.$isElementNode(node) && !node.isInline());
if (!lexical.$isElementNode(blockNode)) {
{
throw Error(`Expected node ${startNode.__key} to have closest block element node.`);
}
}
return blockNode;
}
/**
* Starts with a node and moves up the tree (toward the root node) to find a matching node based on
* the search parameters of the findFn. (Consider JavaScripts' .find() function where a testing function must be
* passed as an argument. eg. if( (node) => node.__type === 'div') ) return true; otherwise return false
* @param startingNode - The node where the search starts.
* @param findFn - A testing function that returns true if the current node satisfies the testing parameters.
* @returns A parent node that matches the findFn parameters, or null if one wasn't found.
*/
const $findMatchingParent = (startingNode, findFn) => {
let curr = startingNode;
while (curr !== lexical.$getRoot() && curr != null) {
if (findFn(curr)) {
return curr;
}
curr = curr.getParent();
}
return null;
};
/**
* Attempts to resolve nested element nodes of the same type into a single node of that type.
* It is generally used for marks/commenting
* @param editor - The lexical editor
* @param targetNode - The target for the nested element to be extracted from.
* @param cloneNode - See {@link $createMarkNode}
* @param handleOverlap - Handles any overlap between the node to extract and the targetNode
* @returns The lexical editor
*/
function registerNestedElementResolver(editor, targetNode, cloneNode, handleOverlap) {
const $isTargetNode = node => {
return node instanceof targetNode;
};
const $findMatch = node => {
// First validate we don't have any children that are of the target,
// as we need to handle them first.
const children = node.getChildren();
for (let i = 0; i < children.length; i++) {
const child = children[i];
if ($isTargetNode(child)) {
return null;
}
}
let parentNode = node;
let childNode = node;
while (parentNode !== null) {
childNode = parentNode;
parentNode = parentNode.getParent();
if ($isTargetNode(parentNode)) {
return {
child: childNode,
parent: parentNode
};
}
}
return null;
};
const elementNodeTransform = node => {
const match = $findMatch(node);
if (match !== null) {
const {
child,
parent
} = match;
// Simple path, we can move child out and siblings into a new parent.
if (child.is(node)) {
handleOverlap(parent, node);
const nextSiblings = child.getNextSiblings();
const nextSiblingsLength = nextSiblings.length;
parent.insertAfter(child);
if (nextSiblingsLength !== 0) {
const newParent = cloneNode(parent);
child.insertAfter(newParent);
for (let i = 0; i < nextSiblingsLength; i++) {
newParent.append(nextSiblings[i]);
}
}
if (!parent.canBeEmpty() && parent.getChildrenSize() === 0) {
parent.remove();
}
}
}
};
return editor.registerNodeTransform(targetNode, elementNodeTransform);
}
/**
* Clones the editor and marks it as dirty to be reconciled. If there was a selection,
* it would be set back to its previous state, or null otherwise.
* @param editor - The lexical editor
* @param editorState - The editor's state
*/
function $restoreEditorState(editor, editorState) {
const FULL_RECONCILE = 2;
const nodeMap = new Map();
const activeEditorState = editor._pendingEditorState;
for (const [key, node] of editorState._nodeMap) {
const clone = selection.$cloneWithProperties(node);
if (lexical.$isTextNode(clone)) {
if (!lexical.$isTextNode(node)) {
throw Error(`Expected node be a TextNode`);
}
clone.__text = node.__text;
}
nodeMap.set(key, clone);
}
if (activeEditorState) {
activeEditorState._nodeMap = nodeMap;
}
editor._dirtyType = FULL_RECONCILE;
const selection$1 = editorState._selection;
lexical.$setSelection(selection$1 === null ? null : selection$1.clone());
}
/**
* If the selected insertion area is the root/shadow root node (see {@link lexical!$isRootOrShadowRoot}),
* the node will be appended there, otherwise, it will be inserted before the insertion area.
* If there is no selection where the node is to be inserted, it will be appended after any current nodes
* within the tree, as a child of the root node. A paragraph node will then be added after the inserted node and selected.
* @param node - The node to be inserted
* @returns The node after its insertion
*/
function $insertNodeToNearestRoot(node) {
const selection = lexical.$getSelection() || lexical.$getPreviousSelection();
if (lexical.$isRangeSelection(selection)) {
const {
focus
} = selection;
const focusNode = focus.getNode();
const focusOffset = focus.offset;
if (lexical.$isRootOrShadowRoot(focusNode)) {
const focusChild = focusNode.getChildAtIndex(focusOffset);
if (focusChild == null) {
focusNode.append(node);
} else {
focusChild.insertBefore(node);
}
node.selectNext();
} else {
let splitNode;
let splitOffset;
if (lexical.$isTextNode(focusNode)) {
splitNode = focusNode.getParentOrThrow();
splitOffset = focusNode.getIndexWithinParent();
if (focusOffset > 0) {
splitOffset += 1;
focusNode.splitText(focusOffset);
}
} else {
splitNode = focusNode;
splitOffset = focusOffset;
}
const [, rightTree] = lexical.$splitNode(splitNode, splitOffset);
rightTree.insertBefore(node);
rightTree.selectStart();
}
} else {
if (selection != null) {
const nodes = selection.getNodes();
nodes[nodes.length - 1].getTopLevelElementOrThrow().insertAfter(node);
} else {
const root = lexical.$getRoot();
root.append(node);
}
const paragraphNode = lexical.$createParagraphNode();
node.insertAfter(paragraphNode);
paragraphNode.select();
}
return node.getLatest();
}
/**
* Wraps the node into another node created from a createElementNode function, eg. $createParagraphNode
* @param node - Node to be wrapped.
* @param createElementNode - Creates a new lexical element to wrap the to-be-wrapped node and returns it.
* @returns A new lexical element with the previous node appended within (as a child, including its children).
*/
function $wrapNodeInElement(node, createElementNode) {
const elementNode = createElementNode();
node.replace(elementNode);
elementNode.append(node);
return elementNode;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/**
* @param object = The instance of the type
* @param objectClass = The class of the type
* @returns Whether the object is has the same Klass of the objectClass, ignoring the difference across window (e.g. different iframs)
*/
function objectKlassEquals(object, objectClass) {
return object !== null ? Object.getPrototypeOf(object).constructor.name === objectClass.name : false;
}
/**
* Filter the nodes
* @param nodes Array of nodes that needs to be filtered
* @param filterFn A filter function that returns node if the current node satisfies the condition otherwise null
* @returns Array of filtered nodes
*/
function $filter(nodes, filterFn) {
const result = [];
for (let i = 0; i < nodes.length; i++) {
const node = filterFn(nodes[i]);
if (node !== null) {
result.push(node);
}
}
return result;
}
/**
* Appends the node before the first child of the parent node
* @param parent A parent node
* @param node Node that needs to be appended
*/
function $insertFirst(parent, node) {
const firstChild = parent.getFirstChild();
if (firstChild !== null) {
firstChild.insertBefore(node);
} else {
parent.append(node);
}
}
exports.$splitNode = lexical.$splitNode;
exports.isHTMLAnchorElement = lexical.isHTMLAnchorElement;
exports.isHTMLElement = lexical.isHTMLElement;
exports.$dfs = $dfs;
exports.$filter = $filter;
exports.$findMatchingParent = $findMatchingParent;
exports.$getNearestBlockElementAncestorOrThrow = $getNearestBlockElementAncestorOrThrow;
exports.$getNearestNodeOfType = $getNearestNodeOfType;
exports.$insertFirst = $insertFirst;
exports.$insertNodeToNearestRoot = $insertNodeToNearestRoot;
exports.$restoreEditorState = $restoreEditorState;
exports.$wrapNodeInElement = $wrapNodeInElement;
exports.addClassNamesToElement = addClassNamesToElement;
exports.isMimeType = isMimeType;
exports.markSelection = markSelection;
exports.mediaFileReader = mediaFileReader;
exports.mergeRegister = mergeRegister;
exports.objectKlassEquals = objectKlassEquals;
exports.positionNodeOnRange = positionNodeOnRange;
exports.registerNestedElementResolver = registerNestedElementResolver;
exports.removeClassNamesFromElement = removeClassNamesFromElement;