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>
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.