在Textarea中的光标位置显示DIV
对于我的一个项目,我很乐意为特定的textarea提供自动完成功能。类似于智能感知/全残式的工作原理。但是为此,我必须找出绝对光标位置,以便知道DIV应该出现的位置。
事实证明:(几乎希望如此)这是不可能实现的。有谁有一些巧妙的想法如何解决这个问题?
解决方案
该博客文章似乎解决了问题,但不幸的是,作者承认他仅在IE 6中对其进行了测试。
The DOM in IE does not provide information regarding relative position in terms of characters; however, it does provide bounding and offset values for browser-rendered controls. Thus, I used these values to determine the relative bounds of a character. Then, using the JavaScript TextRange, I created a mechanism for working with such measures to calculate the Line and Column position for fixed-width fonts within a given TextArea. First, the relative bounds for the TextArea must be calculated based upon the size of the fixed-width font used. To do this, the original value of the TextArea must be stored in a local JavaScript variable and clear the value. Then, a TextRange is created to determine the Top and Left bounds of the TextArea.
我的Hacky实验版本2
此新版本适用于可以根据需要进行调整的任何字体以及任何textarea大小。
注意到有些人仍在尝试使此方法起作用后,我决定尝试一种新方法。这次,至少在Linux上的Google chrome上,我的结果要好得多。我不再有Windows PC可用,因此我只能在Ubuntu上的chrome / firefox上进行测试。我的结果在Chrome上可以100%一致地工作,并且可以说在Firefox上大约70 80%,但是我不认为很难找到不一致之处。
此新版本依赖于Canvas对象。在我的示例中,我实际上展示了一个非常画布,以便我们可以在实际中看到它,但是使用隐藏的画布对象可以很容易地做到这一点。
当然,这肯定是一种hack,对于那些我认为相当混乱的代码,我会提前道歉。至少在Google chrome中,无论我设置为哪种字体或者textarea的大小,它都能始终如一地工作。我使用了Sam Saffron的示例来显示光标坐标(灰色背景div)。我还添加了一个"随机化"链接,因此我们可以看到它以不同的字体/ texarea大小和样式工作,并实时观察光标位置的更新。我建议我们看一下整个页面的演示,以便更好地查看随行画布。
我将总结一下它是如何工作的...
潜在的想法是我们试图尽可能在画布上重新绘制文本区域。由于浏览器对texarea和texarea使用相同的字体引擎,因此我们可以使用canvas的字体测量功能来确定问题所在。从那里,我们可以使用可用的canvas方法来计算坐标。
首先,我们调整画布以匹配文本区域的尺寸。这完全是出于视觉目的,因为画布大小并不会真正影响我们的结果。由于Canvas实际上并没有提供自动换行的方法,因此我不得不想到一种(窃取/借用/合并在一起)折断行的方式,以尽可能地匹配文本区域。这是我们可能会发现需要进行最多跨浏览器调整的地方。
换行后,其他所有内容都是基础数学。我们将这些行拆分成一个数组以模拟自动换行,现在我们想遍历这些行并一直向下直到当前选择的结束点。为了做到这一点,我们只是在计算字符数,一旦超过" selection.end",我们就知道我们已经下降了很多。将直到此点的线数乘以线高,然后得到一个y坐标。
x坐标非常相似,除了我们使用的是context.measureText之外。只要我们打印出正确数量的字符,这将为我们提供绘制到Canvas的行的宽度,该宽度恰好在最后一个字符(即当前选择之前的字符)之后结束。结束位置。
在尝试针对其他浏览器进行调试时,要寻找的是行没有正确断开的地方。我们会在某些地方看到画布上一行的最后一个单词可能已包裹在文本区域上,反之亦然。这与浏览器处理自动换行有关。只要我们在画布上得到包裹以匹配文本区域,光标就应该正确。
我将在下面粘贴源代码。我们应该能够复制并粘贴它,但是如果这样做,我要求我们下载自己的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方法很有趣,但不幸的是它不是跨浏览器的。我认为下一个最好的方法是将这些字符用作网格。
不幸的是,没有任何浏览器内置字体度量信息,这意味着等宽字体是唯一具有一致度量的字体类型。而且,没有可靠的方法可以从font-height算出font-width。最初,我尝试使用一定百分比的高度,效果很好。然后,我更改了字体大小,一切都陷入了困境。
我尝试了一种找出字符宽度的方法,该方法是创建一个临时文本区域并不断添加字符,直到scrollHeight(或者scrollWidth)改变为止。这似乎是合理的,但是在那条路的大约一半处,我意识到我可以使用textarea上的cols属性,并发现此折磨中有足够多的技巧可以添加另一个。这意味着我们无法通过CSS设置文本区域的宽度。我们必须使用cols才能正常工作。
我遇到的下一个问题是,即使通过css设置了字体,浏览器也会以不同的方式报告字体。当不设置字体时,mozilla默认使用monospace
,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.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=zh-CN
这是翻译版本的代码示例中的一些标记问题,因此我们可以阅读原始俄语文章中的代码。
这个想法很简单。没有简单,通用和跨浏览器的方法来获取光标位置(以像素为单位)。坦白地说,确实存在,但仅适用于Internet Explorer。
在其他浏览器中,如果确实需要计算,则必须...
- 创建一个不可见的DIV
- 将文本框的所有样式和内容复制到该DIV中
- 然后将HTML元素插入插入符号在文本框中的文本的完全相同的位置
- 获取该HTML元素的坐标