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

2

6

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

Nedra answered 16/7, 2013 at 14:9 Comment(0)
K
17

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

https://mcmap.net/q/83217/-persisting-the-changes-of-range-objects-after-selection-in-html

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();
    };
}
Karissakarita answered 17/7, 2013 at 8:26 Comment(7)
thanks a lot, the range is now saved properly but somehow it silently fails to restore the selection. snipt.org/AJaj0Nedra
@koko: Possibly the iframe document isn't fully loaded when the initial script runs. I'd suggest $('iframe').contents().find('body')[0] instead of editable.get(0) inside your click handlers.Karissakarita
The document is definetely fully loaded, I'm calling this function on the paste event. However, the doc variable is undefined (containerEl.ownerDocument). Chrome just doesn't mind about it, while Firefox does. I will take a look at this later, I guess the rest will be relatively easy to resolve.Nedra
@koko: containerEl.ownerDocument being undefined indicates that something is wrong with containerEl, such as having been removed from the document.Karissakarita
Got it now, I had to hardcode some selectors and do var sel = win.getSelection(); instead of window.getSelection(). Just noticed that you are that rangy guy :) I guess you took the code from rangy and forgot to replace this piece. Thanks a lot for your time!Nedra
@koko: Ah yes, I missed a window there. I'll edit my answer.Karissakarita
There is a limitation though i guess. Placing the cursor at the beginning of next empty line and save selection and while restoring selection it places the cursor at the end of previous line. Is there a way to overcome it ??Weekender
C
1

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

Cheney answered 25/7, 2021 at 14:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.