Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't restore selection after HTML modify, even if it's the same HTML

I'm trying to store a selection of a contentEditable element and restore it later.

I want to observe the paste event and store the HTML as it was before, clear the html and then manually insert the pasted text with some changes at the selected position.

Take a look at this example: jsfiddle.net/gEhjZ

When you select a part of the text, hit store, remove the selection again and hit restore, it's working as expected.

But when you first hit store, then replace the HTML with the exact same HTML by hitting overwrite html and then try to restore, nothing happens.

I thought that using .cloneRange() would make a difference, but it won't. Even a deep copy of the object ($.extend(true, {}, oldRange)) won't do the trick. As soon as I overwrite the HTML, the selection object sel is being changed too. It makes sense for me that changing the selection context will wipe the range, but I'm trying to restore it for the exact same HTML.

I know I could use rangy, but I really don't want to use a huge library just for this small feature. What am I missing? Any help would be much appreciated!

Note: only Firefox/Chrome, so no crossbrowser-hacks needed.

Update:

@Tim Down's answer works when using a div, but I'm actually using an iframe. When I made that example, I thought it wouldn't make any difference.

Now when I try to restore the iframe's body, i get the following error: TypeError: Value does not implement interface Node. in the following line preSelectionRange.selectNodeContents(containerEl);. I didn't get much from googling. I tried to wrap the contents of the body and restore the wrap's html, but I get the same error.

jsfiddle isn't working in this case because it is using iframes to display the results itself, so I put an example here: snipt.org/AJad3

And the same without the wrap: snipt.org/AJaf0

Update 2: I figured that I have to use editable.get(0), of course. But now the start and end of the iframe's selection is 0. see snipt.org/AJah2

like image 832
koko Avatar asked Jul 16 '13 14:07

koko


2 Answers

You could save and restore the character position using functions like these:

https://stackoverflow.com/a/13950376/96100

I've adapted these function slightly to work for an element inside an iframe.

Demo: http://jsfiddle.net/timdown/gEhjZ/4/

Code:

var saveSelection, restoreSelection;

if (window.getSelection && document.createRange) {
    saveSelection = function(containerEl) {
        var doc = containerEl.ownerDocument, win = doc.defaultView;
        var range = win.getSelection().getRangeAt(0);
        var preSelectionRange = range.cloneRange();
        preSelectionRange.selectNodeContents(containerEl);
        preSelectionRange.setEnd(range.startContainer, range.startOffset);
        var start = preSelectionRange.toString().length;

        return {
            start: start,
            end: start + range.toString().length
        };
    };

    restoreSelection = function(containerEl, savedSel) {
        var doc = containerEl.ownerDocument, win = doc.defaultView;
        var charIndex = 0, range = doc.createRange();
        range.setStart(containerEl, 0);
        range.collapse(true);
        var nodeStack = [containerEl], node, foundStart = false, stop = false;

        while (!stop && (node = nodeStack.pop())) {
            if (node.nodeType == 3) {
                var nextCharIndex = charIndex + node.length;
                if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
                    range.setStart(node, savedSel.start - charIndex);
                    foundStart = true;
                }
                if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
                    range.setEnd(node, savedSel.end - charIndex);
                    stop = true;
                }
                charIndex = nextCharIndex;
            } else {
                var i = node.childNodes.length;
                while (i--) {
                    nodeStack.push(node.childNodes[i]);
                }
            }
        }

        var sel = win.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    };
} else if (document.selection) {
    saveSelection = function(containerEl) {
        var doc = containerEl.ownerDocument, win = doc.defaultView || doc.parentWindow;
        var selectedTextRange = doc.selection.createRange();
        var preSelectionTextRange = doc.body.createTextRange();
        preSelectionTextRange.moveToElementText(containerEl);
        preSelectionTextRange.setEndPoint("EndToStart", selectedTextRange);
        var start = preSelectionTextRange.text.length;

        return {
            start: start,
            end: start + selectedTextRange.text.length
        };
    };

    restoreSelection = function(containerEl, savedSel) {
        var doc = containerEl.ownerDocument, win = doc.defaultView || doc.parentWindow;
        var textRange = doc.body.createTextRange();
        textRange.moveToElementText(containerEl);
        textRange.collapse(true);
        textRange.moveEnd("character", savedSel.end);
        textRange.moveStart("character", savedSel.start);
        textRange.select();
    };
}
like image 92
Tim Down Avatar answered Oct 14 '22 08:10

Tim Down


Provided solution works very well.

replacing that line

if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {

by

if (!foundStart && savedSel.start >= charIndex && savedSel.start < nextCharIndex) {

prevents Chrome / Edge to select the end of the previous line

like image 31
Markus Avatar answered Oct 14 '22 09:10

Markus