Фокусировка: focus/blur

Говорят, что элемент «получает фокус», когда посетитель фокусируется на нём. Обычно фокусировка автоматически происходит при нажатии на элементе мышкой, но также можно перейти на нужный элемент клавиатурой – через клавишу Tab, нажатие пальцем на планшете и так далее.

Момент получения фокуса и потери очень важен.

При получении фокуса мы можем подгрузить данные для автодополнения, начать отслеживать изменения. При потере – проверить данные, которые ввёл посетитель.

Кроме того, иногда полезно «вручную», из JavaScript перевести фокус на нужный элемент, например, на поле в динамически созданной форме.

События focus/blur

Событие focus вызывается тогда, когда пользователь фокусируется на элементе, а blur – когда фокус исчезает, например посетитель кликает на другом месте экрана.

Давайте сразу посмотрим на них в деле, используем для проверки («валидации») введённых в форму значений.

В примере ниже:

  • Обработчик onblur проверяет, что в поле введено число, если нет – показывает ошибку.
  • Обработчик onfocus, если текущее состояние поля ввода – «ошибка» – скрывает её (потом при onblur будет повторная проверка).

В примере ниже, если набрать что-нибудь в поле «возраст» и завершить ввод, нажав Tab или кликнув в другое место страницы, то введённое значение будет автоматически проверено:

<style> .error { border-color: red; } </style>

Введите ваш возраст: <input type="text" id="input">

<div id="error"></div>

<script>
input.onblur = function() {
  if (isNaN(this.value)) { // введено не число
    // показать ошибку
    this.className = "error";
    error.innerHTML = 'Вы ввели не число. Исправьте, пожалуйста.'
  }
};

input.onfocus = function() {
  if (this.className == 'error') { // сбросить состояние "ошибка", если оно есть
    this.className = "";
    error.innerHTML = "";
  }
};
</script>

Методы focus/blur

Методы с теми же названиями переводят/уводят фокус с элемента.

Для примера модифицируем пример выше, чтобы при неверном вводе посетитель просто не мог уйти с элемента:

<style>
  .error {
    background: red;
  }
</style>

<div>Возраст:
  <input type="text" id="age">
</div>

<div>Имя:
  <input type="text">
</div>

<script>
  age.onblur = function() {
    if (isNaN(this.value)) { // введено не число
      // показать ошибку
      this.classList.add("error");
      //... и вернуть фокус обратно
      age.focus();
    } else {
      this.classList.remove("error");
    }
  };
</script>

Этот пример работает во всех браузерах, кроме Firefox (ошибка).

Если ввести что-то нецифровое в поле «возраст», и потом попытаться табом или мышкой перейти на другой <input>, то обработчик onblur вернёт фокус обратно.

Обратим внимание – если из onblur сделать event.preventDefault(), то такого же эффекта не будет, потому что onblur срабатывает уже после того, как элемент потерял фокус.

HTML5 и CSS3 вместо focus/blur

Прежде чем переходить к более сложным примерам, использующим JavaScript, мы рассмотрим три примера, когда его использовать не надо, а достаточно современного HTML/CSS.

Подсветка при фокусировке

Стилизация полей ввода может быть решена средствами CSS (CSS2.1), а именно – селектором :focus:

<style>
input:focus {
  background: #FA6;
  outline: none;  /* убрать рамку */
}
</style>
<input type="text">

<p>Селектор :focus выделит элемент при фокусировке на нем и уберёт рамку, которой браузер выделяет этот элемент по умолчанию.</p>

В IE (включая более старые) скрыть фокус также может установка специального атрибута hideFocus.

Автофокус

При загрузке страницы, если на ней существует элемент с атрибутом autofocus – браузер автоматически фокусируется на этом элементе. Работает во всех браузерах, кроме IE9-.

<input type="text" name="search" autofocus>

Если нужны старые IE, то же самое может сделать JavaScript:

<input type="text" name="search">
<script>
  document.getElementsByName('search')[0].focus();
</script>

Как правило, этот атрибут используется при изначальной загрузке, для страниц поиска и так далее, где главный элемент очевиден.

Плейсхолдер

Плейсхолдер – это значение-подсказка внутри INPUT, которое автоматически исчезает при фокусировке и существует, пока посетитель не начал вводить текст.

Во всех браузерах, кроме IE9-, это реализуется специальным атрибутом placeholder:

<input type="text" placeholder="E-mail">

В некоторых браузерах этот текст можно стилизовать:

<style>
.my::-webkit-input-placeholder {
  color: red;
  font-style: italic;
}
.my::-moz-input-placeholder {
  color: red;
  font-style: italic;
}
.my::-ms-input-placeholder {
  color: red;
  font-style: italic;
}
</style>

<input class="my" type="text" placeholder="E-mail">
Стилизованный плейсхолдер

Разрешаем фокус на любом элементе: tabindex

По умолчанию не все элементы поддерживают фокусировку.

Перечень элементов немного рознится от браузера к браузеру, например, список для IE описан в MSDN, одно лишь верно всегда – заведомо поддерживают focus/blur те элементы, c которыми посетитель может взаимодействовать: <button>, <input>, <select>, <a> и т.д.

С другой стороны, на элементах для форматирования, таких как <div>, <span>, <table> – по умолчанию сфокусироваться нельзя. Впрочем, существует способ включить фокусировку и для них.

В HTML есть атрибут tabindex.

Его основной смысл – это указать номер элемента при переборе клавишей Tab.

То есть, если есть два элемента, первый имеет tabindex="1", а второй tabindex="2", то нажатие Tab при фокусе на первом элементе – переведёт его на второй.

Исключением являются специальные значения:

  • tabindex="0" делает элемент всегда последним.
  • tabindex="-1" означает, что клавиша Tab будет элемент игнорировать.

Любой элемент поддерживает фокусировку, если у него есть tabindex.

В примере ниже есть список элементов. Кликните на любой из них и нажмите «tab».

Кликните на первый элемент списка и нажмите Tab. Внимание! Дальнейшие нажатия Tab могут вывести за границы iframe'а с примером.
<ul>
  <li tabindex="1">Один</li>
  <li tabindex="0">Ноль</li>
  <li tabindex="2">Два</li>
  <li tabindex="-1">Минус один</li>
</ul>

<style>
  li { cursor: pointer; }
  :focus { outline: 1px dashed green; }
</style>

Порядок перемещения по клавише «Tab» в примере выше должен быть таким: 1 - 2 - 0 (ноль всегда последний). Продвинутые пользователи частенько используют «Tab» для навигации, и ваше хорошее отношение к ним будет вознаграждено :)

Обычно <li> не поддерживает фокусировку, но здесь есть tabindex.

Делегирование с focus/blur

События focus и blur не всплывают.

Это грустно, поскольку мы не можем использовать делегирование с ними. Например, мы не можем сделать так, чтобы при фокусировке в форме она вся подсвечивалась:

<!-- при фокусировке на форме ставим ей класс -->
<form onfocus="this.className='focused'">
  <input type="text" name="name" value="Ваше имя">
  <input type="text" name="surname" value="Ваша фамилия">
</form>

<style> .focused { outline: 1px solid red; } </style>

Пример выше не работает, т.к. при фокусировке на любом <input> событие focus срабатывает только на этом элементе и не всплывает наверх. Так что обработчик onfocus на форме никогда не сработает.

Что делать? Неужели мы должны присваивать обработчик каждому полю <input>?

Это забавно, но хотя focus/blur не всплывают, они могут быть пойманы на фазе перехвата.

Вот так сработает:

<form id="form">
  <input type="text" name="name" value="Ваше имя">
  <input type="text" name="surname" value="Ваша фамилия">
</form>

<style>
  .focused {
    outline: 1px solid red;
  }
</style>

<script>
  // ставим обработчики на фазе перехвата, последний аргумент true
  form.addEventListener("focus", function() {
    this.classList.add('focused');
  }, true);

  form.addEventListener("blur", function() {
    this.classList.remove('focused');
  }, true);
</script>

События focusin/focusout

События focusin/focusout – то же самое, что и focus/blur, только они всплывают.

У них две особенности:

  • Не поддерживаются Firefox (хотя поддерживаются даже старейшими IE), см. https://bugzilla.mozilla.org/show_bug.cgi?id=687787.
  • Должны быть назначены не через on-свойство, а при помощи elem.addEventListener.

Из-за отсутствия подержки Firefox эти события используют редко. Получается, что во всех браузерах можно использовать focus на стадии перехвата, ну а focusin/focusout – в IE8-, где стадии перехвата нет.

Подсветка формы в примере ниже работает во всех браузерах.

<form name="form">
  <input type="text" name="name" value="Ваше имя">
  <input type="text" name="surname" value="Ваша фамилия">
</form>
<style>
  .focused {
    outline: 1px solid red;
  }
</style>

<script>
  function onFormFocus() {
    this.className = 'focused';
  }

  function onFormBlur() {
    this.className = '';
  }

  var form = document.forms.form;

  if (form.addEventListener) {
    // focus/blur на стадии перехвата срабатывают во всех браузерах
    // поэтому используем их
    form.addEventListener('focus', onFormFocus, true);
    form.addEventListener('blur', onFormBlur, true);
  } else {
    // ветка для IE8-, где нет стадии перехвата, но есть focusin/focusout
    form.onfocusin = onFormFocus;
    form.onfocusout = onFormBlur;
  }
</script>

Итого

События focus/blur происходят при получении и снятия фокуса с элемента.

У них есть особенности:

  • Они не всплывают. Но на фазе перехвата их можно перехватить. Это странно, но это так, не спрашивайте почему.

    Везде, кроме Firefox, поддерживаются всплывающие альтернативы focusin/focusout.

  • По умолчанию многие элементы не могут получить фокус. Например, если вы кликните по DIV, то фокусировка на нем не произойдет.

    Но это можно изменить, если поставить элементу атрибут tabIndex. Этот атрибут также дает возможность контролировать порядок перехода при нажатии Tab.

Текущий элемент: document.activeElement

Кстати, текущий элемент, на котором фокус, доступен как document.activeElement.

Задачи

важность: 5

В данном случае достаточно событий input.focus/input.blur.

Если бы мы хотели реализовать это на уровне документа, то применили бы делегирование и события focusin/focusout (эмуляцию для firefox), так как обычные focus/blur не всплывают.

Открыть решение в песочнице.

Реализуйте более удобный плейсхолдер-подсказку на JavaScript через атрибут data-placeholder.

Правила работы плейсхолдера:

  • Элемент изначально содержит плейсхолдер. Специальный класс placeholder придает ему синий цвет.
  • При фокусировке плейсхолдер показывается уже над полем, становясь «подсказкой».
  • При снятии фокуса, подсказка убирается, если поле пустое – плейсхолдер возвращается в него.

Демо:

В этой задаче плейсхолдер должен работать на одном конкретном input. Подумайте, если input много, как здесь применить делегирование?

Открыть песочницу для задачи.

важность: 4

Нам нужно ловить onclick на мышонке и в onkeydown на нём смотреть коды символов. При скан-кодах стрелок двигать мышонка через position:absolute или position:fixed.

Скан-коды для клавиш стрелок можно узнать, нажимая на них на тестовом стенде. Вот они: 37-38-39-40 (влево-вверх-вправо-вниз).

Проблема может возникнуть одна – keydown не возникает на элементе, если на нём нет фокуса.

Чтобы фокус был – нужно добавить мышонку атрибут tabindex через JS или в HTML.

Открыть решение в песочнице.

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

Демо в новом окне

В этой задаче запрещается ставить обработчики куда-либо, кроме элемента #mouse.

Можно изменять атрибуты и классы в HTML.

Открыть песочницу для задачи.

важность: 5

CSS для решения

Как видно из исходного кода, #view – это <div>, который будет содержать результат, а #area – это редактируемое текстовое поле.

Так как мы преобразуем <div> в <textarea> и обратно, нам нужно сделать их практически одинаковыми с виду:

#view,
#area {
  height: 150px;
  width: 400px;
  font-family: arial;
  font-size: 14px;
}

Текстовое поле нужно как-то выделить. Можно добавить границу, но тогда изменится блок: он увеличится в размерах и немного съедет текст.

Для того, чтобы сделать размер #area таким же, как и #view, добавим поля(padding):

#view {
  /* padding + border = 3px */

  padding: 2px;
  border: 1px solid black;
}

CSS для #area заменяет поля границами:

#area {
  border: 3px groove blue;
  padding: 0px;
  display: none;
}

По умолчанию, текстовое поле скрыто. Кстати, этот код убирает дополнительную рамку в ряде браузеров, которая появляется вокруг поля, когда на него попадает фокус:

/*+ no-beautify */
#area:focus {
  outline: none; /* убирает рамку при фокусе */
}

Горячие клавиши

Чтобы отследить горячие клавиши, нам нужны их скан-коды, а не символы. Это важно, потому что горячие клавиши должны работать независимо от языковой раскладки. Поэтому, мы будем использовать keydown:

document.onkeydown = function(e) {
  if (e.keyCode == 27) { // escape
    cancel();
    return false;
  }

  if ((e.ctrlKey && e.keyCode == 'E'.charCodeAt(0)) && !area.offsetHeight) {
    edit();
    return false;
  }

  if ((e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) && area.offsetHeight) {
    save();
    return false;
  }
};

В примере выше, offsetHeight используется для того, чтобы проверить, отображается элемент или нет. Это очень надежный способ для всех элементов, кроме <tr> в некоторых старых браузерах.

В отличие от простой проверки display=='none', этот способ работает с элементом, спрятанным с помощью стилей, а так же для элементов, у которых скрыты родители.

Редактирование

Следующие функции переключают режимы. HTML-код разрешен, поэтому возможна прямая трансформация в <textarea> и обратно.

function edit() {
  view.style.display = 'none';
  area.value = view.innerHTML;
  area.style.display = 'block';
  area.focus();
}

function save() {
  area.style.display = 'none';
  view.innerHTML = area.value;
  view.style.display = 'block';
}

function cancel() {
  area.style.display = 'none';
  view.style.display = 'block';
}

Открыть решение в песочнице.

Создайте <div>, который при нажатии Ctrl+E превращается в <textarea>.

Изменения, внесенные в поле, можно сохранить обратно в <div> сочетанием клавиш Ctrl+S, при этом <div> получит в виде HTML содержимое <textarea>.

Если же нажать Esc, то <textarea> снова превращается в <div>, изменения не сохраняются.

[demo src=«solution»].

Открыть песочницу для задачи.

важность: 5
  1. При клике – заменяем innerHTML ячейки на <textarea> с размерами «под ячейку», без рамки.
  2. В textarea.value присваиваем содержимое ячейки.
  3. Фокусируем посетителя на ячейке вызовом focus().
  4. Показываем кнопки OK/CANCEL под ячейкой.

Открыть решение в песочнице.

Сделать ячейки таблицы td редактируемыми по клику.

  • При клике – ячейка <td> превращается в редактируемую, можно менять HTML. Размеры ячеек при этом не должны меняться.
  • В один момент может редактироваться одна ячейка.
  • При редактировании под ячейкой появляются кнопки для приема и отмена редактирования, только клик на них заканчивает редактирование.

Демо:

Открыть песочницу для задачи.

важность: 5

Вёрстка

Для вёрстки можно использовать отрицательный margin у текста с подсказкой.

Решение в плане вёрстка есть в решении задачи Расположить текст внутри INPUT.

Решение

placeholder.onclick = function() {
  input.focus();
}

// onfocus сработает и вызове input.focus() и при клике на input
input.onfocus = function() {
  if (placeholder.parentNode) {
    placeholder.parentNode.removeChild(placeholder);
  }
}

Открыть решение в песочнице.

Создайте для <input type="password"> красивый, стилизованный плейсхолдер, например (кликните на тексте):

При клике плейсхолдер просто исчезает и дальше не показывается.

Открыть песочницу для задачи.

важность: 3

Алгоритм

JavaScript не имеет доступа к текущему состоянию CapsLock. При загрузке страницы не известно, включён он или нет.

Но мы можем догадаться о его состоянии из событий:

  1. Проверив символ, полученный по keypress. Символ в верхнем регистре без нажатого Shift означает, что включён CapsLock. Аналогично, символ в нижнем регистре, но с Shift говорят о включенном CapsLock. Свойство event.shiftKey показывает, нажат ли Shift. Так мы можем точно узнать, нажат ли CapsLock.
  2. Проверять keydown. Если нажат CapsLock (скан-код равен 20), то переключить состояние, но лишь в том случае, когда оно уже известно. Под Mac так делать не получится, поскольку клавиатурные события с CapsLock работают некорректно.

Имея состояние CapsLock в переменной, можно при фокусировке на INPUT выдавать предупреждение.

Отслеживать оба события: keydown и keypress хорошо бы на уровне документа, чтобы уже на момент входа в поле ввода мы знали состояние CapsLock.

Но при вводе сразу в нужный input событие keypress событие доплывёт до document и поставит состояние CapsLock после того, как сработает на input. Как это обойти – подумайте сами.

Решение

При загрузке страницы, когда еще ничего не набрано, мы ничего не знаем о состоянии CapsLock, поэтому оно равно null:

var capsLockEnabled = null;

Когда нажата клавиша, мы можем попытаться проверить, совпадает ли регистр символа и состояние Shift:

document.onkeypress = function(e) {

  var chr = getChar(e);
  if (!chr) return; // специальная клавиша

  if (chr.toLowerCase() == chr.toUpperCase()) {
    // символ, который не имеет регистра, такой как пробел,
    // мы не можем использовать для определения состояния CapsLock
    return;
  }

  capsLockEnabled = (chr.toLowerCase() == chr && e.shiftKey) || (chr.toUpperCase() == chr && !e.shiftKey);
}

Когда пользователь нажимает CapsLock, мы должны изменить его текущее состояние. Но мы можем сделать это только если знаем, что был нажат CapsLock.

Например, когда пользователь открыл страницу, мы не знаем, включен ли CapsLock. Затем, мы получаем событие keydown для CapsLock. Но мы все равно не знаем его состояния, был ли CapsLock выключен или, наоборот, включен.

if (navigator.platform.substr(0, 3) != 'Mac') { // событие для CapsLock глючит под Mac
  document.onkeydown = function(e) {
    if (e.keyCode == 20 && capsLockEnabled !== null) {
      capsLockEnabled = !capsLockEnabled;
    }
  };
}

Теперь поле. Задание состоит в том, чтобы предупредить пользователя о включенном CapsLock, чтобы уберечь его от неправильного ввода.

  1. Для начала, когда пользователь сфокусировался на поле, мы должны вывести предупреждение о CapsLock, если он включен.

  2. Пользователь начинает ввод. Каждое событие keypress всплывает до обработчика document.keypress, который обновляет состояние capsLockEnabled.

    Мы не можем использовать событие input.onkeypress, для отображения состояния пользователю, потому что оно сработает до document.onkeypress (из-за всплытия) и, следовательно, до того, как мы узнаем состояние CapsLock.

    Есть много способов решить эту проблему. Можно, например, назначить обработчик состояния CapsLock на событие input.onkeyup. То есть, индикация будет с задержкой, но это несущественно.

    Альтернативное решение – добавить на input такой же обработчик, как и на document.onkeypress.

  3. …И наконец, пользователь убирает фокус с поля. Предупреждение может быть видно, если CapsLock включен, но так как пользователь уже ушел с поля, то нам нужно спрятать предупреждение.

Код проверки поля:

<input type="text" onkeyup="checkCapsWarning(event)" onfocus="checkCapsWarning(event)" onblur="removeCapsWarning()" />

<div style="display:none;color:red" id="caps">Внимание: нажат CapsLock!</div>

<script>
  function checkCapsWarning() {
    document.getElementById('caps').style.display = capsLockEnabled ? 'block' : 'none';
  }

  function removeCapsWarning() {
    document.getElementById('caps').style.display = 'none';
  }
</script>

Открыть решение в песочнице.

Создайте поле, которое будет предупреждать пользователя, если включен CapsLock. Выключение CapsLock уберёт предупреждение.

Такое поле может помочь избежать ошибок при вводе пароля.

Открыть песочницу для задачи.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.