script.aculo.usを読み解く

第2回 controls.js(前編)Autocompleter

この記事を読むのに必要な時間:およそ 18 分

今回と次回は,script.aculo.usの中でも斬新なGUI部品のライブラリが詰まっているcontrols.jsを解説します。 controls.jsには,Autocompleter(入力補完機能)と,次回解説予定のInPlaceEditor(その場で編集機能)が入っていますが,それぞれに依存関係はありませんので,回ごとに別々にお読みいただけます。

入力補完機能を加えるAutocompleter

今回は,ブラウザの入力エリアに入力補完(オートコンプリート)機能をつける,Autocompleterというライブラリのコードについて解説します。ブラウザが持っている入力補完機能は貧弱で,ユーザが過去に入力したものしか補完してくれません。検索窓については,Googleサジェストが,膨大な検索インデックスから検索頻度や人気度をもとにユーザの入力を予想し補完してくれるおかげで,ずいぶん便利になりました。

Autocompleterを使うと,そのような機能を簡単に実現できます。例えば,ユーザに人名や地名を入力してもらいたいときなど,こちらでどんな入力があるか予想がついているときに,前もって候補をリストアップして用意しておくだけで,ユーザの入力作業を強力に補助することができます。本格的にやるのであれば,Autocompleterからリアルタイムにサーバに補完候補を問い合わせる機能があるので,Googleサジェストのように,サーバ側に用意された膨大なデータベースを背景とした入力補完を提供することもできます。

Autocompleter を利用したテストページを作成してみましたので,ご覧ください。

いったいどんな仕組みで,この機能は実現されているのでしょう。例えば,fooと入力した場合何が起こるかを見ていきましょう。

Ajax.Autocompleterの方法

fooと打つと,Ajax.Autocompleterは,サーバに非同期的にアクセスして,fooではじまる言葉を問い合わせます。非同期的にアクセスするというのは,ブラウザが表ではそのままページを表示しつつ,裏でサーバに問い合わせをするということで,サーバの返答を待つ間もユーザのページ操作をリアルタイムに受けつけます。これは Ajax と呼ばれています。Ajax.Autocompleterは,hoge.cgi?value=fooとサーバのcgiにAjaxでアクセスし,サーバはこのアクセスに対して,次のようなHTMLの表現で補完候補のリストを返します。

<ul>
  <li>foobar</li>
  <li>foofoo</li>
</ul>

入力エリアの下にこのHTMLを挿入して,候補メニューとして表示します。

Autocompleter.Localの方法

Autocompleter.Localは,このサーバのcgiの動作も,ブラウザのJavaScriptでやってしまおうというわけです。そうすれば,サーバと通信する必要がありません。

そのためにまずは,全ての語を配列にして,Autocompleter.Localに渡しておきます。 ここでは,["foo","foobar","bar foobar","barfoobar"]という配列を渡したとしましょう。fooという入力にたいして,この配列のなかから補完候補としてふさわしいものを選ぶには,どんな検索をすればよいでしょうか。

まずは,もっとも単純である前方検索が使われます。

foo|
----
foo
foobar
----

デフォルトではさらに,次のような単語別前方検索も使われます。

foo|
----
foo
foobar
bar foobar
----

日本語の入力を常とする私たちには,ちょっとなじみがない補完のしかたですね。単語別前方検索では,列挙した語に含まれる単語に前方一致します。例えば"bar foobar"という語に含まれる単語のfoobarが前方一致します。この単語の区切りの判定は,正規表現の\sが使われていて,以下はマッチしません。

bar-foobar
bar,foobar
barfoobar (語がfooを含んでいるだけ)

デフォルトの前方検索と単語別前方検索の他に,オプションとして,全文検索が用意されており,これだと,列挙した語でとにかくfooを含むものが候補になります。

これらの検索アルゴリズムのコードについての説明は後述します。

キャレット位置からトークンを取り出す仕組み

ここからは,Autocompleterの心臓部の説明をしていきます。

Autocompleterの心臓部は,次のようになっています。

  • キャレットの位置を大雑把に求める
  • キャレットの位置の付近にある文字列の切りだし(この文字列をトークンといいます)
  • トークンから候補を導く

キャレットとは,入力ボックスでパカパカ点滅している,あの縦棒のことです。キャレットが何文字目にあるかを,ブラウザから取得することはできません。

キャレットの位置を大雑把に求めるのには,取得しておいた過去の内容と,現在の内容とを見比べて,内容が変わりはじめた位置の付近に,キャレットがあるという方法をとります。

この方法では,キャレットの正確な位置を求めることにはならないのですが,ここではキャレットの付近にあるトークンを求めるのが目的なので,少々の誤差は許されます(あまり勢いをつけてタイプすると,内容が変わりはじめた位置と実際のキャレットの位置との誤差が大きくなりすぎて,機能しないことがあります。実用上は問題ありません)。

次に,このキャレットの位置の付近の,トークンを切り出します。例えば,コンマがトークン区切り文字である場合,

以前の内容 hoge,,hoge
現在の内容 hoge,foo,hoge

で,内容に変わりはじめた位置である6文字目を中心に,トークン区切り文字のコンマについて前後に検索をかけて,結果,fooがトークンとなります。

キーレスポンスを犠牲にしないための工夫

ここで,もう一点,どのようなタイミングで候補の検索が行われるか,について解説します。 単純に考えれば,キー入力のたびに検索をかけるように思えます。しかし,候補の検索は重い処理なので,それではキーレスポンスが悪くなってしまいます。

ここで,タイピングの性質について考えてみましょう。タイピングの様子をみると,タタタン,タタタタンと,連続してキーを叩く合間に,小休止があることがわかります。この小休止のときに,候補の検索をすれば,レスポンスの悪さをユーザに感じさせないようにできるわけです。

小休止を検知するために,次のように,タイマをうまく使っています。

0129: onKeyPress: function(event) {
...
0162:   if(this.observer) clearTimeout(this.observer);
0163:     this.observer = 
0164:       setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);

this.options.frequencyはデフォルトで0.4です。

つまり,キー押下イベントのたび0.4秒待つタイマを作って,0.4秒待たずにキー押下がまたあったら,タイマを作りなおします。このタイマが発動するのは,最後のキー押下から0.4秒の間,キー押下がなかったとき,つまり,小休止のときです。そのタイミングでonObserverEventが実行されます。

このように,タイピングの性質を利用して小休止のとき検索することで,キーレスポンスを犠牲にせずに,入力補完を実現しています。

著者プロフィール

源馬照明(げんまてるあき)

名古屋大学大学院多元数理科学研究科1年。学部生のときにSchemeの素晴らしさを知ったのをきっかけに,関数型言語の世界へ。JavaScriptに,ブラウザからすぐに試せる関数型言語としての魅力と将来性を感じている。

ブログ:Gemmaの日記

コメント

コメントの記入