Ответ 1
Версия 2 моего Hacky Experiment
Эта новая версия работает с любым шрифтом, который можно настроить по требованию, и любым размером текстового поля.
Заметив, что некоторые из вас все еще пытаются заставить это работать, я решил попробовать новый подход. На этот раз мои результаты FAR лучше - по крайней мере, на google chrome на linux. У меня больше нет компьютера Windows, поэтому я могу только проверить на chrome/firefox на Ubuntu. Мои результаты работают 100% последовательно в Chrome, и скажем, где-то около 70 - 80% на Firefox, но я не думаю, что было бы невероятно сложно найти несоответствия.
Эта новая версия основана на объекте Canvas. В моем пример, я на самом деле показываю это очень полотно - просто чтобы вы могли видеть его в действии, но его можно было бы легко сделать с помощью скрытого canvas.
Это, безусловно, взломать, и я заранее извиняюсь за свой довольно сложенный код. По крайней мере, в google chrome он работает последовательно, независимо от того, какой шрифт я ему устанавливаю, или размером текстового поля. Я использовал пример Сэма Шаффрона, чтобы показать координаты курсора (серо-фоновый div). Я также добавил ссылку "Рандомизировать", чтобы вы могли видеть, как она работает в разных размерах и стилях font/texarea и следит за обновлением положения курсора на лету. Я рекомендую посмотреть полноэкранную демонстрацию, чтобы вы могли лучше видеть холст холста.
Я расскажу, как это работает...
Основная идея заключается в том, что мы пытаемся перерисовать текстовую область на холсте, насколько это возможно. Поскольку браузер использует один и тот же механизм шрифтов для обеих сторон и тексарию, мы можем использовать функцию измерения шрифта холста, чтобы выяснить, где это происходит. Оттуда мы можем использовать доступные методы холста, чтобы выяснить наши координаты.
Прежде всего, мы корректируем наш холст в соответствии с размерами текстового поля. Это целиком для визуальных целей, так как размер холста на самом деле не влияет на наш результат. Поскольку Canvas на самом деле не предоставляет средства обертывания слов, мне приходилось колдовать (украсть/заимствовать/объединить вместе), чтобы разбить линии на максимально возможное соответствие текстовой области. Здесь вы, вероятно, обнаружите, что вам нужно сделать самую кросс-браузерную настройку.
После переноса слов все остальное является базовой математикой. Мы разделяем строки на массив, чтобы имитировать перенос слов, и теперь мы хотим прокрутить эти строки и дойти до тех пор, пока не закончится наш текущий выбор. Чтобы сделать это, мы просто подсчитываем символы, и как только мы превосходим selection.end
, мы знаем, что мы спустились достаточно далеко. Умножьте подсчет линии до этой точки с высотой линии, и у вас есть координата y
.
Координата x
очень похожа, за исключением того, что мы используем context.measureText
. Пока мы печатаем нужное количество символов, это даст нам ширину линии, которая тянется к Canvas, которая заканчивается после последнего выписанного символа, который является символом перед currentl selection.end
положение.
При попытке отладить это для других браузеров, нужно искать, где линии не сломаются должным образом. В некоторых местах вы увидите, что последнее слово в строке в холсте может быть завернуто в текстовое поле или наоборот. Это связано с тем, как браузер обрабатывает переносы слов. Пока вы получаете обертывание в холсте в соответствии с текстовым полем, ваш курсор должен быть правильным.
Я вставлю источник ниже. Вы должны иметь возможность копировать и вставлять его, но если вы это сделаете, я прошу вас загрузить свою собственную копию 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
Оставляя это здесь, чтобы вы могли видеть прогресс без необходимости прокручивать историю изменений.
Это не идеально, и это наиболее определенно рушится, но я получил его, чтобы он работал очень хорошо в WinXP IE, FF, Safari, Chrome и Opera.
Насколько я могу судить, нет никакого способа напрямую узнать x/y курсора в любом браузере. метод IE, упоминается Adam Bellaire интересен, но, к сожалению, не кросс-браузер. Я подумал, что лучше всего использовать символы в виде сетки.
К сожалению, нет никакой информации о метрике шрифта, встроенной в любой из браузеров, что означает, что моноширинный шрифт является единственным типом шрифта, который будет иметь согласованное измерение. Кроме того, нет надежных средств определения ширины шрифта от высоты шрифта. Сначала я пробовал использовать процент высоты, который отлично работал. Затем я изменил размер шрифта, и все ушло в ад.
Я попробовал один метод для определения ширины символов, который должен был создать временную текстовую область и продолжать добавлять символы до тех пор, пока не изменится scrollHeight (или scrollWidth). Это кажется правдоподобным, но примерно наполовину вниз по этой дороге, я понял, что могу просто использовать атрибут 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 вместо пикселей. Char высота - только размер шрифта. Вероятно, предварительно установите это в свой CSS.
Кроме того, еще одна предварительная настройка, прежде чем мы начнем размещать персонажей, что действительно заставило меня почесывать голову. Для то есть и mozilla, символы texarea < cols, все остальное - <= chars. Таким образом, Chrome может поместиться 50 символов в поперечнике, но mozilla и т.д. Сломает последнее слово с линии.
Теперь мы создадим массив позиций первого символа для каждой строки. Мы просматриваем каждый Char в текстовом поле. Если это новая строка, мы добавляем новую позицию в наш линейный массив. Если это пробел, мы пытаемся выяснить, будет ли текущее "слово" вписываться в строку, на которой мы находимся, или если она будет перенесена на следующую строку. Пунктуация считается частью "слова". Я не тестировал с вкладками, но там есть строка для добавления 4 символов для вкладки char.
Как только у нас будет массив позиций линии, мы прокручиваемся и пытаемся найти, в какой строке находится курсор. Мы используем 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>