resources/js/wysiwyg/lexical/core/LexicalGC.ts
/**
* 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 {ElementNode} from '.';
import type {LexicalEditor} from './LexicalEditor';
import type {EditorState} from './LexicalEditorState';
import type {NodeKey, NodeMap} from './LexicalNode';
import {$isElementNode} from '.';
import {cloneDecorators} from './LexicalUtils';
export function $garbageCollectDetachedDecorators(
editor: LexicalEditor,
pendingEditorState: EditorState,
): void {
const currentDecorators = editor._decorators;
const pendingDecorators = editor._pendingDecorators;
let decorators = pendingDecorators || currentDecorators;
const nodeMap = pendingEditorState._nodeMap;
let key;
for (key in decorators) {
if (!nodeMap.has(key)) {
if (decorators === currentDecorators) {
decorators = cloneDecorators(editor);
}
delete decorators[key];
}
}
}
type IntentionallyMarkedAsDirtyElement = boolean;
function $garbageCollectDetachedDeepChildNodes(
node: ElementNode,
parentKey: NodeKey,
prevNodeMap: NodeMap,
nodeMap: NodeMap,
nodeMapDelete: Array<NodeKey>,
dirtyNodes: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
): void {
let child = node.getFirstChild();
while (child !== null) {
const childKey = child.__key;
// TODO Revise condition below, redundant? LexicalNode already cleans up children when moving Nodes
if (child.__parent === parentKey) {
if ($isElementNode(child)) {
$garbageCollectDetachedDeepChildNodes(
child,
childKey,
prevNodeMap,
nodeMap,
nodeMapDelete,
dirtyNodes,
);
}
// If we have created a node and it was dereferenced, then also
// remove it from out dirty nodes Set.
if (!prevNodeMap.has(childKey)) {
dirtyNodes.delete(childKey);
}
nodeMapDelete.push(childKey);
}
child = child.getNextSibling();
}
}
export function $garbageCollectDetachedNodes(
prevEditorState: EditorState,
editorState: EditorState,
dirtyLeaves: Set<NodeKey>,
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
): void {
const prevNodeMap = prevEditorState._nodeMap;
const nodeMap = editorState._nodeMap;
// Store dirtyElements in a queue for later deletion; deleting dirty subtrees too early will
// hinder accessing .__next on child nodes
const nodeMapDelete: Array<NodeKey> = [];
for (const [nodeKey] of dirtyElements) {
const node = nodeMap.get(nodeKey);
if (node !== undefined) {
// Garbage collect node and its children if they exist
if (!node.isAttached()) {
if ($isElementNode(node)) {
$garbageCollectDetachedDeepChildNodes(
node,
nodeKey,
prevNodeMap,
nodeMap,
nodeMapDelete,
dirtyElements,
);
}
// If we have created a node and it was dereferenced, then also
// remove it from out dirty nodes Set.
if (!prevNodeMap.has(nodeKey)) {
dirtyElements.delete(nodeKey);
}
nodeMapDelete.push(nodeKey);
}
}
}
for (const nodeKey of nodeMapDelete) {
nodeMap.delete(nodeKey);
}
for (const nodeKey of dirtyLeaves) {
const node = nodeMap.get(nodeKey);
if (node !== undefined && !node.isAttached()) {
if (!prevNodeMap.has(nodeKey)) {
dirtyLeaves.delete(nodeKey);
}
nodeMap.delete(nodeKey);
}
}
}