IME-sichere Eingabe zum Senden

Webfunktionen: Tastaturereignisse

Bei vielen Chat-Oberflächen wird die Nachricht gesendet, wenn der Nutzer in einem <textarea> die Taste Enter drückt.

Das funktioniert für Nutzer, die mit einer direkten lateinischen Tastatureingabe (z.B. Englisch) tippen, aber nicht für Nutzer, die einen Eingabemethoden-Editor (IME) verwenden, um Text in Sprachen wie Japanisch, Chinesisch oder Koreanisch zu verfassen.

In diesen Kontexten wird die Taste Enter/Return verwendet, um den aktuellen Kandidaten für die Zeichenkonvertierung zu bestätigen. Wenn benutzerdefiniertes JavaScript auf keydown für Enter wartet und sofort sendet, wird die Nachricht des Nutzers gesendet, während er noch Zeichen konvertiert. Das führt zu unvollständigen, fragmentierten oder falschen Nachrichten.

Die Prüfung, ob die Eingabe aktiv ist, gilt für alle benutzerdefinierten keydown-Handler, die auf Enter reagieren, nicht nur für mehrzeilige <textarea>-Felder. Einzeilige <input>-Felder mit eigener Enter-Verarbeitung, z. B. Such- und Suchleisten, Inline-Felder zum Umbenennen, modale Dialogfelder und Befehlspaletten, erhalten denselben IME-bestätigenden Enter-Keydown wie ein <textarea>, wobei isComposing auf true gesetzt ist. Das native implizite Senden eines <form>-Elements aus einem <input>-Element ohne benutzerdefinierten Enter-Handler erfordert keine explizite Verarbeitung, da Browser das implizite Senden unterdrücken, wenn der Enter-Tastendruck von einem IME verarbeitet wird.

Implementierungsstrategie

Prüfen Sie bei einem <textarea> oder <input> mit benutzerdefinierter Eingabe zum Senden das native isComposing-Attribut des KeyboardEvent, bevor Sie den Inhalt senden. Rufen Sie auch event.preventDefault() auf: Die Standardaktion von Enter fügt einen Zeilenumbruch in ein <textarea> ein. In einem <input> innerhalb eines <form> wird die implizite Übermittlung des Browsers ausgelöst, was zu einer zweiten Übermittlung führen würde.

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

Verwenden Sie für ein einzeiliges <input> denselben Handler ohne die !event.shiftKey-Bedingung, die nur vorhanden ist, weil Shift+Enter einen Zeilenumbruch in ein <textarea> einfügt.

Barrierefreiheit und Tests

  1. Explizite Schaltfläche zum Senden: Fügen Sie immer ein <button type="submit">-Element ein. Tastenkombinationen sind Hilfsmittel und dürfen die nativen Formularübermittlungspfade nicht ersetzen.
  2. Barrierefreie Eingaben: Achten Sie darauf, dass alle <input>- und <textarea>-Elemente programmatisch mit einem <label> verknüpft sind, indem Sie übereinstimmende id- und for-Attribute verwenden.

Fallback-Strategien

In neueren Versionen von Safari führte ein Problem mit der Ereignisreihenfolge (WebKit-Fehler 165004) dazu, dass compositionend vor dem bestätigenden Enter keydown an Skript-Handler übermittelt wurde, obwohl die zugrunde liegenden Ereignisse in der umgekehrten Reihenfolge gesendet wurden. Als der Keydown-Handler ausgeführt wurde, war event.isComposing bereits auf false zurückgesetzt worden. Das bedeutet, dass die Standardprüfung allein das vorzeitige Senden nicht verhindert hätte.

Wenn Sie die browserübergreifende Kompatibilität mit älteren Versionen von Safari und anderen Plattformen unterstützen müssen, wenden Sie eine der folgenden Fallback-Strategien an:

Wenn Sie event.isComposing mit einer Prüfung auf event.keyCode === 229 kombinieren, können Sie die fehlerhafte Bestätigung von Enter-keydown in Safari zuverlässig abfangen. Da dies unter dem event.key === 'Enter'-Gate verschachtelt ist, ist es vor virtuellen Tastaturen auf Mobilgeräten geschützt, die möglicherweise 229 für die normale Zeicheneingabe verwenden.

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

Strategie 2: Problemumgehung mit event.timeStamp-Fenster (Alternative)

Bei Codebasen, in denen die Verwendung von verworfenen APIs wie keyCode strengstens verboten ist, oder wenn es bekannte Probleme mit der 229-Prüfung in bestimmten Zielumgebungen gibt, können Sie stattdessen den vom Browser gemeldeten Zeitstempel für die Ereignisweiterleitung erfassen.

Da Safari das bestätigende Enter-keydown-Ereignis extrem nah am compositionend-Ereignis sendet (oft innerhalb von 5 ms und manchmal mit einem etwas früheren keydown-Zeitstempel aufgrund der umgekehrten Reihenfolge der Handler-Ausführung), ist die Überprüfung der Zeitdifferenz sehr zuverlässig und führt unter normalen Bedingungen wahrscheinlich nicht zu falsch positiven Ergebnissen auf mobilen Bildschirmtastaturen.

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