שליחה בטוחה של IME באמצעות Enter

תכונות באינטרנט: אירועי מקלדת

בהרבה ממשקי צ'אט, ההודעה נשלחת כשהמשתמש לוחץ על Enter ב-<textarea>.

השיטה הזו עובדת למשתמשים שמקלידים ישירות במקלדת לטינית (למשל, אנגלית), אבל לא עובדת למשתמשים שמקלידים באמצעות עורך שיטת קלט (IME) כדי לכתוב טקסט בשפות כמו יפנית, סינית או קוריאנית.

בהקשרים האלה, המקש Enter/Return משמש לאישור המועמד הנוכחי להמרת תווים. אם קוד JavaScript בהתאמה אישית מאזין לאירוע keydown ב-Enter ושולח מיד, ההודעה של המשתמש נשלחת בזמן שהוא עדיין ממיר תווים, וכתוצאה מכך ההודעות לא שלמות, מקוטעות או שגויות.

הבדיקה של composition-active חלה על כל handler מותאם אישית של keydown שפועל על Enter, ולא רק על שדות <textarea> עם כמה שורות. שדות <input> בשורה אחת עם טיפול משלהם ב-Enter, כמו סרגלי חיפוש ומציאה, שדות לשינוי שם בתוך השורה, שדות של תיבות דו-שיח מודאליות ולוחות פקודות, מקבלים את אותו אירוע keydown של Enter לאישור IME כמו <textarea>, כאשר isComposing מוגדר ל-true. שליחה מרומזת של <form> מ-<input> ללא handler מותאם אישית של 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> בשורה אחת, משתמשים באותו handler בלי התנאי !event.shiftKey, שקיים רק כי Shift+Enter מוסיף שורה חדשה ב-<textarea>.

נגישות ובדיקות

  1. לחצן שליחה מפורש: תמיד צריך לכלול רכיב <button type="submit">. מקשי קיצור הם כלי עזר, והם לא אמורים להחליף את הנתיבים המקוריים לשליחת טפסים.
  2. רכיבי קלט נגישים: ודאו שכל הרכיבים מסוג <input> ו-<textarea> משויכים באופן פרוגרמטי ל-<label> באמצעות מאפייני id ו-for תואמים.

אסטרטגיות חלופיות

בגרסאות האחרונות של Safari, בעיה בסדר האירועים (באג 165004 ב-WebKit) גרמה לכך ש-compositionend נשלח ל-script handlers לפני אישור Enter keydown, למרות שהאירועים הבסיסיים נשלחו בסדר ההפוך. עד שה-handler של keydown פעל, event.isComposing כבר אופס ל-false, כלומר הבדיקה הרגילה לבדה לא הייתה מונעת שליחה מוקדמת.

אם אתם צריכים לתמוך בתאימות בין דפדפנים בגרסאות ישנות יותר של Safari ובפלטפורמות אחרות, אתם יכולים להשתמש באחת מהאסטרטגיות החלופיות הבאות:

אם משלבים את event.isComposing עם בדיקה של event.keyCode === 229, אפשר לזהות באופן מהימן את האישור הלא תקין של Safari ללחיצה על מקש Enter. מכיוון שהשער הזה מקונן מתחת לשער 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 שולח את אירוע ה-keydown המאשר של Enter קרוב מאוד לאירוע של compositionend (לרוב בטווח של 5 אלפיות השנייה, ולפעמים עם חותמת זמן של keydown שהיא מעט מוקדמת יותר בגלל היפוך סדר המסירה של ה-handler), בדיקת הפרש הזמן היא אמינה מאוד ולא סביר שתפעיל תוצאות חיוביות מוטעות במקלדות וירטואליות בנייד בתנאים רגילים.

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