Skip to content

Insert a dropped attachment above the block when the caret is at its start - #1251

Draft
ikraamg wants to merge 1 commit into
basecamp:mainfrom
ikraamg:fix/upload-at-block-start
Draft

ikraamg wants to merge 1 commit into
basecamp:mainfrom
ikraamg:fix/upload-at-block-start

Conversation

@ikraamg

@ikraamg ikraamg commented Aug 21, 2026 •

Copy link
Copy Markdown

Dropping a file with the caret at the very start of a heading throws Lexical invariant #66 ("Expected node to have a parent") and the file is lost. The editor is left inconsistent, so a Backspace straight after throws invariant #75.

RangeSelection.insertNodes keeps a reference to the caret's block while it calls insertParagraph(). HeadingNode.insertNewAfter replaces that block with a paragraph when the caret is at offset 0, so insertNodes goes on to insert after a node that has already left the document.

Lexical fixed this on their side in 0.47.0, by re-resolving firstBlock when it is no longer attached. Lexxy pins ^0.44.0, which under npm's 0.x caret can never reach that. I tried the bump instead of this change: lexical 0.49.0 does fix the crash with no source change, but it fails six existing tests here:

  • formatting/block_formatting.test.js > toggle nested bullet list off
  • formatting/list_in_quote.test.js > backspace after clearing the first bullet's text removes the blank line
  • formatting/list_in_quote.test.js > backspace on a blank first list item merges the gap instead of stranding a paragraph
  • formatting/list_item_deletion.test.js > deleting empty middle list item keeps cursor in the list
  • paste/paste_into_quote.test.js > pasting multi-line content into a quote started via the markdown shortcut keeps every line inside the quote
  • paste/paste_into_quote.test.js > the QuoteNode transform does not re-wrap toolbar-button quote children

So this adds BlockStartNodeInserter, which places a dropped attachment above the block when the caret sits at its start rather than splitting the block around the caret. Empty blocks are left to Lexical, since they are already a usable insertion point. It puts the attachment in the same place lexical 0.47 does, so it can be dropped whenever the bump happens.

It also moves the block-decorator check out of ListItemNodeInserter into $isBlockDecoratorNode, now that two inserters need it.

  • reproduces on lexical 0.38.2 through 0.46.0, driven through Contents#uploadFiles the way a document-level drop zone does
  • the new test fails on main and passes with the change
  • yarn lint clean, yarn test green, yarn test:browser green on chromium, firefox and webkit, apart from tables/toolbar.test.js which already fails on main

…start

RangeSelection.insertNodes keeps a reference to the caret's block while it calls
insertParagraph(). HeadingNode.insertNewAfter replaces that block with a paragraph
when the caret is at offset 0, so insertNodes went on to insert after a node that
had already left the document and Lexical threw invariant basecamp#66 ("Expected node to
have a parent"). Dropping a file at the start of a heading lost the file and left
the editor inconsistent — a following Backspace then threw invariant basecamp#75.

An attachment dropped at the start of a block belongs above that block, so
BlockStartNodeInserter places it there instead of splitting the block around the
caret. Empty blocks are left to Lexical: they are already a usable insertion point,
and inserting above one would strand it.

Co-Authored-By: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant