TssGrid MIT

なぜ Web グリッドは日本語入力で「Enter を奪う」のか

設計 株式会社スリースターソフトウェア

TssGrid の看板の1つに 「日本語 IME に強い」 があります。地味な売り文句ですが、 日本の業務システムで Excel ライクな入力画面を作ると、ここが一番ハマるところです。 「変換を確定しただけなのにセルが下に飛ぶ」「打ち始めの1文字目が消える」―― グリッド系のライブラリで日本語を入力したことがある人なら、一度は見た光景だと思います。 この記事は、その原因と、TssGrid がどう設計でそれを避けたかの記録です。

結論から: 編集セルを 本物の <input> にして、 IME 変換中(isComposing / keyCode === 229)はキー処理を一切しない。 この2つだけで「Enter を奪う」「1文字目が落ちる」はほぼ消えます。

症状:日本語を打つと何が起きるか

たとえば氏名欄に「高橋」と入れたいとき、人はこう打ちます。

  1. t a k a h a s i と打つ(「たかはし」と表示される)
  2. スペースで変換 → 「高橋」になる
  3. 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文字目も落ちず、フリガナまで取れる。 軽量グリッドのまま日本の業務フォームで戦える理由が、ここにあります。