Set cursor position on contentEditable

后端 未结 8 2041
悲&欢浪女
悲&欢浪女 2020-11-22 09:05

I am after a definitive, cross-browser solution to set the cursor/caret position to the last known position when a contentEditable=\'on\'

regains focus. It appears
相关标签:
8条回答
  • 2020-11-22 09:28

    This is compatible with the standards-based browsers, but will probably fail in IE. I'm providing it as a starting point. IE doesn't support DOM Range.

    var editable = document.getElementById('editable'),
        selection, range;
    
    // Populates selection and range variables
    var captureSelection = function(e) {
        // Don't capture selection outside editable region
        var isOrContainsAnchor = false,
            isOrContainsFocus = false,
            sel = window.getSelection(),
            parentAnchor = sel.anchorNode,
            parentFocus = sel.focusNode;
    
        while(parentAnchor && parentAnchor != document.documentElement) {
            if(parentAnchor == editable) {
                isOrContainsAnchor = true;
            }
            parentAnchor = parentAnchor.parentNode;
        }
    
        while(parentFocus && parentFocus != document.documentElement) {
            if(parentFocus == editable) {
                isOrContainsFocus = true;
            }
            parentFocus = parentFocus.parentNode;
        }
    
        if(!isOrContainsAnchor || !isOrContainsFocus) {
            return;
        }
    
        selection = window.getSelection();
    
        // Get range (standards)
        if(selection.getRangeAt !== undefined) {
            range = selection.getRangeAt(0);
    
        // Get range (Safari 2)
        } else if(
            document.createRange &&
            selection.anchorNode &&
            selection.anchorOffset &&
            selection.focusNode &&
            selection.focusOffset
        ) {
            range = document.createRange();
            range.setStart(selection.anchorNode, selection.anchorOffset);
            range.setEnd(selection.focusNode, selection.focusOffset);
        } else {
            // Failure here, not handled by the rest of the script.
            // Probably IE or some older browser
        }
    };
    
    // Recalculate selection while typing
    editable.onkeyup = captureSelection;
    
    // Recalculate selection after clicking/drag-selecting
    editable.onmousedown = function(e) {
        editable.className = editable.className + ' selecting';
    };
    document.onmouseup = function(e) {
        if(editable.className.match(/\sselecting(\s|$)/)) {
            editable.className = editable.className.replace(/ selecting(\s|$)/, '');
            captureSelection();
        }
    };
    
    editable.onblur = function(e) {
        var cursorStart = document.createElement('span'),
            collapsed = !!range.collapsed;
    
        cursorStart.id = 'cursorStart';
        cursorStart.appendChild(document.createTextNode('—'));
    
        // Insert beginning cursor marker
        range.insertNode(cursorStart);
    
        // Insert end cursor marker if any text is selected
        if(!collapsed) {
            var cursorEnd = document.createElement('span');
            cursorEnd.id = 'cursorEnd';
            range.collapse();
            range.insertNode(cursorEnd);
        }
    };
    
    // Add callbacks to afterFocus to be called after cursor is replaced
    // if you like, this would be useful for styling buttons and so on
    var afterFocus = [];
    editable.onfocus = function(e) {
        // Slight delay will avoid the initial selection
        // (at start or of contents depending on browser) being mistaken
        setTimeout(function() {
            var cursorStart = document.getElementById('cursorStart'),
                cursorEnd = document.getElementById('cursorEnd');
    
            // Don't do anything if user is creating a new selection
            if(editable.className.match(/\sselecting(\s|$)/)) {
                if(cursorStart) {
                    cursorStart.parentNode.removeChild(cursorStart);
                }
                if(cursorEnd) {
                    cursorEnd.parentNode.removeChild(cursorEnd);
                }
            } else if(cursorStart) {
                captureSelection();
                var range = document.createRange();
    
                if(cursorEnd) {
                    range.setStartAfter(cursorStart);
                    range.setEndBefore(cursorEnd);
    
                    // Delete cursor markers
                    cursorStart.parentNode.removeChild(cursorStart);
                    cursorEnd.parentNode.removeChild(cursorEnd);
    
                    // Select range
                    selection.removeAllRanges();
                    selection.addRange(range);
                } else {
                    range.selectNode(cursorStart);
    
                    // Select range
                    selection.removeAllRanges();
                    selection.addRange(range);
    
                    // Delete cursor marker
                    document.execCommand('delete', false, null);
                }
            }
    
            // Call callbacks here
            for(var i = 0; i < afterFocus.length; i++) {
                afterFocus[i]();
            }
            afterFocus = [];
    
            // Register selection again
            captureSelection();
        }, 10);
    };
    
    0 讨论(0)
  • 2020-11-22 09:29

    This solution works in all major browsers:

    saveSelection() is attached to the onmouseup and onkeyup events of the div and saves the selection to the variable savedRange.

    restoreSelection() is attached to the onfocus event of the div and reselects the selection saved in savedRange.

    This works perfectly unless you want the selection to be restored when the user clicks the div aswell (which is a bit unintuitative as normally you expect the cursor to go where you click but code included for completeness)

    To achieve this the onclick and onmousedown events are canceled by the function cancelEvent() which is a cross browser function to cancel the event. The cancelEvent() function also runs the restoreSelection() function because as the click event is cancelled the div doesn't receive focus and therefore nothing is selected at all unless this functions is run.

    The variable isInFocus stores whether it is in focus and is changed to "false" onblur and "true" onfocus. This allows click events to be cancelled only if the div is not in focus (otherwise you would not be able to change the selection at all).

    If you wish to the selection to be change when the div is focused by a click, and not restore the selection onclick (and only when focus is given to the element programtically using document.getElementById("area").focus(); or similar then simply remove the onclick and onmousedown events. The onblur event and the onDivBlur() and cancelEvent() functions can also safely be removed in these circumstances.

    This code should work if dropped directly into the body of an html page if you want to test it quickly:

    <div id="area" style="width:300px;height:300px;" onblur="onDivBlur();" onmousedown="return cancelEvent(event);" onclick="return cancelEvent(event);" contentEditable="true" onmouseup="saveSelection();" onkeyup="saveSelection();" onfocus="restoreSelection();"></div>
    <script type="text/javascript">
    var savedRange,isInFocus;
    function saveSelection()
    {
        if(window.getSelection)//non IE Browsers
        {
            savedRange = window.getSelection().getRangeAt(0);
        }
        else if(document.selection)//IE
        { 
            savedRange = document.selection.createRange();  
        } 
    }
    
    function restoreSelection()
    {
        isInFocus = true;
        document.getElementById("area").focus();
        if (savedRange != null) {
            if (window.getSelection)//non IE and there is already a selection
            {
                var s = window.getSelection();
                if (s.rangeCount > 0) 
                    s.removeAllRanges();
                s.addRange(savedRange);
            }
            else if (document.createRange)//non IE and no selection
            {
                window.getSelection().addRange(savedRange);
            }
            else if (document.selection)//IE
            {
                savedRange.select();
            }
        }
    }
    //this part onwards is only needed if you want to restore selection onclick
    var isInFocus = false;
    function onDivBlur()
    {
        isInFocus = false;
    }
    
    function cancelEvent(e)
    {
        if (isInFocus == false && savedRange != null) {
            if (e && e.preventDefault) {
                //alert("FF");
                e.stopPropagation(); // DOM style (return false doesn't always work in FF)
                e.preventDefault();
            }
            else {
                window.event.cancelBubble = true;//IE stopPropagation
            }
            restoreSelection();
            return false; // false = IE style
        }
    }
    </script>
    
    0 讨论(0)
  • 2020-11-22 09:30

    I took Nico Burns's answer and made it using jQuery:

    • Generic: For every div contentEditable="true"
    • Shorter

    You'll need jQuery 1.6 or higher:

    savedRanges = new Object();
    $('div[contenteditable="true"]').focus(function(){
        var s = window.getSelection();
        var t = $('div[contenteditable="true"]').index(this);
        if (typeof(savedRanges[t]) === "undefined"){
            savedRanges[t]= new Range();
        } else if(s.rangeCount > 0) {
            s.removeAllRanges();
            s.addRange(savedRanges[t]);
        }
    }).bind("mouseup keyup",function(){
        var t = $('div[contenteditable="true"]').index(this);
        savedRanges[t] = window.getSelection().getRangeAt(0);
    }).on("mousedown click",function(e){
        if(!$(this).is(":focus")){
            e.stopPropagation();
            e.preventDefault();
            $(this).focus();
        }
    });
    

    savedRanges = new Object();
    $('div[contenteditable="true"]').focus(function(){
        var s = window.getSelection();
        var t = $('div[contenteditable="true"]').index(this);
        if (typeof(savedRanges[t]) === "undefined"){
            savedRanges[t]= new Range();
        } else if(s.rangeCount > 0) {
            s.removeAllRanges();
            s.addRange(savedRanges[t]);
        }
    }).bind("mouseup keyup",function(){
        var t = $('div[contenteditable="true"]').index(this);
        savedRanges[t] = window.getSelection().getRangeAt(0);
    }).on("mousedown click",function(e){
        if(!$(this).is(":focus")){
            e.stopPropagation();
            e.preventDefault();
            $(this).focus();
        }
    });
    div[contenteditable] {
        padding: 1em;
        font-family: Arial;
        outline: 1px solid rgba(0,0,0,0.5);
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <div contentEditable="true"></div>
    <div contentEditable="true"></div>
    <div contentEditable="true"></div>

    0 讨论(0)
  • 2020-11-22 09:33

    You can leverage selectNodeContents which is supported by modern browsers.

    var el = document.getElementById('idOfYoursContentEditable');
    var selection = window.getSelection();
    var range = document.createRange();
    selection.removeAllRanges();
    range.selectNodeContents(el);
    range.collapse(false);
    selection.addRange(range);
    el.focus();
    
    0 讨论(0)
  • 2020-11-22 09:35

    After playing around I've modified eyelidlessness' answer above and made it a jQuery plugin so you can just do one of these:

    var html = "The quick brown fox";
    $div.html(html);
    
    // Select at the text "quick":
    $div.setContentEditableSelection(4, 5);
    
    // Select at the beginning of the contenteditable div:
    $div.setContentEditableSelection(0);
    
    // Select at the end of the contenteditable div:
    $div.setContentEditableSelection(html.length);
    

    Excuse the long code post, but it may help someone:

    $.fn.setContentEditableSelection = function(position, length) {
        if (typeof(length) == "undefined") {
            length = 0;
        }
    
        return this.each(function() {
            var $this = $(this);
            var editable = this;
            var selection;
            var range;
    
            var html = $this.html();
            html = html.substring(0, position) +
                '<a id="cursorStart"></a>' +
                html.substring(position, position + length) +
                '<a id="cursorEnd"></a>' +
                html.substring(position + length, html.length);
            console.log(html);
            $this.html(html);
    
            // Populates selection and range variables
            var captureSelection = function(e) {
                // Don't capture selection outside editable region
                var isOrContainsAnchor = false,
                    isOrContainsFocus = false,
                    sel = window.getSelection(),
                    parentAnchor = sel.anchorNode,
                    parentFocus = sel.focusNode;
    
                while (parentAnchor && parentAnchor != document.documentElement) {
                    if (parentAnchor == editable) {
                        isOrContainsAnchor = true;
                    }
                    parentAnchor = parentAnchor.parentNode;
                }
    
                while (parentFocus && parentFocus != document.documentElement) {
                    if (parentFocus == editable) {
                        isOrContainsFocus = true;
                    }
                    parentFocus = parentFocus.parentNode;
                }
    
                if (!isOrContainsAnchor || !isOrContainsFocus) {
                    return;
                }
    
                selection = window.getSelection();
    
                // Get range (standards)
                if (selection.getRangeAt !== undefined) {
                    range = selection.getRangeAt(0);
    
                    // Get range (Safari 2)
                } else if (
                    document.createRange &&
                    selection.anchorNode &&
                    selection.anchorOffset &&
                    selection.focusNode &&
                    selection.focusOffset
                ) {
                    range = document.createRange();
                    range.setStart(selection.anchorNode, selection.anchorOffset);
                    range.setEnd(selection.focusNode, selection.focusOffset);
                } else {
                    // Failure here, not handled by the rest of the script.
                    // Probably IE or some older browser
                }
            };
    
            // Slight delay will avoid the initial selection
            // (at start or of contents depending on browser) being mistaken
            setTimeout(function() {
                var cursorStart = document.getElementById('cursorStart');
                var cursorEnd = document.getElementById('cursorEnd');
    
                // Don't do anything if user is creating a new selection
                if (editable.className.match(/\sselecting(\s|$)/)) {
                    if (cursorStart) {
                        cursorStart.parentNode.removeChild(cursorStart);
                    }
                    if (cursorEnd) {
                        cursorEnd.parentNode.removeChild(cursorEnd);
                    }
                } else if (cursorStart) {
                    captureSelection();
                    range = document.createRange();
    
                    if (cursorEnd) {
                        range.setStartAfter(cursorStart);
                        range.setEndBefore(cursorEnd);
    
                        // Delete cursor markers
                        cursorStart.parentNode.removeChild(cursorStart);
                        cursorEnd.parentNode.removeChild(cursorEnd);
    
                        // Select range
                        selection.removeAllRanges();
                        selection.addRange(range);
                    } else {
                        range.selectNode(cursorStart);
    
                        // Select range
                        selection.removeAllRanges();
                        selection.addRange(range);
    
                        // Delete cursor marker
                        document.execCommand('delete', false, null);
                    }
                }
    
                // Register selection again
                captureSelection();
            }, 10);
        });
    };
    
    0 讨论(0)
  • 2020-11-22 09:35

    In Firefox you might have the text of the div in a child node (o_div.childNodes[0])

    var range = document.createRange();
    
    range.setStart(o_div.childNodes[0],last_caret_pos);
    range.setEnd(o_div.childNodes[0],last_caret_pos);
    range.collapse(false);
    
    var sel = window.getSelection(); 
    sel.removeAllRanges();
    sel.addRange(range);
    
    0 讨论(0)
提交回复
热议问题