25 березня 2022 р.

Фокусування: focus/blur

Елемент отримує фокус тоді, коли користувач або клікає на нього або натискає Tab на клавіатурі. Також є HTML-атрибут autofocus, який автоматично встановлює фокус на елементі. Є й інші способи отримання фокусу, про них далі.

Фокусування на елементі загалом означає: “підготуватися до прийняття даних на елементі”, тому це підходящий момент щоб запустити код, який ініціалізує необхідну функціональність.

Момент втрати фокусу(“blur”) ще важливіший. Це відбувається, коли користувач клікає деінде або натискає Tab щоб перейти до наступного поля форми. Є й інші причини встрати фокусу, про них далі.

Втрата фокусу загалом означає: “дані вже введені”, тому можна виконати перевірку або навіть зберегти їх на сервер і таке інше.

У роботі з подіями фокусування є важливі особливості. Ми спробуємо розібрати їх далі.

Події focus/blur

Подія focus викликається в момент фокусування, а подія blur – коли елемент втрачає фокус.

Використаймо їх для валідації поля форми.

В наведеному нижче прикладі:

  • Обробник події blur перевіряє чи заповнено поле email, а якщо ні – показує помилку.
  • Обробник події focus ховає повідомлення про помилку (на blur перевірку буде виконано ще раз):
<style>
  .invalid { border-color: red; }
  #error { color: red }
</style>

Ваша електронна адреса: <input type="email" id="input">

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

<script>
input.onblur = function() {
  if (!input.value.includes('@')) { // це не електронна адреса
    input.classList.add('invalid');
    error.innerHTML = 'Будь ласка, введіть правильну електронну адресу.'
  }
};

input.onfocus = function() {
  if (this.classList.contains('invalid')) {
    // видалити індикатор помилки, тому що користувач хоче ввести дані заново
    this.classList.remove('invalid');
    error.innerHTML = "";
  }
};
</script>

Сучасний HTML дозволяє нам виконувати багато валідацій(перевірок даних) за допомогою власних атрибутів елементів: required, pattern та інших. Іноді це саме те, що нам потрібно. Проте коли ми хочему більше гнучкості, можна скористатися JavaScript. Також ми можемо автоматично відправити оновлене значення поля на сервер, якщо воно правильне.

Методи focus/blur

Методи elem.focus() та elem.blur() встановлюють/прибирають фокус на елементі.

Наприклад, заборонимо відвідувачеві залишити поле, якщо введене значення не відповідає заданим умовам:

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

Введіть вашу електронну адресу: <input type="email" id="input">
<input type="text" style="width:300px" placeholder="введіть невірну електронну адресу та встановіть фокус сюди">

<script>
  input.onblur = function() {
    if (!this.value.includes('@')) { // це не електронна адреса
      // показати помилку
      this.classList.add("error");
      // ...та повернути фокус
      input.focus();
    } else {
      this.classList.remove("error");
    }
  };
</script>

Це спрацює у всіх браузерах окрім Firefox (bug).

Якщо ми введемо щось в поле форми, а потім спробуємо натиснути Tab або клікнути в іншому місці, тоді onblur поверне фокус полю.

Зверніть увагу, що ми не можемо “відвернути втрату фокусу” викликавши event.preventDefault() в обробнику onblur тому що onblur cпрацьовує після того як елемент втратив фокус.

Втрата фокусу викликана JavaScript

Елемент може втратити фокус з різних причин.

Одна з них – відвідувач клікнув в іншому місці. Але й сам JavaScript може спричинити це, наприклад:

  • alert зміщує фокус на себе, тому елемент втрачає фокус (подія blur), а коли alert закривається – фокус повертається елементу (подія focus).
  • Якщо елемент видалили з DOM, це також призводить до втрати фокусу. Якщо пізніше його вставити знову, то фокус не повернеться.

Через ці особливості, поведінка обробників focus/blur може бути непередбачуваною – вони викликатимуться коли нам це не потрібно.

Використовуйте ці події з обережністю. Якщо ми хочемо відслідковувати втрату фокусу ініційовану користувачем, уникайте можливостей спричинити її власноруч.

Дозволити фокусування на будь-якому елементі: tabindex

Багато елементів не підтримують фокусування за замовчуванням.

Їх список відрізняється у різних браузерах, але одне завжди вірно: підтримка focus/blur гарантована для елементів з якими користувач може взаємодіяти: <button>, <input>, <select>, <a> та інших.

З іншого боку, елементи призначені для форматуваня, такі як <div>, <span>, <table> – за замовчуванням не фокусуються. Метод elem.focus() не працює з ними, а події focus/blur ніколи не генеруються.

Цю поведінку можна змінити за допомогою HTML-атрибуту tabindex.

Якщо елемент має атрибут tabindex, то на ньому можна фокусуватися. Значенням атрибуту є порядковий номер елементу, коли для переходу між елементами використовується клавіша Tab (або аналогічна).

Тобто: якщо в нас є два елементи, у першого tabindex="1", а у другого tabindex="2", тоді перебуваючи на першому елементі і натиснувши клавішу Tab фокус зміститься на другий елемент.

Порядок наступний: елементи з заданим tabindex від 1 і вище йдуть першими(в порядку значень tabindex), а далі елементи без tabindex (наприклад, звичайний <input>).

Елементи з однаковим значенням tabindex отримують фокус у типовому порядку, так як вони розміщені в документі.

Є два спеціальні значення:

  • tabindex="0" включає елемент у звичайний порядок елементів без tabindex. Тобто, коли ми перемикаємося між елементами, ті, що мають tabindex=0 йдуть після тих, що з tabindex ≥ 1.

    Зазвичай цей прийом використовують, щоб на елементі можна було сфокусуватися не змінюючи стандартний порядок перемикання. Щоб елемент став частиною форми на рівні з <input>.

  • tabindex="-1" дозволяє лише програмне фокусування на елементі. Клавіша Tab ігнорує такі елементи, проте метод elem.focus() спрацьовує.

Наприклад, є список. Клікніть на першому елементі та натисніть 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>

Порядок буде таким: 1 - 2 - 0. Зазвичай, <li> не підтримує фокусування, але tabindex вмикає його, разом із подіями та стилізацією псевдокласу :focus.

Властивість elem.tabIndex також працює

Ми можемо додати tabindex з JavaScript за допомогою властивості elem.tabIndex. Це має такий самий ефект.

Делегування: focusin/focusout

Події focus та blur не спливають.

Наприклад, ми не можемо встановити onfocus на <form> щоб виділити її:

<!-- додати класс при фокусуванні на формі -->
<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 відбувається тільки на самому полі input. Вона не підіймається вище. Тому form.onfocus ніколи не виконується.

Існує два рішення.

Перше, є цікава історична особливість: focus/blur не спливає, проте розповсюджується вниз на фазі захоплення.

Ось це спрацює:

<form id="form">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

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

<script>
  // встановіть обробник на фазі захоплення (останній аргумент true)
  form.addEventListener("focus", () => form.classList.add('focused'), true);
  form.addEventListener("blur", () => form.classList.remove('focused'), true);
</script>

Друге, є події focusin та focusout – такі ж самі як focus/blur, тільки вони спливають.

Зауважте, що вони мають використовуватися з elem.addEventListener, а не з on<event>.

Отже, ще один робочий варіант:

<form id="form">
  <input type="text" name="name" value="Ім’я">
  <input type="text" name="surname" value="Прізвище">
</form>

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

<script>
  form.addEventListener("focusin", () => form.classList.add('focused'));
  form.addEventListener("focusout", () => form.classList.remove('focused'));
</script>

Підсумки

Події focus та blur викликають фокусування або втрату фокусу на елементі.

Їхніми особливостями є:

  • Вони не спливають. Натомість можна використовувати фазу захоплення або focusin/focusout.
  • Більшість елементів не підтримує фокусування за замовчуванням. Використовуйте tabindex щоб на елементі можна було встановити фокус.

Поточний елемент в фокусі можна отримати з document.activeElement.

Завдання

важливість: 5

Створіть <div>, який при кліканні на ньому, перетворюється на <textarea>.

Текстова область дозволяє редагувати HTML всередині <div>.

Коли користувач натискає Enter або <textarea> втрачає фокус, тоді знову з’являється <div>, який містить в собі HTML введений в <textarea>.

Демонстрація в новому вікні

Відкрити пісочницю для завдання.

важливість: 5

Створіть таблицю, клітини якої можна було б редагувати клікнувши на них.

  • По кліку – в клітині з’являється <textarea>, в якій можна редагувати вміст в форматі HTML. Висота та ширина клітини при цьому не змінюється.
  • Кнопки ЗГОДА та ВІДМІНА з’являються внизу клітини щоб підтвердити/відмінити зміни.
  • Одночасно можна редагувати лише одну клітину. Поки <td> в “режимі редагування”, кліки на інших клітинах ігноруються.
  • В талиці може бути багато клітин. Використовуйте делегування подій.

Демо:

Відкрити пісочницю для завдання.

  1. По кліку – замінити innerHTML клітини на <textarea> того ж розміру, без рамки. Задати розміри можна як в CSS так і в JavaScript.
  2. Взяти значення textarea.value з td.innerHTML.
  3. Встановити фокус в текстову область.
  4. Показати кнопки ЗГОДА/ВІДМІНА під клітиною, опрацювати кліки на них.

Відкрити рішення в пісочниці.

важливість: 4

Встановіть фокус на мишу. Використовуйте стрілки на клавіатурі, щоб рухати нею:

Демонстрація в новому вікні

P.S. Не додавайте обробники подій нікуди окрім елементу #mouse.

P.P.S. Не змінюйте HTML/CSS, рішення повинно бути універсальним і працювати з будь-яким елементом.

Відкрити пісочницю для завдання.

Ми можемо скористатися mouse.onclick щоб опрацювати клік та зробити мишу “рухливою” задавши їй position:fixed, а потім mouse.onkeydown щоб опрацювати натискання на стрілки клавіатури.

Єдиний недолік в тому, що keydown перемикається лише між елементами, на яких можна встановити фокус. Тому нам потрібно додати елементу tabindex. Оскільки нам заборонено змінювати HTML, ми можемо використати для цього властивість mouse.tabIndex.

P.S. Також можна замінити mouse.onclick на mouse.onfocus.

Відкрити рішення в пісочниці.

Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)