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