Bezpieczne przesyłanie za pomocą klawisza Enter w edytorze IME

Funkcje internetowe: Zdarzenia klawiatury

Wiele interfejsów czatu przesyła wiadomość, gdy użytkownik naciśnie Enter w <textarea>.

Działa to w przypadku użytkowników, którzy piszą bezpośrednio na klawiaturze łacińskiej (np. w języku angielskim), ale nie działa w przypadku użytkowników, którzy piszą za pomocą edytora metody wprowadzania (IME) w językach takich jak japoński, chiński czy koreański.

W tych kontekstach klawisz Enter/Return służy do potwierdzania bieżącego kandydata do konwersji znaków. Jeśli niestandardowy kod JavaScript nasłuchuje zdarzenia keydown w elemencie Enter i natychmiast przesyła wiadomość, jest ona wysyłana, gdy użytkownik nadal konwertuje znaki, co powoduje, że wiadomości są niekompletne, fragmentaryczne lub nieprawidłowe.

Sprawdzanie, czy kompozycja jest aktywna, dotyczy każdego niestandardowego modułu obsługi keydown, który działa na Enter, a nie tylko wielowierszowych pól <textarea>. Jednowierszowe pola <input> z własną obsługą Enter, takie jak paski wyszukiwania i znajdowania, wbudowane pola zmiany nazwy, pola okien modalnych i palety poleceń, otrzymują to samo potwierdzające IME naciśnięcie klawisza Enter co <textarea>, z właściwością isComposing ustawioną na true. Niejawne przesyłanie natywne elementu <form> z elementu <input> bez niestandardowego modułu obsługi Enter nie wymaga jawnej obsługi, ponieważ przeglądarki blokują niejawne przesyłanie, gdy naciśnięcie klawisza Enter jest wykorzystywane przez edytor IME.

Strategia wdrażania

W przypadku elementu <textarea> lub <input> z niestandardowym przesyłaniem za pomocą klawisza Enter przed przesłaniem treści sprawdź właściwość isComposing elementu KeyboardEvent. Wywołaj też event.preventDefault(): domyślne działanie Enter wstawia znak nowego wiersza w <textarea>, a w <input> wewnątrz <form> wywołuje niejawne przesłanie przeglądarki, co spowodowałoby ponowne przesłanie.

<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();
  }
});

W przypadku jednolinijkowego elementu <input> użyj tego samego modułu obsługi bez warunku !event.shiftKey, który istnieje tylko dlatego, że Shift+Enter wstawia znak nowego wiersza w elemencie <textarea>.

Ułatwienia dostępu i testowanie

  1. Przycisk przesyłania: zawsze dodawaj element <button type="submit">. Skróty klawiszowe są pomocne, ale nie powinny zastępować natywnych ścieżek przesyłania formularzy.
  2. Dostępne dane wejściowe: upewnij się, że wszystkie elementy <input> i <textarea> są powiązane programowo z elementem <label> za pomocą pasujących atrybutów id i for.

Strategie zastępcze

W ostatnich wersjach Safari problem z kolejnością zdarzeń (błąd WebKit 165004) powodował, że zdarzenie compositionend było dostarczane do modułów obsługi skryptów przed potwierdzeniem Enter keydown, mimo że zdarzenia bazowe były wysyłane w odwrotnej kolejności. Zanim moduł obsługi zdarzenia keydown został uruchomiony, wartość event.isComposing została już zresetowana do false, co oznacza, że samo standardowe sprawdzenie nie zapobiegłoby przedwczesnemu przesłaniu.

Jeśli chcesz zapewnić zgodność z różnymi przeglądarkami, w tym ze starszymi wersjami Safari i innymi platformami, zastosuj jedną z tych strategii zastępczych:

Łącząc event.isComposing ze sprawdzaniem event.keyCode === 229, możesz niezawodnie wykrywać nieprawidłowe potwierdzanie przez Safari naciśnięcia klawisza Enter. Ponieważ jest to zagnieżdżone w bramce event.key === 'Enter', jest bezpieczne przed mobilnymi klawiaturami wirtualnymi, które mogą używać 229 do wprowadzania normalnego układu znaków.

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();
  }
});

Strategia 2. Obejście problemu z oknem event.timeStamp (alternatywa)

W przypadku baz kodu, które ściśle zabraniają używania wycofanych interfejsów API, takich jak keyCode, lub jeśli występują znane problemy ze sprawdzaniem 229 w określonych środowiskach docelowych, możesz zamiast tego śledzić sygnaturę czasową wysyłania zdarzenia zgłoszoną przez przeglądarkę.

Safari wysyła potwierdzające zdarzenie Enter keydown bardzo blisko zdarzenia compositionend (często w ciągu 5 ms, a czasami sygnatura czasowa keydown jest nieco wcześniejsza ze względu na odwrócenie kolejności dostarczania modułu obsługi). Dlatego sprawdzanie różnicy czasu jest bardzo niezawodne i jest mało prawdopodobne, aby w standardowych warunkach powodowało fałszywe alarmy na klawiaturach wirtualnych na urządzeniach mobilnych.

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();
  }
});