5

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!

4

3 回答 3

4

可编辑的 Div 元素不能解决整个问题吗?

突出显示的代码:

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

不幸的是,这使得一些编辑功能丢失,例如Ctrl + Z... 并且在粘贴文本时,插入符号停留在粘贴文本的开头。

我已经结合其他答案的代码来生成这个代码,所以请给他们信用。

编辑:我发现了一些有趣的东西......如果您使用 contentEditable 元素,则会出现本机插入符号,并且在其中您使用另一个具有不可见字体的元素:

<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/

于 2013-07-23T21:22:02.700 回答
2

我认为滞后是由于 keyup 触发 doStuff 有点太晚了,但是 keydown 有点太早了。

试试这个而不是 jQuery 事件连接(通常我更喜欢事件而不是轮询,但在这种情况下它可能会给人更好的感觉)......

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

...或这里的小提琴:http: //jsfiddle.net/2t5pu/27/

于 2013-07-23T15:42:04.780 回答
2

这似乎工作得很好并且不使用任何民意调查,就像我在评论中所说的那样。

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/

也许我遗漏了一些东西,但我认为这非常可靠,并且它比使用间隔来创建自己的事件更好。

编辑:添加了一个计时器以防止 que 堆叠。

于 2013-07-23T21:15:32.643 回答