在文本区域中显示DIV在光标位置

61

我希望为我的一个项目提供特定textarea的自动完成功能,类似于intellisense/omnicomplete的工作原理。为此,我需要确定绝对光标位置,以便知道DIV应该出现在哪里。

事实证明:这几乎是不可能实现的。是否有人有一些巧妙的想法来解决这个问题?


1
考虑使用 contenteditable div 替代 textarea 是否更好呢?只是随便提一下。 - Cade Roux
3
赏金提示:您需要修复的 jsfiddle 链接是:http://jsfiddle.net/eMwKd/1/。 - Sam Saffron
越来越接近 Chrome:http://jsbin.com/egadoj/1/edit - Sam Saffron
@xyu,是的,但我担心它们在这里被认为有点沉重。 - Sam Saffron
1
@xyu 基于 DOM 的编辑器(CodeMirror / Ace)可以使用 DOM 插入技术来确定位置,这非常简单明了。 - Sam Saffron
显示剩余3条评论
11个回答

35
我的Hacky实验的第二个版本
这个新版本可以适用于任何字体,可以根据需要进行调整,并且可以适应任何文本区域大小。
在注意到有些人仍然在尝试使其工作之后,我决定尝试一种新的方法。这次我的结果要好得多 - 至少在Linux上的Google Chrome上是如此。我现在已经没有Windows PC可用了,所以我只能在Ubuntu上测试Chrome/Firefox。我的结果在Chrome上100%地一致,而在Firefox上大约70-80%左右,但我不认为找到不一致会非常困难。
这个新版本依赖于一个Canvas对象。在我的示例中,我实际上展示了那个Canvas - 只是让你看到它在行动中,但它可以很容易地通过隐藏的Canvas对象来完成。
这肯定是一个黑客技巧,我提前道歉我的代码有些混乱。至少在谷歌浏览器中,无论我设置什么字体或文本框大小,它都能一致地工作。我使用了Sam Saffron的示例来显示光标坐标(一个灰色背景的div)。我还添加了一个“随机化”链接,这样你就可以看到它在不同的字体/文本框大小和样式下工作,并实时观察光标位置更新。我建议查看完整页面演示,这样你就可以更好地看到伴侣画布的表现。
我会简要概述它的工作原理...
基本思想是我们试图在画布上重新绘制尽可能接近的文本框。由于浏览器为文本框和文本区域使用相同的字体引擎,因此我们可以使用画布的字体测量功能来确定事物的位置。从那里开始,我们可以利用可用的画布方法来确定我们的坐标。
首先,我们调整画布的尺寸以匹配文本区域的尺寸。这完全是为了视觉效果,因为画布大小对结果并没有真正的影响。由于 Canvas 实际上并没有提供自动换行的方式,我不得不想出(窃取/借用/拼凑)一种方法来分割行,以尽可能地与文本区域匹配。这就是你可能需要进行最多跨浏览器调整的地方。
在换行之后,其他所有操作都是基本的数学计算。我们将行分割成一个数组来模仿自动换行,然后我们要循环遍历这些行,一直到当前选择结束的位置。为了做到这一点,我们只需计算字符数,一旦超过 selection.end,我们就知道已经下降足够远了。将该点前的行数乘以行高,就得到了 y 坐标。

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

在尝试为其他浏览器调试此问题时,要查找的是断行不正确的地方。您会看到,在一些地方,Canvas中一行中的最后一个单词可能已在文本区域上换行或反之亦然。这与浏览器处理换行有关。只要在Canvas中实现包装与文本区域匹配,您的光标就应该正确。

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

我还提供了新演示fiddle

祝你好运!

<!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://dev59.com/bXA85IYBdhLWcg3wHvs0#11124580
            // https://dev59.com/bXA85IYBdhLWcg3wHvs0#3960916

            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>

错误
我记得有一个错误。如果你把光标放在一行的第一个字母之前,它会显示“位置”为上一行的最后一个字母。这与选择.end的工作方式有关。我认为找到这种情况并相应地修复它不应该太困难。

版本1

我把这个放在这里,这样你就不必深入编辑历史记录来查看进展情况了。

它并不完美,肯定是一种黑客方式,但我在WinXP IE、FF、Safari、Chrome和Opera上让它工作得相当好。

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

很遗憾,任何浏览器都没有内置字体度量信息,这意味着等宽字体是唯一具有一致测量的字体类型。此外,没有可靠的方法从字体高度中计算出字体宽度。起初我尝试使用高度的百分比,效果很好。然后我改变了字体大小,一切都变得混乱了。
我尝试了一种方法来计算字符宽度,那就是创建一个临时文本区域,并不断添加字符,直到scrollHeight(或scrollWidth)发生变化。这似乎是可行的,但在进行了一半之后,我意识到我可以只使用文本区域的cols属性,因为在这个过程中已经有足够的hack了。这意味着您不能通过css设置文本区域的宽度。您必须使用cols属性才能使其工作。
我遇到的下一个问题是,即使您通过css设置字体,浏览器也会以不同的方式报告字体。当您不设置字体时,Mozilla默认使用monospace,IE使用Courier New,Opera使用"Courier New"(带引号),Safari使用'Lucida Grand'(带单引号)。当您将字体设置为monospace时,Mozilla和IE会采用您提供的内容,Safari会显示为-webkit-monospace,而Opera仍然使用"Courier New"
因此,现在我们初始化一些变量。确保在css中设置您的行高。Firefox报告了正确的行高,但IE报告为“normal”,我没有关注其他浏览器。我只是在我的css中设置了行高,并解决了差异。我还没有使用ems而不是像素进行测试。字符高度就是字体大小。最好在css中预先设置它。
此外,在我们开始放置字符之前,还有一个预设 - 这让我非常困惑。对于IE和Mozilla,textarea的列数是
现在,我们要为每一行创建一个第一个字符位置的数组。我们循环遍历textarea中的每个字符。如果它是换行符,我们就向我们的行数组添加一个新位置。如果是空格,我们尝试确定当前的“单词”是否适合我们所在的行,或者它是否将被推到下一行。标点符号算作“单词”的一部分。我没有测试过制表符,但有一行代码用于添加4个字符的制表符。
一旦我们有了一组行位置的数组,我们就循环遍历并尝试找出光标所在的行。我们使用选择的“End”作为我们的光标。
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>

1
不错,但我发现在Chrome(2.0.172.37)中有些问题。要重现,请先将光标设置在文本区域的顶部,然后按两次pagedown,使文本区域滚动并将光标移动到底部。之后,“Here I Am!”标签总是定位得太低(大约两行文本)。Firefox似乎也遭受同样的综合症,但它很快就会修复位置(div仅在错误位置显示一次)。这里可能有什么诀窍? - Tuukka Mustonen
我不想挑剔,因为这太棒了;但是,如果您将文本区域变宽,那么就会破坏它。我建议还要添加一个“on resize”事件。然而,即使您继续点击,它仍然无法正常工作。也许宽度/高度是基于元素的原始宽度/高度计算的?(在OSX上使用Chrome) - Parris
我刚刚查看了版本2的fiddle,但是无论我点击或编辑文本,工具提示div似乎都卡在0|0(左上角)。可能是因为所有光标相关的东西都已从enobrev.com中删除了... - Cobra_Fast
我会尝试在今天晚些时候恢复示例。 - enobrev
1
哇!这个答案付出了惊人的努力,但是有一种更简单的解决方案-将textarea镜像到具有相同样式的div中,并在selectionStart处创建一个span。这段代码更容易理解,并且在Chrome、Firefox和IE9中完美运行。 - Dan Dascalescu
显示剩余8条评论

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>

Bevis的脚本存在缺陷并且不再得到维护(https://github.com/beviz/jquery-caret-position-getter/issues/5)。我知道这是因为我评估了GitHub上的所有8个textarea坐标获取器插件。迄今为止最好的插件是[component.io的textarea-caret-position](https://dev59.com/GXVD5IYBdhLWcg3wR5ko#22446703)。它更简单、跨浏览器,并且不需要jQuery。 - Dan Dascalescu

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元素的坐标

这是一般的算法,但涉及到浏览器兼容性时有各种微妙之处。Component.io团队已经开发了一个简单的跨浏览器插件,可以解决所有边缘情况,而且不需要jQuery - Dan Dascalescu

4
请注意,这个问题是一个月之前提出的一个重复问题,并且我在 这里 回答了它。 我只会在那个链接中维护答案,因为这个问题应该在多年前就被关闭了。

答案副本

我寻找一个适用于meteor-autocomplete的 textarea 光标坐标插件,所以我评估了 GitHub 上所有的 8 个插件。胜出者是来自 Component 的textarea-caret-position

功能

  • 像素精度
  • 毫无依赖关系
  • 浏览器兼容性:Chrome、Safari、Firefox(尽管它有两个错误:一个另一个),IE9+;可能可以在 Opera、IE8 或更早版本中使用,但未经测试
  • 支持任何字体家族和大小,以及文本转换
  • 文本区域可以具有任意填充或边框
  • 不会因文本区域中的水平或垂直滚动条而困惑
  • 支持硬回车、制表符(在 IE 上除外)和文本中连续的空格
  • 在文本区域的行长超过列数时,位置正确
  • 当包装长单词时,没有在空白处留下“幽灵”位置

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

enter image description here

它是如何工作的

创建一个镜像 <div> 并将其设置成与 <textarea> 完全相同的样式。然后,将 textarea 中的文本复制到 div 中的光标位置,并在此之后插入一个 <span>。然后,将 span 的文本内容设置为 textarea 中剩余的文本,以便忠实地再现虚假 div 中的换行。

这是唯一保证处理所有有关长行包装的边缘情况的方法。GitHub 也使用它来确定其@用户下拉菜单的位置。


复制粘贴以下行:"iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii" 并享受结果。在Firefox上完全失败,Chrome也有问题。 - daliusd

1

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


这正是您所需要的,它使用 prototype.js 框架并完成了工作。该代码是针对 click 事件编写的,因此您可以将 onkeyup 事件添加到其中,就可以使用了。我还在 IE、Chrome、Firefox、Safari 和 Opera 上进行了测试,绝对可以正常工作。希望您会发现它有用。 - Mahyar
在GitHub上的项目是一个不错的尝试,但并不十分牢固。@enobrev有一个更紧密的实现。特别是GitHub上的代码没有注入word-wrap break-word,也没有正确地进行空格插入替换(多个空格会导致它出错)。 - Sam Saffron
我最近让那个插件的作者弃用它,转而使用textarea-caret-position。@SamSaffron:我在enobrev的GitHub上没有看到任何类似的存储库? - Dan Dascalescu

1

已经修复了:http://jsfiddle.net/eMwKd/4/

唯一的缺点是,已提供的函数getCaret()在按键按下时会解析到错误的位置。因此,除非您释放按键,否则红色光标似乎在真实光标后面。

我会再看一下。

更新:嗯,如果行太长,换行不准确。


这是与解决问题只有一步之遥的最接近的代码:http://jsbin.com/egadoj/1/edit - Sam Saffron
越来越接近了 http://jsbin.com/egadoj/4/ - Sam Saffron
这很不错,你只需要检查一下是否到达了字符串的末尾(在这种情况下,前瞻会失败)。 - lrsjng
顺便说一下,我也检查了 getCaret() 函数,似乎不可能在 keydown 上使它工作。无法修复的情况是当您使用光标浏览上下文。 - lrsjng
确实不是一个容易的问题,但 Component.io 团队已经开发了一个 textarea-caret-position 插件,它非常完美(无依赖、跨浏览器、只有 80 行代码、处理滚动条、换行、任何字体组合等)。 - Dan Dascalescu

0

我不知道如何解决 textarea 的问题,但是对于带有 contenteditablediv 它确实有效。

你可以使用 Range API。像这样:(是的,你真的只需要这三行代码)

// 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个字符,并获取该范围的边界矩形,这将使弹出式div似乎“粘”在单词上。

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

我可以给你另一个提示:看看rangy库。它试图成为一个功能齐全的跨浏览器兼容的范围库。你不是必须要用它,但如果你正在处理旧浏览器,它可能值得一试。


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


1
那只是光标位置,跨浏览器解决起来非常简单,并且有很好的文档记录。 - Sam Saffron

0

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

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

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


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接