mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-31 15:24:31 +03:00
Lexical: Imported core lexical libs
Imported at 0.17.1, Modified to work in-app. Added & configured test dependancies. Tests need to be altered to avoid using non-included deps including react dependancies.
This commit is contained in:
322
resources/js/wysiwyg/lexical/core/LexicalMutations.ts
Normal file
322
resources/js/wysiwyg/lexical/core/LexicalMutations.ts
Normal file
@ -0,0 +1,322 @@
|
||||
/**
|
||||
* 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 {TextNode} from '.';
|
||||
import type {LexicalEditor} from './LexicalEditor';
|
||||
import type {BaseSelection} from './LexicalSelection';
|
||||
|
||||
import {IS_FIREFOX} from 'lexical/shared/environment';
|
||||
|
||||
import {
|
||||
$getSelection,
|
||||
$isDecoratorNode,
|
||||
$isElementNode,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
$setSelection,
|
||||
} from '.';
|
||||
import {DOM_TEXT_TYPE} from './LexicalConstants';
|
||||
import {updateEditor} from './LexicalUpdates';
|
||||
import {
|
||||
$getNearestNodeFromDOMNode,
|
||||
$getNodeFromDOMNode,
|
||||
$updateTextNodeFromDOMContent,
|
||||
getDOMSelection,
|
||||
getWindow,
|
||||
internalGetRoot,
|
||||
isFirefoxClipboardEvents,
|
||||
} from './LexicalUtils';
|
||||
// The time between a text entry event and the mutation observer firing.
|
||||
const TEXT_MUTATION_VARIANCE = 100;
|
||||
|
||||
let isProcessingMutations = false;
|
||||
let lastTextEntryTimeStamp = 0;
|
||||
|
||||
export function getIsProcessingMutations(): boolean {
|
||||
return isProcessingMutations;
|
||||
}
|
||||
|
||||
function updateTimeStamp(event: Event) {
|
||||
lastTextEntryTimeStamp = event.timeStamp;
|
||||
}
|
||||
|
||||
function initTextEntryListener(editor: LexicalEditor): void {
|
||||
if (lastTextEntryTimeStamp === 0) {
|
||||
getWindow(editor).addEventListener('textInput', updateTimeStamp, true);
|
||||
}
|
||||
}
|
||||
|
||||
function isManagedLineBreak(
|
||||
dom: Node,
|
||||
target: Node,
|
||||
editor: LexicalEditor,
|
||||
): boolean {
|
||||
return (
|
||||
// @ts-expect-error: internal field
|
||||
target.__lexicalLineBreak === dom ||
|
||||
// @ts-ignore We intentionally add this to the Node.
|
||||
dom[`__lexicalKey_${editor._key}`] !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function getLastSelection(editor: LexicalEditor): null | BaseSelection {
|
||||
return editor.getEditorState().read(() => {
|
||||
const selection = $getSelection();
|
||||
return selection !== null ? selection.clone() : null;
|
||||
});
|
||||
}
|
||||
|
||||
function $handleTextMutation(
|
||||
target: Text,
|
||||
node: TextNode,
|
||||
editor: LexicalEditor,
|
||||
): void {
|
||||
const domSelection = getDOMSelection(editor._window);
|
||||
let anchorOffset = null;
|
||||
let focusOffset = null;
|
||||
|
||||
if (domSelection !== null && domSelection.anchorNode === target) {
|
||||
anchorOffset = domSelection.anchorOffset;
|
||||
focusOffset = domSelection.focusOffset;
|
||||
}
|
||||
|
||||
const text = target.nodeValue;
|
||||
if (text !== null) {
|
||||
$updateTextNodeFromDOMContent(node, text, anchorOffset, focusOffset, false);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUpdateTextNodeFromMutation(
|
||||
selection: null | BaseSelection,
|
||||
targetDOM: Node,
|
||||
targetNode: TextNode,
|
||||
): boolean {
|
||||
if ($isRangeSelection(selection)) {
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
if (
|
||||
anchorNode.is(targetNode) &&
|
||||
selection.format !== anchorNode.getFormat()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached();
|
||||
}
|
||||
|
||||
export function $flushMutations(
|
||||
editor: LexicalEditor,
|
||||
mutations: Array<MutationRecord>,
|
||||
observer: MutationObserver,
|
||||
): void {
|
||||
isProcessingMutations = true;
|
||||
const shouldFlushTextMutations =
|
||||
performance.now() - lastTextEntryTimeStamp > TEXT_MUTATION_VARIANCE;
|
||||
|
||||
try {
|
||||
updateEditor(editor, () => {
|
||||
const selection = $getSelection() || getLastSelection(editor);
|
||||
const badDOMTargets = new Map();
|
||||
const rootElement = editor.getRootElement();
|
||||
// We use the current editor state, as that reflects what is
|
||||
// actually "on screen".
|
||||
const currentEditorState = editor._editorState;
|
||||
const blockCursorElement = editor._blockCursorElement;
|
||||
let shouldRevertSelection = false;
|
||||
let possibleTextForFirefoxPaste = '';
|
||||
|
||||
for (let i = 0; i < mutations.length; i++) {
|
||||
const mutation = mutations[i];
|
||||
const type = mutation.type;
|
||||
const targetDOM = mutation.target;
|
||||
let targetNode = $getNearestNodeFromDOMNode(
|
||||
targetDOM,
|
||||
currentEditorState,
|
||||
);
|
||||
|
||||
if (
|
||||
(targetNode === null && targetDOM !== rootElement) ||
|
||||
$isDecoratorNode(targetNode)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === 'characterData') {
|
||||
// Text mutations are deferred and passed to mutation listeners to be
|
||||
// processed outside of the Lexical engine.
|
||||
if (
|
||||
shouldFlushTextMutations &&
|
||||
$isTextNode(targetNode) &&
|
||||
shouldUpdateTextNodeFromMutation(selection, targetDOM, targetNode)
|
||||
) {
|
||||
$handleTextMutation(
|
||||
// nodeType === DOM_TEXT_TYPE is a Text DOM node
|
||||
targetDOM as Text,
|
||||
targetNode,
|
||||
editor,
|
||||
);
|
||||
}
|
||||
} else if (type === 'childList') {
|
||||
shouldRevertSelection = true;
|
||||
// We attempt to "undo" any changes that have occurred outside
|
||||
// of Lexical. We want Lexical's editor state to be source of truth.
|
||||
// To the user, these will look like no-ops.
|
||||
const addedDOMs = mutation.addedNodes;
|
||||
|
||||
for (let s = 0; s < addedDOMs.length; s++) {
|
||||
const addedDOM = addedDOMs[s];
|
||||
const node = $getNodeFromDOMNode(addedDOM);
|
||||
const parentDOM = addedDOM.parentNode;
|
||||
|
||||
if (
|
||||
parentDOM != null &&
|
||||
addedDOM !== blockCursorElement &&
|
||||
node === null &&
|
||||
(addedDOM.nodeName !== 'BR' ||
|
||||
!isManagedLineBreak(addedDOM, parentDOM, editor))
|
||||
) {
|
||||
if (IS_FIREFOX) {
|
||||
const possibleText =
|
||||
(addedDOM as HTMLElement).innerText || addedDOM.nodeValue;
|
||||
|
||||
if (possibleText) {
|
||||
possibleTextForFirefoxPaste += possibleText;
|
||||
}
|
||||
}
|
||||
|
||||
parentDOM.removeChild(addedDOM);
|
||||
}
|
||||
}
|
||||
|
||||
const removedDOMs = mutation.removedNodes;
|
||||
const removedDOMsLength = removedDOMs.length;
|
||||
|
||||
if (removedDOMsLength > 0) {
|
||||
let unremovedBRs = 0;
|
||||
|
||||
for (let s = 0; s < removedDOMsLength; s++) {
|
||||
const removedDOM = removedDOMs[s];
|
||||
|
||||
if (
|
||||
(removedDOM.nodeName === 'BR' &&
|
||||
isManagedLineBreak(removedDOM, targetDOM, editor)) ||
|
||||
blockCursorElement === removedDOM
|
||||
) {
|
||||
targetDOM.appendChild(removedDOM);
|
||||
unremovedBRs++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedDOMsLength !== unremovedBRs) {
|
||||
if (targetDOM === rootElement) {
|
||||
targetNode = internalGetRoot(currentEditorState);
|
||||
}
|
||||
|
||||
badDOMTargets.set(targetDOM, targetNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we process each of the unique target nodes, attempting
|
||||
// to restore their contents back to the source of truth, which
|
||||
// is Lexical's "current" editor state. This is basically like
|
||||
// an internal revert on the DOM.
|
||||
if (badDOMTargets.size > 0) {
|
||||
for (const [targetDOM, targetNode] of badDOMTargets) {
|
||||
if ($isElementNode(targetNode)) {
|
||||
const childKeys = targetNode.getChildrenKeys();
|
||||
let currentDOM = targetDOM.firstChild;
|
||||
|
||||
for (let s = 0; s < childKeys.length; s++) {
|
||||
const key = childKeys[s];
|
||||
const correctDOM = editor.getElementByKey(key);
|
||||
|
||||
if (correctDOM === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentDOM == null) {
|
||||
targetDOM.appendChild(correctDOM);
|
||||
currentDOM = correctDOM;
|
||||
} else if (currentDOM !== correctDOM) {
|
||||
targetDOM.replaceChild(correctDOM, currentDOM);
|
||||
}
|
||||
|
||||
currentDOM = currentDOM.nextSibling;
|
||||
}
|
||||
} else if ($isTextNode(targetNode)) {
|
||||
targetNode.markDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture all the mutations made during this function. This
|
||||
// also prevents us having to process them on the next cycle
|
||||
// of onMutation, as these mutations were made by us.
|
||||
const records = observer.takeRecords();
|
||||
|
||||
// Check for any random auto-added <br> elements, and remove them.
|
||||
// These get added by the browser when we undo the above mutations
|
||||
// and this can lead to a broken UI.
|
||||
if (records.length > 0) {
|
||||
for (let i = 0; i < records.length; i++) {
|
||||
const record = records[i];
|
||||
const addedNodes = record.addedNodes;
|
||||
const target = record.target;
|
||||
|
||||
for (let s = 0; s < addedNodes.length; s++) {
|
||||
const addedDOM = addedNodes[s];
|
||||
const parentDOM = addedDOM.parentNode;
|
||||
|
||||
if (
|
||||
parentDOM != null &&
|
||||
addedDOM.nodeName === 'BR' &&
|
||||
!isManagedLineBreak(addedDOM, target, editor)
|
||||
) {
|
||||
parentDOM.removeChild(addedDOM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any of those removal mutations
|
||||
observer.takeRecords();
|
||||
}
|
||||
|
||||
if (selection !== null) {
|
||||
if (shouldRevertSelection) {
|
||||
selection.dirty = true;
|
||||
$setSelection(selection);
|
||||
}
|
||||
|
||||
if (IS_FIREFOX && isFirefoxClipboardEvents(editor)) {
|
||||
selection.insertRawText(possibleTextForFirefoxPaste);
|
||||
}
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
isProcessingMutations = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function $flushRootMutations(editor: LexicalEditor): void {
|
||||
const observer = editor._observer;
|
||||
|
||||
if (observer !== null) {
|
||||
const mutations = observer.takeRecords();
|
||||
$flushMutations(editor, mutations, observer);
|
||||
}
|
||||
}
|
||||
|
||||
export function initMutationObserver(editor: LexicalEditor): void {
|
||||
initTextEntryListener(editor);
|
||||
editor._observer = new MutationObserver(
|
||||
(mutations: Array<MutationRecord>, observer: MutationObserver) => {
|
||||
$flushMutations(editor, mutations, observer);
|
||||
},
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user