การกด Enter เพื่อส่งที่ปลอดภัยสำหรับ IME

ฟีเจอร์บนเว็บ: เหตุการณ์แป้นพิมพ์

อินเทอร์เฟซแชทจำนวนมากจะส่งข้อความเมื่อผู้ใช้กด Enter ใน <textarea>

ซึ่งวิธีนี้ใช้ได้กับผู้ใช้ที่พิมพ์ด้วยการป้อนข้อมูลโดยตรงจากแป้นพิมพ์ภาษาละติน (เช่น ภาษาอังกฤษ) แต่จะใช้ไม่ได้กับผู้ใช้ที่พิมพ์ด้วยตัวแก้ไขวิธีการป้อนข้อมูล (IME) เพื่อเขียนข้อความในภาษาต่างๆ เช่น ญี่ปุ่น จีน หรือเกาหลี

ในบริบทเหล่านี้ แป้น Enter/Return จะใช้เพื่อยืนยันตัวเลือกการแปลงอักขระปัจจุบัน หาก JavaScript ที่กำหนดเองคอยฟัง keydown ใน Enter และส่งทันที ระบบจะส่งข้อความของผู้ใช้ขณะที่ยังแปลงอักขระอยู่ ซึ่งส่งผลให้ข้อความไม่สมบูรณ์ กระจัดกระจาย หรือไม่ถูกต้อง

การตรวจสอบการเปิดใช้งานการจัดองค์ประกอบจะมีผลกับแฮนเดิล keydown ที่กำหนดเองซึ่งดำเนินการกับ Enter ไม่ใช่เฉพาะช่อง <textarea> แบบหลายบรรทัด ช่อง <input> บรรทัดเดียวที่มีการจัดการ Enter ของตัวเอง เช่น แถบค้นหาและค้นพบ ช่องเปลี่ยนชื่อในบรรทัด ช่องกล่องโต้ตอบโมดัล และชุดคำสั่ง จะได้รับคีย์ดาวน์ Enter ที่ยืนยัน IME เหมือนกับที่ <textarea> ได้รับ โดยตั้งค่า isComposing เป็น true การส่ง <form> โดยนัยแบบเนทีฟจาก <input> ที่ไม่มีเครื่องมือจัดการ Enter ที่กำหนดเองไม่จำเป็นต้องมีการจัดการที่ชัดเจน เนื่องจากเบราว์เซอร์จะระงับการส่งโดยนัยเมื่อ IME ใช้การกดแป้นพิมพ์ Enter

กลยุทธ์การติดตั้งใช้งาน

สำหรับ <textarea> หรือ <input> ที่มีการกด Enter เพื่อส่งแบบกำหนดเอง ให้ตรวจสอบพร็อพเพอร์ตี้ isComposing แบบเนทีฟของ KeyboardEvent ก่อนส่งเนื้อหา นอกจากนี้ ให้เรียก event.preventDefault(): การดำเนินการเริ่มต้นของ Enter จะแทรกการขึ้นบรรทัดใหม่ใน <textarea> และใน <input> ภายใน <form> จะทริกเกอร์การส่งโดยนัยของเบราว์เซอร์ ซึ่งจะส่งเป็นครั้งที่ 2

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