Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mimicng caret in a textarea

I am trying to mimic the caret of a textarea for the purpose of creating a very light-weight rich-textarea. I don't want to use something like codemirror or any other massive library because I will not use any of their features.

I have a <pre> positioned behind a textarea with a transparent background so i can simulate a highlighting effect in the text. However, I also want to be able to change the font color (so its not always the same). So I tried color: transparent on the textarea which allows me to style the text in any way I want because it only appears on the <pre> element behind the textarea, but the caret disappears.

I have gotten it to work fairly well, although it is not perfect. The main problem is that when you hold down a key and spam that character, the caret seems to always lag one character behind. Not only that, it seems to be quite resource heavy..

If you see any other things in the code that need improvement, feel free to comment on that too!

Here's a fiddle with the code: http://jsfiddle.net/2t5pu/25/

And for you who don't want to visit jsfiddle for whatever reason, here's the entire code:

CSS:

textarea, #fake_area {
    position: absolute;
    margin: 0;
    padding: 0;
    height: 400px;
    width: 600px;
    font-size: 16px;
    font: 16px "Courier New", Courier, monospace;
    white-space: pre;
    top: 0;
    left: 0;
    resize: none;
    outline: 0;
    border: 1px solid orange;
    overflow: hidden;
    word-break: break-word;
    padding: 5px;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    -ms-box-sizing: border-box;
    box-sizing: border-box;
}
#fake_area {
    /* hide */
    opacity: 0;
}
#caret {
    width: 1px;
    height: 18px;
    position: absolute;
    background: #f00;
    z-index: 100;
}

HTML:

<div id="fake_area"><span></span></div>
<div id="caret"></div>
<textarea id="textarea">test</textarea>

JAVASCRIPT:

var fake_area = document.getElementById("fake_area").firstChild;
var fake_caret = document.getElementById("caret");
var real_area = document.getElementById("textarea");

$("#textarea").on("input keydown keyup propertychange click", function () {
    // Fill the clone with textarea content from start to the position of the caret. 
    // The replace /\n$/ is necessary to get position when cursor is at the beginning of empty new line.
doStuff();    
});

var timeout;
function doStuff() {
    if(timeout) clearTimeout(timeout);
    timeout=setTimeout(function() {
        fake_area.innerHTML = real_area.value.substring(0, getCaretPosition(real_area)).replace(/\n$/, '\n\u0001');
    setCaretXY(fake_area, real_area, fake_caret, getPos("textarea"));
    }, 10);
}


    function getCaretPosition(el) {
        if (el.selectionStart) return el.selectionStart;
        else if (document.selection) {
            //el.focus();
            var r = document.selection.createRange();
            if (r == null) return 0;

            var re = el.createTextRange(), rc = re.duplicate();
            re.moveToBookmark(r.getBookmark());
            rc.setEndPoint('EndToStart', re);

            return rc.text.length;
        }
        return 0;
    }

    function setCaretXY(elem, real_element, caret, offset) {
        var rects = elem.getClientRects();
        var lastRect = rects[rects.length - 1];

        var x = lastRect.left + lastRect.width - offset[0] + document.body.scrollLeft,
            y = lastRect.top - real_element.scrollTop - offset[1] + document.body.scrollTop;

        caret.style.cssText = "top: " + y + "px; left: " + x + "px";
        //console.log(x, y, offset);
    }

    function getPos(e) {
        e = document.getElementById(e);
        var x = 0;
        var y = 0;
        while (e.offsetParent !== null){
            x += e.offsetLeft;
            y += e.offsetTop;
            e = e.offsetParent;
        }
        return [x, y];
    }

Thanks in advance!

like image 773
Firas Dib Avatar asked Jul 08 '13 12:07

Firas Dib


3 Answers

Doesn't an editable Div element solve the entire problem?

Code that does the highlighting:

http://jsfiddle.net/masbicudo/XYGgz/3/

var prevText = "";
var isHighlighting = false;
$("#textarea").bind("paste drop keypress input textInput DOMNodeInserted", function (e){
    if (!isHighlighting)
    {
        var currentText = $(this).text();
        if (currentText != prevText)
        {
            doSave();
            isHighlighting = true;
            $(this).html(currentText
                   .replace(/\bcolored\b/g, "<font color=\"red\">colored</font>")
                   .replace(/\bhighlighting\b/g, "<span style=\"background-color: yellow\">highlighting</span>"));
            isHighlighting = false;
            prevText = currentText;
            doRestore();
        }
    }
});

Unfortunately, this made some editing functions to be lost, like Ctrl + Z... and when pasting text, the caret stays at the beginning of the pasted text.

I have combined code from other answers to produce this code, so please, give them credit.

  • How do I make an editable DIV look like a text field?
  • Get a range's start and end offset's relative to its parent container

EDIT: I have discovered something interesting... the native caret appears if you use a contentEditable element, and inside of it you use another element with the invisible font:

<div id="textarea" contenteditable style="color: red"><div style="color: transparent; background-color: transparent;">This is some hidden text.</div></div>

http://jsfiddle.net/masbicudo/qsRdg/4/

like image 84
Miguel Angelo Avatar answered Oct 20 '22 01:10

Miguel Angelo


The lag is I think due to the keyup triggering the doStuff a bit too late, but the keydown is a bit too soon.

Try this instead of the jQuery event hookup (normally I'd prefer events to polling, but in this case it might give a better feel)...

setInterval(function () { doStuff(); }, 10); // 100 checks per second

function doStuff() {
    var newHTML = real_area.value.substring(0, getCaretPosition(real_area)).replace(/\n$/, '\n\u0001');
    if (fake_area.innerHTML != newHTML) {
        fake_area.innerHTML = newHTML;
        setCaretXY(fake_area, real_area, fake_caret,                         getPos("textarea"));
    }
}

...or here for the fiddle: http://jsfiddle.net/2t5pu/27/

like image 34
Chris Sterling Avatar answered Oct 19 '22 23:10

Chris Sterling


this seems to work great and doesn't use any polls, just like i was talking about in the comments.

var timer=0;
$("#textarea").on("input keydown keyup propertychange click paste cut copy mousedown mouseup change", function () {
    clearTimeout(timer);
    timer=setTimeout(update, 10);    
});

http://jsfiddle.net/2t5pu/29/

maybe i'm missing something, but i think this is pretty solid, and it behaves better than using intervals to create your own events.

EDIT: added a timer to prevent que stacking.

like image 42
dandavis Avatar answered Oct 19 '22 23:10

dandavis