Search code examples
jqueryhtmlcsscontenteditablecentering

Centering the currently focused line in the window?


I've got a contentEditable div in an app I'm working on. I'd like the line that the cursor is currently on to be vertically centered in the window.

When the user scrolls up or down the cursor should always stay centered, with the contentEditable div moving up or down to adjust.

How do I go about doing this?

Update

I've gotten some of the way there with this

Demo: http://jsfiddle.net/n1dyezmg/

$(function () {     
    // fire our function on keyup   
    $(window).bind('keyup', function(event) {       
        if (!event.ctrlKey && !event.shiftKey) 
            getCaretXY();
    });
    // ... and also on page load
    getCaretXY();
});

function getCaretXY(){      
    // this has got to be a performance drag, but...
    // paste a special span at the cursor pos
    pasteHtmlAtCaret("<span id='XY'></span>")
    // get the XY offset of that element
    var XYoffset = $("#XY").offset();
    // remove our special element
    $("#XY").remove();
    // do something with the values
    $("#caretXY").html(XYoffset.top + "," + XYoffset.left);     
}

function pasteHtmlAtCaret(html) {
    var sel, range;
    if (window.getSelection) {
        // IE9 and non-IE
        sel = window.getSelection();
        if (sel.getRangeAt && sel.rangeCount) {
            range = sel.getRangeAt(0);
            range.deleteContents();
            
            // Range.createContextualFragment() would be useful here but is
            // only relatively recently standardized and is not supported in
            // some browsers (IE9, for one)
            var el = document.createElement("div");
            el.innerHTML = html;
            var frag = document.createDocumentFragment(), node, lastNode;
            while ( (node = el.firstChild) ) {
                lastNode = frag.appendChild(node);
            }
            range.insertNode(frag);
            
            // Preserve the selection
            if (lastNode) {
                range = range.cloneRange();
                range.setStartAfter(lastNode);
                range.collapse(true);
                sel.removeAllRanges();
                sel.addRange(range);
            }
        }
    } else if (document.selection && document.selection.type != "Control") {
        // IE < 9
        document.selection.createRange().pasteHTML(html);
    }
}

I got the paste at caret function from here

The x,y values of the cursor are recorded on keydown, and it's actually not as much of a performance hog as I expected, but unfortunately it has some issues.

  1. It's inaccurate. If moving the cursor down the left extremity, the span used to get the X,Y coords sometimes gets pushed to the previous line.

  2. If I shift highlight any of the text, and press the arrow keys the text is deleted. This is obviously a major no-no for an editor! I could possible fix this by having a shadow div of the same dimensions hidden somewhere on the page. getCaretXY could then paste the text into this div and use that to determine the height. The inaccuracy issue remains though.

I also didn't have much luck using this info to adjust the position of the editor, although that's just a case of fiddling with the CSS for long enough


Solution

  • I had an epiphany earlier while driving and listening to Kate Bush's Running up that hill. There's a much, much easier way to do what I want to do.

    Create a div with the content. Create a second, floating div one line high. Use the scrollTop height from the second div to set the height of the first. It works beautifully.

    demo: http://jsfiddle.net/aheydvjk/1/

    HTML

    <div class="editorbox" id="editor" contenteditable="true"></div>
    <div class="editorbox" id="overlay" contenteditable="true"></div>
    

    CSS

    html, body{
        overflow: hidden;
    }
    
    .editorbox {
        overflow: hidden;
        position: absolute;
        left: 50%;
        top: 200px;
        margin-left: -200px;
        margin-top: -20px;
        width: 400px;
        min-height: 20px;
        line-height: 20px;
        padding: 10px;
        box-shadow: 0 0 0 1px #ddd;
        height: 100%;
        color: #777;
    }
    
    #overlay {
        height: 20px;
        background: #fff;
        color: #222;
        box-shadow: none;
    }
    

    Javascript

    $(function(){
        $("#overlay").on("scroll", function(){
            $("#editor").css("top", 200 -$("#overlay").scrollTop() + "px");
        });
    
        $("#overlay").on("keyup", function(){
            $("#editor").html($("#overlay").html());
        });    
    });
    

    There are a couple of little things I need to sort, like clicking somewhere on the text or selecting more than one line, but this is a good place to start working from. Thanks for all the help!