0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chromeはマウス確定でinputを出しません。日本語IMEの確定イベントを6環境で測り直した

0
Last updated at Posted at 2026-06-02

先に結論を書きます。

日本語の確定を検知するなら、input ではなく compositionend に寄せてください。

理由は3つあります。Safari は確定時に input が2回出ます。Firefox は compositionend のあとにもう1回 input が出ます。そして Chrome と Edge は、マウスのクリックで確定したとき input を一度も出しません。

3つ目がいちばん厄介でした。日本語を使う人は、変換中に入力欄の外をクリックして確定する操作を無意識にやります。そのとき Chrome では確定が一度も検知されません。Safari と Firefox では input が飛ぶので、Chrome で初めて表面化します。

わたしはこれで「たまにメッセージが空で送られる」という再現しにくい不具合を抱えていました。

先に断っておくと

この分野は2019〜2023年あたりの良い記事がいくつもあって、何度も助けられました。isComposing を見ればいい、というのはもう常識です。

ただ、Safari のバージョンが上がって Edge が完全に Chromium になった今、確定まわりの出方が当時とは少しずれています。**古いメモのまま書いたら、Safari と Firefox で普通に取りこぼしました。**それで全部測り直しています。

書くのは、2026年6月時点の6環境ぶんの実測値です。

検証環境(2026-06-02 実測)
Chrome 148 (macOS) / Safari 26.5 (macOS) / Firefox 149 (macOS) / Edge 147 (macOS) / Chrome 148 (Windows) / Edge 148 (Windows)
入力はすべて「あい」。IMEは macOS が ATOK 35、Windows が Microsoft IME。自作のイベントロガー(単一HTML)で全イベントをタイムスタンプ付きに記録しました。
IMEが変わると発火の出方も変わりうるので、環境を明記しておきます。同じ Chrome でも ATOK と Google 日本語入力で差が出る可能性は否定できません。

この記事でわかること

  • 日本語を確定(Enter)したとき、compositionend と input がどの順で出るか。ブラウザで違います
  • keydown.key がブラウザ・OSで a だったり Process だったりします。keyCode は全環境で 229
  • Safari だけ確定時に input が2回。Firefox だけ compositionend の後にもう1回 input
  • 変換をやめたとき(Esc・全消し)は、全環境で compositionend.data が空。確定とキャンセルはここで見分けられます
  • Chrome / Edge はマウスのクリック確定だと input が一切出ません
  • それぞれが実務でどう刺さるか(Enter確定の判定、input回数を数える処理、キャンセル検知)

きっかけ

自作プラグインのチャットUIで、入力欄でEnterを押したら送信、という普通の処理を書いていました。日本語の変換確定のEnterと、送信のEnterを切り分けるあれです。

Safariだけ送信が二重に走る現象が出ました。

最初はデバウンスの設定ミスか、自分のステート管理の問題だと思って、そちらを半日いじっていました。何度ログを仕込んでも、Safariのときだけ送信処理が2回呼ばれます。やっとイベントそのものを疑って、input の発火回数を数えたら、Safariは確定の瞬間に input を2回出していました。Chromeでは1回です。

前提にしていた「確定でinputは1回」が、そもそもブラウザで違っていたわけです。

ここで「Safariが変」で片付けてもよかったのですが、だとすると他のブラウザはどうなのか、自分のチャットUIは結局どの並びを前提に書けば安全なのかが分からないままになります。それで、手持ちの6環境で一度きちんと測り直すことにしました。横着して1ブラウザだけ見て直すと、たいてい別のブラウザで別の穴が開くので。


測り方

入力欄ひとつに、keydown keyup compositionstart compositionupdate compositionend beforeinput input の7つのリスナーを付けただけのページです。発火順に番号・isComposing・inputType・keyCode・経過msを記録します。サーバーは要りません。各ブラウザで開いて、同じ操作をするだけです。コードは記事末に丸ごと置きます。

操作は5パターン。確定(1・2)と、取りやめ(4・5)を中心に見ます。

  1. 「あい」と打って変換し、Enterで確定
  2. 変換せずそのままEnterで確定
  3. 変換中に欄の外をクリックして確定
  4. 変換中にEscでキャンセル
  5. 変換中にBackspaceで全消しして取りやめ

測定の前提と限界

先に、この調査がカバーしていない範囲を書いておきます。あとで「うちでは違った」となったとき、たいていここが原因なので。

測ったのは <input type="text"> だけです。contenteditable な要素やリッチエディタは、composition周りの挙動がさらに変わることが知られていて、今回は対象外にしました。

IMEは、macがATOK 35、WindowsがMicrosoft IME。IMEが変わると未確定文字の出方が変わる場合があるので、たとえばGoogle日本語入力やmacの標準IMだと、細部が今回の表とずれるかもしれません。とくに**macの結果は「ATOKでの値」だと思って読んでください。**OS標準IMでの追試はまだしていません。

それから、物理キーボードでローマ字入力した場合の話です。フリック入力や音声入力、サジェスト確定では、また別の並びになる可能性があります。ここも測っていません。

この記事は「6ブラウザ × 2 OS、input要素、ATOK/MS-IME、ローマ字入力」という条件での実測です。条件が動けば結果も動く前提で、気になる組み合わせは末尾のロガーで試してもらうのが確実です。


いちばんの落とし穴:マウスで確定したとき

冒頭に書いた件です。変換中に、Enterではなく入力欄の外をマウスでクリックして確定するパターンから見ていきます。

**Chrome と Edge(mac・win)は、クリック確定だと compositionend(data:あい)がいきなり1回だけ発火しました。**Enter確定のときに前に出ていた compositionupdate も beforeinput も input も、出ません。

つまりChrome系では、マウス確定のときだけ input がまったく飛びません。

確定処理を input に引っかけて書いていると、Chromeでマウス確定したときだけ、確定が一度も検知されません。Enterやキー操作では拾えていたのに、クリックで確定したときだけ値が取れない、という嫌な取りこぼし方をします。

一方 Safari は、クリック確定でもEnter確定と同じく beforeinput(deleteCompositionText)→ input → beforeinput(insertFromComposition)→ input → compositionend の二段。Firefox も同じく、beforeinput → input → compositionend → 追いかけ input。

**SafariとFirefoxは、Enterだろうがクリックだろうが input を出してくれるので、input頼みでも一応動いてしまいます。**だからChromeで初めて表面化する、という気づきにくさがありました。


変換してEnterで確定したときの発火順

次に、キーボードで確定したときに何がどの順で出るかです。各ブラウザのログから、確定キー以降だけを抜き出して並べます。同じ「あい」を確定しただけなのに、ここまで違うのかと驚きました。

Chrome (macOS) / Edge (macOS)

keydown(Enter, isComposing:true, keyCode:229)
compositionupdate(愛)
beforeinput(愛, insertCompositionText, isComposing:true)
input(愛, insertCompositionText, isComposing:true)
compositionend(愛)
keyup(Enter, isComposing:false, keyCode:13)

compositionend が最後(keyupの直前)。input は確定の前に1回だけ。素直な並びです。

Chrome (Windows) / Edge (Windows)

keydown(Process, isComposing:true, keyCode:229)
compositionupdate(愛)
beforeinput(愛, insertCompositionText, isComposing:true)
input(愛, insertCompositionText, isComposing:true)
compositionend(愛)
keyup(Process, isComposing:false, keyCode:229)   ← keyupがProcess/229のまま

順序はmacと同じです。ただし keydown.key も keyup.key も Process で、確定のkeyupでも keyCode が 13 ではなく 229 のままでした。Windowsでは「keyupのkeyCodeが13だから確定」という判定が効きません。

Safari (macOS)

beforeinput(-, deleteCompositionText, isComposing:true)   ← いったん未確定文字を削除
input(-, deleteCompositionText, isComposing:true)
beforeinput(愛, insertFromComposition, isComposing:true)  ← 確定文字を挿し直し
input(愛, insertFromComposition, isComposing:true)
compositionend(愛)
keydown(Enter, isComposing:false, keyCode:229)            ← キーイベントが後ろに回る
keyup(Enter, isComposing:false, keyCode:13)

Safariだけ別物でした。確定時に「未確定を消す→確定文字を入れる」の二段で動き、input が2回出ます。inputType も deleteCompositionText と insertFromComposition という、他のブラウザでは見ない値が入ります。さらに keydown / keyup が compositionend より後ろに回ります。

Firefox (macOS)

keydown(Process, isComposing:true, keyCode:229)
beforeinput(愛, insertCompositionText, isComposing:true)
input(愛, insertCompositionText, isComposing:true)
compositionend(愛)
input(愛, insertCompositionText, isComposing:false)        ← 確定後にもう1回input
keyup(Enter, isComposing:false, keyCode:13)

Firefoxは compositionend の後に、もう1回 input が出ます。しかもこのinputだけ isComposing:false。「composition中はisComposingで弾く」処理だと、この最後のinputだけがすり抜けます。


確定パターンの早見表

確定時の input 回数 compositionend と input の順 特記
Chrome (mac) 1回 input → compositionend 素直
Edge (mac) 1回 input → compositionend Chromeと一致
Chrome (win) 1回 input → compositionend keyup.keyCodeが229のまま
Edge (win) 1回 input → compositionend 同上
Safari (mac) 2回 input ×2 → compositionend delete→insertの二段、inputType特殊
Firefox (mac) 2回 compositionend → input 確定後の追加inputがisComposing:false

そしてChrome / Edge はクリック確定だと input が0回です。上の表はキーボード確定の値なので、そこだけ別に覚えてください。

「inputの回数を数えて処理する」「compositionendを合図に確定処理する」系のコードは、この差で確実にズレます。


変換をやめたとき(Escキャンセル・Backspace全消し)

確定の裏返しで、変換を取りやめたときも測りました。ここが実装でいちばん事故ります。「キャンセルされた」をどう検知するかが、ブラウザでばらつくからです。

6環境すべてで共通していたのは、一点だけでした。キャンセル時も compositionend は必ず発火し、その event.data が空文字です。

確定したときは compositionend の data に確定文字(「愛」「あい」)が入ります。やめたときは空です。**確定とキャンセルの最終的な見分けは、キー名でもinputTypeでもなく compositionend の data が空かどうかで付きます。**これがいちばん安定した判定でした。

途中の出方はブラウザで割れます。Escキャンセルの場合だけ抜き出すと、こうなりました。

**Chrome / Edge(mac・win共通)**は、Escを押すと compositionupdate(data空)→ beforeinput(insertCompositionText)→ input → compositionend(data空)。確定のときと同じ並びで、中身だけ空になります。

Safari は、beforeinput(deleteCompositionText)→ input → compositionend(data空)。確定のときに出ていた insertFromComposition(確定文字の挿し直し)が、キャンセルでは出ません。delete だけで終わります。Safariは、確定なら delete + insert、キャンセルなら delete のみ、で inputType から区別が付きます。

Firefox は、compositionupdate(data空)→ beforeinput → input → compositionend(data空)→ さらに input(isComposing:false)。確定のときと同じく、compositionend の後にもう一回 input が来ます。キャンセルでもこの追いかけinputは消えませんでした。

Backspaceで一文字ずつ消して全消しした場合も、最後の一文字が消えた瞬間に上と同じ「空のcompositionend」で終わります。違いは、消す途中で compositionupdate が「あ」→空と段階的に出ること(Backspace押下のたびにupdateが発火)。Firefox / WindowsのChrome・Edgeでは、この間の keydown.key も Process のままでした。

変換せずそのままEnterで確定した場合(未変換確定)も触れておくと、並びは変換確定とまったく同じで、compositionend の data が漢字ではなくかな(「あい」)になるだけでした。ここはブラウザ差がありません。


keydown.key と keyCode のブラウザ × OS 差

IME入力中(変換が確定する前)の keydown で取れる値です。

環境 keydown.key keydown.keyCode
Chrome (mac) 実際のキー(a i Enter) 229
Edge (mac) 実際のキー 229
Safari (mac) 実際のキー(ただし発火が後ろ) 229
Firefox (mac) Process 229
Chrome (win) Process 229
Edge (win) Process 229

同じChromeでも、macは生のキー、Windowsは Process を返します。エンジン(Chromium)ではなくOSで割れる、というのが実測のいちばんの驚きでした。

keyCode はIME入力中、全6環境で一貫して 229 でした。if (e.keyCode === 229) での判定は今も全環境で動くものの、keyCode 自体が非推奨です。新しく書くなら isComposing を使うのが筋です。

なぜ 229 で、なぜ Process なのか

この 229 と Process は、出どころが同じです。OSがキー入力をIMEに渡して処理させている最中、ブラウザはそのキーが何の文字になるかをまだ確定できません。そこで「いまIMEが処理中」を表す番兵の値を返します。それが keyCode では 229、KeyboardEvent.key では "Process" です。古い記事で見かける keyCode === 229 判定は、この番兵を拾ってIME入力中かどうかを当てる手法でした。

ただ keyCode は仕様上すでに deprecated で、いずれ消える前提の値です。代わりに用意されたのが KeyboardEvent.isComposing で、2016年あたりのモダンブラウザから入りました。今回の実測でも、IME入力中の keydown は6環境すべてで isComposing:true を返したので、判定はこちらに寄せて問題ありません。番兵を覚えておく必要はもうない、ということです。

key が Process になるか実際のキー名になるかは、上で見たとおりOSで割れました。仕様としては「IMEが処理しているなら Process」が正で、macのChrome・Edge・Safariが生のキー名を返すほうが、むしろ仕様から外れている挙動です。なので「macは親切に実キーをくれる」とあてにせず、key の値はIME入力中は信用しない、と決めておくのが安全でした。


isComposing はどこで true / false になるか

compositionstart から compositionend までが true、というのが基本仕様で、6環境ともそこは守られていました。ただし境界に癖があります。

確定キーの keydown の時点では、6環境とも isComposing:true です。これは助かります。「keydownのisComposingがtrueなら変換確定のEnterなので送信しない」が、全環境で正しく効きます。Enter送信を切り分けたいなら、ここがいちばん安定した判定点でした。

注意は確定後です。Firefoxの確定後の追加 input は isComposing:false、Safariの確定の input(insertFromComposition)は isComposing:true。inputイベント側で isComposing を見る場合、ブラウザで真偽が割れるので、確定の検知はinputではなくkeydownかcompositionendに寄せるのが安全でした。


実務でハマる3点(自分が踏んだ順)

**ひとつ目、Safariの二重input。**確定時にinputが2回出るので、「inputのたびに送信候補を組み立てる」処理がSafariだけ2回走りました。最初の送信二重化はこれが原因です。確定の合図をinputではなく compositionend(か、keydownのisComposing判定)に寄せたら直りました。

ふたつ目、Firefoxの確定後input。compositionend で確定処理を済ませた後に isComposing:false のinputがもう1回来るので、ここで値を読み直す処理を入れていると、Firefoxだけ一回多く走ります。compositionend の中で完結させて、その直後のinputは無視する、で揃いました。

**みっつ目、Chromeのクリック確定。**冒頭に書いたものです。inputが一度も飛ばないので、input頼みだと確定を丸ごと取りこぼします。これも compositionend に寄せると消えました。

Enter送信の切り分けは、keydown で isComposing を見るのが全環境で一番安定でした。Windowsで keyCode が229のままだったり、key が Process だったりするので、key名やkeyCodeでの分岐はやめて、isComposing 一本に倒すのが事故りません。

field.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && !e.isComposing) {
    // 変換確定ではないEnter。ここで送信
    send();
  }
});

e.key === 'Enter' の判定は、Windows / Firefoxで keydown.key が Process になる間は通らず、確定しきって isComposing:false になった本物のEnterだけ通ります。結果的に、これがいちばん素直に効きました。

入力が確定したのか取りやめられたのかまで知りたいなら、compositionend の data を見ます。「やめたときは空」がそのまま効きます。全環境で共通だったので、ここはブラウザ分岐が要りません。

// 確定とキャンセルを分けて拾う。compositionend.data が空ならキャンセル。
field.addEventListener('compositionend', (e) => {
  if (e.data) {
    // e.data が確定文字(「愛」「あい」など)。ここで確定処理を1回だけ行う
    onCommit(e.data);
  } else {
    // 空=Escや全消しで取りやめた。確定処理は走らせない
    onCancel();
  }
});

compositionend は確定でもキャンセルでも、Enterでもクリックでも、各操作につき1回だけです。ここに寄せると、6環境で回数が揃いました。最初に踏んだSafariの二重発火も、Chromeのクリック確定の取りこぼしも、これで両方消えています。

Reactなどで onChange(中身は input)を使っているときも考え方は同じです。isComposing 中の input は値の確定とみなさず状態だけ更新し、確定の確定は compositionend で取る。これで未確定文字が確定値として漏れるのを防げます。

やりたいこと別・どのイベントを見るか

6環境ぶんを踏まえて、目的ごとに「これを見ておけば全環境で揃う」というのを表にしました。自分が次にチャットUIを書くときの早見表でもあります。

やりたいこと 見るもの なぜ
Enterでの送信と変換確定を分ける keydown の isComposing 確定キーのkeydownは全環境でtrue。key名やkeyCodeはOSで割れる
入力が確定した値を取る compositionend の data 各操作で1回だけ。inputの回数はブラウザで割れる
確定とキャンセルを見分ける compositionend.data が空かどうか 空ならEsc/全消し。全環境共通
変換中の途中文字を表示する compositionupdate の data キー入力ごとに最新の未確定文字が入る
IME入力中かを常時持っておく compositionstart/compositionend で自前フラグ isComposing の境界が確定後にぶれる環境があるため

最後の行だけ補足します。isComposing はだいたい信用できるのですが、確定直後のinput(Firefoxの追いかけinputや、Safariのinsert)でtrue / falseがブラウザでぶれます。なので「いまIME中か」を厳密に持ちたいなら、compositionstart で自分のフラグを立て、compositionend で倒す、という昔ながらのやり方が、結局いちばん事故りませんでした。

let composing = false;
field.addEventListener('compositionstart', () => { composing = true; });
field.addEventListener('compositionend',   () => { composing = false; });
// どこからでも composing で「IME中か」を判定できる

測れていない範囲

手元のVivaldi・Braveは測り切れていません。中身がChromiumなので、おそらくOSに従ってmacはChrome(mac)、WindowsはChrome(win)と同じ並びになるはずですが、「はず」で書くのは趣旨に反するので、測れたら追記します。Firefox(Windows)も同様に未確認です。

contenteditable も対象外です。リッチエディタを使っている方は、同じ結論がそのまま当てはまらない可能性があります。


検証に使ったロガー

単一HTMLです。保存してブラウザで開き、入力欄に「あい」と打って各パターンを試すと、発火順のログが出ます。「結果をコピー」でタブ区切りテキストになります。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IME入力イベント検証ロガー</title>
<style>
  :root {
    --bg: #14161a;
    --panel: #1c1f26;
    --line: #2a2f3a;
    --ink: #e6e9ef;
    --muted: #9aa3b2;
    --accent: #ffd166;
    --true: #6ee7a0;
    --false: #6b7280;
    --kbd: #7cc4ff;
    --comp: #ffd166;
    --inp: #f4a8d0;
    --beforeinp: #c4a8f4;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0; background: var(--bg); color: var(--ink);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace;
    line-height: 1.5; padding: 20px;
  }
  h1 { font-size: 18px; margin: 0 0 4px; }
  .sub { color: var(--muted); font-size: 13px; margin: 0 0 16px; }
  .env {
    background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
    padding: 12px 14px; margin-bottom: 16px; font-size: 13px; color: var(--muted);
    word-break: break-all;
  }
  .env b { color: var(--ink); }
  .controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
  label.field { font-size: 13px; color: var(--muted); display: flex; flex-direction: column; gap: 4px; }
  input[type=text] {
    background: #0e1014; border: 1px solid var(--line); color: var(--ink);
    border-radius: 6px; padding: 10px 12px; font: inherit; font-size: 15px; min-width: 280px;
  }
  input[type=text]:focus { outline: 2px solid var(--accent); border-color: var(--accent); }
  button {
    background: #262b34; color: var(--ink); border: 1px solid var(--line);
    border-radius: 6px; padding: 9px 14px; font: inherit; font-size: 13px; cursor: pointer;
  }
  button:hover { border-color: var(--accent); }
  button.primary { background: var(--accent); color: #111; border-color: var(--accent); font-weight: 700; }
  .hint { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; margin-bottom: 16px; font-size: 13px; }
  .hint ol { margin: 6px 0 0; padding-left: 20px; }
  .hint li { margin: 3px 0; }
  table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
  th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
  th { color: var(--muted); font-weight: 600; position: sticky; top: 0; background: var(--bg); }
  .logwrap { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; overflow: auto; max-height: 60vh; }
  .ev { font-weight: 700; }
  .ev-keydown, .ev-keyup { color: var(--kbd); }
  .ev-compositionstart, .ev-compositionupdate, .ev-compositionend { color: var(--comp); }
  .ev-input { color: var(--inp); }
  .ev-beforeinput { color: var(--beforeinp); }
  .t { color: var(--true); }
  .f { color: var(--false); }
  .n { color: var(--muted); }
  .seq { color: var(--muted); }
  .marker td { background: #0e1014; color: var(--accent); text-align: center; font-style: italic; }
  textarea {
    width: 100%; height: 140px; margin-top: 12px; background: #0e1014; color: var(--ink);
    border: 1px solid var(--line); border-radius: 8px; padding: 12px; font: inherit; font-size: 12px;
  }
</style>
</head>
<body>
  <h1>IME入力イベント検証ロガー</h1>
  <p class="sub">日本語を入力・変換・確定して、発火するイベントとプロパティを発火順に記録します。サーバー不要。各ブラウザで同じ操作をして結果を見比べてください。</p>

  <div class="env" id="env">User Agent: 読み込み中…</div>

  <div class="hint">
    <b>計測の手順(5パターンを順番に。各パターンの前に「区切りを挿入」を押すとログが見やすくなります)</b>
    <ol>
      <li>「あい」と打って <b>変換 → Enterで確定</b></li>
      <li>「あい」と打って <b>変換せずそのままEnterで確定</b></li>
      <li>「あい」と打って <b>変換中にマウスで欄の外をクリックして確定</b></li>
      <li>「あい」と打って <b>変換中にEscでキャンセル</b></li>
      <li>「あい」と打って <b>Backspaceで全部消して取りやめ</b></li>
    </ol>
  </div>

  <div class="controls">
    <label class="field">入力欄(ここに日本語を打つ)
      <input type="text" id="field" autocomplete="off" placeholder="ここで日本語を入力・変換・確定" />
    </label>
    <button id="marker">区切りを挿入</button>
    <button id="clear">ログをクリア</button>
    <button class="primary" id="copy">結果をコピー</button>
  </div>

  <div class="logwrap">
    <table>
      <thead>
        <tr>
          <th>#</th><th>event</th><th>key / data</th>
          <th>isComposing</th><th>inputType</th><th>keyCode</th><th>+ms</th>
        </tr>
      </thead>
      <tbody id="log"></tbody>
    </table>
  </div>

  <textarea id="out" readonly placeholder="「結果をコピー」を押すと、ここに貼り付け用のテキストが入ります(記事の表のもとになります)"></textarea>

<script>
(function () {
  var field = document.getElementById('field');
  var logBody = document.getElementById('log');
  var out = document.getElementById('out');
  var seq = 0;
  var last = null;
  var rows = []; // 保存用

  document.getElementById('env').innerHTML =
    '<b>User Agent:</b> ' + navigator.userAgent +
    '<br><b>記録開始:</b> ' + new Date().toLocaleString();

  function val(v) {
    if (v === true) return '<span class="t">true</span>';
    if (v === false) return '<span class="f">false</span>';
    if (v === undefined || v === null || v === '') return '<span class="n">—</span>';
    return v;
  }

  function logEvent(e) {
    seq++;
    var now = performance.now();
    var delta = last === null ? 0 : Math.round(now - last);
    last = now;

    var isComp =
      (typeof e.isComposing === 'boolean') ? e.isComposing : undefined; // KeyboardEvent / InputEvent
    var inputType = (e.inputType !== undefined) ? e.inputType : undefined;
    var keyCode = (e.keyCode !== undefined && (e.type === 'keydown' || e.type === 'keyup')) ? e.keyCode : undefined;

    // data: composition系は e.data、key系は e.key
    var dataField;
    if (e.type.indexOf('composition') === 0) dataField = e.data;
    else if (e.type === 'input' || e.type === 'beforeinput') dataField = e.data;
    else dataField = e.key;

    rows.push({
      seq: seq, type: e.type, data: dataField,
      isComposing: isComp, inputType: inputType, keyCode: keyCode, delta: delta
    });

    var tr = document.createElement('tr');
    tr.innerHTML =
      '<td class="seq">' + seq + '</td>' +
      '<td class="ev ev-' + e.type + '">' + e.type + '</td>' +
      '<td>' + val(dataField) + '</td>' +
      '<td>' + val(isComp) + '</td>' +
      '<td>' + val(inputType) + '</td>' +
      '<td>' + val(keyCode) + '</td>' +
      '<td class="n">' + delta + '</td>';
    logBody.appendChild(tr);
    tr.scrollIntoView({ block: 'nearest' });
  }

  // beforeinput は input の前に発生。順序確認のため両方取る
  ['keydown', 'keyup', 'compositionstart', 'compositionupdate',
   'compositionend', 'beforeinput', 'input'].forEach(function (type) {
    field.addEventListener(type, logEvent);
  });

  document.getElementById('marker').addEventListener('click', function () {
    rows.push({ marker: true });
    var tr = document.createElement('tr');
    tr.className = 'marker';
    tr.innerHTML = '<td colspan="7">— 区切り —</td>';
    logBody.appendChild(tr);
    last = null;
  });

  document.getElementById('clear').addEventListener('click', function () {
    rows = []; seq = 0; last = null; logBody.innerHTML = ''; out.value = '';
  });

  document.getElementById('copy').addEventListener('click', function () {
    var lines = [];
    lines.push('UA: ' + navigator.userAgent);
    lines.push('time: ' + new Date().toISOString());
    lines.push('seq\tevent\tkey/data\tisComposing\tinputType\tkeyCode\t+ms');
    rows.forEach(function (r) {
      if (r.marker) { lines.push('--- 区切り ---'); return; }
      function s(v) { return (v === undefined || v === null || v === '') ? '-' : v; }
      lines.push([r.seq, r.type, s(r.data), s(r.isComposing), s(r.inputType), s(r.keyCode), r.delta].join('\t'));
    });
    out.value = lines.join('\n');
    out.select();
    try { document.execCommand('copy'); } catch (err) {}
  });
})();
</script>
</body>
</html>

次の自分に渡すメモ

日本語入力まわりは、「isComposingを見れば大丈夫」で片付けてきましたが、確定の瞬間のinputの回数と順序は、ブラウザでまだこれだけ割れています。

次に同じ実装をするときは、確定の合図をinputに置かない。keydownのisComposingか、compositionendに寄せる。これだけ守れば、また半日溶かさずに済むはずです。

数字は2026年6月時点の、手元の実測です。しかもmacはATOK、WindowsはMicrosoft IME、input要素、ローマ字入力、という限られた条件での値です。ブラウザのバージョンが上がっても、IMEが違っても、細部は変わります。

鵜呑みにせず、気になったら同じロガーで自分の環境を測り直してください。**別のIMEやcontenteditableで試した結果をコメントで教えてもらえると、この表がもっと正確になります。**むしろ、それを集めるためにロガーを同梱したようなものです。


ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?