Range
objects and document.execCommand
allow to manipulate selection pretty easily. The main problem in your case is saving the range object in a text format.
Basically what you need is to get the startContainer
, startOffset
, endContainer
and endOffset
, which are the values needed to create Range objects. Offsets
are number so it's pretty straightforward. Containers are Nodes, which you can't directly save as strings, so that's the main problem. One thing you can do is add keys to your DOM and save the key. But then, since in ranges containers are text nodes, you'll need to save the index of the text node. Something like this should allow to tag the DOM with keys, using a recursive function:
function addKey(element) {
if (element.children.length > 0) {
Array.prototype.forEach.call(element.children, function(each, i) {
each.dataset.key = key++;
addKey(each)
});
}
};
addKey(document.body);
Once this is done, you can convert range objects to an object that you can save as a string. Like this:
function rangeToObj(range) {
return {
startKey: range.startContainer.parentNode.dataset.key,
startTextIndex: Array.prototype.indexOf.call(range.startContainer.parentNode.childNodes, range.startContainer),
endKey: range.endContainer.parentNode.dataset.key,
endTextIndex: Array.prototype.indexOf.call(range.endContainer.parentNode.childNodes, range.endContainer),
startOffset: range.startOffset,
endOffset: range.endOffset
}
}
Using this, you can save each selection that the user creates to an array. Like this:
document.getElementById('textToSelect').addEventListener('mouseup', function(e) {
if (confirm('highlight?')) {
var range = document.getSelection().getRangeAt(0);
selectArray.push(rangeToObj(range));
document.execCommand('hiliteColor', false, 'yellow')
}
});
To save the highlights, you save each object to JSON. To test this, you can just get the JSON string from your range objects array. Like this (this is using the get Seletion button at the top):
document.getElementById('getSelectionString').addEventListener('click', function() {
alert('Copy string to save selections: ' + JSON.stringify(selectArray));
});
Then when loading the empty HTML, you can use a reverse function that will create ranges from the objects you saved in JSON. Like this:
function objToRange(rangeStr) {
range = document.createRange();
range.setStart(document.querySelector('[data-key="' + rangeStr.startKey + '"]').childNodes[rangeStr.startTextIndex], rangeStr.startOffset);
range.setEnd(document.querySelector('[data-key="' + rangeStr.endKey + '"]').childNodes[rangeStr.endTextIndex], rangeStr.endOffset);
return range;
}
So you could have an array of ranges in strings that you convert to objects, and then convert to Range objects that you can add. Then using execCommand, you set some formatting. Like this (this is using the set selection button at the top, you do this after refreshing the fiddle):
document.getElementById('setSelection').addEventListener('click', function() {
var selStr = prompt('Paste string');
var selArr = JSON.parse(selStr);
var sel = getSelection();
selArr.forEach(function(each) {
sel.removeAllRanges();
sel.addRange(objToRange(each));
document.execCommand('hiliteColor', false, 'yellow')
})
});
See: https://jsfiddle.net/sek4tr2f/3/
Note that there are cases where this won't work, main problematic case is when user selects content in already highlighted content. These cases can be handled, but you'll need more conditions.