Skip to content

Conversation

matthewlipski
Copy link
Collaborator

@matthewlipski matthewlipski commented Oct 15, 2025

Summary

Currently, when there is only one block in a column, removing it via removeBlocks/replaceBlocks won't collapse the column as it should, leading to confusing UX when deleting the block from the side menu. This behavior is also different to removing the block by hitting backspace at its start.

This PR fixes the behavior for removing the only block in a column using removeBlocks/replaceBlocks. The logic is now shared between those functions and the keyboard handler. The only difference is that the keyboard handler moves the block to the previous/next column, whereas removeBlocks/replaceBlocks also removes it after.

Closes #1323

Rationale

The current behavior of the remove block button in the side menu is unintuitive when used on the only block in a column, and should be improved.

Changes

  • Added moveFirstBlockInColumn function.
  • Made removeAndInsertBlocks and relevant keyboard handler use moveFirstBlockInColumn.

TODO:

  • While removing blocks collapses columns/column lists, removing entire columns doesn't. Need to implement an additional fix for this.

Impact

Since removeAndInsertBlocks can now remove column/columnList nodes that it's actively traversing in doc.descendants, there's an edge case where I'm not sure what will happen.

E.g. consider this structure:

  • columnList
    • column
      • blockContainer 1
    • column
      • blockContainer 2
    • column
      • blockContainer 3

If we call removeBlocks for both blockContainer 2 & 3, I'm not sure this will work properly. Worth adding tests for this?

EDIT: Added tests, works ok for both removing and replacing blocks 👍

Testing

See above.

Screenshots/Video

N/A

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

Copy link

vercel bot commented Oct 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
blocknote Ready Ready Preview Oct 16, 2025 10:54am
blocknote-website Ready Ready Preview Oct 16, 2025 10:54am

Copy link

pkg-pr-new bot commented Oct 15, 2025

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/ariakit@2110

@blocknote/code-block

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/code-block@2110

@blocknote/core

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@2110

@blocknote/mantine

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/mantine@2110

@blocknote/react

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@2110

@blocknote/server-util

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/server-util@2110

@blocknote/shadcn

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/shadcn@2110

@blocknote/xl-ai

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-ai@2110

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-docx-exporter@2110

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-email-exporter@2110

@blocknote/xl-multi-column

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@2110

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-odt-exporter@2110

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-pdf-exporter@2110

commit: a521b8f

Comment on lines +21 to +33
/**
* Moves the first block in a column to the previous/next column and handles
* all necessary collapsing of `column`/`columnList` nodes. Only moves the
* block to the start of the next column if it's in the first column.
* Otherwise, moves the block to the end of the previous column.
* @param tr The transaction to apply changes to.
* @param blockBeforePos The position just before the first block in the column.
* @returns The position just before the block, after it's moved.
*/
export function moveFirstBlockInColumn(
tr: Transaction,
blockBeforePos: ResolvedPos,
): ResolvedPos {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read this like 3 times, and couldn't understand what it was doing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hard for me to evaluate this logic, I don't completely understand what this logic is trying to achieve. What I was sort of expecting from this was that given a transaction where some operation involving a column was involved, this would "fix up" the columns to make them make sense (i.e. remove columns/columnLists etc.). I'm unsure whether this is feasible, but I prefer something more generic, than trying to infer things while content is being moved around

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think a function that applies a transaction to check and clean up a given invalid columnList makes more sense tbh.

The reason this function is the way it is, is just to maximize how much code can be reused from the existing keyboard handler. Since the logic is fairly complex, I felt like refactoring was gonna put me down a rabbithole and I didn't wanna spend too much time on it. If you think it's worth it though, I can spend a day or 2 refactoring it into a fixColumnList function or smth along those lines.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is worth doing together. The way that I see this implemented would follow what you outlined in the comment here:

There are 3 different cases:
a) remove entire column list (if no columns would be remaining)
b) remove just a column (if no blocks inside a column would be remaining)
c) keep columns (if there are blocks remaining inside a column)

So, what I'd do is these 3 passes in reverse order. Something like:

  1. Check that a column has non-empty blocks within it
    • If it does, then it is valid, & bail
    • If not, then empty the column of any blocks it may have & continue
  2. Check if a column is empty
    • If it is, remove it
    • If not, bail
  3. Check that a columnList has only 1 column
    • If it only has 1, take out it's contents, & remove the columnList completely
    • If not, bail

Theoretically, at the end of all of these passes, we should be left with a valid column structure

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot convert from Multi Column to single column

2 participants