我的 Hacky 实验的版本 2
这个新版本适用于任何字体(可以根据需要进行调整)和任何文本区域大小。
在注意到你们中的一些人仍在尝试使其发挥作用后,我决定尝试一种新方法。这次我的结果要好得多 - 至少在 Linux 上的 google chrome 上是这样。我不再有可用的 Windows PC,所以我只能在 Ubuntu 上的 chrome/firefox 上进行测试。我的结果在 Chrome 上 100% 一致,假设在 Firefox 上大约 70 - 80%,但我不认为找到不一致的地方会非常困难。
这个新版本依赖于 Canvas 对象。在我的example http://enobrev.info/cursor2/,我实际上展示了那个画布 - 只是为了让您可以看到它的运行情况,但它可以很容易地使用隐藏的画布对象来完成。
这肯定是一个黑客攻击,我提前为我的代码而道歉。至少,在谷歌浏览器中,无论我将其设置为什么字体或文本区域的大小,它都能一致地工作。我用了萨姆·萨弗伦 https://stackoverflow.com/users/17174/sam-saffron的示例显示光标坐标(灰色背景 div)。我还添加了一个“随机化”链接,这样您就可以看到它在不同的字体/texarea 大小和样式下工作,并实时观看光标位置更新。我建议看看整页演示 http://enobrev.info/cursor2/这样您就可以更好地看到同伴画布一起玩。
我将总结一下它是如何工作的...
基本思想是我们试图尽可能地在画布上重新绘制文本区域。由于浏览器对 texarea 和 texarea 使用相同的字体引擎,因此我们可以使用 canvas 的字体测量功能来找出事物的位置。从那里,我们可以使用可用的画布方法来计算坐标。
首先也是最重要的,我们调整画布以匹配文本区域的尺寸。这完全是为了视觉目的,因为画布大小并不会真正影响我们的结果。由于 Canvas 实际上并不提供自动换行的方法,因此我不得不想出(一起窃取/借用/咀嚼)一种分解行的方法,以尽可能与文本区域匹配。您可能会发现您需要进行最多的跨浏览器调整。
自动换行之后,其他一切都是基础数学。我们将这些行分成一个数组来模拟自动换行,现在我们想要循环这些行并一直向下,直到当前选择结束的点。为了做到这一点,我们只是计算字符,一旦超过selection.end
,我们知道我们已经走得够远了。将直到该点的行数乘以行高,就得到了y
协调。
The x
坐标非常相似,除了我们使用context.measureText
。只要我们打印出正确数量的字符,这将为我们提供绘制到 Canvas 的线条的宽度,该线条恰好在写出的最后一个字符(即当前字符之前的字符)之后结束。selection.end
位置。
当尝试为其他浏览器调试此内容时,要查找的是行没有正确中断的地方。您会在某些地方看到画布中一行的最后一个单词可能已经覆盖在文本区域上,反之亦然。这与浏览器处理自动换行的方式有关。只要画布中的换行与文本区域相匹配,您的光标就应该是正确的。
我将在下面粘贴源。您应该能够复制并粘贴它,但如果您这样做,我要求您下载自己的 jquery-fieldselection 副本,而不是点击我服务器上的副本。
我也升了一个新的演示 http://enobrev.info/cursor2/也a fiddle http://jsfiddle.net/9QAtz/.
祝你好运!
<!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>
Bug
我记得有一个错误。如果将光标放在一行的第一个字母之前,则会将“位置”显示为上一行的最后一个字母。这与 Selection.end 的工作方式有关。我认为查找该案例并相应地修复它应该不会太困难。
版本1
将其保留在此处,以便您可以看到进度,而无需深入研究编辑历史记录。
它并不完美,而且绝对是一个 hack,但我让它在 WinXP IE、FF、Safari、Chrome 和 Opera 上运行得很好。
据我所知,没有办法直接找出任何浏览器上光标的 x/y。这IE法 http://weblogs.asp.net/skillet/archive/2005/03/24/395838.aspx, 提及 https://stackoverflow.com/questions/128342/display-div-at-cursor-position-in-textarea#128461 by 亚当·贝莱尔 https://stackoverflow.com/users/21632/adam-bellaire很有趣,但不幸的是不能跨浏览器。我认为下一个最好的办法就是使用字符作为网格。
不幸的是,任何浏览器都没有内置字体度量信息,这意味着等宽字体是唯一具有一致度量的字体类型。此外,没有可靠的方法可以从字体高度计算出字体宽度。起初我尝试使用高度的百分比,效果很好。然后我改变了字体大小,一切都陷入困境。
我尝试了一种方法来计算字符宽度,即创建一个临时文本区域并不断添加字符,直到滚动高度(或滚动宽度)发生变化。这似乎是合理的,但大约在这条路的中间,我意识到我可以在文本区域上使用 cols 属性,并认为在这个考验中有足够的技巧来添加另一个。这意味着您无法通过 css 设置文本区域的宽度。您必须使用 cols 才能实现此目的。
我遇到的下一个问题是,即使您通过 css 设置字体,浏览器也会以不同的方式报告字体。当你没有设置字体时,mozilla 使用monospace
默认情况下,IE 使用Courier New
, Opera "Courier New"
(带引号),Safari,'Lucida Grand'
(带单引号)。当您将字体设置为monospace
,mozilla 和 ie 接受你给他们的东西,Safari 的结果是-webkit-monospace
和歌剧一起"Courier New"
.
现在我们初始化一些变量。确保在 css 中设置行高。 Firefox 报告了正确的行高,但 IE 报告“正常”,我没有打扰其他浏览器。我只是在 css 中设置了行高,就解决了差异。我还没有测试过使用 em 代替像素。字符高度只是字体大小。也许也应该在你的 CSS 中预先设置它。
另外,在我们开始放置角色之前,还有一项预设 - 这确实让我摸不着头脑。对于 ie 和 mozilla,texarea 字符是
现在我们将为每一行创建一个第一个字符位置的数组。我们循环遍历文本区域中的每个字符。如果它是换行符,我们会向行数组添加一个新位置。如果它是一个空格,我们会尝试弄清楚当前的“单词”是否适合我们所在的行,或者是否会被推到下一行。标点符号算作“单词”的一部分。我还没有测试过制表符,但有一行用于为制表符添加 4 个字符。
一旦我们有了行位置数组,我们就会循环并尝试找到光标所在的行。我们使用选择的“结束”作为光标。
x = (光标位置 - 光标行第一个字符位置) * 字符宽度
y = ((光标行 + 1) * 行高) - 滚动位置
我在用着jquery 1.2.6 http://docs.jquery.com/Downloading_jQuery, jquery-fieldselection http://laboratorium.0xab.cd/jquery/fieldselection/0.1.0/test.html, and jquery 维度 http://plugins.jquery.com/project/dimensions
演示:http://enobrev.info/cursor/ 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>