Search code examples
javascriptvue.jsvuejs2contenteditablerangy

Error message "Marker element has been removed" when restoring text ranges with Rangy inside Vue component


I have a rather complex Vue component which involves a contenteditable div. I'd like to highlight words in this div using Rangy and add additional markup and keep this markup even when the text is edited.

Originally, I was going to post a question because at some point dealing with additional markup made the contenteditable div uneditable, I just could not delete or add characters. But when I tried setting up a code snippet, I got another error message.

I expect three things to happen when editing the contenteditable div:

  • In the storeIndexes method, I create and store ranges for each element in the highlights array. This method is called @beforeinput. This event is not available in all browsers, I'm using Chrome.

  • Next, I expect the text inside the contenteditable div to be updated.

  • Finally, the ranges should be restored by the restoreIndexes method which is called @input.

I'm aware my code should not have any visible effect. My problem is that there's an error message when trying to edit the text: Rangy warning: Module SaveRestore: Marker element has been removed. Cannot restore selection.

What's wrong here?

new Vue({

  el: '#app',
  
  data: {
    currentHighlights: [],
    highlights: [
      { 
        start: 10,
        end: 20
      }
    ],
  },
  
  methods: {
    // What happens just before an edit is applied
    storeIndexes: function(event) {
      // Create a new range object
      let range = rangy.createRange();

      // Get contenteditable element 
      let container = document.getElementById('text-with-highlights');

      // Store all currently highlights and addd DOM markers
      this.highlights.forEach(highlight => {
        // Move range based on character indexes
        range.selectCharacters(container, highlight.start, highlight.end);
        // Set DOM markers and store range
        this.currentHighlights.push(rangy.saveRange(range))
      });
    },
    
    // What happens after an edit was made
    restoreIndexes: function(event) {
      // Create a new range object
      let range = rangy.createRange();

      // Get range based on character indexes
      let container = document.getElementById('text-with-highlights');


      this.currentHighlights.forEach(highlight => {
        range.selectCharacters(container, highlight.start, highlight.end);
        rangy.restoreRange(range);
      });


      this.currentHighlights = [];
    },
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/rangy/1.3.0/rangy-core.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/rangy/1.3.0/rangy-selectionsaverestore.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/rangy/1.3.0/rangy-textrange.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id='app'>
  <div @beforeinput='storeIndexes' @input='restoreIndexes' contenteditable id='text-with-highlights'>
    Just some text to show the problem.
  </div>  
</div>


Solution

  • Turns out this was not a Vue problem, but rather one of code running asynchronously: storeIndexes was not finished when restoreIndexes attempted to restore ranges.

    setTimeout did the trick. I'm not sure if there's any better way than delaying the method by some random interval,

    // What happens after an edit was made
    restoreIndexes: function(event) {
      setTimeout(() => {
        // Create a new range object
        let range = rangy.createRange();
    
        // Get range based on character indexes
        let container = document.getElementById('text-with-highlights');
    
    
        this.currentHighlights.forEach(highlight => {
          range.selectCharacters(container, highlight.start, highlight.end);
          rangy.restoreRange(range);
        });
      }, 10);
    
      // Restore highlights
      this.currentHighlights = [];
    },
    

    However, I could get rid of my storeIndexes method completely using the v-runtime-template library. This is an alternative to v-html but also works for programmatically inserted elements such as the highlights in my problem.

    Now my highlights simply react on changing indexes in $data and I don't need to move them manually when the contenteditable div is updated.