How to attach onClick handler to every word in editor?

Lets say I want to show the text of the song on a page with an audio player. Suppose every word is in sync with a song through some playPosition props and clicking every word shall call player.seek() handler. How do I achieve that?

Do I need to send some props when I build ‘doc’ structure through attr: {}? Do I need to dispatch transactions with my ‘seek’ props? Or do I need to create new plugin and attach seekHandler in .apply()? Or do I need to call handleClickOn (which I see is a part of EditorProps interface, but I unfortunately I have no idea what part of the initial setup is using this interface.

Please, point me in the right direction.

How are you modelling the concept of a “word”? Are you using Marks?

As an example, if you do use marks to surround each word, you can then detect the mark via state.selection object and grab the play position. There is probably also an alternate approach to using a NodeView per word with the play position attribute.

How you are getting the play position mapped to particular words is going to define how you solve this problem.

Question though, did you also want the text to highlight or something as the song plays? That is also an interesting, cool idea (although one can argue the value, but it does play nicely with “click to seek” you are proposing here.)

I think the easiest approach might be to register an editor-wide event handler and use posAtCoords to figure out where in the document the mouse click occurred.

1 Like

Yes, highlighting is might be needed at some point to produce ‘karaoke’ effect.

How you are getting the play position mapped to particular words is going to define how you solve this problem.

This is exactly what I am struggling right now, initially, I have words array like:

words: {
     word: {
            text: 'yesterday',
            startTime: '0.2'
     }
}

So when constructing ProseMirrow doc object, I just map through words and trying to set attrs with:

   const result = {
        type: 'doc',
        content: [{
          type: 'paragraph',
          content: words.map(word =>  
            {
              type: 'text',
              text: word.text,
              attrs: {
                  dataStart: word.startTime,
              },
            },
        })],
      }

Is it the right way to do this, or shall I use some schema methods to construct initial object to load content into ProseMirror?

Dear Marijn,

Thanks for the comment! This is exactly what I was researching yesterday last night when I stumbled upon handleClickOn, so I will try to integrate it

  1. You can put each word inside its own node, which I think is more intuitive (since it’ll also the unit of user interaction), and also easier to implement single-word level visual effects like highlighting
  2. or you can use paragraph as the smallest unit, then you can attach the timing info to each paragraph (or the whole song), and have a way to map a given pos to a give time, e.g. some function like fn(pos: number) → time: number