javascriptjqueryhtmlcontenteditablecursor-position

Set cursor position on contentEditable <div>


I am after a definitive, cross-browser solution to set the cursor/caret position to the last known position when a contentEditable='on' <div> regains focus. It appears default functionality of a content editable div is to move the caret/cursor to the beginning of the text in the div each time you click on it, which is undesirable.

I believe I would have to store in a variable the current cursor position when they are leaving focus of the div, and then re-set this when they have focus inside again, but I have not been able to put together, or find a working code sample yet.

If anybody has any thoughts, working code snippets or samples I'd be happy to see them.

I don't really have any code yet but here is what I do have:

<script type="text/javascript">
// jQuery
$(document).ready(function() {
   $('#area').focus(function() { .. }  // focus I would imagine I need.
}
</script>
<div id="area" contentEditable="true"></div>

PS. I have tried this resource but it appears it does not work for a <div>. Perhaps only for textarea (How to move cursor to end of contenteditable entity)


Solution

  • 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);
    };