60

For a project of mine I would love to provide auto completion for a specific textarea. Similar to how intellisense/omnicomplete works. For that however I have to find out the absolute cursor position so that I know where the DIV should appear.

Turns out: that's (nearly I hope) impossible to achieve. Does anyone has some neat ideas how to solve that problem?

4

11 回答 11

35

我的 Hacky 实验的第 2 版

这个新版本适用于任何字体,可以按需调整,以及任何文本区域大小。

在注意到你们中的一些人仍在努力让它发挥作用后,我决定尝试一种新方法。这次我的结果要好得多——至少在 linux 上的 google chrome 上。我不再有可用的 Windows PC,所以我只能在 Ubuntu 上的 chrome / firefox 上进行测试。我的结果在 Chrome 上 100% 一致,假设在 Firefox 上大约 70-80%,但我不认为找到不一致会非常困难。

这个新版本依赖于一个 Canvas 对象。在我的示例中,我实际上展示了那个画布 - 只是为了让您可以看到它的实际效果,但可以很容易地使用隐藏的画布对象来完成。

这肯定是一个 hack,我提前为我相当拼凑的代码道歉。至少,在谷歌浏览器中,它始终如一地工作,无论我将它设置为什么字体,或者 textarea 的大小。我使用Sam Saffron的示例来显示光标坐标(灰色背景 div)。我还添加了一个“随机化”链接,因此您可以看到它以不同的字体/texarea 大小和样式工作,并实时观察光标位置更新。我建议您查看完整页面演示,以便您可以更好地看到伴随画布的播放。

我将总结它是如何工作的......

基本思想是我们试图在画布上重绘文本区域,尽可能接近。由于浏览器对 texarea 和 texarea 都使用相同的字体引擎,我们可以使用 canvas 的字体测量功能来确定事物的位置。从那里,我们可以使用我们可用的画布方法来计算我们的坐标。

首先,我们调整画布以匹配文本区域的尺寸。这完全是出于视觉目的,因为画布大小并没有真正影响我们的结果。由于 Canvas 实际上并没有提供自动换行的方法,所以我不得不想出一种方法(一起窃取/借用/混淆)分解行以尽可能匹配文本区域。这是您可能会发现需要进行最多跨浏览器调整的地方。

自动换行后,其他一切都是基础数学。我们将这些行拆分为一个数组来模拟自动换行,现在我们想要遍历这些行并一直向下直到我们当前选择结束的点。为了做到这一点,我们只计算字符数,一旦超过selection.end,我们就知道我们已经走得够远了。将直到该点的行数乘以行高,您就有了一个y坐标。

坐标非常相似,x除了我们使用context.measureText. 只要我们打印出正确数量的字符,这将为我们提供绘制到 Canvas 的线条的宽度,该线条恰好在最后一个写出的字符之后结束,即当前selection.end位置之前的字符。

当尝试为其他浏览器调试时,要寻找的是线路没有正确中断的地方。您会在某些地方看到画布中一行的最后一个单词可能已经覆盖在文本区域上,反之亦然。这与浏览器如何处理自动换行有关。只要您在画布中进行包装以匹配文本区域,您的光标就应该是正确的。

我将在下面粘贴源代码。您应该能够复制和粘贴它,但如果您这样做了,我要求您下载自己的 jquery-fieldselection 副本,而不是点击我服务器上的那个。

我还增加了一个新的演示小提琴

祝你好运!

<!DOCTYPE html>
<html lang="en-US">
    <head>
        <meta charset="utf-8" />
        <title>Tooltip 2</title>
        <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
        <script type="text/javascript" src="http://enobrev.info/cursor/js/jquery-fieldselection.js"></script>
        <style type="text/css">
            form {
                float: left;
                margin: 20px;
            }

            #textariffic {
                height: 400px;
                width: 300px;
                font-size: 12px;
                font-family: 'Arial';
                line-height: 12px;
            }

            #tip {
                width:5px;
                height:30px;
                background-color: #777;
                position: absolute;
                z-index:10000
            }

            #mock-text {
                float: left;
                margin: 20px;
                border: 1px inset #ccc;
            }

            /* way the hell off screen */
            .scrollbar-measure {
                width: 100px;
                height: 100px;
                overflow: scroll;
                position: absolute;
                top: -9999px;
            }

            #randomize {
                float: left;
                display: block;
            }
        </style>
        <script type="text/javascript">
            var oCanvas;
            var oTextArea;
            var $oTextArea;
            var iScrollWidth;

            $(function() {
                iScrollWidth = scrollMeasure();
                oCanvas      = document.getElementById('mock-text');
                oTextArea    = document.getElementById('textariffic');
                $oTextArea   = $(oTextArea);

                $oTextArea
                        .keyup(update)
                        .mouseup(update)
                        .scroll(update);

                $('#randomize').bind('click', randomize);

                update();
            });

            function randomize() {
                var aFonts      = ['Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Impact', 'Times New Roman', 'Verdana', 'Webdings'];
                var iFont       = Math.floor(Math.random() * aFonts.length);
                var iWidth      = Math.floor(Math.random() * 500) + 300;
                var iHeight     = Math.floor(Math.random() * 500) + 300;
                var iFontSize   = Math.floor(Math.random() * 18)  + 10;
                var iLineHeight = Math.floor(Math.random() * 18)  + 10;

                var oCSS = {
                    'font-family':  aFonts[iFont],
                    width:          iWidth + 'px',
                    height:         iHeight + 'px',
                    'font-size':    iFontSize + 'px',
                    'line-height':  iLineHeight + 'px'
                };

                console.log(oCSS);

                $oTextArea.css(oCSS);

                update();
                return false;
            }

            function showTip(x, y) {
                $('#tip').css({
                      left: x + 'px',
                      top: y + 'px'
                  });
            }

            // https://stackoverflow.com/a/11124580/14651
            // https://stackoverflow.com/a/3960916/14651

            function wordWrap(oContext, text, maxWidth) {
                var aSplit = text.split(' ');
                var aLines = [];
                var sLine  = "";

                // Split words by newlines
                var aWords = [];
                for (var i in aSplit) {
                    var aWord = aSplit[i].split('\n');
                    if (aWord.length > 1) {
                        for (var j in aWord) {
                            aWords.push(aWord[j]);
                            aWords.push("\n");
                        }

                        aWords.pop();
                    } else {
                        aWords.push(aSplit[i]);
                    }
                }

                while (aWords.length > 0) {
                    var sWord = aWords[0];
                    if (sWord == "\n") {
                        aLines.push(sLine);
                        aWords.shift();
                        sLine = "";
                    } else {
                        // Break up work longer than max width
                        var iItemWidth = oContext.measureText(sWord).width;
                        if (iItemWidth > maxWidth) {
                            var sContinuous = '';
                            var iWidth = 0;
                            while (iWidth <= maxWidth) {
                                var sNextLetter = sWord.substring(0, 1);
                                var iNextWidth  = oContext.measureText(sContinuous + sNextLetter).width;
                                if (iNextWidth <= maxWidth) {
                                    sContinuous += sNextLetter;
                                    sWord = sWord.substring(1);
                                }
                                iWidth = iNextWidth;
                            }
                            aWords.unshift(sContinuous);
                        }

                        // Extra space after word for mozilla and ie
                        var sWithSpace = (jQuery.browser.mozilla || jQuery.browser.msie) ? ' ' : '';
                        var iNewLineWidth = oContext.measureText(sLine + sWord + sWithSpace).width;
                        if (iNewLineWidth <= maxWidth) {  // word fits on current line to add it and carry on
                            sLine += aWords.shift() + " ";
                        } else {
                            aLines.push(sLine);
                            sLine = "";
                        }

                        if (aWords.length === 0) {
                            aLines.push(sLine);
                        }
                    }
                }
                return aLines;
            }

            // http://davidwalsh.name/detect-scrollbar-width
            function scrollMeasure() {
                // Create the measurement node
                var scrollDiv = document.createElement("div");
                scrollDiv.className = "scrollbar-measure";
                document.body.appendChild(scrollDiv);

                // Get the scrollbar width
                var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;

                // Delete the DIV
                document.body.removeChild(scrollDiv);

                return scrollbarWidth;
            }

            function update() {
                var oPosition  = $oTextArea.position();
                var sContent   = $oTextArea.val();
                var oSelection = $oTextArea.getSelection();

                oCanvas.width  = $oTextArea.width();
                oCanvas.height = $oTextArea.height();

                var oContext    = oCanvas.getContext("2d");
                var sFontSize   = $oTextArea.css('font-size');
                var sLineHeight = $oTextArea.css('line-height');
                var fontSize    = parseFloat(sFontSize.replace(/[^0-9.]/g, ''));
                var lineHeight  = parseFloat(sLineHeight.replace(/[^0-9.]/g, ''));
                var sFont       = [$oTextArea.css('font-weight'), sFontSize + '/' + sLineHeight, $oTextArea.css('font-family')].join(' ');

                var iSubtractScrollWidth = oTextArea.clientHeight < oTextArea.scrollHeight ? iScrollWidth : 0;

                oContext.save();
                oContext.clearRect(0, 0, oCanvas.width, oCanvas.height);
                oContext.font = sFont;
                var aLines = wordWrap(oContext, sContent, oCanvas.width - iSubtractScrollWidth);

                var x = 0;
                var y = 0;
                var iGoal = oSelection.end;
                aLines.forEach(function(sLine, i) {
                    if (iGoal > 0) {
                        oContext.fillText(sLine.substring(0, iGoal), 0, (i + 1) * lineHeight);

                        x = oContext.measureText(sLine.substring(0, iGoal + 1)).width;
                        y = i * lineHeight - oTextArea.scrollTop;

                        var iLineLength = sLine.length;
                        if (iLineLength == 0) {
                            iLineLength = 1;
                        }

                        iGoal -= iLineLength;
                    } else {
                        // after
                    }
                });
                oContext.restore();

                showTip(oPosition.left + x, oPosition.top + y);
            }

        </script>
    </head>
    <body>

        <a href="#" id="randomize">Randomize</a>

        <form id="tipper">
            <textarea id="textariffic">Aliquam urna. Nullam augue dolor, tincidunt condimentum, malesuada quis, ultrices at, arcu. Aliquam nunc pede, convallis auctor, sodales eget, aliquam eget, ligula. Proin nisi lacus, scelerisque nec, aliquam vel, dictum mattis, eros. Curabitur et neque. Fusce sollicitudin. Quisque at risus. Suspendisse potenti. Mauris nisi. Sed sed enim nec dui viverra congue. Phasellus velit sapien, porttitor vitae, blandit volutpat, interdum vel, enim. Cras sagittis bibendum neque. Proin eu est. Fusce arcu. Aliquam elit nisi, malesuada eget, dignissim sed, ultricies vel, purus. Maecenas accumsan diam id nisi.

Phasellus et nunc. Vivamus sem felis, dignissim non, lacinia id, accumsan quis, ligula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed scelerisque nulla sit amet mi. Nulla consequat, elit vitae tempus vulputate, sem libero rhoncus leo, vulputate viverra nulla purus nec turpis. Nam turpis sem, tincidunt non, congue lobortis, fermentum a, ipsum. Nulla facilisi. Aenean facilisis. Maecenas a quam eu nibh lacinia ultricies. Morbi malesuada orci quis tellus.

Sed eu leo. Donec in turpis. Donec non neque nec ante tincidunt posuere. Pellentesque blandit. Ut vehicula vestibulum risus. Maecenas commodo placerat est. Integer massa nunc, luctus at, accumsan non, pulvinar sed, odio. Pellentesque eget libero iaculis dui iaculis vehicula. Curabitur quis nulla vel felis ullamcorper varius. Sed suscipit pulvinar lectus.</textarea>

        </form>

        <div id="tip"></div>

        <canvas id="mock-text"></canvas>
    </body>
</html>

漏洞

我记得有一个错误。如果将光标放在一行的第一个字母之前,它会将“位置”显示为上一行的最后一个字母。这与 selection.end 的工作方式有关。我认为寻找那个案例并相应地修复它应该不会太难。


版本 1

将其留在这里,这样您就可以查看进度,而无需深入了解编辑历史记录。

它并不完美,它绝对是一个 hack,但我让它在 WinXP IE、FF、Safari、Chrome 和 Opera 上运行良好。

据我所知,没有办法在任何浏览器上直接找出光标的 x/y。Adam Bellaire提到IE 方法很有趣,但不幸的是不是跨浏览器。我认为下一个最好的方法是将字符用作网格。

不幸的是,任何浏览器都没有内置字体度量信息,这意味着等宽字体是唯一具有一致度量的字体类型。此外,没有可靠的方法可以从字体高度找出字体宽度。起初我尝试使用高度的百分比,效果很好。然后我改变了字体大小,一切都变糟了。

我尝试了一种计算字符宽度的方法,即创建一个临时文本区域并继续添加字符,直到滚动高度(或滚动宽度)发生变化。这似乎是合理的,但在这条路的一半左右,我意识到我可以只使用 textarea 上的 cols 属性,并认为在这个考验中有足够的 hack 可以添加另一个。这意味着您不能通过 css 设置 textarea 的宽度。你必须使用 cols 才能工作。

我遇到的下一个问题是,即使您通过 css 设置字体,浏览器也会以不同的方式报告字体。不设置字体时,mozillamonospace默认使用,IE使用Courier New,Opera "Courier New"(带引号),Safari,'Lucida Grand'(带单引号)。当您确实将字体设置为monospace, mozilla 并接受您提供的内容时,Safari 会出现,-webkit-monospace而 Opera 会保留"Courier New".

所以现在我们初始化一些变量。确保在 css 中也设置行高。Firefox 报告正确的行高,但 IE 报告“正常”,我没有打扰其他浏览器。我只是在我的css中设置了行高,这就解决了差异。我没有使用 ems 代替像素进行测试。字符高度只是字体大小。可能也应该在你的css中预先设置它。

此外,在我们开始放置角色之前还要进行一项预设——这真的让我摸不着头脑。对于 ie 和 mozilla,texarea 字符是 < cols,其他的都是 <= 字符。因此 Chrome 可以容纳 50 个字符,但 mozilla 和 ie 会中断最后一个单词。

现在我们将为每一行创建一个首字符位置数组。我们遍历文本区域中的每个字符。如果它是换行符,我们将一个新位置添加到我们的行数组中。如果它是一个空格,我们会尝试确定当前的“单词”是否适合我们所在的行,或者它是否会被推到下一行。标点符号算作“单词”的一部分。我没有测试过制表符,但是有一行可以为制表符添加 4 个字符。

一旦我们有一个行位置数组,我们循环并尝试找到光标在哪一行。我们使用选择的“结束”作为光标。

x = (光标位置 - 光标行的第一个字符位置) * 字符宽度

y = ((光标行 + 1) * 行高) - 滚动位置

我正在使用jquery 1.2.6jquery-fieldselectionjquery-dimensions

演示:http ://enobrev.info/cursor/

和代码:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Tooltip</title>
        <script type="text/javascript" src="js/jquery-1.2.6.js"></script>
        <script type="text/javascript" src="js/jquery-fieldselection.js"></script>
        <script type="text/javascript" src="js/jquery.dimensions.js"></script>
        <style type="text/css">
            form {
                margin: 20px auto;
                width: 500px;
            }

            #textariffic {
                height: 400px;
                font-size: 12px;
                font-family: monospace;
                line-height: 15px;
            }

            #tip {
                position: absolute;
                z-index: 2;
                padding: 20px;
                border: 1px solid #000;
                background-color: #FFF;
            }
        </style>
        <script type="text/javascript">
            $(function() {
                $('textarea')
                    .keyup(update)
                    .mouseup(update)
                    .scroll(update);
            });

            function showTip(x, y) {                
                y = y + $('#tip').height();

                $('#tip').css({
                    left: x + 'px',
                    top: y + 'px'
                });
            }

            function update() {
                var oPosition = $(this).position();
                var sContent = $(this).val();

                var bGTE = jQuery.browser.mozilla || jQuery.browser.msie;

                if ($(this).css('font-family') == 'monospace'           // mozilla
                ||  $(this).css('font-family') == '-webkit-monospace'   // Safari
                ||  $(this).css('font-family') == '"Courier New"') {    // Opera
                    var lineHeight   = $(this).css('line-height').replace(/[^0-9]/g, '');
                        lineHeight   = parseFloat(lineHeight);
                    var charsPerLine = this.cols;
                    var charWidth    = parseFloat($(this).innerWidth() / charsPerLine);


                    var iChar = 0;
                    var iLines = 1;
                    var sWord = '';

                    var oSelection = $(this).getSelection();
                    var aLetters = sContent.split("");
                    var aLines = [];

                    for (var w in aLetters) {
                        if (aLetters[w] == "\n") {
                            iChar = 0;
                            aLines.push(w);
                            sWord = '';
                        } else if (aLetters[w] == " ") {    
                            var wordLength = parseInt(sWord.length);


                            if ((bGTE && iChar + wordLength >= charsPerLine)
                            || (!bGTE && iChar + wordLength > charsPerLine)) {
                                iChar = wordLength + 1;
                                aLines.push(w - wordLength);
                            } else {                
                                iChar += wordLength + 1; // 1 more char for the space
                            }

                            sWord = '';
                        } else if (aLetters[w] == "\t") {
                            iChar += 4;
                        } else {
                            sWord += aLetters[w];     
                        }
                    }

                    var iLine = 1;
                    for(var i in aLines) {
                        if (oSelection.end < aLines[i]) {
                            iLine = parseInt(i) - 1;
                            break;
                        }
                    }

                    if (iLine > -1) {
                        var x = parseInt(oSelection.end - aLines[iLine]) * charWidth;
                    } else {
                        var x = parseInt(oSelection.end) * charWidth;
                    }
                    var y = (iLine + 1) * lineHeight - this.scrollTop; // below line

                    showTip(oPosition.left + x, oPosition.top + y);
                }
            }

        </script>
    </head>
    <body>
        <form id="tipper">
            <textarea id="textariffic" cols="50">
Aliquam urna. Nullam augue dolor, tincidunt condimentum, malesuada quis, ultrices at, arcu. Aliquam nunc pede, convallis auctor, sodales eget, aliquam eget, ligula. Proin nisi lacus, scelerisque nec, aliquam vel, dictum mattis, eros. Curabitur et neque. Fusce sollicitudin. Quisque at risus. Suspendisse potenti. Mauris nisi. Sed sed enim nec dui viverra congue. Phasellus velit sapien, porttitor vitae, blandit volutpat, interdum vel, enim. Cras sagittis bibendum neque. Proin eu est. Fusce arcu. Aliquam elit nisi, malesuada eget, dignissim sed, ultricies vel, purus. Maecenas accumsan diam id nisi.

Phasellus et nunc. Vivamus sem felis, dignissim non, lacinia id, accumsan quis, ligula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed scelerisque nulla sit amet mi. Nulla consequat, elit vitae tempus vulputate, sem libero rhoncus leo, vulputate viverra nulla purus nec turpis. Nam turpis sem, tincidunt non, congue lobortis, fermentum a, ipsum. Nulla facilisi. Aenean facilisis. Maecenas a quam eu nibh lacinia ultricies. Morbi malesuada orci quis tellus.

Sed eu leo. Donec in turpis. Donec non neque nec ante tincidunt posuere. Pellentesque blandit. Ut vehicula vestibulum risus. Maecenas commodo placerat est. Integer massa nunc, luctus at, accumsan non, pulvinar sed, odio. Pellentesque eget libero iaculis dui iaculis vehicula. Curabitur quis nulla vel felis ullamcorper varius. Sed suscipit pulvinar lectus. 
            </textarea>

        </form>

        <p id="tip">Here I Am!!</p>
    </body>
</html>
于 2008-10-02T16:47:34.453 回答
4

我不会再解释与这些东西相关的问题,因为它们在其他帖子中有很好的解释。只是会指出一个可能的解决方案,它有一些错误,但它是一个起点。

幸运的是,Github 上有一个脚本可以计算插入符号相对于其容器的位置,但它需要 jQuery。GitHub 页面在这里: jquery-caret-position-getter,感谢 Bevis.Zhao。

基于它,我实现了下一个代码:在 jsFiddle.net中检查它

<html><head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>- jsFiddle demo by mjerez</title>
    <script type="text/javascript" src="http://code.jquery.com/jquery-1.8.2.js"></script>
    <link rel="stylesheet" type="text/css" href="http://jsfiddle.net/css/normalize.css">
    <link rel="stylesheet" type="text/css" href="http://jsfiddle.net/css/result-light.css">   
    <script type="text/javascript" src="https://raw.github.com/beviz/jquery-caret-position-getter/master/jquery.caretposition.js"></script>     
    <style type="text/css">
        body{position:relative;font:normal 100% Verdana, Geneva, sans-serif;padding:10px;}
        .aux{background:#ccc;opacity: 0.5;width:50%;padding:5px;border:solid 1px #aaa;}
        .hidden{display:none}
        .show{display:block; position:absolute; top:0px; left:0px;}
    </style>
    <script type="text/javascript">//<![CDATA[ 
    $(document).keypress(function(e) {
        if ($(e.target).is('input, textarea')) {
            var key = String.fromCharCode(e.which);
            var ctrl = e.ctrlKey;
            if (ctrl) {
                var display = $("#autocomplete");
                var editArea = $('#editArea');            
                var pos = editArea.getCaretPosition();
                var offset = editArea.offset();
                // now you can use left, top(they are relative position)
                display.css({
                    left: offset.left + pos.left,
                    top:  offset.top + pos.top,
                    color : "#449"
                })
                display.toggleClass("show");
                return false;
            }
        }

    });
    window.onload = (function() {
        $("#editArea").blur(function() {
            if ($("#autocomplete").hasClass("show")) $("#autocomplete").toggleClass("show");
        })
    });
    //]]>  
    </script>
</head>
<body>
    <p>Click ctrl+space to while you write to diplay the autocmplete pannel.</p>
    </br>
    <textarea id="editArea" rows="4" cols="50"></textarea>
    </br>
    </br>
    </br>
    <div id="autocomplete" class="aux hidden ">
        <ol>
            <li>Option a</li>
            <li>Option b</li>
            <li>Option c</li>
            <li>Option d</li>
        </ol>
    </div>
</body>
于 2012-10-26T08:23:48.027 回答
4

我在俄罗斯 JavaScript 网站上发布了一个与此问题相关的主题。

如果您不懂俄语,请尝试使用 Google 版本翻译:http://translate.google.ru/translate?js=y&prev=_t&hl=ru&ie=UTF-8&layout=1&eotf=1&u=http: //javascript.ru/forum /events/7771-poluchit-koordinaty-kursora-v-tekstovom-pole-v-pikselyakh.html&sl=ru&tl=en

翻译版本的代码示例中存在一些标记问题,因此您可以阅读原始俄语帖子中的代码

这个想法很简单。没有简单、通用和跨浏览器的方法来获取以像素为单位的光标位置。坦率地说,有,但仅适用于 Internet Explorer。

在其他浏览器中,如果您确实需要计算它,您必须...

  • 创建一个不可见的 DIV
  • 将文本框的所有样式和内容复制到该 DIV
  • 然后在文本框中插入符号所在的文本中完全相同的位置插入 HTML 元素
  • 获取该 HTML 元素的坐标
于 2010-02-18T17:26:30.560 回答
4

请注意,此问题与一个月前提出的问题重复,我已在此处回答。我只会在那个链接上保留答案,因为这个问题应该在几年前就已经被关闭了。

答案副本

我已经为meteor-autocomplete寻找一个 textarea caret 坐标插件,所以我评估了 GitHub 上的所有 8 个插件。到目前为止,获胜者是来自Component的 textarea-caret-position

特征

  • 像素精度
  • 没有任何依赖
  • 浏览器兼容性:Chrome、Safari、Firefox(尽管有两个 错误)、IE9+;可以工作,但未在 Opera、IE8 或更早版本中测试
  • 支持任何字体系列和大小,以及文本转换
  • 文本区域可以有任意填充或边框
  • 不会被文本区域中的水平或垂直滚动​​条弄糊涂
  • 支持硬回车、制表符(IE 除外)和文本中的连续空格
  • 比文本区域中的列长的行上的正确位置
  • 换行时在行尾的空白处没有“幽灵”位置

这是一个演示 - http://jsfiddle.net/dandv/aFPA7/

在此处输入图像描述

这个怎么运作

镜子<div>是在屏幕外创建的,其样式与<textarea>. 然后,直到插入符号的文本区域的文本被复制到 div 中,并在<span>其后插入 a。然后,将 span 的文本内容设置为 textarea 中文本的其余部分,以便忠实地再现假 div 中的换行。

这是保证处理所有与换行长线有关的边缘情况的唯一方法。GitHub 也使用它来确定其@user 下拉列表的位置

于 2014-03-17T12:50:26.357 回答
1

这个博客似乎也很接近回答这个问题。我自己没有尝试过,但作者说它在 FF3、Chrome、IE、Opera、Safari 上进行了测试。代码在GitHub 上

于 2012-10-24T21:48:31.967 回答
1

在这里修复它:http: //jsfiddle.net/eMwKd/4/

唯一的缺点是已经提供的功能getCaret()解决了键按下的错误位置。因此,除非您松开按键,否则红色光标似乎位于真实光标的后面。

我会再调查一下。

更新:嗯,如果行太长,自动换行不准确..

于 2012-10-27T20:51:43.230 回答
0

这篇博文似乎解决了你的问题,但不幸的是作者承认他只在 IE 6 中测试过。

IE 中的 DOM 不提供有关字符相对位置的信息;但是,它确实为浏览器呈现的控件提供了边界和偏移值。因此,我使用这些值来确定字符的相对边界。然后,使用 JavaScript TextRange,我创建了一种机制来使用这些度量来计算给定 TextArea 内固定宽度字体的行和列位置。

首先,必须根据所使用的固定宽度字体的大小来计算 TextArea 的相对边界。为此,必须将 TextArea 的原始值存储在本地 JavaScript 变量中并清除该值。然后,创建一个 TextRange 以确定 TextArea 的顶部和左侧边界。

于 2008-09-24T17:18:41.960 回答
0

有关于插入符号偏移的一种技巧的描述: Textarea X/Y 插入符号坐标 - jQuery 插件

contenteditable如果您可以使用 html5 功能,那么使用带有属性的 div 元素会更好。

于 2012-10-30T19:14:05.753 回答
0

我不知道解决方案,textarea但它确实适用于divwith contenteditable

您可以使用RangeAPI。像这样:(是的,你真的只需要这 3 行代码)

// get active selection
var selection = window.getSelection();
// get the range (you might want to check selection.rangeCount
// to see if it's popuplated)
var range = selection.getRangeAt(0);

// will give you top, left, width, height
console.log(range.getBoundingClientRect());

我不确定浏览器的兼容性,但我发现它适用于最新的 Chrome、Firefox 甚至 IE7(我想我测试了 7,否则是 9)。

你甚至可以做这样的“疯狂”事情:如果你正在打字"#hash"并且光标在最后一个h,你可以在当前范围内查找#字符,将范围向后移动一个n字符并获取该范围的边界矩形,这将使popup-div 似乎“坚持”这个词。

一个小缺点是contenteditable有时可能有点错误。光标喜欢去不可能的地方,你现在必须处理 HTML 输入。但我确信浏览器供应商会解决这些问题,因为更多的网站开始使用它们。

我可以给出的另一个提示是:查看rangy库。它试图成为一个功能齐全的交叉兼容范围库。你不需要它,但如果你正在处理旧浏览器,它可能值得你花时间。

于 2012-10-26T00:06:13.177 回答
0

也许这会让你满意,它会告诉你选择的位置和光标的位置,所以尝试检查计时器以获得自动位置或通过单击获取选择按钮取消选中以获得位置

   <form>
 <p>
 <input type="button" onclick="evalOnce();" value="Get Selection">
timer:
<input id="eval_switch" type="checkbox" onclick="evalSwitchClicked(this)">
<input id="eval_time" type="text" value="200" size="6">
ms
</p>
<textarea id="code" cols="50" rows="20">01234567890123456789012345678901234567890123456789 01234567890123456789012345678901234567890123456789 01234567890123456789012345678901234567890123456789 01234567890123456789012345678901234567890123456789 01234567890123456789012345678901234567890123456789 Sample text area. Please select above text. </textarea>
<textarea id="out" cols="50" rows="20"></textarea>
</form>
<div id="test"></div>
<script>

function Selection(textareaElement) {
this.element = textareaElement;
}
Selection.prototype.create = function() {
if (document.selection != null && this.element.selectionStart == null) {
return this._ieGetSelection();
} else {
return this._mozillaGetSelection();
}
}
Selection.prototype._mozillaGetSelection = function() {
return {
start: this.element.selectionStart,
end: this.element.selectionEnd
 };
 }
Selection.prototype._ieGetSelection = function() {
this.element.focus();
var range = document.selection.createRange();
var bookmark = range.getBookmark();
var contents = this.element.value;
var originalContents = contents;
var marker = this._createSelectionMarker();
while(contents.indexOf(marker) != -1) {
marker = this._createSelectionMarker();
 }
var parent = range.parentElement();
if (parent == null || parent.type != "textarea") {
return { start: 0, end: 0 };
}
range.text = marker + range.text + marker;
contents = this.element.value;
var result = {};
result.start = contents.indexOf(marker);
contents = contents.replace(marker, "");
result.end = contents.indexOf(marker);
this.element.value = originalContents;
range.moveToBookmark(bookmark);
range.select();
return result;
}
Selection.prototype._createSelectionMarker = function() {
return "##SELECTION_MARKER_" + Math.random() + "##";
}

var timer;
var buffer = "";
function evalSwitchClicked(e) {
if (e.checked) {
evalStart();
} else {
evalStop();
}
}
function evalStart() {
var o = document.getElementById("eval_time");
timer = setTimeout(timerHandler, o.value);
}
function evalStop() {
clearTimeout(timer);
}
function timerHandler() {
clearTimeout(timer);
var sw = document.getElementById("eval_switch");
if (sw.checked) {
evalOnce();
evalStart();
}
}
function evalOnce() {
try {
var selection = new Selection(document.getElementById("code"));
var s = selection.create();
var result = s.start + ":" + s.end;
buffer += result;
flush();
 } catch (ex) {
buffer = ex;
flush();
}
}
function getCode() {
// var s.create()
// return document.getElementById("code").value;
}
function clear() {
var out = document.getElementById("out");
out.value = "";
}
function print(str) {
buffer += str + "\n";
}
function flush() {
var out = document.getElementById("out");
out.value = buffer;
buffer = "";
 } 
</script>

看这里的演示:jsbin.com

于 2012-10-28T18:13:54.287 回答
-1

将 span 元素附加到克隆 div 并根据此 span 的偏移量设置假光标怎么样?我在这里更新了你的小提琴。这里也只是 JS 位

// http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea
var map = [];
var pan = '<span>|</span>'

//found @ http://davidwalsh.name/detect-scrollbar-width

function getScrollbarWidth() {
    var scrollDiv = document.createElement("div");
    scrollDiv.className = "scrollbar-measure";
    document.body.appendChild(scrollDiv);

    // Get the scrollbar width
    var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;

    // Delete the DIV 
    document.body.removeChild(scrollDiv);

    return scrollbarWidth;
}

function getCaret(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() {
    var span = $('#pos span');
    var textarea = $('textarea');

    var note = $('#note');

    css = getComputedStyle(document.getElementById('textarea'));
    try {
        for (i in css) note.css(css[i]) && (css[i] != 'width' && css[i] != 'height') && note.css(css[i], css.getPropertyValue(css[i]));
    } catch (e) {}

    note.css('max-width', '300px');
    document.getElementById('note').style.visibility = 'hidden';
    var height = note.height();
    var fakeCursor, hidePrompt;

    textarea.on('keyup click', function(e) {
        if (document.getElementById('textarea').scrollHeight > 100) {
            note.css('max-width', 300 - getScrollbarWidth());
        }

        var pos = getCaret(textarea[0]);

        note.text(textarea.val().substring(0, pos));
        $(pan).appendTo(note);
        span.text(pos);

        if (hidePrompt) {
            hidePrompt.remove();
        }
        if (fakeCursor) {
            fakeCursor.remove();
        }

        fakeCursor = $("<div style='width:5px;height:30px;background-color: #777;position: absolute;z-index:10000'>&nbsp;</div>");

        fakeCursor.css('opacity', 0.5);
        fakeCursor.css('left', $('#note span').offset().left + 'px');
        fakeCursor.css('top', textarea.offset().top + note.height() - (30 + textarea.scrollTop()) + 'px');

        hidePrompt = fakeCursor.clone();
        hidePrompt.css({
            'width': '2px',
            'background-color': 'white',
            'z-index': '1000',
            'opacity': '1'
        });

        hidePrompt.appendTo(textarea.parent());
        fakeCursor.appendTo(textarea.parent());



        return true;
    });
});

更新 :如果第一行不包含硬换行符,我可以看到有一个错误,但如果它确实运行良好。

于 2012-10-31T05:11:08.630 回答