日本語フォームに混入する半角カナや全角英数を JavaScript で自動変換したい。そう思って、まず input イベントに手を伸ばしました。私もそうでした。ところが検証用の HTML を組んで実測すると、input ハンドラは想像と違う場面でしか動きません。先に答えを書きます。input イベントは IME 経由の入力では発火せず、実質コピペ専用でした。だから今回は、コピペは input で即時変換し、IME 経由を含む最終チェックは blur に任せる、という役割分担にしました。日本語フォーム実装シリーズの仕上げ(サジェスト前後編・フリガナ・IME Enter に続く5本目)です。
検証環境:macOS Tahoe 26.4.1 / Google Chrome 148 / ATOK 35.0.3(2026年5月)。Safari、Firefox、Windows + MS-IME、Google 日本語入力、iOS / Android は未確認です。サーバー側の正規化(WordPress なら mb_convert_kana 一発)には深く触れません。
そもそも、半角カナはどこから混入するのか
自分から進んで半角カナを打つ人は少ないです。でも混入経路は意外と多い。いちばん多いのがコピペで、銀行系の古い帳票や基幹システムは、いまも半角カナが標準の場面があります。そこから値をコピーして貼れば、半角カナのまま入る。Excel や CSV からのコピペでは全角英数(「2024」)も混じります。iOS / Android のソフトウェアキーボードも、機種や設定でカタカナを半角で出すことがあり、ユーザーは気づきません。残るは IME で意図的に出すパターンですが、ここが想像と違いました。ATOK 35 + Chrome 148 で全角英数「ABC」を出そうとすると、ATOK は最初から半角の「ABC」を返してくる。全角英数を出すには、半角で打ってから「全角に変換」というワンアクションが要ります。つまり、半角カナや全角英数の主な混入経路は、IME での意図的入力ではなく、コピペや別アプリからの流入でした。この事実が、後半の実装判断をほぼ決めます。
クライアントとサーバーのどちらで正規化するかは、両方で重ねるのが現実的です。サーバーの mb_convert_kana は確実(JavaScript は無効化されうるし bot には効かない)、クライアントは UX が良い(その場で正しい形に変わるのが見える)。クライアントで第一段階の変換をして UX を担保し、サーバーで念のためもう一度正規化する。保険を二重にかける感覚です。この記事はクライアント側に絞ります。
実測したら、input は IME を素通りしていた
変換を「いつ」走らせるかは、input(入力中)、compositionend(IME 確定直後)、blur(フォーカス離脱)、submit(送信時)の選択肢があります。検証用 HTML を組んで実測したところ、いちばんの驚きは input でした。ATOK で「あいうえお」を打って半角カナ「アイウエオ」に変換しても、value を書き換えるハンドラが一度も発火しません。
| 1 2 3 4 | Bcompositionupdate data="アイウエオ" Binput data="アイウエオ" isComposing=true value="アイウエオ" Bcompositionend data="アイウエオ" value="アイウエオ" Bblur value="アイウエオ" |
表示される input は、すべて isComposing=true です。ハンドラ先頭で if (e.isComposing) return; としているので、IME 経由では本体に到達しない。compositionend のあとに input が追加発火することもなく、value はそのまま。では input ハンドラはいつ動くのか。別アプリから「アイウ」をコピペすると、こうなりました。
| 1 2 3 | Bpaste pastedText="アイウ" Binput data="アイウ" isComposing=false value="アイウ" B⚙ value rewrite "アイウ" → "アイウ" cursor: 3 → 3 |
isComposing=false の input が発火して、書き換えまで一気に動く。コピペした瞬間に半角カナが全角カナへ置き換わり、カーソル位置も維持される。ここで腑に落ちました。入力中ハンドラの本当の用途は、IME 経由の対応ではなく、コピペで流入した値の即時変換だったのです。コピペは文字数が変わらないことが多いので、setSelectionRange を呼ばなくてもカーソルは保たれます。なお、これは ATOK + Chrome の結果で、Windows + MS-IME や Google 日本語入力では IME 経由でも isComposing=false の input が追加発火する可能性があります。
結局、input(コピペ対策)と blur(IME 経由を含む最終チェック)の組み合わせを基本にしました。input は見た目が派手で「反応してる」安心感を与えますが、IME には届かない。blur はコピペでも IME 経由でも最終的に必ず通るので、保険として一番強い。submit 時の一括変換は確実ですが、ユーザーが結果を確認できないので、最後の保険に留めます。
NFKC は便利だが、①や㈱まで分解する
変換の中身は、String.prototype.normalize(‘NFKC’) がほぼ一発です。半角カナを全角カナに、全角英数を半角英数に、両方とも1行で変換できます。
| 1 2 | 'ヤマダ'.normalize('NFKC'); // → "ヤマダ" '2024'.normalize('NFKC'); // → "2024" |
ただ、これに乗ると地雷を踏みます。NFKC は「互換等価」の文字まで展開するからです。
| 1 2 3 | '①'.normalize('NFKC'); // → "1" '㈱'.normalize('NFKC'); // → "(株)" '㎏'.normalize('NFKC'); // → "kg" |
丸付き数字、株式会社の合字、単位記号が全部分解される。ユーザーが「①対応希望」と入れてフォーカスを外した瞬間、「1対応希望」になります。文字数は同じでも、ユーザーから見れば打った文字と違う。だから、NFKC をかけるかはフィールドごとに決めるのが現実的です。姓名・カナ・電話・郵便番号のように文字種が絞られた欄にだけ NFKC を当て、自由記述や住所には触らない。副作用を確実に避けたいなら、半角カナと全角カナ、全角英数と半角英数の対応表だけを持つ自前テーブルを書く手もあります。コードは長くなりますが、それ以外の文字は触らずに済みます。手間と安全のトレードオフです。
カタカナとひらがなは、0x60 ずらすだけ
フリガナ欄で「全角カタカナのみ」にしたい場面があります。カタカナとひらがなは Unicode 上で 0x60(96)離れてきれいに並んでいるので、コードポイントを足し引きするだけで変換できます。
| 1 2 3 4 5 6 7 8 9 | function hiraganaToKatakana(str) { return str.replace(/[\u3041-\u3096]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) + 0x60) ); } function normalizeKana(str) { return hiraganaToKatakana(str.normalize('NFKC')); } |
長音記号「ー」(U+30FC)は、ひらがなにもカタカナのレンジにも入っていません。共通で使う記号なので、変換対象から外しておけば問題ありません。「ヴ」(U+30F4)は範囲に含まれ、-0x60 で「ゔ」になるので、ここは気にしなくて大丈夫です。これで、ユーザーが「やまだ」「ヤマダ」のどちらを入れても、最終的に「ヤマダ」へ揃います。「Yamada」のようなローマ字は別の話なので、バリデーションで弾くか、ローマ字変換を別途仕込みます。
完成形のコードと、リリース前のチェック
ここまでの判断を入れた最小実装です。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | document.querySelectorAll('.js-normalize-kana').forEach((input) => { // コピペ対策(input は isComposing=false のときだけ動く) input.addEventListener('input', (e) => { if (e.isComposing) return; const pos = e.target.selectionStart; const normalized = normalizeKana(e.target.value); if (normalized !== e.target.value) { e.target.value = normalized; e.target.setSelectionRange(pos, pos); } }); // 最終チェック(IME 経由を含むすべての経路の保険) input.addEventListener('blur', (e) => { e.target.value = normalizeKana(e.target.value); }); }); // 送信時の最後の保険 document.querySelector('form').addEventListener('submit', () => { document.querySelectorAll('.js-normalize-kana').forEach((input) => { input.value = normalizeKana(input.value); }); }); |
input 先頭の if (e.isComposing) return; は、実測どおり IME 経由では isComposing=true の input しか来ないので、IME 中の動作に影響せず、コピペで isComposing=false の input が来たときだけ反応します。setSelectionRange は、文字数が変わる変換(半角濁点付きカナのコピペなど)への保険です。blur は最終チェック、submit は JavaScript が無効化された場合などの最後の保険で、サーバー側の正規化と二重に走っても害はありません。
WordPress のフォームプラグイン(Contact Form 7 など)でも、フィールドにクラス名を振って同じ JavaScript を読み込めば流用できます。条件付きフィールドや複数ステップで DOM が動的に増える場合は、MutationObserver で後付けのリスナー登録が要ります。リリース前は、半角カナの入力とコピペ、全角英数、ひらがな→カタカナ、自由記述で①が1にならないか、IME 変換中に値が勝手に書き換わらないか、そしてモバイル実機を確認しておきます。
もう一度、あの送信ログを思い出す
数年前、受託で作ったフォームの送信ログに「ヤマダ タロウ」のような半角カナが混じっていました。当時はサーバーの mb_convert_kana を1行通すだけで片付けて、画面には半角カナのまま残っていた。いま同じ案件をやるなら、クライアント側で先回りして変換する選択肢も出せます。シリーズを通して繰り返し効いたのは、入力規則をどこまで自動化して、どこからユーザーの責任にするか、という判断でした。フリガナ欄を編集可能のままにするのも、自由記述に NFKC をかけないのも、根は同じです。完璧な自動化を目指すと、必ずどこかで副作用が出る。そして検証してログを取ると、予想と違う場面が必ず出てきました。フリガナ実装ではひらがなフィルタが推測変換を意図せず救い、この記事では入力中ハンドラがコピペ専用だった。動かしてみないと分からない。これが、シリーズ5本で繰り返し効いてきた感覚です。
参考にした公式ドキュメント
- MDN Web Docs: String.prototype.normalize()
- Unicode Technical Report #15: Unicode Normalization Forms
- PHP マニュアル: mb_convert_kana
- MDN Web Docs: MutationObserver
関連記事
- 姓名フォームのフリガナ自動入力を composition イベントで自前実装した話。本記事と同じシリーズで、compositionend での値の取り方を解説しています。
- 日本語入力の Enter でフォームが誤送信される問題を直した話|Safari・React・Vue 対応。同じ IME まわりで、Enter の誤送信を防いだ記録です。
- Contact Form 7 で zipaddr-jp が動かなかった話|郵便番号から住所自動入力で踏んだ id 命名規則の罠。同じ Contact Form 7 を使ったフォーム実装です。
- 「あ」で「雨」「赤」を出す日本語サジェストを、HTML と JavaScript だけで作る(基本編)。シリーズ1本目、日本語サジェスト UI の基礎です。


コメント