検証環境(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が一切出ない。input頼みの確定処理は穴が開く。 - それぞれが実務でどう刺さるか(Enter確定の判定、input回数を数える処理、キャンセル検知)。
この分野は2019〜2023年あたりの良い記事がいくつもあって、自分も何度も助けられました。ただ、Safariのバージョンが上がってEdgeが完全にChromiumになった今、確定まわりの出方が当時とは少しずれています。古いメモのまま書いたら、SafariとFirefoxで普通に取りこぼした。それで全部測り直しました。2026年6月時点の、手元の実測値です。
きっかけ
自作プラグインのチャットUIで、入力欄でEnterを押したら送信、という普通の処理を書いていました。日本語の変換確定のEnterと、送信のEnterを切り分けるあれです。isComposing を見ればいい、というのはもう常識になっていますが、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)を中心に見ます。
- 「あい」と打って変換し、Enterで確定
- 変換せずそのままEnterで確定
- 変換中に欄の外をクリックして確定
- 変換中にEscでキャンセル
- 変換中に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 (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のまま
keyup(Escape...) 等
順序は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だけがすり抜けます。
変換をやめたとき(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 が漢字ではなくかな(「あい」)になるだけでした。ここはブラウザ差がありません。
マウスで確定したとき(クリック確定)
ここがいちばんの落とし穴でした。変換中に、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で初めて表面化する、という気づきにくさがありました。
結論はキャンセルのときと同じです。確定の合図を input に置くと、Chromeのクリック確定で穴が開く。compositionend に寄せれば、Enter・未変換Enter・クリック・全環境で1回ずつ拾えて揃います。この記事でいちばん伝えたいのは、突き詰めるとこの一点です。
確定パターンの早見表
| 確定時の 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 |
「inputの回数を数えて処理する」「compositionendを合図に確定処理する」系のコードは、この差で確実にズレます。
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は無視する、で揃いました。
みっつ目、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();
}
});
大事なのは、確定処理を input ではなく compositionend に置くことです。Safariは確定時に input が2回、Firefoxは compositionend の後にもう1回 input が来るので、inputを数える設計だとその差をまともに食らいます。さらにChrome/Edgeはマウスのクリック確定で input を一切出さないので、input頼みだとその確定を丸ごと取りこぼします。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 で倒す、という昔ながらのやり方が、結局いちばん事故りませんでした。イベントの isComposing を毎回読むより、自前フラグのほうが安定する場面がまだ残っています。
let composing = false;
field.addEventListener('compositionstart', () => { composing = true; });
field.addEventListener('compositionend', () => { composing = false; });
// どこからでも composing で「IME中か」を判定できる
Chromium系(Vivaldi / Brave)と Firefox(Windows) について
手元のVivaldi・Braveはまだ測り切れていません。中身がChromiumなので、おそらくOSに従ってmacはChrome(mac)、WindowsはChrome(win)と同じ並びになるはずですが、「はず」で書くのは趣旨に反するので、測れたら追記します。Firefox(Windows)も同様に未確認です。ここは自信のない箇所として残しておきます。
検証に使ったロガー
単一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で試した結果をコメントで教えてもらえると、この表がもっと正確になります。むしろ、それを集めるためにロガーを同梱したようなものです。