我希望为我的一个项目提供特定textarea的自动完成功能,类似于intellisense/omnicomplete的工作原理。为此,我需要确定绝对光标位置,以便知道DIV应该出现在哪里。
事实证明:这几乎是不可能实现的。是否有人有一些巧妙的想法来解决这个问题?
我希望为我的一个项目提供特定textarea的自动完成功能,类似于intellisense/omnicomplete的工作原理。为此,我需要确定绝对光标位置,以便知道DIV应该出现在哪里。
事实证明:这几乎是不可能实现的。是否有人有一些巧妙的想法来解决这个问题?
selection.end
,我们就知道已经下降足够远了。将该点前的行数乘以行高,就得到了 y
坐标。
x
坐标非常相似,只是我们使用了context.measureText
。只要我们打印出正确数量的字符,这将为我们提供正在绘制到Canvas的线条的宽度,该线条恰好在写出的最后一个字符之后结束,该字符是当前selection.end
位置之前的字符。
在尝试为其他浏览器调试此问题时,要查找的是断行不正确的地方。您会看到,在一些地方,Canvas中一行中的最后一个单词可能已在文本区域上换行或反之亦然。这与浏览器处理换行有关。只要在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://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>
版本1
我把这个放在这里,这样你就不必深入编辑历史记录来查看进展情况了。
它并不完美,肯定是一种黑客方式,但我在WinXP IE、FF、Safari、Chrome和Opera上让它工作得相当好。
据我所知,在任何浏览器上都没有直接查找光标x/y的方法。 IE方法,提到的 Adam Bellaire很有趣,但不幸的是不跨浏览器。 我认为下一个最好的事情是使用字符作为网格。
很遗憾,任何浏览器都没有内置字体度量信息,这意味着等宽字体是唯一具有一致测量的字体类型。此外,没有可靠的方法从字体高度中计算出字体宽度。起初我尝试使用高度的百分比,效果很好。然后我改变了字体大小,一切都变得混乱了。monospace
,IE使用Courier New
,Opera使用"Courier New"
(带引号),Safari使用'Lucida Grand'
(带单引号)。当您将字体设置为monospace
时,Mozilla和IE会采用您提供的内容,Safari会显示为-webkit-monospace
,而Opera仍然使用"Courier New"
。<?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>
0|0
(左上角)。可能是因为所有光标相关的东西都已从enobrev.com
中删除了... - Cobra_Fasttextarea
镜像到具有相同样式的div
中,并在selectionStart处创建一个span
。这段代码更容易理解,并且在Chrome、Firefox和IE9中完美运行。 - Dan Dascalescu幸运的是,在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>
我寻找一个适用于meteor-autocomplete的 textarea 光标坐标插件,所以我评估了 GitHub 上所有的 8 个插件。胜出者是来自 Component 的textarea-caret-position。
创建一个镜像 <div>
并将其设置成与 <textarea>
完全相同的样式。然后,将 textarea 中的文本复制到 div 中的光标位置,并在此之后插入一个 <span>
。然后,将 span 的文本内容设置为 textarea 中剩余的文本,以便忠实地再现虚假 div 中的换行。
这是唯一保证处理所有有关长行包装的边缘情况的方法。GitHub 也使用它来确定其@用户下拉菜单的位置。
已经修复了:http://jsfiddle.net/eMwKd/4/
唯一的缺点是,已提供的函数getCaret()
在按键按下时会解析到错误的位置。因此,除非您释放按键,否则红色光标似乎在真实光标后面。
我会再看一下。
更新:嗯,如果行太长,换行不准确。
getCaret()
函数,似乎不可能在 keydown
上使它工作。无法修复的情况是当您使用光标浏览上下文。 - lrsjng我不知道如何解决 textarea
的问题,但是对于带有 contenteditable
的 div
它确实有效。
你可以使用 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
库。它试图成为一个功能齐全的跨浏览器兼容的范围库。你不是必须要用它,但如果你正在处理旧浏览器,它可能值得一试。
<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
这篇博客文章似乎解答了你的问题,但不幸的是作者承认他仅在IE 6中测试过。
IE中的DOM未提供有关字符相对位置的信息;但是,它提供了浏览器呈现控件的边界和偏移值。 因此,我使用这些值来确定字符的相对边界。 然后,使用JavaScript TextRange创建了一种机制,用于计算给定TextArea内固定宽度字体的行列位置。
首先,必须基于使用的固定宽度字体的大小计算TextArea的相对边界。 为此,必须将TextArea的原始值存储在本地JavaScript变量中并清除该值。 然后,创建TextRange以确定TextArea的顶部和左边界。
这里有一个关于插入符偏移的技巧描述: Textarea X/Y caret coordinates - jQuery plugin
如果可以使用HTML5特性,最好使用带有contenteditable
属性的div元素。