From ba89989c3c54ac24bf607490fc364ad529675dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Fri, 17 Oct 2025 18:57:06 -0500 Subject: [PATCH] Allow deletion of orphan nodes! --- .../src/convert/markdown/convertToMarkdown.ts | 4 +-- .../lexical/src/nodes/JupyterOutputNode.tsx | 16 +++++++++-- .../src/nodes/JupyterOutputNodeUtils.ts | 27 +++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 packages/lexical/src/nodes/JupyterOutputNodeUtils.ts diff --git a/packages/lexical/src/convert/markdown/convertToMarkdown.ts b/packages/lexical/src/convert/markdown/convertToMarkdown.ts index 15bc75cf8..dbdfbcfba 100644 --- a/packages/lexical/src/convert/markdown/convertToMarkdown.ts +++ b/packages/lexical/src/convert/markdown/convertToMarkdown.ts @@ -25,7 +25,7 @@ import { } from './utils'; export function $convertToMarkdownString(): string { - const output = []; + const output: string[] = []; const children = $getRoot().getChildren(); for (const child of children) { @@ -56,7 +56,7 @@ function exportTopLevelElementOrDecorator(node: LexicalNode): string | null { } function exportChildren(node: ElementNode): string { - const output = []; + const output: string[] = []; const children = node.getChildren(); for (const child of children) { diff --git a/packages/lexical/src/nodes/JupyterOutputNode.tsx b/packages/lexical/src/nodes/JupyterOutputNode.tsx index 9a1f08190..a2bf01771 100644 --- a/packages/lexical/src/nodes/JupyterOutputNode.tsx +++ b/packages/lexical/src/nodes/JupyterOutputNode.tsx @@ -28,6 +28,7 @@ import { Kernel, } from '@datalayer/jupyter-react'; import { createNoKernelWarning } from './jupyterUtils'; +import { isJupyterOutputNodeOrphaned } from './JupyterOutputNodeUtils'; export type SerializedJupyterOutputNode = Spread< { @@ -191,7 +192,10 @@ export class JupyterOutputNode extends DecoratorNode { /** @override */ isIsolated(): boolean { - return false; + // Treat orphaned output nodes as isolated blocks for editing purposes + // Return true only for orphaned nodes so they are handled as isolated blocks + // Normal output nodes with valid parents remain non-isolated + return isJupyterOutputNodeOrphaned(this); } /** @override */ @@ -223,7 +227,15 @@ export class JupyterOutputNode extends DecoratorNode { /** @override */ remove(_preserveEmptyParent?: boolean): void { - // Do not delete JupyterOutputNode. + // Check if this output node is orphaned (parent input node was deleted) + if (isJupyterOutputNodeOrphaned(this)) { + // Allow deletion of orphaned output nodes + // Explicitly set preserveEmptyParent to false for orphaned nodes, + // since their parent has already been deleted and we do not want to preserve an + // empty parent. + super.remove(false); + } + // Otherwise, do not delete (output nodes with valid parents are protected) } removeForce(): void { diff --git a/packages/lexical/src/nodes/JupyterOutputNodeUtils.ts b/packages/lexical/src/nodes/JupyterOutputNodeUtils.ts new file mode 100644 index 000000000..fe6a9d689 --- /dev/null +++ b/packages/lexical/src/nodes/JupyterOutputNodeUtils.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { INPUT_UUID_TO_CODE_KEY } from '../plugins/JupyterInputOutputPlugin'; +import type { JupyterOutputNode } from './JupyterOutputNode'; + +/** + * Check if a JupyterOutputNode is orphaned (its parent input node was deleted). + * + * An output node is considered orphaned if: + * 1. Its parent input node UUID is not in the INPUT_UUID_TO_CODE_KEY map, OR + * 2. The output node has no parent in the Lexical tree + * + * @param outputNode - The JupyterOutputNode to check + * @returns true if the output node is orphaned, false otherwise + */ +export function isJupyterOutputNodeOrphaned( + outputNode: JupyterOutputNode, +): boolean { + const inputNodeKey = INPUT_UUID_TO_CODE_KEY.get( + outputNode.getJupyterInputNodeUuid(), + ); + return !inputNodeKey || !outputNode.getParent(); +}