-
-
Save timdown/244ae2ea7302e26ba932a43cb0ca3908 to your computer and use it in GitHub Desktop.
/** | |
* This is ported from Rangy's selection save and restore module and has no dependencies. | |
* Copyright 2019, Tim Down | |
* Licensed under the MIT license. | |
* | |
* Documentation: https://github.com/timdown/rangy/wiki/Selection-Save-Restore-Module | |
* Use "rangeSelectionSaveRestore" instead of "rangy" | |
*/ | |
var rangeSelectionSaveRestore = (function() { | |
var markerTextChar = "\ufeff"; | |
var selectionHasExtend = (typeof window.getSelection().extend !== "undefined"); | |
function gEBI(id, doc) { | |
return (doc || document).getElementById(id); | |
} | |
function removeNode(node) { | |
node.parentNode.removeChild(node); | |
} | |
// Utility function to support direction parameters in the API that may be a string ("backward", "backwards", | |
// "forward" or "forwards") or a Boolean (true for backwards). | |
function isDirectionBackward(dir) { | |
return (typeof dir == "string") ? /^backward(?:s)?$/i.test(dir) : !!dir; | |
} | |
function isSelectionBackward(sel) { | |
var backward = false; | |
if (!sel.isCollapsed) { | |
var range = document.createRange(); | |
range.setStart(sel.anchorNode, sel.anchorOffset); | |
range.setEnd(sel.focusNode, sel.focusOffset); | |
backward = range.collapsed; | |
} | |
return backward; | |
} | |
function selectRangeBackwards(sel, range) { | |
if (selectionHasExtend) { | |
var endRange = range.cloneRange(); | |
endRange.collapse(false); | |
sel.removeAllRanges(); | |
sel.addRange(endRange); | |
sel.extend(range.startContainer, range.startOffset); | |
return true; | |
} else { | |
// Just select the range forwards | |
sel.removeAllRanges(); | |
sel.addRange(range); | |
return false; | |
} | |
} | |
function insertRangeBoundaryMarker(range, atStart) { | |
var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2); | |
var markerEl; | |
var doc = range.startContainer.ownerDocument; | |
// Clone the Range and collapse to the appropriate boundary point | |
var boundaryRange = range.cloneRange(); | |
boundaryRange.collapse(atStart); | |
// Create the marker element containing a single invisible character using DOM methods and insert it | |
markerEl = doc.createElement("span"); | |
markerEl.id = markerId; | |
markerEl.style.lineHeight = "0"; | |
markerEl.style.display = "none"; | |
markerEl.textContent = markerTextChar; | |
boundaryRange.insertNode(markerEl); | |
return markerEl; | |
} | |
function setRangeBoundary(doc, range, markerId, atStart) { | |
var markerEl = gEBI(markerId, doc); | |
if (markerEl) { | |
range[atStart ? "setStartBefore" : "setEndBefore"](markerEl); | |
removeNode(markerEl); | |
} else { | |
module.warn("Marker element has been removed. Cannot restore selection."); | |
} | |
} | |
function compareRanges(r1, r2) { | |
return r2.compareBoundaryPoints(r1.START_TO_START, r1); | |
} | |
function saveRange(range, direction) { | |
var startEl, endEl, doc = range.startContainer.ownerDocument, text = range.toString(); | |
if (range.collapsed) { | |
endEl = insertRangeBoundaryMarker(range, false); | |
return { | |
document: doc, | |
markerId: endEl.id, | |
collapsed: true | |
}; | |
} else { | |
endEl = insertRangeBoundaryMarker(range, false); | |
startEl = insertRangeBoundaryMarker(range, true); | |
return { | |
document: doc, | |
startMarkerId: startEl.id, | |
endMarkerId: endEl.id, | |
collapsed: false, | |
backward: isDirectionBackward(direction), | |
toString: function() { | |
return "original text: '" + text + "', new text: '" + range.toString() + "'"; | |
} | |
}; | |
} | |
} | |
function restoreRange(rangeInfo) { | |
var doc = rangeInfo.document; | |
if (typeof normalize == "undefined") { | |
normalize = true; | |
} | |
var range = doc.createRange(doc); | |
if (rangeInfo.collapsed) { | |
var markerEl = gEBI(rangeInfo.markerId, doc); | |
if (markerEl) { | |
markerEl.style.display = "inline"; | |
var previousNode = markerEl.previousSibling; | |
if (previousNode && previousNode.nodeType == 3) { | |
removeNode(markerEl); | |
range.setStart(previousNode, previousNode.length); | |
range.collapse(true); | |
} else { | |
range.setEndBefore(markerEl); | |
range.collapse(false); | |
removeNode(markerEl); | |
} | |
} else { | |
console.warn("Marker element has been removed. Cannot restore selection."); | |
} | |
} else { | |
setRangeBoundary(doc, range, rangeInfo.startMarkerId, true); | |
setRangeBoundary(doc, range, rangeInfo.endMarkerId, false); | |
} | |
return range; | |
} | |
function saveRanges(ranges, direction) { | |
// Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched | |
ranges = ranges.slice(0); | |
ranges.sort(compareRanges); | |
var backward = isDirectionBackward(direction); | |
var rangeInfos = ranges.map(function(range) { | |
return saveRange(range, backward) | |
}); | |
// Now that all the markers are in place and DOM manipulation is over, adjust each range's boundaries to lie | |
// between its markers | |
for (var i = ranges.length - 1, range, doc; i >= 0; --i) { | |
range = ranges[i]; | |
doc = range.startContainer.ownerDocument; | |
if (range.collapsed) { | |
range.setStartAfter(gEBI(rangeInfos[i].markerId, doc)); | |
range.collapse(true); | |
} else { | |
range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc)); | |
range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc)); | |
} | |
} | |
return rangeInfos; | |
} | |
function saveSelection(win) { | |
win = win || window; | |
var sel = win.getSelection(); | |
var ranges = []; | |
for (var i = 0; i < sel.rangeCount; ++i) { | |
ranges.push( sel.getRangeAt(i) ); | |
} | |
var backward = (ranges.length == 1 && isSelectionBackward(sel)); | |
var rangeInfos = saveRanges(ranges, backward); | |
// Ensure current selection is unaffected | |
sel.removeAllRanges(); | |
if (backward) { | |
selectRangeBackwards(sel, ranges[0]); | |
} else { | |
ranges.forEach(function(range) { | |
sel.addRange(range); | |
}); | |
} | |
return { | |
win: win, | |
rangeInfos: rangeInfos, | |
restored: false | |
}; | |
} | |
function restoreRanges(rangeInfos) { | |
var ranges = []; | |
// Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid | |
// normalization affecting previously restored ranges. | |
var rangeCount = rangeInfos.length; | |
for (var i = rangeCount - 1; i >= 0; i--) { | |
ranges[i] = restoreRange(rangeInfos[i], true); | |
} | |
return ranges; | |
} | |
function restoreSelection(savedSelection, preserveDirection) { | |
if (!savedSelection.restored) { | |
var rangeInfos = savedSelection.rangeInfos; | |
var sel = savedSelection.win.getSelection(); | |
var ranges = restoreRanges(rangeInfos); | |
var rangeCount = rangeInfos.length; | |
sel.removeAllRanges(); | |
if (rangeCount == 1 && preserveDirection && selectionHasExtend && rangeInfos[0].backward) { | |
selectRangeBackwards(sel, ranges[0]); | |
} else { | |
ranges.forEach(function(range) { | |
sel.addRange(range); | |
}); | |
} | |
savedSelection.restored = true; | |
} | |
} | |
function removeMarkerElement(doc, markerId) { | |
var markerEl = gEBI(markerId, doc); | |
if (markerEl) { | |
removeNode(markerEl); | |
} | |
} | |
function removeMarkers(savedSelection) { | |
savedSelection.rangeInfos.forEach(function(rangeInfo) { | |
if (rangeInfo.collapsed) { | |
removeMarkerElement(savedSelection.doc, rangeInfo.markerId); | |
} else { | |
removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId); | |
removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId); | |
} | |
}); | |
} | |
return { | |
saveRange: saveRange, | |
restoreRange: restoreRange, | |
saveRanges: saveRanges, | |
restoreRanges: restoreRanges, | |
saveSelection: saveSelection, | |
restoreSelection: restoreSelection, | |
removeMarkerElement: removeMarkerElement, | |
removeMarkers: removeMarkers | |
}; | |
})(); |
+1, this is exactly what I needed
Doesn't work for me, got message saying that normalize
is not defined and then I removed it and got a different one:
gmail.bundle.js:1451 Uncaught TypeError: Cannot read property 'createElement' of null at insertRangeBoundaryMarker
.
So I assigned a document to doc variable instead of what was there and got this:
gmail.bundle.js:1457 Uncaught DOMException: Failed to execute 'insertNode' on 'Range': Can't insert an element before a doctype. at insertRangeBoundaryMarker
Didn't manage to hook it up easily, anyone know what the issue might be? The code is:
const cursorPosition = rangeSelectionSaveRestore.saveSelection(); messageBox.innerHTML = newHtml; rangeSelectionSaveRestore.restoreSelection(cursorPosition);
messageBox
is just a div.
just delete those 3 lines i guess.. i dont see them being used anywhere.
const cursorPosition = rangeSelectionSaveRestore.saveSelection();
....
.... user adds stuff here
....
rangeSelectionSaveRestore.restoreSelection(cursorPosition);
This worked for me like a charm. I tested in chrome, FF and Edge.
Thanks a lot. I will definitely study the rangy code in future.
Thank you
Thanks for this!