
Позиция курсора в TextBox на JavaScript: чтение, установка и вставка текста
Вставить шаблон в место курсора, подсказать формат по мере ввода, вернуть каретку после маски: всё это начинается с вопроса «где сейчас курсор». Разбираем кроссбраузерный ответ.
Зачем знать позицию каретки
Пока поле формы просто принимает текст, позиция курсора никого не интересует. Она становится важной, как только скрипт начинает вмешиваться во ввод: кнопка «вставить подпись» в редакторе комментариев, автодополнение тегов, маска телефона, которая расставляет скобки и при этом не должна отбрасывать курсор в конец строки.
В 2009 году эта задача решалась двумя разными способами. Firefox, Opera, Safari и Chrome поддерживали простые свойства selectionStart и selectionEnd. Internet Explorer до девятой версии их не знал и предлагал собственную модель TextRange, с которой приходится работать через вычисления. Ниже обе ветки собраны в одну функцию.
Стандартный способ: selectionStart и selectionEnd
У элементов input и textarea есть два числовых свойства. selectionStart показывает индекс начала выделения, selectionEnd индекс конца. Если ничего не выделено, оба равны позиции курсора. Индексы считаются с нуля, так что курсор перед первым символом даёт 0, а курсор в конце строки даёт value.length.
Свойства работают только у текстовых полей. У полей type=«email» и type=«number» многие браузеры при обращении к ним бросают исключение, поэтому чтение стоит обернуть в try.
Старый Internet Explorer и объект TextRange
В IE текущее выделение доступно через document.selection.createRange(). Этот диапазон ничего не знает о поле, поэтому позицию приходится вычислять: создаём второй диапазон на весь текст поля через createTextRange, сдвигаем его конец к началу выделения методом setEndPoint и считаем длину получившегося текста.
У этого метода есть известная неточность в textarea. IE хранит перевод строки как два символа \r\n, а свойство text диапазона может отбрасывать завершающие переводы строк. Если курсор стоит в начале новой строки, результат иногда оказывается меньше на один или два символа. Для однострочных полей способ точен, для многострочных его хватает для большинства задач вставки.
Универсальная функция GetCaret
Функция сначала проверяет стандартные свойства, а к TextRange обращается только при их отсутствии. Так современные браузеры идут коротким путём, а старый IE получает свою ветку. Возвращаем объект с началом и концом выделения: он нужен и для чтения, и для последующей вставки.
// Возвращает {start, end} выделения в input или textarea
function GetCaret(box) {
try {
if (typeof box.selectionStart === 'number') {
return { start: box.selectionStart, end: box.selectionEnd };
}
} catch (e) { /* поле без поддержки выделения */ }
if (document.selection && box.createTextRange) {
box.focus();
var sel = document.selection.createRange();
var len = sel.text.length;
var full = box.createTextRange();
full.setEndPoint('EndToStart', sel); // от начала поля до курсора
var start = full.text.length;
return { start: start, end: start + len };
}
return { start: box.value.length, end: box.value.length };
}Установка курсора и вставка текста
Обратная операция нужна не реже. Метод setSelectionRange(начало, конец) ставит выделение или, при равных аргументах, просто перемещает курсор. В IE то же делается через createTextRange: диапазон схлопывается в начало, сдвигается методами moveStart и moveEnd и выделяется методом select.
Вставка текста комбинирует обе функции: берём позицию, склеиваем строку из части до курсора, нового фрагмента и части после, затем ставим курсор сразу за вставкой. Выделенный текст при этом заменяется, как при обычном вводе с клавиатуры.
function SetCaret(box, start, end) {
if (end == null) end = start;
if (box.setSelectionRange) {
box.focus();
box.setSelectionRange(start, end);
} else if (box.createTextRange) {
var r = box.createTextRange();
r.collapse(true);
r.moveEnd('character', end);
r.moveStart('character', start);
r.select();
}
}
// Вставляет text на место курсора или выделения
function InsertAtCaret(box, text) {
var c = GetCaret(box);
var v = box.value;
box.value = v.slice(0, c.start) + text + v.slice(c.end);
SetCaret(box, c.start + text.length);
}Какие события использовать
Чтобы показывать позицию в реальном времени, обработчик подписывают на несколько событий: keyup и mouseup покрывают перемещение стрелками и клик. Событие keydown срабатывает до того, как символ попал в поле, поэтому позиция в нём отстаёт на одно нажатие. Для логики, которая должна видеть уже изменённое значение, keydown не подходит.
Если вставка выполняется по кнопке, сохраните позицию заранее: в обработчиках keyup, mouseup и blur самого поля. Тогда клик по кнопке, который уводит фокус, не потеряет место вставки.
var box = document.getElementById('txtComment');
var saved = { start: 0, end: 0 };
function remember() { saved = GetCaret(box); }
box.onkeyup = remember;
box.onmouseup = remember;
box.onblur = remember;
document.getElementById('btnSign').onclick = function () {
SetCaret(box, saved.start, saved.end); // вернуть выделение
InsertAtCaret(box, '\n-- \nС уважением, служба поддержки');
return false;
};Как это делают сегодня
Вопросы и ответы
Почему позиция в textarea в IE неточная?
Перевод строки там занимает два символа \r\n, а свойство text диапазона может терять завершающие переводы строк. В однострочных полях результат точный, в многострочных возможна ошибка у начала строки.
Почему selectionStart вызывает ошибку?
Для некоторых типов полей, например email и number, браузеры не поддерживают выделение и бросают исключение. Оберните обращение в try или используйте type=«text» с проверкой формата.
Как поставить курсор в конец поля?
Вызовите SetCaret(box, box.value.length) после установки фокуса. В современных браузерах это то же самое, что box.setSelectionRange(len, len).