なぜ Web グリッドは日本語入力で「Enter を奪う」のか
TssGrid の看板の1つに 「日本語 IME に強い」 があります。地味な売り文句ですが、 日本の業務システムで Excel ライクな入力画面を作ると、ここが一番ハマるところです。 「変換を確定しただけなのにセルが下に飛ぶ」「打ち始めの1文字目が消える」―― グリッド系のライブラリで日本語を入力したことがある人なら、一度は見た光景だと思います。 この記事は、その原因と、TssGrid がどう設計でそれを避けたかの記録です。
<input> にして、
IME 変換中(isComposing / keyCode === 229)はキー処理を一切しない。
この2つだけで「Enter を奪う」「1文字目が落ちる」はほぼ消えます。症状:日本語を打つと何が起きるか
たとえば氏名欄に「高橋」と入れたいとき、人はこう打ちます。
t a k a h a s iと打つ(「たかはし」と表示される)- スペースで変換 → 「高橋」になる
- Enterで変換を確定する
この最後の Enter は「変換の確定」であって、「入力を終えて次のセルへ」ではありません。 ところが多くのグリッドは、セルの上で押された Enter を無条件に「次のセルへ移動」と解釈します。 結果、変換を確定したつもりがセルが下に飛び、もう一度 Enter を押す羽目になる。 1セルにつき毎回これが起きると、名簿を100行打つ頃には指が覚えるほどのストレスです。
もう1つの定番が 「1文字目が落ちる」。セルを選んだ状態(編集モードに入る前)でいきなり 日本語を打ち始めると、編集用の入力欄にフォーカスが移るより先に最初のキーが捨てられ、 「橋」と打ったはずが「し」だけ残る、といった取りこぼしです。
原因:IME 変換中のキーは「ふつうのキー」ではない
ブラウザは IME で変換している最中、keydown を特別な形で送ってきます。
具体的には event.isComposing === true、古い判定では event.keyCode === 229。
この「変換中サイン」を見ずに key === 'Enter' だけで分岐すると、
確定の Enter と移動の Enter が区別できません。だからセルが飛ぶ。
さらに根が深いのが「セルをどう作っているか」です。セルを <div> や
contenteditable で組み、キー入力をコンテナ要素でまとめて拾う設計だと、
IME の composition(変換)をブラウザのネイティブ機構に正しく預けられず、
変換途中の状態管理を自前で抱え込むことになります。ここを丁寧にやらないと、
前述の取りこぼしや「変換候補ウィンドウが変な位置に出る」が起きます。
TssGrid の解法①:アクティブセルに本物の <input> を重ねる
TssGrid は、いま選んでいるセルの上に本物の <input> 要素を1枚重ねてフォーカスを当てます。
IME は OS/ブラウザのネイティブな仕組みで、この <input> に対して動く――
つまり変換も候補ウィンドウの位置も composition イベントも、全部ブラウザ任せで“正しく”なります。
自前で変換状態を管理しないのが、いちばん堅い。
この設計だと「選択中にいきなり打ち始める」も素直に扱えます。変換が始まった瞬間(compositionstart)に編集モードへ切り替えるので、
最初の打鍵が落ちません。
// 変換が始まった瞬間に nav → edit へ。最初の打鍵を落とさない。 this.editor.addEventListener('compositionstart', () => { if (this.mode !== 'nav') return; this.toEdit(null, false); // 同じ <input> で変換が続くのでシームレス });
TssGrid の解法②:変換中はキー処理を「全部やめる」
キーハンドラの先頭で変換中かどうかを判定し、変換中なら Enter も Tab もショートカットも 一切横取りしません。確定の Enter はブラウザ(IME)にそのまま渡り、セルは飛びません。
_onKey(e) {
const composing = e.isComposing || e.keyCode === 229; // ← 変換中サイン
if (!composing && this._runShortcuts(e)) return; // 変換中はショートカットも無効
if (this.mode === 'edit') {
if (composing) return; // ★ 変換中は Enter / Tab を奪わない
if (e.key === 'Enter') { e.preventDefault(); this.commit(); this._advance('enter'); return; }
// …Tab / Escape も同様
}
}
「変換中サインを見て、見たら手を引く」――やっていることはこれだけです。 派手な仕掛けはありません。が、グリッドの全キー操作(移動・確定・コピペ・Undo)が この1つのガードの内側にあるので、例外なく効きます。
副産物:辞書なしで「フリガナ」が取れる
本物の <input> に composition が正しく届く、ということは
変換前の「読み(かな)」がイベントから取れるということです。
compositionupdate で変換前のかなを覚えておき、compositionend で確定された漢字とペアにすれば、
辞書を一切持たずにフリガナ列を自動入力できます(漢字→読みの辞書引きではなく、
ユーザーが実際に打った読みをそのまま使う方式)。
これは plugins/tss-furigana.js という30行ほどのプラグインで、コア本体は一切いじっていません。
重要なのは、composition を確実に取れる土台があるから、この機能が“あとから載せられる”という点です。
セルを <div> で組んで composition を取りこぼす設計では、そもそも作れません。
「IME に強い」が機能ではなく設計の帰結だ、というのはこういう意味です。
動かしてみる
下のグリッドの氏名欄に、漢字を IME で変換して入力してみてください
(例: たかはし → 変換 → 高橋)。
フリガナ列(カタカナ)が自動で埋まり、変換確定の Enter でセルが飛ばないことが確かめられます。
フリガナは手修正も可能です。
※ このデモは実際の IME 入力で動きます。コピー&ペーストや、最初から入っている見本行からは読みを取得できません(実際に打った読みだけが取れる、という正直な仕様です)。
<input> に任せ、変換中はブラウザの邪魔をしない――
この2つの設計判断の結果として、Enter は奪われず、1文字目も落ちず、フリガナまで取れる。
軽量グリッドのまま日本の業務フォームで戦える理由が、ここにあります。