在Textarea的光标位置显示DIV

对于我的项目,我很乐意为特定的textarea提供自动完成。 类似于intellisense / omnicomplete如何工作。 为此,我必须找出绝对的光标位置,以便我知道DIV应该出现在哪里。

原来:这是(几乎我希望)不可能实现的。 有没有人有一些简洁的想法如何解决这个问题?

我的哈克实验版本2

这个新版本的任何字体,可以根据需要进行调整,以及任何textarea大小。

在注意到你们中的一些人仍然试图让这个工作,我决定尝试一种新的方法。 我的结果是FAR更好这一次 – 至less在Linux上的谷歌浏览器。 我不再有Windows PC可用,所以我只能在Ubuntu上使用chrome / firefox进行testing。 我的结果在Chrome浏览器上一直保持100%的效果,我们假设在Firefox的70%-80%左右,但是我不认为find这些不一致是非常困难的。

这个新版本依赖于一个Canvas对象。 在我的例子中 ,我实际上展示了非常的canvas – 只是让你可以看到它的动作,但是用隐藏的canvas对象很容易做到。

这当然是一个黑客,我提前道歉,我相当一起代码。 至less,在谷歌浏览器,它始终如一地工作,无论我设置它的字体,或者textarea的大小。 我用Sam Saffron的例子来显示光标坐标(一个灰色背景div)。 我还添加了一个“随机”链接,所以你可以看到它工作在不同的字体/ texarea大小和样式,并在飞行中观看光标位置更新。 我build议查看整个页面演示,以便您可以更好地看到伴随canvas。

我将总结一下它是如何工作的

根本的想法是,我们试图尽可能地在canvas上重新绘制textarea。 由于浏览器对texarea和texarea都使用相同的字体引擎,所以我们可以使用canvas的字体测量function来确定事物的位置。 从那里,我们可以使用可用的canvas方法来找出我们的坐标。

首先,我们调整我们的canvas以匹配textarea的尺寸。 这完全是为了视觉上的目的,因为canvas的大小并没有真正改变我们的结果。 由于Canvas实际上并没有提供自动换行的方法,所以我不得不想出一个方法来分割线条,使其尽可能匹配textarea。 这是你可能会发现你需要做最多的跨浏览器调整的地方。

换句话说,其他的一切都是基本的math。 我们将这些行分成一个数组来模拟单词换行,现在我们要循环这些行并一路走下去,直到我们当前的select结束。 为了做到这一点,我们只是数字,一旦超过selection.end 。我们知道我们已经走得很远了。 将行数乘以行高,直到具有行高并且具有y坐标。

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

当试图为其他浏览器进行debugging时,需要查找的是哪些行不能正确地打破。 您会在某些地方看到,canvas上的最后一个单词可能已经包裹在textarea上,反之亦然。 这与浏览器如何处理文字换行有关。 只要你在canvas上的包装,以配合textarea,你的光标应该是正确的。

我将粘贴下面的来源。 你应该能够复制和粘贴它,但是如果你这样做,我要求你下载你自己的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如何工作有关。 我认为不应该太难find这种情况,并相应地解决这个问题。


版本1

离开这里,所以你可以看到进步,而不必深入了解编辑历史。

这不是完美的,它绝对是一个黑客,但我得到它在WinXP的IE浏览器,FF,Safari浏览器,Chrome和Opera的工作得很好。

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

不幸的是,没有任何浏览器内置的字体指标信息,这意味着等宽字体是唯一的字体types,将有一个一致的测量。 而且,没有可靠的方法从字体高度中计算字体宽度。 起初,我试图使用一个百分比的高度,这很好。 然后我改变了字体大小,一切都变成了地狱。

我试图找出字符宽度,这是创build一个临时textarea,并不断添加字符,直到scrollHeight(或scrollWidth)更改。 看起来似乎是可行的,但是在这条路的中途,我意识到我可以使用textarea上的cols属性,并且认为在这个折磨中有足够的黑客来添加另一个属性。 这意味着你不能通过css设置textarea的宽度。 你必须使用cols才能正常工作。

我遇到的下一个问题是,即使通过CSS设置字体,浏览器报告字体的不同。 当你不设置字体,默认情况下,Mozilla使用monospace字体,IE使用Courier New ,Opera "Courier New" (带引号),Safari, 'Lucida Grand' (带单引号)。 当你将字体设置为monospace ,mozilla,也就是采取你给他们的东西时,Safari会以-webkit-monospace而Opera会以"Courier New"保留下来。

所以现在我们初始化一些variables。 确保在css中设置你的行高。 火狐报告正确的行高,但IE报告“正常”,我没有打扰其他浏览器。 我只是在我的CSS中设置行高,并解决了差异。 我没有使用ems而不是像素进行testing。 字符高度只是字体大小。 也许应该在你的CSS中预先设置好。

另外,在我们开始放置angular色之前还有一个预先设定 – 这真的让我挠了脑袋。 对于ie和mozilla,texarea的字符是<cols,其他的都是<= chars。 所以Chrome可以容纳50个字符,但是mozilla和ie会打破最后一个字。

现在我们将为每一行创build一个第一个字符位置的数组。 我们遍历textarea中的每个字符。 如果是换行符,我们在行数组中增加一个新的位置。 如果它是一个空格,我们试图找出当前的“单词”是否适合我们所在的行,或者是否会被推到下一行。 标点符号是“单词”的一部分。 我还没有testing标签,但有一行添加4个字符的选项卡字符。

一旦我们有了一系列的行位置,我们循环,并尝试find光标所在的行。 我们使用hte“End”作为光标。

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

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

我使用的是jquery 1.2.6 , jquery-fieldselection和jquery-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> 

我在俄罗斯的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元素的坐标

我不会再解释有关这个问题的问题,因为他们在其他职位很好地解释。 只要指出一个可能的解决scheme,它有一些错误,但这是一个起点。

幸运的是,Github上有一个脚本来计算相对于它的容器的插入位置,但是它需要jQuery。 GitHub页面在这里: jquery-caret-position-getter, Thanxs to 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> 

请注意,这个问题是一个月前提出的问题的重复,我已经在这里回答 。 我只会在这个环节保持答案,因为这个问题本应该是几年前一样的。

答案的副本

我已经找了一个textarea插入符号meteor自动完成的插件,所以我已经评估了GitHub上的所有8个插件。 目前为止,胜利者是来自Component的 textarea-caret-position 。

特征

  • 像素精度
  • 没有任何依赖关系
  • 浏览器兼容性:Chrome,Safari,Firefox(尽pipe它有两个 bug ),IE9 +; 可能工作,但没有在Opera,IE8或更旧的testing
  • 支持任何字体系列和大小,以及文本转换
  • 文本区域可以有任意的填充或边框
  • 不会被textarea中的水平或垂直滚动​​条所困惑
  • 支持硬返回,标签(IE除外)和文本中的连续空格
  • 在比文本区域中的列更长的行上的正确位置
  • 在包装长词时,在行尾没有“鬼”的位置

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

在这里输入图像说明

怎么运行的

镜像<div>是在屏幕外创build的,样式完全像<textarea> 。 然后,textarea的文字被复制到div中,并在它后面插入一个<span> 。 然后,跨度的文本内容被设置为textarea中文本的其余部分,以忠实地再现人造div中的包装。

这是保证处理所有关于缠绕长线的边缘情况的唯一方法。 它也被GitHub用来确定@ user下拉的位置。

这个博客似乎也接近回答这个问题。 我没有尝试过我自己,但作者说,其testing与FF3,铬,IE浏览器,歌剧,Safari浏览器。 代码在GitHub上

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

唯一不足的是,已经提供的函数getCaret()解决了关键的错误位置。 因此,除非您释放键,否则红色光标似乎位于真实光标的后面。

我会再看看它。

更新:hm,如果行太长,换行是不准确的。

这个博客文章似乎解决您的问题,但不幸的是作者承认,他只在IE 6中testing它。

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

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

也许这会请你,它会告诉你的select位置和光标的位置,所以尝试检查定时器获得自动位置或取消选中获取位置通过单击获取selectbutton

  <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

我不知道textarea的解决scheme,但它确实适用于div contenteditable

您可以使用Range API。 像这样:(是的,你真的只需要这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上运行(我想我testing过7,否则是9)。

你甚至可以做这样的“疯狂”的事情:如果你input"#hash"而光标在最后一个h ,你可以在当前范围内查找#字符,将范围移回n字符并得到该范围的边界 ,这将使popup-div似乎“粘”在单词上。

一个小缺点是可能会有点bug。 光标喜欢去不可能的地方,你现在不得不处理HTMLinput。 但是我确信浏览器厂商将解决这些问题,更多的网站开始使用它们。

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

有一个hack的插入偏移描述: Textarea X / Y插入符号 – jQuery插件

如果你可以使用html5的特性,最好还是使用带有contenteditable属性的div元素。

那么如何将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; }); }); 

UPDATE : I can see that there's an error if the first line contains no hard line-breaks but if it does it seems to work well.