Безопасная отправка по нажатию Enter в IME

Веб-функции: события клавиатуры.

Во многих интерфейсах чатов сообщение отправляется, когда пользователь нажимает Enter в <textarea>.

Это работает для пользователей, которые вводят текст напрямую с латинской клавиатуры (например, на английском языке), но не работает для пользователей, которые вводят текст с помощью редактора метода ввода (IME) для составления текста на таких языках, как японский, китайский или корейский.

В этом случае клавиша Enter/Return используется для подтверждения текущего варианта преобразования символов. Если специальный код JavaScript прослушивает keydown в Enter и сразу отправляет сообщение, оно будет отправлено до того, как пользователь закончит преобразование символов. В результате сообщение будет неполным, фрагментированным или неправильным.

Проверка composition-active применяется к любому пользовательскому обработчику keydown, который воздействует на Enter, а не только к многострочным полям <textarea>. Однострочные поля <input> с собственной обработкой Enter, такие как строки поиска, встроенные поля переименования, поля модальных диалоговых окон и палитры команд, получают то же нажатие клавиши Enter, подтверждающее IME, что и <textarea>, со значением isComposing, установленным на true. Встроенная неявная отправка <form> из <input> без пользовательского обработчика Enter не требует явной обработки, поскольку браузеры подавляют неявную отправку, когда нажатие клавиши Enter используется IME.

Стратегия реализации

Для <textarea> или <input> с настраиваемой отправкой по нажатию Enter проверьте нативное свойство isComposing элемента KeyboardEvent перед отправкой контента. Также вызовите event.preventDefault(): действие по умолчанию Enter вставляет новую строку в <textarea>, а в <input> внутри <form> запускает неявную отправку браузера, которая приведет к повторной отправке.

<form id="chat-form">
  <label for="chat-input" class="visually-hidden">Message</label>
  <textarea id="chat-input" placeholder="Type a message..."></textarea>
  <button type="submit" id="send-button">Send</button>
</form>
const textarea = document.getElementById('chat-input');
const form = document.getElementById('chat-form');

textarea.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' && !event.shiftKey) {
    // Prevent the default action (newline in a textarea, implicit form submission in an input)
    event.preventDefault();

    // If the user is composing text, return early.
    if (event.isComposing) {
      return;
    }

    form.requestSubmit();
  }
});

Для однострочного элемента <input> используйте тот же обработчик без условия !event.shiftKey, которое существует только потому, что Shift+Enter вставляет новую строку в <textarea>.

Специальные возможности и тестирование

  1. Кнопка отправки. Всегда добавляйте элемент <button type="submit">. Быстрые клавиши – это вспомогательный инструмент. Они не должны заменять стандартные способы отправки форм.
  2. Доступные поля ввода. Убедитесь, что все элементы <input> и <textarea> программно связаны с <label> с помощью соответствующих атрибутов id и for.

Резервные стратегии

В последних версиях Safari из-за проблемы с порядком событий (ошибка WebKit 165004) обработчики скриптов получали compositionend до подтверждения Enter keydown, хотя базовые события отправлялись в обратном порядке. К моменту запуска обработчика keydown значение event.isComposing уже было сброшено до false, а значит, стандартная проверка не смогла бы предотвратить преждевременную отправку.

Если вам нужно обеспечить совместимость со старыми версиями Safari и другими платформами, используйте одну из следующих резервных стратегий:

Сочетание event.isComposing с проверкой event.keyCode === 229 позволяет надежно отслеживать нажатие клавиши Enter в Safari. Поскольку этот код вложен в условие event.key === 'Enter', он защищен от виртуальных клавиатур мобильных устройств, которые могут использовать 229 для ввода обычных символов.

textarea.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault();

    // Block submission if composing natively or if keyCode is 229
    if (event.isComposing || event.keyCode === 229) {
      return;
    }

    form.requestSubmit();
  }
});

Стратегия 2. Обходной путь с event.timeStamp-дневным периодом (альтернативный вариант)

Если в вашей базе кода строго запрещено использовать устаревшие API, такие как keyCode, или если в определенных целевых средах возникают проблемы с проверкой 229, вы можете отслеживать временную метку отправки события, которую сообщает браузер.

Поскольку Safari отправляет подтверждающее событие Enter keydown очень близко к событию compositionend (часто в пределах 5 мс, а иногда временная метка keydown оказывается немного раньше из-за инверсии порядка доставки обработчика), проверка разницы во времени является очень надежной и вряд ли приведет к ложным срабатываниям на мобильных виртуальных клавиатурах в стандартных условиях.

let lastCompositionEndAt = null;

textarea.addEventListener('compositionend', (event) => {
  // IMPORTANT: Always use event.timeStamp, not Date.now() or performance.now().
  // Handler-time measurements are vulnerable to drift when the main thread is blocked.
  lastCompositionEndAt = event.timeStamp;
});

textarea.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault();

    if (event.isComposing) {
      return;
    }

    // Block submission if the event occurs within a 50ms window of composition ending.
    // Math.abs handles Safari's inverted event delivery timing bug.
    if (
      lastCompositionEndAt !== null &&
      Math.abs(event.timeStamp - lastCompositionEndAt) < 50
    ) {
      return;
    }

    form.requestSubmit();
  }
});