Skip to content

Bug/74325 inserting hash inside text removes content after cursor#122

Open
ihordubas99 wants to merge 3 commits intodevfrom
bug/74325-inserting-hash-inside-text-removes-content-after-cursor
Open

Bug/74325 inserting hash inside text removes content after cursor#122
ihordubas99 wants to merge 3 commits intodevfrom
bug/74325-inserting-hash-inside-text-removes-content-after-cursor

Conversation

@ihordubas99
Copy link
Copy Markdown
Collaborator

@ihordubas99 ihordubas99 commented Apr 23, 2026

Ticket

https://community.openproject.org/projects/communicator-stream/work_packages/74325

What are you trying to accomplish?

Fix a data loss bug where inserting # in the middle of a text block would silently delete all content after the cursor position.

What approach did you choose and why?

The root cause was in clearTriggerText: the old implementation searched for the trigger node in BlockNote's content tree and sliced everything from that index onward - which discarded all content after the cursor.
The fix rewrites clearTriggerText to operate directly on the TipTap transaction layer. Instead of manipulating the BlockNote content tree, it now reads the 50 characters before the cursor, matches the trailing #+\S* pattern, and issues a precise tr.delete for exactly those characters. This leaves the rest of the block untouched.
As a secondary cleanup, size and blockId are now captured at interaction time (via refs) rather than at render time, eliminating stale closure values on keyboard selection. insertWpChipIntoBlock was simplified to a thin wrapper around insertWpChip since both mouse and keyboard paths now share the same cursor-based insertion logic.
Considered keeping the BlockNote content-tree approach and patching the slice logic, but the tree manipulation is inherently fragile when the cursor is mid-block - TipTap transactions are the correct abstraction for precise text deletion.

Merge checklist

  • Added/updated tests
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...)

@ihordubas99 ihordubas99 self-assigned this Apr 23, 2026
@ihordubas99 ihordubas99 requested a review from judithroth April 23, 2026 14:28
Copy link
Copy Markdown
Contributor

@judithroth judithroth left a comment

Choose a reason for hiding this comment

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

The code works, there is no more content removed when inserting with #.
However, I find the code very hard to understand. Also, this is using TipTap API directly and BlockNote might not support that for ever. If somehow possible, please try to solve the problem with BlockNote "materials on board".

Comment thread lib/components/HashMenu/HashWpMenu.tsx
Comment thread lib/components/HashMenu/HashWpMenu.tsx Outdated
const originalBlockId = originalBlockIdRef.current ?? currentBlockId;
const savedSelection = savedSelectionRef.current;

pendingSizeRef.current = "xxs";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be covered by const pendingSizeRef = useRef<InlineWpSize>("xxs"); already?

Comment thread lib/components/HashMenu/HashWpMenu.tsx Outdated
clearTriggerText(editor);
insertWpChipIntoBlock(editor, blockId, wp, size);
if (savedSelection) {
const tiptap = (editor as any)._tiptapEditor;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nick from BlockNote wrote that they are not sure if they will keep the tiptap APIs (https://matrix.to/#/!yWsguKHvTVyakhzlui:openproject.org/$namCWPK10P-jifVJIjDTaj7bYnnw19XbSC2pSjO8JkY?via=openproject.org&via=matrix.org) and that we should not rely on them.

Is it possible to achieve this without tiptap? If not, why?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good news: I removed the TipTap dependency from the UI component (HashWpMenu.tsx). By keeping focus on the editor during a click, we no longer need TipTap to manage the cursor there.

However, for clearTriggerText, using TipTap remains the most reliable approach at this stage. Standard browser tools (DOM) get confused by our custom WorkPackage chips: they count every letter inside the chip, while BlockNote's internal model sees the whole chip as a single item. This "offset mismatch" makes it very risky to delete the # trigger using native methods, as it often leads to deleting the wrong text or data loss.

TipTap is currently the most stable way to find the exact cursor position within BlockNote's structure and ensure we only delete the trigger text

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for removing the TipTap dependency from HashWpMenu.tsx!

Standard browser tools (DOM) get confused by our custom WorkPackage chips: they count every letter inside the chip, while BlockNote's internal model sees the whole chip as a single item. This "offset mismatch" makes it very risky to delete the # trigger using native methods, as it often leads to deleting the wrong text or data loss.

TipTap is currently the most stable way to find the exact cursor position within BlockNote's structure and ensure we only delete the trigger text

Okay, I see the problem but I am not convinced we necessarily have this problem 🙂
See my comment here: https://github.com/opf/op-blocknote-extensions/pull/122/changes#r3189104054

Comment thread test/lib/components/integration/editor.inlineChip.browser.test.tsx Outdated
Comment thread test/lib/components/hashMenu.test.ts Outdated
Comment thread test/lib/components/hashMenu.test.ts Outdated
@ihordubas99 ihordubas99 requested a review from judithroth April 27, 2026 12:07
Copy link
Copy Markdown
Contributor

@judithroth judithroth left a comment

Choose a reason for hiding this comment

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

This looks already a lot better! The code is at lot easier to understand now 👍 thanks for improving this!
And it works as expected and does not delete more than necessary ✨

However, it still relies on using TipTap in some places and I am not (yet?) convinced we really need that. I'd also like to have one real integration (browser) test for the insertion via "#" notation (one with text before and after where the link is inserted, so we can test that both are not removed accidentally).


// BlockNote's default "Enter" behavior splits the block.
// If the block ID changed, it was a keyboard Enter, so we remove the new empty block.
const isKeyboardEnter = currentBlock && currentBlock.id !== currentBlockId;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we simplify this by using something like event.preventDefault and event.stopPropagation?

const text = triggerNode.text;
const hashIndex = text.search(/#/);
const textBefore = hashIndex > 0 ? text.slice(0, hashIndex) : null;
const textBefore = $from.parent.textBetween(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of using TipTap here, would it be possible to use BlockNote's setTextCursorPosition to set the position right before the newly inserted block (which I assume is inserted right after the search text the user entered), then you can delete backwards until the "#" sign, without having to worry about having a BlockNote block messing with the counting - since the search via "#" only is text, there can be no other block than the one that was just created.

If that is not possible I am okay with keeping TipTap, but at the moment I don't see why this approach should cause problems and why we necessarily need TipTap here 😅

it("does nothing if no block", () => {
const editor = { getTextCursorPosition: () => null, updateBlock: vi.fn() };
const editor = {
_tiptapEditor: null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of using TipTap, please try to create an empty BlockNote editor or just remove the contents from the one you create with createTestEditor instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for adding all those tests!
Please add at least one real integration test that interacts with a browser. I see value in these unit tests here since they cover edge cases like having multiple texts with "#" in a line and seeing that it is deleted only until the first # (from right) 👍
However, integration tests often uncover problems the developer or AI hasn't thought of yet. And those are typically very hard to test in unit tests :)

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants