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

88 views Asked by At

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>

1

There are 1 answers

0
Pida On BEST ANSWER

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.