获取插入符号在textarea中的偏移位置(以像素为单位)

在我的项目中,我试图以像素为单位在textarea中获得插入符号的偏移位置。 可以这样做吗?

在问这里之前,我已经经历过许多链接 ,尤其是Tim Down,但我找不到适用于IE8 +,Chrome和Firefox的解决方案。 Tim Down正在努力解决这个问题 。

我发现的其他一些链接有很多问题,比如找不到插入位置的顶部偏移量。

我试图得到插入符号的偏移位置,因为我想在textarea中显示一个自动完成的建议框,方法是根据插入符的偏移位置进行定位。

PS:我不能使用contenteditable div因为我写了很多与textarea相关的代码。

这是一种使用rangyinputs , rangy和jQuery的方法 。

它基本上将整个文本从textarea内部复制到相同大小的div中。 我已经设置了一些CSS来确保在每个浏览器中, textareadiv以完全相同的方式包装它们的内容。

单击textarea ,我会读出插入符所在的字符索引,然后在div中的同一索引处插入一个插入符号。 通过这样做,我最终遇到问题,如果用户点击一行的开头,则插入符号跳回到上一行。 为了解决这个问题,我检查前一个字符是否是一个space (这将允许换行),如果是true ,我将它包装在一个span ,然后我将下一个单词(直接位于插入符号位置之后)包装起来一个span 。 现在我比较这两个span之间的最高值,如果它们不同,有一些包装正在进行,所以我假设#nextword spantopleft值等同于插入位置。

这种方法仍然可以改进,我敢肯定我没有想到可能出错的一切,即使我有,那么我也没有为所有这些实现修复而烦恼,因为我没有现在是时候这么做了,你需要注意的一些事情:

  • 它还没有处理用Enter插入的硬回车 (固定)
  • 在连续输入多个空格时定位中断 (固定)
  • 我认为连字符也允许内容包装发生..

目前,它在Windows 8上使用最新版本的Chrome,Firefox,IE和Safari的浏览器的工作方式完全相同。 我的测试不是很严格。

这是一个jsFiddle

我希望它会对你有所帮助,至少它可能会给你一些建议。

一些特点:

  • 我已经包含了一个位于正确位置的ul ,并修复了一个Firefox问题,其中textarea选择在DOM操作之后没有重新设置回原始位置。

  • 我已经添加了IE7-IE9支持并修复了评论中指出的多字选择问题。

  • 我添加了对使用Enter插入的硬回车和连续多个空格的支持。

  • 我修复了ctrl + shift + 左箭头文本选择方法的默认行为问题。

JavaScript的

 function getTextAreaXandY() { // Don't do anything if key pressed is left arrow if (e.which == 37) return; // Save selection start var selection = $(this).getSelection(); var index = selection.start; // Copy text to div $(this).blur(); $("div").text($(this).val()); // Get current character $(this).setSelection(index, index + 1); currentcharacter = $(this).getSelection().text; // Get previous character $(this).setSelection(index - 1, index) previouscharacter = $(this).getSelection().text; var start, endchar; var end = 0; var range = rangy.createRange(); // If current or previous character is a space or a line break, find the next word and wrap it in a span var linebreak = previouscharacter.match(/(\r\n|\n|\r)/gm) == undefined ? false : true; if (previouscharacter == ' ' || currentcharacter == ' ' || linebreak) { i = index + 1; // Start at the end of the current space while (endchar != ' ' && end < $(this).val().length) { i++; $(this).setSelection(i, i + 1) var sel = $(this).getSelection(); endchar = sel.text; end = sel.start; } range.setStart($("div")[0].childNodes[0], index); range.setEnd($("div")[0].childNodes[0], end); var nextword = range.toHtml(); range.deleteContents(); var position = $("" + nextword + "")[0]; range.insertNode(position); var nextwordtop = $("#nextword").position().top; } // Insert `#caret` at the position of the caret range.setStart($("div")[0].childNodes[0], index); var caret = $("")[0]; range.insertNode(caret); var carettop = $("#caret").position().top; // If preceding character is a space, wrap it in a span if (previouscharacter == ' ') { range.setStart($("div")[0].childNodes[0], index - 1); range.setEnd($("div")[0].childNodes[0], index); var prevchar = $("")[0]; range.insertNode(prevchar); var prevchartop = $("#prevchar").position().top; } // Set textarea selection back to selection start $(this).focus(); $(this).setSelection(index, selection.end); // If the top value of the previous character span is not equal to the top value of the next word, // there must have been some wrapping going on, the previous character was a space, so the wrapping // would have occured after this space, its safe to assume that the left and top value of `#nextword` // indicate the caret position if (prevchartop != undefined && prevchartop != nextwordtop) { $("label").text('X: ' + $("#nextword").position().left + 'px, Y: ' + $("#nextword").position().top); $('ul').css('left', ($("#nextword").position().left) + 'px'); $('ul').css('top', ($("#nextword").position().top + 13) + 'px'); } // if not, then there was no wrapping, we can take the left and the top value from `#caret` else { $("label").text('X: ' + $("#caret").position().left + 'px, Y: ' + $("#caret").position().top); $('ul').css('left', ($("#caret").position().left) + 'px'); $('ul').css('top', ($("#caret").position().top + 14) + 'px'); } $('ul').css('display', 'block'); } $("textarea").click(getTextAreaXandY); $("textarea").keyup(getTextAreaXandY); 

HTML

 
  • Why don't you type this..

CSS

 body { font-family: Verdana; font-size: 12px; line-height: 14px; } textarea, div { font-family: Verdana; font-size: 12px; line-height: 14px; width: 300px; display: block; overflow: hidden; border: 1px solid black; padding: 0; margin: 0; resize: none; min-height: 300px; position: absolute; -moz-box-sizing: border-box; white-space: pre-wrap; } span { display: inline-block; height: 14px; position: relative; } span#caret { display: inline; } label { display: block; margin-left: 320px; } ul { padding: 0px; margin: 9px; position: absolute; z-index: 999; border: 1px solid #000; background-color: #FFF; list-style-type:none; display: none; } @media screen and (-webkit-min-device-pixel-ratio:0) { span { white-space: pre-wrap; } } div { /* Firefox wrapping fix */ -moz-padding-end: 1.5px; -moz-padding-start: 1.5px; /* IE8/IE9 wrapping fix */ padding-right: 5px\0/; width: 295px\0/; } span#caret { display: inline-block\0/; } 

您可以创建一个单独的(不可见)元素,并从开始到光标位置填充textarea内容。 Textarea和“clone”应该具有匹配的CSS(字体属性,填充/边距/边框和宽度)。 然后将这些元素堆叠在一起。

让我从一个工作示例开始,然后遍历代码: http : //jsfiddle.net/g7rBk/

更新小提琴 ( 使用IE8修复

HTML:

  

Textarea是不言自明的。 输出是一个隐藏元素,我们将传递文本内容并进行测量。 重要的是我们将使用内联元素。 “xy”div只是测试目的的指标。

CSS:

 /* identical styling to match the dimensions and position of textarea and its "clone" */ #input, #output { position:absolute; top:0; left:0; font:14px/1 monospace; padding:5px; border:1px solid #999; white-space:pre; margin:0; background:transparent; width:300px; max-width:300px; } /* make sure the textarea isn't obscured by clone */ #input { z-index:2; min-height:200px; } #output { border-color:transparent; } /* hide the span visually using opacity (not display:none), so it's still measurable; make it break long words inside like textarea does. */ #output span { opacity:0; word-wrap: break-word; overflow-wrap: break-word; } /* the cursor position indicator */ #xy { position:absolute; width:4px; height:4px; background:#f00; } 

JavaScript的:

 /* get references to DOM nodes we'll use */ var input = document.getElementById('input'), output = document.getElementById('output').firstChild, position = document.getElementById('position'), /* And finally, here it goes: */ update = function(){ /* Fill the clone with textarea content from start to the position of the caret. You may need to expand here to support older IE [1]. The replace /\n$/ is necessary to get position when cursor is at the beginning of empty new line. */ output.innerHTML = input.value.substr( 0, input.selectionStart ).replace(/\n$/,"\n\001"); /* the fun part! We use an inline element, so getClientRects[2] will return a collection of rectangles wrapping each line of text. We only need the position of the last rectangle. */ var rects = output.getClientRects(), lastRect = rects[ rects.length - 1 ], top = lastRect.top - input.scrollTop, left = lastRect.left+lastRect.width; /* position the little div and see if it matches caret position :) */ xy.style.cssText = "top: "+top+"px;left: "+left+"px"; } 

[1] 在textarea中的插入位置,从一开始就是字符

[2] https://developer.mozilla.org/en/docs/DOM/element.getClientRects

编辑:此示例仅适用于固定宽度的textarea。 要使其与用户可resize的textarea一起使用,您需要向resize事件添加事件侦听器,并设置#output维度以匹配新的#input维度。

与其他答案中提供的相比,有一种更简单的解决方案可以获得像素位置。

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

副本的答案

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

特征

  • 像素精度
  • 没有任何依赖
  • 浏览器兼容性:Chrome,Safari,Firefox(尽管有两个 漏洞 ),IE9 +; 可以工作,但未在Opera,IE8或更早版本中测试过
  • 支持任何字体系列和大小,以及文本转换
  • 文本区域可以具有任意填充或边框
  • 不要被textarea中的水平或垂直滚动​​条混淆
  • 支持硬回车,标签(IE上除外)和文本中的连续空格
  • 在比文本区域中的列长的行上的正确位置
  • 包裹长词时,在行尾的空白处没有“鬼”位置

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

在此处输入图像描述

这个怎么运作

镜像

在屏幕外创建,其样式与完全相同。 然后,直到插入符号的textarea文本被复制到div中,并在其后插入 。 然后,跨度的文本内容被设置为textarea中文本的剩余部分,以便忠实地再现人造div中的包装。

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

工作实例的JsFiddle: http : //jsfiddle.net/42zHC/2/

基本上,我们计算出宽度中有多少列(因为它是等宽的)。 我们必须强制滚动条始终在那里,否则计算结束。 然后我们划分适合宽度的列数,并得到每个字符的x偏移量。 然后我们在textarea上设置行高。 由于我们知道连续多少个字符,我们可以将其除以字符数,然后得到行号。 通过行高,我们现在有y偏移量。 然后我们得到textarea的scrollTop并减去它,这样一旦它开始使用滚动条,它仍然显示在正确的位置。

使用Javascript:

 $(document).ready(function () { var cols = document.getElementById('t').cols; var width = document.getElementById('t').clientWidth; var height = $('textarea').css('line-height'); var pos = $('textarea').position(); $('#t').on('keyup', function () { el = document.getElementById("t"); if (el.selectionStart) { selection = el.selectionStart; } else if (document.selection) { el.focus(); var r = document.selection.createRange(); if (r == null) { selection = 0; } var re = el.createTextRange(), rc = re.duplicate(); re.moveToBookmark(r.getBookmark()); rc.setEndPoint('EndToStart', re); selection = rc.text.length; } else { selection = 0 } var row = Math.floor((selection-1) / cols); var col = (selection - (row * cols)); var x = Math.floor((col*(width/cols))); var y = (parseInt(height)*row); $('span').html("row: " + row + "
columns" + col + "
width: " + width + "
x: " + x +"px
y: " + y +"px
Scrolltop: "+$(this).scrollTop()).css('top',pos.top+y-$(this).scrollTop()).css('left',pos.left+x+10); }); });

HTML:

  

CSS:

 textarea { height: 80px; line-height: 12px; overflow-y:scroll; } span { position: absolute; } 

我无法得到类似于工作的东西,所以我的解决方案是在textarea中找到插入符号的字符位置,剪切当前段落并在textarea旁边显示它。

使用偏移量,我在可编辑文本的此视图中放置了一个假光标( divdisplay:inline ,1px宽, border-left: 1px solid black )。

这样,您可以创建一个可视反馈区域,您可以在其中显示效果的结果(就像写入答案时stackoverflow一样)。