tiptap icon indicating copy to clipboard operation
tiptap copied to clipboard

[Bug]: shouldShow broken in BubbleMenu extension from Vue 3 package

Open mchestnut opened this issue 1 year ago • 2 comments

Which packages did you experience the bug in?

core, pm, starter-kit, vue-3

What Tiptap version are you using?

2.2.2

What’s the bug you are facing?

I'm noticing that the shouldShow callback is not functioning as expected in the Vue implementation of the Bubble Menu. When using editor.isActive as part of the condition, the value returned from isActive doesn't represent that state that I expect.

What browser are you using?

Chrome

Code example

https://stackblitz.com/edit/vitejs-vite-kgmrcn?file=src%2Fcomponents%2FTiptap.vue,package.json

What did you expect to happen?

Consider shouldShow: ({ editor }) => editor.isActive('bold'). I expect on first click into a bolded word that the bubble menu would become visible. However, it isn't and logging out the value of isActive in the method at that point returns false. But upon clicking again on any text (bold or not), the value then returns true. It's as if the editor state inside the callback is from before the click is registered.

Anything to add? (optional)

I do not see this same behavior in the vanilla JS implementation of Bubble Menu. I have not tested any other frameworks.

For comparison to the code example below, here is a working vanilla JS port of the same code. https://stackblitz.com/edit/vitejs-vite-djsnpl?file=index.html,main.js&terminal=dev

Did you update your dependencies?

  • [X] Yes, I’ve updated my dependencies to use the latest version of all packages.

Are you sponsoring us?

  • [ ] Yes, I’m a sponsor. 💖

mchestnut avatar Feb 09 '24 17:02 mchestnut

Finally found a workaround to this issue. The underlying problem seems to stem from the reactiveState property in the Editor class of the Vue implementation. For some reason this reactive state is not yet updated when the bubble menu plugin is calling the shouldShow callback. I thought at first it was because of the use of requestAnimationFrame found in the debounce function that reactiveState uses, but disabling the rAF calls didn't solve the issue. My guess now is it's something in the timing of Vue's reactivity system in general.

For now, I'm using the following workaround in my shouldShow callback.

const shouldShowBubbleMenu = ({ editor }) => {
    return isMarkActive(editor.view.state, 'link');
};

I've found importing isMarkActive directly from @tiptap/core allows me to directly pass in the view state, which seems to be up-to-date at the time shouldShow is called. Using editor.isActive returns the wrong value because it uses editor.state under the hood, which in turn uses the out-of-sync editor.reactiveState property.

Using isNodeActive instead of isMarkActive could also work for those checking if the selection is in a node.

I hope this helps others!

mchestnut avatar Feb 20 '24 18:02 mchestnut

I can confirm @mchestnut's workaround is valid, you must use the state from editor.view. In my case, I wanted to show a BubbleMenu when text is selected, so I adapted the isTextSelected function like this:

function isTextSelected({ editor }: { editor: Editor }) {
  const {
    state: {
      doc,
      selection,
      selection: { empty, from, to },
    },
  } = editor.view // destructure from editor.view instead of just editor

  const isEmptyTextBlock = !doc.textBetween(from, to).length && isTextSelection(selection)

  if (empty || isEmptyTextBlock || !editor.isEditable)
    return false

  return true
}```

sytexa-julia avatar Apr 08 '24 04:04 sytexa-julia

@mchestnut thanks for sharing your fix, I was pulling my hair out for ages! FWIW this fixes the same issue for @tiptap/react

nateeo avatar Jul 26 '24 04:07 nateeo