diff --git a/packages/outline-playground/src/plugins/KeywordsPlugin.js b/packages/outline-playground/src/plugins/KeywordsPlugin.js index 492eb0e1e..3ea56841d 100644 --- a/packages/outline-playground/src/plugins/KeywordsPlugin.js +++ b/packages/outline-playground/src/plugins/KeywordsPlugin.js @@ -111,6 +111,7 @@ function textTransform(node: TextNode, state: State): void { } function traverseNodes(node: BlockNode): void { + debugger let child = node.getFirstChild(); while (child !== null) { diff --git a/packages/outline/src/core/OutlineEditor.js b/packages/outline/src/core/OutlineEditor.js index 9dfcef75e..afbe63b29 100644 --- a/packages/outline/src/core/OutlineEditor.js +++ b/packages/outline/src/core/OutlineEditor.js @@ -144,8 +144,9 @@ export function resetEditor( editor._pendingEditorState = pendingEditorState; editor._compositionKey = null; editor._dirtyType = NO_DIRTY_NODES; + editor._cloneNotNeeded.clear(); editor._dirtyNodes = new Set(); - editor._dirtyBlocks = new Map(); + editor._dirtySubTrees.clear(); editor._log = []; editor._updates = []; const observer = editor._observer; @@ -207,8 +208,9 @@ class BaseOutlineEditor { _textContent: string; _config: EditorConfig<{...}>; _dirtyType: 0 | 1 | 2; + _cloneNotNeeded: Set; _dirtyNodes: Set; - _dirtyBlocks: Map; + _dirtySubTrees: Set; _observer: null | MutationObserver; _log: Array; @@ -254,8 +256,9 @@ class BaseOutlineEditor { this._pendingDecorators = null; // Used to optimize reconcilation this._dirtyType = NO_DIRTY_NODES; + this._cloneNotNeeded = new Set(); this._dirtyNodes = new Set(); - this._dirtyBlocks = new Map(); + this._dirtySubTrees = new Set(); // Handling of DOM mutations this._observer = null; // Logging for updates @@ -430,8 +433,9 @@ declare export class OutlineEditor { _pendingDecorators: null | {[NodeKey]: ReactNode}; _config: EditorConfig<{...}>; _dirtyType: 0 | 1 | 2; + _cloneNotNeeded: Set; _dirtyNodes: Set; - _dirtyBlocks: Map; + _dirtySubTrees: Set; _observer: null | MutationObserver; _log: Array; diff --git a/packages/outline/src/core/OutlineNode.js b/packages/outline/src/core/OutlineNode.js index ede0477cd..8e0fe8f35 100644 --- a/packages/outline/src/core/OutlineNode.js +++ b/packages/outline/src/core/OutlineNode.js @@ -24,7 +24,6 @@ import { internallyMarkNodeAsDirty, markParentBlocksAsDirty, setCompositionKey, - getBlockDepth, } from './OutlineUtils'; import invariant from 'shared/invariant'; import { @@ -510,15 +509,12 @@ export class OutlineNode { // Ensure we get the latest node from pending state const latestNode = this.getLatest(); const parent = latestNode.__parent; - const dirtyBlocks = editor._dirtyBlocks; + const dirtySubTrees = editor._dirtySubTrees; if (parent !== null) { - markParentBlocksAsDirty(parent, nodeMap, dirtyBlocks); + markParentBlocksAsDirty(parent, nodeMap, dirtySubTrees); } - const dirtyNodes = editor._dirtyNodes; - if (dirtyNodes.has(key)) { - if (isBlockNode(this)) { - dirtyBlocks.set(key, getBlockDepth(this)); - } + const cloneNotNeeded = editor._cloneNotNeeded; + if (cloneNotNeeded.has(key)) { return latestNode; } const constructor = latestNode.constructor; @@ -533,6 +529,7 @@ export class OutlineNode { mutableNode.__format = latestNode.__format; mutableNode.__style = latestNode.__style; } + cloneNotNeeded.add(key); mutableNode.__key = key; internallyMarkNodeAsDirty(mutableNode); // Update reference in node map diff --git a/packages/outline/src/core/OutlineReconciler.js b/packages/outline/src/core/OutlineReconciler.js index bbbb43c81..3e8459703 100644 --- a/packages/outline/src/core/OutlineReconciler.js +++ b/packages/outline/src/core/OutlineReconciler.js @@ -47,7 +47,7 @@ let editorTextContent = ''; let activeEditorConfig: EditorConfig<{...}>; let activeEditor: OutlineEditor; let treatAllNodesAsDirty: boolean = false; -let activeDirtyBlocks: Map; +let activeDirtySubTrees: Set; let activeDirtyNodes: Set; let activePrevNodeMap: NodeMap; let activeNextNodeMap: NodeMap; @@ -331,7 +331,7 @@ function reconcileNode( const isDirty = treatAllNodesAsDirty || activeDirtyNodes.has(key) || - activeDirtyBlocks.has(key); + activeDirtySubTrees.has(key); const dom = getElementByKeyOrThrow(activeEditor, key); if (prevNode === nextNode && !isDirty) { @@ -526,7 +526,7 @@ function reconcileRoot( editor: OutlineEditor, selection: null | OutlineSelection, dirtyType: 0 | 1 | 2, - dirtyBlocks: Map, + dirtySubTrees: Set, dirtyNodes: Set, ): void { subTreeTextContent = ''; @@ -536,7 +536,7 @@ function reconcileRoot( treatAllNodesAsDirty = dirtyType === FULL_RECONCILE; activeEditor = editor; activeEditorConfig = editor._config; - activeDirtyBlocks = dirtyBlocks; + activeDirtySubTrees = dirtySubTrees; activeDirtyNodes = dirtyNodes; activePrevNodeMap = prevEditorState._nodeMap; activeNextNodeMap = nextEditorState._nodeMap; @@ -551,7 +551,7 @@ function reconcileRoot( // $FlowFixMe activeEditor = undefined; // $FlowFixMe - activeDirtyBlocks = undefined; + activeDirtySubTrees = undefined; // $FlowFixMe activeDirtyNodes = undefined; // $FlowFixMe @@ -579,7 +579,7 @@ export function updateEditorState( if (needsUpdate && observer !== null) { const dirtyType = editor._dirtyType; - const dirtyBlocks = editor._dirtyBlocks; + const dirtySubTrees = editor._dirtySubTrees; const dirtyNodes = editor._dirtyNodes; observer.disconnect(); @@ -590,7 +590,7 @@ export function updateEditorState( editor, pendingSelection, dirtyType, - dirtyBlocks, + dirtySubTrees, dirtyNodes, ); } finally { diff --git a/packages/outline/src/core/OutlineUpdates.js b/packages/outline/src/core/OutlineUpdates.js index 2367f33a1..d635051ea 100644 --- a/packages/outline/src/core/OutlineUpdates.js +++ b/packages/outline/src/core/OutlineUpdates.js @@ -166,7 +166,7 @@ function isNodeValidForTransform( function applyAllTransforms( editorState: EditorState, dirtyNodes: Set, - dirtyBlocks: Map, + dirtySubTrees: Set, editor: OutlineEditor, ): void { const transforms = editor._transforms; @@ -207,15 +207,13 @@ function applyAllTransforms( } } if (blockTransforms.size > 0 || rootTransforms.size > 0) { - const dirtyNodesArr = Array.from(dirtyBlocks); + const dirtyNodesArr = Array.from(dirtySubTrees); const blockTransformsArr = Array.from(blockTransforms); const rootTransformsArr = Array.from(rootTransforms); const blockTransformsArrLength = blockTransformsArr.length; const rootTransformsArrLength = rootTransformsArr.length; - // Sort the blocks by their depth, so we deal with deepest first - dirtyNodesArr.sort((a, b) => b[1] - a[1]); for (let s = 0; s < dirtyNodesArr.length; s++) { - const nodeKey = dirtyNodesArr[s][0]; + const nodeKey = dirtyNodesArr[s]; const node = nodeMap.get(nodeKey); if (isNodeValidForTransform(node, compositionKey)) { @@ -383,8 +381,9 @@ export function commitPendingUpdates(editor: OutlineEditor): void { editor._log = []; if (needsUpdate) { editor._dirtyType = NO_DIRTY_NODES; + editor._cloneNotNeeded.clear(); editor._dirtyNodes = new Set(); - editor._dirtyBlocks = new Map(); + editor._dirtySubTrees.clear(); } garbageCollectDetachedDecorators(editor, pendingEditorState); const pendingDecorators = editor._pendingDecorators; @@ -514,14 +513,14 @@ export function beginUpdate( applySelectionTransforms(pendingEditorState, editor); if (editor._dirtyType !== NO_DIRTY_NODES) { const dirtyNodes = editor._dirtyNodes; - const dirtyBlocks = editor._dirtyBlocks; + const dirtySubTrees = editor._dirtySubTrees; if (pendingEditorState.isEmpty()) { invariant( false, 'updateEditor: the pending editor state is empty. Ensure the root not never becomes empty from an update.', ); } - applyAllTransforms(pendingEditorState, dirtyNodes, dirtyBlocks, editor); + applyAllTransforms(pendingEditorState, dirtyNodes, dirtySubTrees, editor); processNestedUpdates(editor, deferred); garbageCollectDetachedNodes( currentEditorState, @@ -556,8 +555,9 @@ export function beginUpdate( // Restore existing editor state to the DOM editor._pendingEditorState = currentEditorState; editor._dirtyType = FULL_RECONCILE; + editor._cloneNotNeeded.clear(); editor._dirtyNodes = new Set(); - editor._dirtyBlocks = new Map(); + editor._dirtySubTrees.clear(); editor._log.push('UpdateRecover'); commitPendingUpdates(editor); return; diff --git a/packages/outline/src/core/OutlineUtils.js b/packages/outline/src/core/OutlineUtils.js index b6744a04a..3ce7b111e 100644 --- a/packages/outline/src/core/OutlineUtils.js +++ b/packages/outline/src/core/OutlineUtils.js @@ -138,6 +138,7 @@ export function generateKey(node: OutlineNode): NodeKey { const key = generateRandomKey(); editorState._nodeMap.set(key, node); editor._dirtyNodes.add(key); + editor._cloneNotNeeded.add(key); editor._dirtyType = HAS_DIRTY_NODES; return key; } @@ -145,18 +146,18 @@ export function generateKey(node: OutlineNode): NodeKey { export function markParentBlocksAsDirty( parentKey: NodeKey, nodeMap: NodeMap, - dirtyBlocks: Map, + dirtySubTress: Set, ): void { let nextParentKey = parentKey; while (nextParentKey !== null) { - if (dirtyBlocks.has(nextParentKey)) { + if (dirtySubTress.has(nextParentKey)) { return; } const node = nodeMap.get(nextParentKey); if (node === undefined) { break; } - dirtyBlocks.set(nextParentKey, getBlockDepth(node)); + dirtySubTress.add(nextParentKey); nextParentKey = node.__parent; } } @@ -169,17 +170,14 @@ export function internallyMarkNodeAsDirty(node: OutlineNode): void { const editorState = getActiveEditorState(); const editor = getActiveEditor(); const nodeMap = editorState._nodeMap; - const dirtyBlocks = editor._dirtyBlocks; + const dirtySubTrees = editor._dirtySubTrees; if (parent !== null) { - markParentBlocksAsDirty(parent, nodeMap, dirtyBlocks); + markParentBlocksAsDirty(parent, nodeMap, dirtySubTrees); } const dirtyNodes = editor._dirtyNodes; const key = latest.__key; editor._dirtyType = HAS_DIRTY_NODES; - dirtyNodes.add(latest.__key); - if (isBlockNode(node)) { - dirtyBlocks.set(key, getBlockDepth(node)); - } + dirtyNodes.add(key); } export function setCompositionKey(compositionKey: null | NodeKey): void { @@ -253,16 +251,6 @@ export function getEditorStateTextContent(editorState: EditorState): string { return editorState.read((view) => view.getRoot().getTextContent()); } -export function getBlockDepth(startingNode: OutlineNode): number { - let node = startingNode.getParent(); - let depth = 0; - while (node !== null) { - depth++; - node = node.getParent(); - } - return depth; -} - export function markAllNodesAsDirty( editor: OutlineEditor, type: 'text' | 'decorator' | 'block' | 'root', diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index aaf71fa77..cfcae0e27 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -7,41 +7,41 @@ "5": "insertNodes: cannot insert a non-block into a root node", "6": "insertNodes: cloned parent clone is not a block", "7": "insertText: first node is not a text node", - "8": "setEditorState: the editor state is empty. Ensure the editor state's root node never becomes empty.", - "9": "updateDOM: prevInnerDOM is null or undefined", - "10": "updateDOM: innerDOM is null or undefined", - "11": "setFormat: can only be used on non-immutable nodes", - "12": "setStyle: can only be used on non-immutable nodes", - "13": "setTextContent: can only be used on non-immutable text nodes", - "14": "spliceText: can only be used on non-immutable text nodes", - "15": "spliceText: selection not found", - "16": "splitText: can only be used on non-immutable text nodes", - "17": "select: cannot be called on root nodes", - "18": "remove: cannot be called on root nodes", - "19": "replace: cannot be called on root nodes", - "20": "insertBefore: cannot be called on root nodes", - "21": "insertAfter: cannot be called on root nodes", - "22": "rootNode.append: Only block nodes can be appended to the root node", - "23": "createNodeFromParse: type \"%s\" + not found", - "24": "Point.getNode: node not found", - "25": "decorate: base method not extended", - "26": "Cannot use method in read-only mode.", - "27": "Unable to find an active editor state. State helpers or node methods can only be used synchronously during the callback of editor.update() or editorState.read().", - "28": "Unable to find an active editor. This method can only be used synchronously during the callback of editor.update().", - "29": "updateEditor: the pending editor state is empty. Ensure the root not never becomes empty from an update.", - "30": "updateEditor: selection has been lost because the previously selected nodes have been removed and selection wasn't moved to another node. Ensure selection changes after removing/replacing a selected node.", - "31": "createNode: node does not exist in nodeMap", - "32": "reconcileNode: prevNode or nextNode does not exist in nodeMap", - "33": "reconcileNode: parentDOM is null", - "34": "Reconciliation: could not find DOM element for node key \"${key}\"", - "35": "OutlineNode: Node type %s does not implement .clone().", - "36": "Expected node %s to have a parent.", - "37": "Expected node %s to have a parent block.", - "38": "Expected node %s to have a top parent block.", - "39": "getNodesBetween: ancestor is null", - "40": "getLatest: node not found", - "41": "createDOM: base method not extended", - "42": "updateDOM: base method not extended", - "43": "setFlags: can only be used on non-immutable nodes", - "44": "Expected node with key %s to exist but it's not in the nodeMap." + "8": "decorate: base method not extended", + "9": "setEditorState: the editor state is empty. Ensure the editor state's root node never becomes empty.", + "10": "updateDOM: prevInnerDOM is null or undefined", + "11": "updateDOM: innerDOM is null or undefined", + "12": "setFormat: can only be used on non-immutable nodes", + "13": "setStyle: can only be used on non-immutable nodes", + "14": "setTextContent: can only be used on non-immutable text nodes", + "15": "spliceText: can only be used on non-immutable text nodes", + "16": "spliceText: selection not found", + "17": "splitText: can only be used on non-immutable text nodes", + "18": "Point.getNode: node not found", + "19": "createNodeFromParse: type \"%s\" + not found", + "20": "select: cannot be called on root nodes", + "21": "remove: cannot be called on root nodes", + "22": "replace: cannot be called on root nodes", + "23": "insertBefore: cannot be called on root nodes", + "24": "insertAfter: cannot be called on root nodes", + "25": "rootNode.append: Only block nodes can be appended to the root node", + "26": "OutlineNode: Node type %s does not implement .clone().", + "27": "Expected node %s to have a parent.", + "28": "Expected node %s to have a parent block.", + "29": "Expected node %s to have a top parent block.", + "30": "getNodesBetween: ancestor is null", + "31": "getLatest: node not found", + "32": "createDOM: base method not extended", + "33": "updateDOM: base method not extended", + "34": "setFlags: can only be used on non-immutable nodes", + "35": "Expected node with key %s to exist but it's not in the nodeMap.", + "36": "Cannot use method in read-only mode.", + "37": "Unable to find an active editor state. State helpers or node methods can only be used synchronously during the callback of editor.update() or editorState.read().", + "38": "Unable to find an active editor. This method can only be used synchronously during the callback of editor.update().", + "39": "updateEditor: the pending editor state is empty. Ensure the root not never becomes empty from an update.", + "40": "updateEditor: selection has been lost because the previously selected nodes have been removed and selection wasn't moved to another node. Ensure selection changes after removing/replacing a selected node.", + "41": "createNode: node does not exist in nodeMap", + "42": "reconcileNode: prevNode or nextNode does not exist in nodeMap", + "43": "reconcileNode: parentDOM is null", + "44": "Reconciliation: could not find DOM element for node key \"${key}\"" }