Javascript-форум (https://javascript.ru/forum/)
-   Events/DOM/Window (https://javascript.ru/forum/events/)
-   -   Получить координаты курсора в текстовом поле в пикселях (https://javascript.ru/forum/events/7771-poluchit-koordinaty-kursora-v-tekstovom-pole-v-pikselyakh.html)

prike 18.02.2010 20:07

Получить координаты курсора в текстовом поле в пикселях
 
Товарищи хочу поделиться опытом решения следующей задачи: "Необходимо получить координаты курсора находящегося в текстовом поле в пикселях".

Собственно сложность заключается как раз в том как вычислить положение курсора в пикселях. Получить положение курсора в символах (т.е. индекс символа перед которым стоит курсор) достаточно просто. Но универсального кроссбраузерного способа получить положение курсора в пикселях, который работал бы со всеми ньюансами расположения его (курсора) в текстовом поле и для всех видов текстовых полей (input, textarea) не существует!

Далее привожу основные идеи которые необходимо реализовать для решения задач подобных этой. Сразу оговорюсь, что стартовой точкой для моего исследования стала библиотека vegUI (http://vegui.org), и метод vsk_frm_cursor_offset (http://vskdoc.vegui.org/files/vegui-...mtools-js.html) этой библиотеки :

1) В IE все относительно просто. Объект selection предоставляет методы получения положения курсора относительно текстового поля(!) в котором находится курсор. (подробнее об объекте selection здесь http://habrahabr.ru/blogs/javascript/55922/)
Вот пример получения x координаты курсора в однострочном текстовом поле (input):
var r = document.selection.createRange();
return r.offsetLeft;
Этот код вернет x-координату начала выделенного текста в текстовом поле, в случае если текст не выделен, то начало и конец выделенной области совпадают и представляют из себя положение в которое в данный момент установлен курсор.

2) Для того что бы вычислить положение в других браузерах необходимо создать невидимый элемент div с теми же стилями влияющими на размер букв текста и смещения текста внутри поля (т.е. все font, padding, border стили) скопировать туда все содержимое текстового поля. После символа за которым расположен курсор в текстовом поле вставить какой-либо inline элемент HTML, и получить его смещения относительно родительского div элемента. Эти смещения и будут искомыми координатами.
Здесь есть одно но в том числе из-за которого функция vsk_frm_cursor_offset не сработала для меня. Если требуется узнать положение курсора в однострочном текстовом поле (input элемент) и при этом содержимое поля достаточно длинное, т.е. не умещается в поле по ширине, то если необходимо вычислить положение после того как пользователь прокрутил содержимое поля вправо, необходимо учесть смещение на которое пользователь прокрутил содержимое. В объектной модели DOM браузера Fire Fox (у меня версия 3.5.7) и возможно дугих Gecko браузеров нет универсальной возможности получить данное смещение. Поэтому вот два обходных пути которые можно реализовать в данном случае:
  • использовать вместо элемента input элемент textarea. При этом потребуется перехватывать события JavaScript'ом и запрещать реакцию по умолчанию на них, например нажатие Enter (так же необходимо подумать о вставке из буфера обмена), если необходимо что бы поле оставалось однострочным. В этом случае свойство scrollLeft вернет требуемое нам смещение в пикселях. В случае многострочного поля scrollTop возвращает смещение по вертикали.
  • использовать элемент input. Но при этом, т.к. scrollLeft для input элементов всегда равен 0, придется определять требуемое нам смещение "обходными путями", которые могут иногда не работать, а именно если вы собираетесь программно изменять содержимое текстового поля и позиционировать курсор, то из-за проблемы описанной в этой ветке форума http://javascript.ru/forum/css-html-firefox-mizilla/7615-prokrutit-tekst-do-nuzhnojj-pozicii-v-text-box'e.html следующий подход может не сработать - подумайте об textarea в этом случае. Что бы определить требуемое нам смещение придется создать невидимое поле textarea такой же ширины, с теми же стилями текста и стилями влияющими на отступы текста от границ поля. Скопировать туда содержимое текстового поля, затем спозиционировать курсор и принудительно прокрутить поле до курсора, как это описано в упомянутой выше ветке (http://javascript.ru/forum/css-html-firefox-mizilla/7615-prokrutit-tekst-do-nuzhnojj-pozicii-v-text-box'e.html). После чего свойство scrollLeft будет содержать требуемое нам смещение.

Пожалуйста комментируйте, критикуйте предлагайте более оптимальные решения. Любые комментарии приветствуются!

PS. Надеюсь комму-нибудь этот пост будет полезен или сэкономит время, у меня ушло 3 дня на исследование и реализацию.

prike 18.02.2010 20:09

Далее привожу исходный текст функции vsk_frm_cursor_offset библиотеки vegUI и пояснения почему она не работала у меня.

/*****************************************************************************/
/** Function: vsk_frm_cursor_offset
  * Returns the (pixels) position of the caret in an input style element
  *
  * Parameters:
  *
  *	Node input - input or textarea node
  *
  * Returns:
  *
  * 	Object - containing x and y for the caret position
  *
  * (start code)
  * { x : n, y : n }
  * (end)
  */

function vsk_frm_cursor_offset(input) {
  if (document.selection) { 
    
    var r = document.selection.createRange();
    
    var i;
   
    if(input.nodeName == 'TEXTAREA') {
      var x = r.offsetLeft - r.boundingLeft;
      var y = r.offsetTop - r.boundingTop;
    } else {
      var x = r.offsetLeft;
      var y = r.offsetTop;
    }
   
    return {
      x : x,
      y : y
    };
  
  } else if(typeof input.setSelectionRange != 'undefined') {
    
    var n = document.createElement('div'), i;
    n.style.wrap = 'hard';
    n.style.whiteSpace = 'pre';
    n.style.position = 'absolute';
    n.style.zIndex = -10;
    
    if(input.parentNode.position != 'absolute' && input.parentNode.position!='relative')
      input.parentNode.position = 'relative';
    
    // !!! Здесь временный элемент div вставляется в родительский элемент текcтового поля, если это - ячейка таблицы
    // разметка будет "прыгать" при выполнении этой функции, лучше добавлять к body
    input.parentNode.appendChild(n);
    
    // !!! Копирование стилей не работает в Fire Fox, т.к. 
    // в цикле переменная i представляет из себя не имя стиля а его индекс, т.к. s - массив а не объект, 
    // а конструкция for in применяется к объектам JavaScript
    var s = document.defaultView.getComputedStyle(input,null);

    for(i in s) {
      if(
        i.indexOf('font')>-1 ||
	i.indexOf('padding')>-1
      ) {
        n.style[i] = s[i];
      }
    }

    n.style.width = input.offsetWidth+'px';
    n.style.height = input.offsetHeight+'px';
    n.style.left = input.offsetLeft+'px';
    n.style.top = input.offsetTop+'px';
    n.style.overflow = 'auto';

   
    n.innerHTML = input.value.replace(
      new RegExp("(.{1,"+(input.cols+1)+"})",'g'),"$1\n"
    );
   
    n.scrollLeft = input.scrollLeft;
    n.scrollTop = input.scrollTop;

    var r = document.createRange();
    var e = document.createElement('span');

    r.setStart(n.firstChild, input.selectionStart);
    r.setEnd(n.firstChild, input.selectionStart);
    r.surroundContents(e);

    var obj = { 
      x : e.offsetLeft - n.scrollLeft,
      y : e.offsetTop - n.scrollTop
    };

    n.parentNode.removeChild(n);
    
    return obj; 
  }
}


Кроме проблем указанных в комментариях этот код не учитывает возможности смещение текста для элемента input, если пользователь прокрутил содержимое текстового поля, т.к. scrollLeft для input равен 0 в Fire Fox.

prike 18.02.2010 20:09

Далее привожу код реализации для решения своей задачи. Она немного отличается от сформулированной в самом начале топика. У меня есть поле редактирования в котором строится формула с определенным синтаксисом, при вводе в это поле каких-либо символов появляется список лексем разрешенных в этом месте (наподобии IntelliSense в Microsoft Visual Studio или в своем роде Google Suggest), при щелчке по одной из выведенных лексем введенные символы заменяются на эту лексему и курсор позиционируется после этой лексемы, а сама лексема выделяется на пол секунды, что бы привлечь внимание пользователя (Выделение реализовано наложением полупрозрачного div'а поверх поля редактирования на ту область, которую занимает лексема).

Соответственно отличия от исходной задачи состоят в том, что мне
  • требуется только горизонтальная координата, т.к. поле однострочное (элемент input)
  • кроме положения курсора необходимо перевести длину лексемы в символах поверх которой будет накладываться div в ширину этого div'а в пикселях
Все вычисления делаются в одном методе. Комментарии на английском, но думаю будут ясны.
// Returns as object {xCoordinate, lengthInPixels} x-coordinate (with document.body acts as offset paret) of the cursor in an input element
// (single line text field) and the length in pixels the area that ends on the character after which the cursor is positioned
function GetCursorXCoordinateAndLength(elementOrItsId, lengthInCharacters)
{
	var element = elementOrItsId;
	
	if (typeof(elementOrItsId) == "string")
	{
		element = document.getElementById(elementOrItsId);
	}

	if (!/input/i.test(element.nodeName))
	{
		throw "Method GetCursorXCoordinate supports only INPUT elements.";
	}

	// IE
	if (document.selection)
	{
		var r = document.selection.createRange();
		r.collapse(false);
		r.moveStart("character", -lengthInCharacters);

		// A trick to make method GetOffsetLeft work for IE selection range object
		r.offsetParent = r.parentElement();
		var offsetLeft = GetOffsetLeft(r, document.body);
		
		var result = { xCoordinate:  offsetLeft + r.boundingWidth, lengthInPixels: r.boundingWidth};
		r.collapse(false);
		return result;
	}
	// Fire Fox (not tested for other Gecko browsers so may not work for them)
	else if (typeof(element.setSelectionRange) != 'undefined')
	{
		// Create text area with the same content and set cursor to the same position to get scrollLeft attribute
		// we have to do it to properly handle the case when value don't fit the input so user can
		// scroll it so we need find out what scrollLeft is but it is always = 0 for input element
		var textarea = document.createElement("textarea");
		textarea.style.visibility = "hidden";
		document.body.appendChild(textarea);

		textarea.setAttribute("wrap", "off");
		textarea.style.width = element.offsetWidth + "px";
		textarea.style.overflow = "auto";

		var styles = document.defaultView.getComputedStyle(element, "");
		var s;
		for (var i = 0; i < styles.length; i++)
		{
			s = styles[i];
			if (s.indexOf("border") > -1 ||
				s.indexOf("padding") > -1 ||
				s.indexOf("font") > -1)
			{
				setStyle(textarea, s, getStyle(element, s));
			}
		}
		textarea.style.position = "absolute";
		textarea.style.zIndex = -1;
		textarea.style.left = GetOffsetLeft(element, document.body) + "px";
		textarea.style.top = GetOffsetTop(element, document.body) + "px";

		var textNode = document.createTextNode(element.value);
		textarea.appendChild(textNode);

		textarea.setSelectionRange(element.selectionStart, element.selectionStart);

		// Here is Fire Fox hack to scroll cursor into view (enter space and then remove it)
		// thus this method will return proper x coordinate only if the cursor in the element
		// was positioned with the same hack. It means that it may work wrong sometimes if cursor
		// was positioned by user by hand.
		var e = document.createEvent("KeyboardEvent");
		e.initKeyEvent("keypress", true, true, null, false, false, false, false, 0, 32);
		textarea.dispatchEvent(e);

		e = document.createEvent("KeyboardEvent");
		e.initKeyEvent("keypress", true, true, null, false, false, false, false, 8, 0);
		textarea.dispatchEvent(e);

		var scrollLeft = textarea.scrollLeft;
		document.body.removeChild(textarea);

		// Now create DIV with the same content and set cursor to the same position
		// to get position of the cursor in pixels
		var div = document.createElement("div");
		div.style.visibility = "hidden";
		document.body.appendChild(div);
		
		styles = document.defaultView.getComputedStyle(element, "");
		for (i = 0; i < styles.length; i++)
		{
			s = styles[i];
			if (s.indexOf("border") > -1 ||
				s.indexOf("padding") > -1 ||
				s.indexOf("font") > -1)
			{
				setStyle(div, s, getStyle(element, s));
			}
		}

		div.style.overflow = "visible";
		div.style.position = "absolute";
		div.style.zIndex = -1;
		div.style.left = GetOffsetLeft(element, document.body) + "px";
		div.style.top = GetOffsetTop(element, document.body) + "px";


		textNode = document.createTextNode(element.value);
		div.appendChild(textNode);

		var r = document.createRange();
		var elementRight = document.createElement("span");

		r.setStart(textNode, element.selectionStart);
		r.setEnd(textNode, element.selectionStart);
		r.surroundContents(elementRight);

		var xCoordinate = GetOffsetLeft(elementRight, document.body) - scrollLeft;

		// Calculate length in pixels
		var startPosition = element.selectionStart - lengthInCharacters;
		if (startPosition < 0)
		{
			startPosition = 0;
		}

		var elementLeft = document.createElement("span");
		r.setStart(textNode, startPosition);
		r.setEnd(textNode, startPosition);
		r.surroundContents(elementLeft);
		
		var lengthInPixels = elementRight.offsetLeft - elementLeft.offsetLeft;

		document.body.removeChild(div);

		return {xCoordinate: xCoordinate, lengthInPixels: lengthInPixels};
	}

	return null;
}

// Cross-browser method of calculation offsetLeft value for specified offsetParent
// (it returns the number of pixels from the left edge of element supplied as 'offsetParent' parameter
// till the left edge of the element supplied as 'element' parameter)
function GetOffsetLeft(element, offsetParent)
{
	var possiblyOffsetParent = offsetParent;
	var result = 0;

	do
	{
		result += element.offsetLeft;
		element = element.offsetParent;
	}
	while (element && element != possiblyOffsetParent);

	// If offsetParent is not one of real offset parents of the element calculate offset left considering the
	// document as mutual parent for both elements - so the result will be the differentce of offsetLeft values
	if (!element || !offsetParent)
	{
		var parentOffsetLeft = 0;
		while (offsetParent && offsetParent.offsetParent)
		{
			parentOffsetLeft += offsetParent.offsetLeft;
			offsetParent = offsetParent.offsetParent;
		}
		return result - parentOffsetLeft;
	}

	return result;
}

// Cross-browser method of calculation offsetTop value for specified offsetParent
// (it returns the number of pixels from the top of element supplied as 'offsetParent' parameter
// till the top of the element supplied as 'element' parameter)
function GetOffsetTop(element, offsetParent)
{
	var possiblyOffsetParent = offsetParent;
	var result = 0;

	do
	{
		result += element.offsetTop;
		element = element.offsetParent;
	}
	while (element && element != possiblyOffsetParent);

	// If offsetParent is not one of real offset parents of the element calculate offset top considering the
	// document as mutual parent for both elements - so the result will be the differentce of offsetTop values
	if (!element || !offsetParent)
	{
		var parentOffsetTop = 0;
		while (offsetParent && offsetParent.offsetParent)
		{
			parentOffsetTop += offsetParent.offsetTop;
			offsetParent = offsetParent.offsetParent;
		}
		return result - parentOffsetTop;
	}

	return result;
}


Некоторые пояснения по коду:
1) Метод поддерживает только элемент input, поэтому в нем не реализована логика обработки поля textarea, реализованная в vsk_frm_cursor_offset
2) Метод возвращает смещение по оси x относительно тела документа а не самого поля редактирования
3) GetOffsetTop, GetOffsetLeft - вспомогательные методы

Octane 18.02.2010 20:36

В новом Firefox будет поддержка getBoundingClientRect для Range, за Fx, думаю, и другие браузеры подтянутся, а в IE это давно есть.

Axdr 23.05.2013 04:35

А воз и ныне там
 
Вот позарез нужна такая возможность, только именно для textarea.
В IE8 проблемы такой нет. Координаты дает хоть так, хоть эдак.
А вот c Firefox/Gecko - всю голову сломал. Никакой Range на textarea не распространяется. Этот элемент как будто исключен из списка достойных. А ведь у меня уже 20-я версия Firefox.

Пробовал вышеописанный способ. Но споткнулся на ровном месте. Создаю два элемента - textarea и div с одинаковыми стилями, но там уже по-разному текст размещается.

<!DOCTYPE html>
<html><head>
<style type="text/css">
.txtar {width:500px; height:200px; font:normal 12px monospace; border:1px solid grey; white-space:pre-wrap; word-break:break-all;}
</style>
<script type="text/javascript">
  var ta, tad;
  window.onload = function() {
    ta = document.getElementById("ta"); 
    tad = document.getElementById("tad");
    tad.textContent = ta.textContent;
  }
</script>
</head><body>
<textarea id="ta" class="txtar">
АБВГДЕ
        ЖЗИЙ
    КЛМНОП
  рстуфх
           0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij
</textarea>
<div id="tad" class="txtar"></div>
</body></html>


То ли я не там копаю, то ли забыть надо про этот textarea.


Часовой пояс GMT +3, время: 19:45.