Search code examples
javascriptime

How can I reliably cancel a compositionstart event?


When only targetting Google Chrome, is it possible to cancel the compositionstart event? According to this draft it is cancelable, however, when using e.preventDefault() as such

document.querySelector("textarea").addEventListener('compositionstart', (e) => {
    e.preventDefault();
});

Chrome still begins a composition when pressing ´ for example.

I currently have a reasonably reliable way of stopping this by using

document.querySelector("textarea").addEventListener('compositionstart', function() {
    this.blur();
    
    setTimeout(() => this.focus(), 0);
});
<textarea>Spam ´ here</textarea>

As long as my textarea does not contain any breaks, this 'cancels' the compositionstart event when pressing ´. My problem right now is that if I spam the ´ key for a few seconds, my method does not always cancel the event and this causes a ´ to appear.

What I'm doing also feels very hacky, so I was wondering if there is a good, possibly cross-browser, way of stopping the compositionstart event?


Solution

  • I've found that a (also hacky) solution against spamming was increasing the timeout to 20 milliseconds. This timeout should be short enough that the user won't notice the blur and refocus, but it's long enough to stop the spamming.

    If I store the selection and range when the compositionstart event is fired, I can then reuse them after the blur, so that I won't get weird side-effects that I was originally having. For my original case of a textarea this solution does not work, because for this a custom implementation of the window.getSelection() method is needed. However, it does work for a div with contenteditable enabled.

    Anyways, this code works for me in Chrome 84 and Firefox 78:

    document.querySelector("#root").addEventListener('compositionstart', function () {
      let sel = window.getSelection();
      let range = sel.getRangeAt(0);
    
      this.blur();
    
      window.getSelection().removeAllRanges();
    
      setTimeout(() => {
        sel.addRange(range);
      }, 20);
    });
    <div id='root' contenteditable="true" style="outline: none">
      <p>Spam ´ here</p>
    </div>

    I'm not very satisfied with the solution, but I've not found anything better yet. Hopefully someone else will find a more valid way of going about this.

    This method does not work on phones, which fire the compositionstart event when the div receives focus.