From c6449f34e56f6f219325c1a17f07ee65843df3e8 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 23 Oct 2025 16:36:17 +0200 Subject: [PATCH 1/2] Added missing Delete key handlers --- .../commands/mergeBlocks/mergeBlocks.ts | 23 +++ .../KeyboardShortcutsExtension.ts | 168 ++++++++++++++---- 2 files changed, 154 insertions(+), 37 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts index 50f8aa346c..4877e549a1 100644 --- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts @@ -50,6 +50,27 @@ export const getPrevBlockInfo = (doc: Node, beforePos: number) => { return prevBlockInfo; }; +/** + * Returns the block info from the sibling block after (below) the given block, + * or undefined if the given block is the last sibling. + */ +export const getNextBlockInfo = (doc: Node, beforePos: number) => { + const $pos = doc.resolve(beforePos); + + const indexInParent = $pos.index(); + + if (indexInParent === $pos.node().childCount - 1) { + return undefined; + } + + const nextBlockBeforePos = $pos.posAtIndex(indexInParent + 1); + + const nextBlockInfo = getBlockInfoFromResolvedPos( + doc.resolve(nextBlockBeforePos), + ); + return nextBlockInfo; +}; + /** * If a block has children like this: * A @@ -88,6 +109,7 @@ const mergeBlocks = ( prevBlockInfo: BlockInfo, nextBlockInfo: BlockInfo, ) => { + debugger; // Un-nests all children of the next block. if (!nextBlockInfo.isBlockContainer) { throw new Error( @@ -143,6 +165,7 @@ export const mergeBlocksCommand = state: EditorState; dispatch: ((args?: any) => any) | undefined; }) => { + debugger; const $pos = state.doc.resolve(posBetweenBlocks); const nextBlockInfo = getBlockInfoFromResolvedPos($pos); diff --git a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index 94230143db..d48df900d6 100644 --- a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -4,6 +4,7 @@ import { TextSelection } from "prosemirror-state"; import { ReplaceAroundStep } from "prosemirror-transform"; import { getBottomNestedBlockInfo, + getNextBlockInfo, getParentBlockInfo, getPrevBlockInfo, mergeBlocksCommand, @@ -296,20 +297,13 @@ export const KeyboardShortcutsExtension = Extension.create<{ } else if ( prevBlockInfo.blockContent.node.type.spec.content === "" ) { - const nonEditableBlockContentStartPos = - prevBlockInfo.blockContent.afterPos - - prevBlockInfo.blockContent.node.nodeSize; - chainedCommands = chainedCommands.setNodeSelection( - nonEditableBlockContentStartPos, + prevBlockInfo.blockContent.beforePos, ); } else { - const blockContentStartPos = - prevBlockInfo.blockContent.afterPos - - prevBlockInfo.blockContent.node.nodeSize; - - chainedCommands = - chainedCommands.setTextSelection(blockContentStartPos); + chainedCommands = chainedCommands.setTextSelection( + prevBlockInfo.blockContent.afterPos - 1, + ); } return chainedCommands @@ -383,7 +377,7 @@ export const KeyboardShortcutsExtension = Extension.create<{ ]); const handleDelete = () => - this.editor.commands.first(({ commands }) => [ + this.editor.commands.first(({ chain, commands }) => [ // Deletes the selection if it's not empty. () => commands.deleteSelection(), // Merges block with the next one (at the same nesting level or lower), @@ -391,42 +385,142 @@ export const KeyboardShortcutsExtension = Extension.create<{ // end of the block. () => commands.command(({ state }) => { - // TODO: Change this to not rely on offsets & schema assumptions const blockInfo = getBlockInfoFromSelection(state); if (!blockInfo.isBlockContainer) { return false; } - const { - bnBlock: blockContainer, - blockContent, - childContainer, - } = blockInfo; + const { bnBlock: blockContainer, blockContent } = blockInfo; - const { depth } = state.doc.resolve(blockContainer.beforePos); - const blockAtDocEnd = - blockContainer.afterPos === state.doc.nodeSize - 3; const selectionAtBlockEnd = state.selection.from === blockContent.afterPos - 1; const selectionEmpty = state.selection.empty; - const hasChildBlocks = childContainer !== undefined; - if ( - !blockAtDocEnd && - selectionAtBlockEnd && - selectionEmpty && - !hasChildBlocks - ) { - let oldDepth = depth; - let newPos = blockContainer.afterPos + 1; - let newDepth = state.doc.resolve(newPos).depth; - - while (newDepth < oldDepth) { - oldDepth = newDepth; - newPos += 2; - newDepth = state.doc.resolve(newPos).depth; + const posBetweenBlocks = blockContainer.afterPos; + + if (selectionAtBlockEnd && selectionEmpty) { + return chain() + .command(mergeBlocksCommand(posBetweenBlocks)) + .scrollIntoView() + .run(); + } + + return false; + }), + // Deletes the current block if it's an empty block with inline content, + // and moves the selection to the next block. + () => + commands.command(({ state }) => { + const blockInfo = getBlockInfoFromSelection(state); + if (!blockInfo.isBlockContainer) { + return false; + } + + const blockEmpty = + blockInfo.blockContent.node.childCount === 0 && + blockInfo.blockContent.node.type.spec.content === "inline*"; + + if (blockEmpty) { + const nextBlockInfo = getNextBlockInfo( + state.doc, + blockInfo.bnBlock.beforePos, + ); + if (!nextBlockInfo || !nextBlockInfo.isBlockContainer) { + return false; + } + + let chainedCommands = chain(); + + if ( + nextBlockInfo.blockContent.node.type.spec.content === + "tableRow+" + ) { + const tableBlockStartPos = blockInfo.bnBlock.afterPos + 1; + const tableBlockContentStartPos = tableBlockStartPos + 1; + const firstRowStartPos = tableBlockContentStartPos + 1; + const firstCellStartPos = firstRowStartPos + 1; + const firstCellParagraphStartPos = firstCellStartPos + 1; + + chainedCommands = chainedCommands.setTextSelection( + firstCellParagraphStartPos, + ); + } else if ( + nextBlockInfo.blockContent.node.type.spec.content === "" + ) { + chainedCommands = chainedCommands.setNodeSelection( + nextBlockInfo.blockContent.beforePos, + ); + } else { + chainedCommands = chainedCommands.setTextSelection( + nextBlockInfo.blockContent.beforePos + 1, + ); } - return commands.command(mergeBlocksCommand(newPos - 1)); + return chainedCommands + .deleteRange({ + from: blockInfo.bnBlock.beforePos, + to: blockInfo.bnBlock.afterPos, + }) + .scrollIntoView() + .run(); + } + + return false; + }), + // Deletes next block if it contains no content and isn't a table, + // when the selection is empty and at the end of the block. Moves the + // current block into the deleted block's place. + () => + commands.command(({ state }) => { + const blockInfo = getBlockInfoFromSelection(state); + + if (!blockInfo.isBlockContainer) { + // TODO + throw new Error(`todo`); + } + + const selectionAtBlockEnd = + state.selection.from === blockInfo.blockContent.afterPos - 1; + const selectionEmpty = state.selection.empty; + + const nextBlockInfo = getNextBlockInfo( + state.doc, + blockInfo.bnBlock.beforePos, + ); + if (!nextBlockInfo) { + return false; + } + if (!nextBlockInfo.isBlockContainer) { + // TODO + throw new Error(`todo`); + } + + if (nextBlockInfo && selectionAtBlockEnd && selectionEmpty) { + const nextBlockNotTableAndNoContent = + nextBlockInfo.blockContent.node.type.spec.content === "" || + (nextBlockInfo.blockContent.node.type.spec.content === + "inline*" && + nextBlockInfo.blockContent.node.childCount === 0); + + if (nextBlockNotTableAndNoContent) { + if (nextBlockInfo.bnBlock.node.childCount === 2) { + const childBlocks = + nextBlockInfo.bnBlock.node.lastChild!.content; + return chain() + .deleteRange({ + from: nextBlockInfo.bnBlock.beforePos, + to: nextBlockInfo.bnBlock.afterPos, + }) + .insertContentAt(blockInfo.bnBlock.afterPos, childBlocks) + .run(); + } + + return chain() + .deleteRange({ + from: nextBlockInfo.bnBlock.beforePos, + to: nextBlockInfo.bnBlock.afterPos, + }) + .run(); + } } return false; From 9787e8715b7044ee0105740d5f5d254578a09151 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 23 Oct 2025 16:47:08 +0200 Subject: [PATCH 2/2] Removed debugger statements --- .../api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts index 4877e549a1..13ced634e9 100644 --- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts @@ -109,7 +109,6 @@ const mergeBlocks = ( prevBlockInfo: BlockInfo, nextBlockInfo: BlockInfo, ) => { - debugger; // Un-nests all children of the next block. if (!nextBlockInfo.isBlockContainer) { throw new Error( @@ -165,7 +164,6 @@ export const mergeBlocksCommand = state: EditorState; dispatch: ((args?: any) => any) | undefined; }) => { - debugger; const $pos = state.doc.resolve(posBetweenBlocks); const nextBlockInfo = getBlockInfoFromResolvedPos($pos);