私が歌川です

@utgwkk が書いている

入力フォームに対してIMEの確定を待ってからEnterでなんかするやつ

入力フォームがあって、Enterを押して入力したテキストをリストに追加したいとしましょう。↓こちらでお試しできます。

  • ここに追加されるよ

こういう感じで実装できます。簡単ですね。

(() => {
  const list = document.querySelector("#list_1");
  const input = document.querySelector("#input_1");

  input.addEventListener("keydown", (e) => {
    if (e.key !== "Enter") {
      return;
    }

    e.preventDefault();

    const text = e.target.value;

    const li = document.createElement("li");
    li.textContent = text;
    list.appendChild(li);

    e.target.value = "";
  });
})();

ところでこの実装には問題があって、IMEの変換をEnterで確定すると、環境によっては確定直前? 直後? の文字がそのまま追加されてしまうことがあります。

以下はMacのGoogle Chrome 91での動作例です。AquaSKKの確定直前の文字 ▼犬 が追加されてしまっています。これはSKKに限らず、たとえばMacデフォルトのIMEでも同じ問題が発生します。困りましたね。

gyazo.com

IMEの変換中はEnterを押してもリストに追加されてほしくないですね。いいプロパティがないか探してみると KeyboardEvent.isComposing というものがあることに気づきます。

developer.mozilla.org

IMEによる変換中は真を、そうでない場合は偽を返すようです。これは使えそうですね。そうして改良した入力フォームが↓こちらです。

  • ここに追加されるよ
(() => {
  const list = document.querySelector("#list_2");
  const input = document.querySelector("#input_2");

  input.addEventListener("keydown", (e) => {
    if (e.key !== "Enter" || e.isComposing) {
      return;
    }

    e.preventDefault();

    const text = e.target.value;

    const li = document.createElement("li");
    li.textContent = text;
    list.appendChild(li);

    e.target.value = "";
  });
})();

これでMacのChromeでも ▼犬 が入力されずに済みました。よかったですね。


ところで、改良した実装でもまだ問題になるケースがあります。Mac Safariで 犬と猫 という文字を追加してみましょう……。

gyazo.com

おや、まだ しか入力していないのにリストに追加されてしまいました。どうやらSafariでは keydown イベントの isComposing プロパティを見る方法でもうまくいかないようです。

2019年の情報ですが、以下の記事が参考になります。Safariでは「Enter押下で確定」時の keydown イベントの isComposing が真にならないようでした。

qiita.com

なんとかできないか、とちょっと考えた末に以下の作戦を考えました。

  • compositionstart イベントと compositionend イベントを自前で監視して、 compositionend イベントが発火した直後かどうかを判定できるようにする
  • Safariでは compositionend イベントの発火直後の keydown イベントを間引く ↑の判定を KeyboardEvent.isComposing の代わりに使う
    • 2021/6/30: 「 keydown イベントを間引く」よりも「自前で compositionend イベントを監視して判定する」の方が表現として適切そうだったので修正しました

これを試したのが↓の入力フォームです。いかがでしたか?

  • ここに追加されるよ
(() => {
  const list = document.querySelector("#list_3");
  const input = document.querySelector("#input_3");

  // SafariっぽいUAのとき、compositionend イベントの直後かどうか判定できるようにする
  const isSafari = navigator.userAgent.includes("Safari/") && navigator.userAgent.includes("Version/");

  let isCompositionFinished = true;

  input.addEventListener("keydown", (e) => {
    if (isSafari && isCompositionFinished) {
      isCompositionFinished = false;
      return;
    }
    if (e.key !== "Enter" || e.isComposing) {
      return;
    }

    e.preventDefault();

    const text = e.target.value;

    const li = document.createElement("li");
    li.textContent = text;
    list.appendChild(li);

    e.target.value = "";
  });

  input.addEventListener("compositionstart", () => {
    isCompositionFinished = false;
  });

  input.addEventListener("compositionend", () => {
    isCompositionFinished = true;
  });
})();

こういうことをしなくても済むようになりたいですね。もっといい方法があれば教えてください。