【第2回】ひらがな入力で漢字候補を表示する検索サジェストの作り方|IME対応とJavaScript実装の基本編

日本語入カイベントを攻略せよ!【実装編】 compositionstart/endを理解して、正確な入力をキャッチ! Program
この記事は約23分で読めます。

Part 1では、「HTMLサジェストで『あ→雨』を実現する」ための基礎知識と問題の本質について解説しました。IMEの変換候補は取得できないこと、代わりに「読みによるプレフィックス検索」を使うこと、そしてIMEとの共存が課題になることを学びました。

Part 2となるこの記事では、いよいよJavaScriptによる実装に入ります。「プログラミングは苦手」という方も、安心してください。一行一行、なぜそのコードが必要なのかを丁寧に解説していきます。

この記事を読み終える頃には、以下のことができるようになっているはずです。

  • JavaScriptのイベント処理の仕組みを理解する
  • IMEの合成中かどうかを判定するコードを書ける
  • debounce(遅延実行)を実装できる
  • プレフィックス検索のロジックを理解し、実装できる

では、始めましょう。


このシリーズの全体像

📚 シリーズ記事一覧

Part 1:なぜ難しい?仕組みを徹底理解

▶ Part 2:IME対応とJavaScript実装の基本(この記事)

Part 3:実践コード完全版と動作デモ

Part 4:応用テクニックと本番運用


第1章:JavaScriptのイベント処理を基礎から理解する

サジェスト機能を実装するには、JavaScriptの「イベント処理」を理解する必要があります。まずは、イベント処理の基本から確認していきましょう。

1-1. イベントとは何か

Webページ上で起きる「出来事」のことを、プログラミングではイベントと呼びます。

例えば、以下のようなものがすべてイベントです。

  • ボタンをクリックした → clickイベント
  • 入力欄に文字を入力した → inputイベント
  • キーボードのキーを押した → keydownイベント
  • ページの読み込みが完了した → loadイベント
  • マウスカーソルを要素の上に乗せた → mouseoverイベント

JavaScriptでは、これらのイベントを「監視」して、イベントが発生したときに特定の処理を実行することができます。この仕組みをイベント駆動(イベントドリブン)と呼びます。

1-2. イベントリスナーの登録方法

イベントを監視するには、イベントリスナーを登録します。「この要素で、このイベントが発生したら、この処理を実行してね」とブラウザに伝えるのです。

基本的な書き方は以下の通りです。

具体例を見てみましょう。入力欄(<input>要素)に文字が入力されたときに、その内容をコンソールに出力するコードです。

このコードでは、inputイベントが発生するたびに、入力欄の現在の値(event.target.value)がコンソールに出力されます。

💡 初心者向けポイント

event.targetは、イベントが発生した要素(この場合は入力欄)を指します。event.target.valueで、その要素の現在の値を取得できます。

1-3. inputイベントとchangeイベントの違い

入力欄に関連するイベントには、inputchangeの2種類があります。似ているようで、動作が異なるので注意が必要です。

inputイベントは、入力欄の値が変わるたびに発火します。1文字入力するごとに、1回ずつイベントが発生します。リアルタイムに入力を追跡したい場合に使います。

changeイベントは、入力欄からフォーカスが外れたとき(他の要素をクリックしたときなど)に発火します。入力が「完了」したタイミングで処理を実行したい場合に使います。

サジェスト機能では、ユーザーの入力にリアルタイムに反応したいので、inputイベントを使います

1-4. アロー関数で書く

最近のJavaScriptでは、関数を短く書けるアロー関数という記法がよく使われます。先ほどのコードをアロー関数で書き直すと、以下のようになります。

この記事では、読みやすさを重視して、アロー関数を使った記法で解説を進めます。


第2章:日本語入力の特殊なイベントを理解する

ここからが、日本語サジェスト実装の核心部分です。日本語入力には、英語入力にはない特殊なイベントがあります。

2-1. 日本語入力で発生するイベントの流れ

日本語をIMEで入力すると、どのようなイベントが発生するのでしょうか。「あめ」と入力して確定するまでの流れを、詳しく見てみましょう。

ステップ1:最初の文字を入力

キーボードで「a」を押すと、以下のイベントが発生します。

1. keydown イベント(キーが押された)

2. compositionstart イベント(IME合成が開始された)

3. compositionupdate イベント(合成中のテキストが「あ」に更新された)

4. input イベント(入力欄の値が変わった)

5. keyup イベント(キーが離された)

ステップ2:続きの文字を入力

キーボードで「m」「e」と押すと、それぞれ以下のイベントが発生します。

「m」を押したとき:

1. keydown イベント

2. compositionupdate イベント(合成中のテキストが「あm」に更新)

3. input イベント

4. keyup イベント

 

「e」を押したとき:

1. keydown イベント

2. compositionupdate イベント(合成中のテキストが「あめ」に更新)

3. input イベント

4. keyup イベント

ステップ3:確定する

Enterキーを押して確定すると、以下のイベントが発生します。

1. keydown イベント

2. compositionend イベント(IME合成が終了した)

3. input イベント

4. keyup イベント

注目すべきポイントは、compositionstartからcompositionendまでの間です。この間、IMEは「合成中」の状態にあり、入力はまだ確定していません。

2-2. compositionイベントの重要性

composition系のイベントは、日本語サジェスト実装において極めて重要です。

compositionstart:IMEによる文字の合成が開始されたときに発火します。このイベントが発生したら、「今からIMEで入力が始まるぞ」と判断できます。

compositionupdate:合成中のテキストが更新されるたびに発火します。ローマ字からひらがなへの変換など、合成中の変化を追跡できます。

compositionend:IMEによる合成が終了したとき(確定したとき)に発火します。このイベントが発生したら、「入力が確定したぞ」と判断できます。

✅ ポイント

サジェスト検索を実行するベストなタイミングは、compositionendが発火したときです。この時点で、ユーザーの入力が確定しているからです。

2-3. isComposingプロパティを活用する

inputイベントが発火したとき、「今、IMEで合成中なのかどうか」を判定する方法がもう一つあります。それが、event.isComposingプロパティです。

isComposingは、イベントオブジェクトのプロパティで、合成中ならtrue、そうでなければfalseを返します。

このコードでは、isComposingtrueのとき(合成中のとき)は早期リターンして、処理をスキップしています。

2-4. フラグ変数を使った確実な判定

isComposingプロパティは便利ですが、ブラウザによっては正しく動作しない場合があります。より確実に判定するために、自前でフラグ変数を管理する方法を併用することをお勧めします。

以下は、compositionstartcompositionendイベントでフラグを管理するコードです。

このコードのポイントは、e.isComposing || isComposingという条件式です。e.isComposingと自前のフラグの両方をチェックすることで、どのブラウザでも確実に判定できます。

⚠️ 注意点

古いバージョンのSafariやiOSでは、isComposingプロパティが正しくtrueを返さないケースが報告されています。自前のフラグ管理を併用することで、これらの環境でも正しく動作させることができます。


第3章:debounce(遅延実行)を実装する

IMEの合成中は検索をスキップする仕組みができました。次に実装するのは、debounce(デバウンス)です。

3-1. なぜdebounceが必要なのか

Part 1で触れたように、debounceは「最後の入力から一定時間経過するまで、処理を遅延させる」仕組みです。

なぜこれが必要なのでしょうか。具体例で考えてみましょう。

ユーザーが「天気予報」と入力したいとします。「て」「ん」「き」「よ」「ほ」「う」と、6回の確定が発生します。debounceがないと、6回の検索リクエストが送信されてしまいます。

しかし、ユーザーが本当に知りたいのは「天気予報」の結果であって、「て」や「てん」の結果ではありません。途中の検索は無駄なのです。

debounceを導入すると、「最後の入力から(例えば)150ミリ秒間、新しい入力がなければ検索を実行する」という動作になります。これにより、ユーザーが入力を終えたタイミングで1回だけ検索が実行されます。

3-2. debounceの仕組みを図で理解する

debounceの動作を、タイムラインで見てみましょう。

【debounceなしの場合】

時間 →→→→→→→→→→→→→→→→→→→→→→

入力: て  てん  てんき  てんきよ  てんきよほう

検索: ⚡  ⚡   ⚡    ⚡     ⚡

↑  ↑   ↑    ↑     ↑

5回の検索が実行される(無駄)

【debounce 150msありの場合】

時間 →→→→→→→→→→→→→→→→→→→→→→→→

入力:て てん てんき てんきよ てんきよほう

↓  ↓   ↓    ↓    ↓   [150ms経過]

リセット リセット リセット リセット タイマー開始→ ⚡

1回だけ検索実行

入力があるたびにタイマーをリセットし、最後の入力から150ミリ秒後に検索が実行されています。

3-3. debounce関数を実装する

debounceを実装するには、setTimeoutclearTimeoutを組み合わせます。

この関数が何をしているのか、一行ずつ解説します。

let timeoutId = null;

タイマーのIDを保存する変数です。setTimeoutは、タイマーを識別するためのIDを返します。このIDを使って、後からタイマーをキャンセルできます。

return function(...args) { ... }

新しい関数を返します。この関数が、実際に呼び出されるdebounce版の関数になります。...argsは、元の関数に渡された引数をすべて受け取るための記法です。

clearTimeout(timeoutId);

前回設定したタイマーをキャンセルします。これにより、前回の処理は実行されなくなります。

timeoutId = setTimeout(() => { ... }, delay);

新しいタイマーを設定します。delayミリ秒後に、元の関数funcを実行します。

3-4. debounce関数の使い方

debounce関数を使って、検索処理を遅延実行する例を見てみましょう。

debounce(performSearch, 150)で、150ミリ秒のdebounceが適用された新しい関数debouncedSearchを作成しています。この関数を何度呼び出しても、最後の呼び出しから150ミリ秒後に1回だけperformSearchが実行されます。

💡 初心者向けポイント

debounceの遅延時間は、100〜300ミリ秒程度が一般的です。短すぎると効果が薄く、長すぎるとレスポンスが悪く感じられます。150ミリ秒は、多くのケースでバランスの良い値です。


第4章:プレフィックス検索のロジックを実装する

IMEの合成中判定とdebounceが実装できました。次は、いよいよプレフィックス検索のロジックを実装します。

4-1. 辞書データの構造

まず、検索対象となる辞書データを用意します。辞書は、「表示する文字」と「読み(ひらがな)」のペアの配列として作成します。

各要素は、text(表示するテキスト)とreading(読み)のプロパティを持つオブジェクトです。

4-2. filterとstartsWithで検索する

JavaScriptの配列メソッドfilterと、文字列メソッドstartsWithを組み合わせて、プレフィックス検索を実装します。

このコードが何をしているのか、解説します。

query.toLowerCase().trim()

入力された文字列を正規化しています。toLowerCase()は小文字に変換し、trim()は前後の空白を除去します。これにより、大文字・小文字の違いや、誤って入力された空白を無視できます。

dictionary.filter(item => { ... })

filterメソッドは、配列の各要素に対して条件をチェックし、条件を満たす要素だけを含む新しい配列を返します。

item.reading.startsWith(normalizedQuery)

startsWithメソッドは、文字列が指定した文字列で始まるかどうかをチェックします。trueなら条件を満たすので、その要素は結果に含まれます。

4-3. テキスト自体も検索対象にする

読みだけでなく、テキスト自体も検索対象にすると、より便利になります。例えば、「アメリカ」という単語は、「あめりか」でも「アメリカ」でもヒットしてほしいですよね。

||(OR演算子)を使って、「読みで始まる」または「テキストで始まる」のどちらかを満たせばヒットするようにしました。

4-4. 検索結果をソートする

検索結果が複数ある場合、どの順番で表示するかが重要です。一般的には、以下のような優先順位でソートします。

  1. 完全一致を最優先(入力と読みが完全に一致するもの)
  2. 読みが短いものを優先(より具体的な単語を上に)

sortメソッドは、配列の要素を並び替えます。比較関数が負の値を返すとaが前に、正の値を返すとbが前に来ます。

slice(0, 10)で、結果を最大10件に制限しています。候補が多すぎると、ユーザーにとって選びづらくなるからです。


第5章:ここまでのコードを統合する

ここまでに学んだ要素を統合して、一つのコードにまとめてみましょう。

5-1. 統合コード

5-2. コードの流れを確認

このコードの動作を、ステップバイステップで確認しましょう。

1. ユーザーが「あ」と入力(IME使用)

  • compositionstartが発火 → isComposing = true
  • inputイベントが発火 → isComposingtrueなのでスキップ

2. ユーザーが「あ」を確定

  • compositionendが発火 → isComposing = falseupdateSuggestions('あ')を実行
  • 検索結果として「雨」「赤」「青」「秋」「朝」「アメリカ」などがヒット

3. ユーザーが続けて「め」と入力(IME使用)

  • 再びcompositionstartからcompositionendの流れ
  • 確定後、updateSuggestions('あめ')を実行
  • 検索結果として「雨」「アメリカ」などがヒット(候補が絞り込まれる)

4. ユーザーが英字「test」と入力(IME不使用)

  • compositionstartは発火しない
  • inputイベントが発火 → isComposingfalseなのでdebouncedUpdateを実行
  • 150ms後に検索が実行される

第6章:この記事のまとめと次回予告

6-1. Part 2で学んだこと

📌 Part 2 の重要ポイント

1. JavaScriptのイベント処理
addEventListenerでイベントリスナーを登録し、特定のイベントが発生したときに処理を実行できる。

2. 日本語入力の特殊なイベント
compositionstartcompositionupdatecompositionendイベントでIMEの合成状態を追跡できる。isComposingプロパティと自前のフラグを併用すると、より確実に判定できる。

3. debounce(遅延実行)
setTimeoutclearTimeoutを使って、最後の入力から一定時間後に処理を実行する仕組みを作れる。これにより、無駄な検索リクエストを削減できる。

4. プレフィックス検索
filterstartsWithを組み合わせて、読みが入力文字列で始まる単語を検索できる。sortで結果を適切な順序に並び替えることで、ユーザビリティが向上する。

6-2. 次回:Part 3 の予告

Part 3では、完全に動作するサジェスト機能を実装します。

具体的には、以下の内容を扱います。

  • HTMLとCSSで候補表示UIを構築
  • キーボード操作(↑↓キー、Enter、Escape)への対応
  • マウスクリックでの候補選択
  • XSS対策(セキュリティ)
  • コピペで動く完全なサンプルコード

Part 2までの知識をベースに、実際に使えるサジェスト機能を組み立てていきます。お楽しみに!

次の記事

Part 3:実践コード完全版と動作デモ編

→ コピペで動く完全なサンプルコードを提供します


最後まで読んでいただき、ありがとうございました。

Part 3では、この記事で学んだロジックを使って、実際に動作するサジェスト機能を完成させます。コードが難しく感じた部分があれば、この記事を読み返してから次に進むことをお勧めします。

コメント

タイトルとURLをコピーしました