jQueryではじめるAjax

第4回検索キーワードを提案するSuggest機能の実装

今回は、検索文字列に関連するキーワードを提案するSuggest機能を実装したいと思います。

実装の前に

Suggest機能の実装に入る前に、第3回までのプログラムをちょっと整理しましょう。第3回までの実装では、とりあえずグローバルな名前空間に関数を追加していました。しかし、名前空間がどんどん汚染されよくありません。必要なものだけをグローバルな名前空間に追加しましょう。

スコープを隠蔽する

jQueryにならって、スコープを隠蔽してみることにします。次のように無名関数を使って実装しますリスト1⁠。

リスト1 スコープの隠蔽
(function(){
    // (1) このスコープは公開されない 
    var local = ・・・

    // (2) 必要なものだけを公開する 
    window.global = ・・・
})();

無名関数の定義(function(){})と、実行()を同時に行っています。 リスト3-(1)のlocalという変数は無名関数のローカルスコープにあるため、外部から直接参照することはできません。 リスト3-(2)のglobalのようにwindowオブジェクトに追加することにより、外部に参照を公開することができます。

Youtubeコンストラクタ関数の定義

Youtubeというコンストラクタ関数を作成し、それを基にした新しいオブジェクトをwindowオブジェクトに追加します。 第3回までのページロード時以外の実装はとりあえずここに押し込めることにしますリスト2⁠。

リスト2 Youtubeコンストラクタ関数
(function(){
    // コンストラクタ関数の定義 
    var Youtube = function() {
        ・・・
    }
    Youtube.prototype = {
        ・・・
    }

    // 名前空間 window.yt に公開 
    window.yt = new Youtube();
})();

これで、名前空間の汚染は最小限になりました。

今回は、単純に名前空間 window.yt という名前空間に実装を押し込めただけで、コンストラクタ関数を作成した意味があまりありません。 必要に応じて実装を拡張してください。

外部ファイル化

さらにこの実装を、 Youtube.jsファイルとして外部ファイルに移動しました。 これで少しすっきりしましたね。

Suggest機能

それでは、Suggest機能の実装に入りたいと思います。

動作イメージ

検索テキストボックスに文字列を入力すると、検索テキストボックスの下に関連するキーワードが表示されます図1⁠。 Suggest機能とは、このようにキーワードを提案(Suggest)して、検索を補助する機能です。

Suggest機能

機能概要

今回実装する機能の要件は次のとおりです。

  • 検索テキストボックスに文字列を入力すると、500ミリ秒後に関連するキーワード(以下 Suggest)を表示する。
  • Suggestが表示されていない場合、検索テキストボックスで「↓」を押すとSuggestを表示する。
  • Suggestが表示されている場合、検索テキストボックスで「↓」を押すとSuggestにフォーカスを移動する。
  • Suggestを選択すると、検索テキストボックスに選択したSuggestを設定する。
  • Suggestをダブルクリックすると、検索テキストボックスにダブルクリックしたSuggestを設定し、検索する。
  • ESCキーを押すとSuggestを閉じる。
  • Suggestがない場合は何も表示しない。

Suggestデータ

Youtube APIにはSuggest用のAPIやデータは用意されていません。 今回は、動画に設定されたキーワード(feed.entry[n].media$group.media$keywords.$t)をSuggestのデータとして使います。

Suggestの実装(1)

画面要素

検索テキストボックスの下に、Suggestを表示するselect要素をid=suggestとして追加しますリスト3⁠。

リスト3 検索フォーム
<!-- 検索フォーム -->
<form id="frmSearch">
  <input type="text" id="keyword" size="35">
  <input type="submit" value="検索"><br>
  <select id="suggest" size="10"></select> <!-- Suggest -->
</form>

イベントハンドラ

検索テキストボックス

検索テキストボックスのイベントを定義しますリスト4⁠。

リスト4 検索テキストボックスイベント
// 検索テキストボックス
$("#keyword").keyup(function(e){
    if (e.keyCode == 27) {          // esc
        $("#suggest").hide();
    } else if (e.keyCode == 40) {   // ↓下矢印
        if ($("#suggest").is(":visible")) {  // suggest が表示されている
            $("#suggest").focus()[0].selectedIndex = 0;
            $(this).val($("#suggest").val());
        } else {
            yt.suggest(true);
        }
    } else {
        yt.suggest();
    }
});

このイベントハンドラのコールバック関数の引数eには、イベントオブジェクトが渡されます。 イベントオブジェクトのkeyCodeを判別し、検索テキストボックスで押下されたキーに応じて処理を振り分けています。

$("#keyword").keyup(function(e){
    if (e.keyCode == xx) {
        // xxが押下されたときの処理
        ・・・
    }
}

まず、escキー(keyCodeは27)が押下されたときの処理を実装します。 Suggestを非表示にします。

if (e.keyCode == 27) {          // esc
    $("#suggest").hide();
}

続けて、↓下矢印キー(keyCodeは40)が押下されたときの処理を実装します。 Suggestが表示されていれば、Suggestの先頭項目を選択し、選択項目の内容を検索ボックスに設定します。 Suggestが表示されていなければ、yt.suggest(true) メソッドを実行してSuggestを表示します。

} else if (e.keyCode == 40) {   // ↓下矢印
    if ($("#suggest").is(":visible")) {  // suggest が表示されている
        $("#suggest").focus()[0].selectedIndex = 0;
        $(this).val($("#suggest").val());
    } else {
        yt.suggest(true);
    }
}

それ以外のキーが押下された場合は、yt.suggest() メソッドを実行してSuggestを表示します。

} else {
    yt.suggest();
}

Suggestセレクトボックス

Suggestセレクトボックスのイベントを定義しますリスト5⁠。

リスト5 Suggestセレクトボックスイベント
// Suggestセレクトボックス
$("#suggest")
    .change(function(){
        $("#keyword").val($(this).val());
    })
    .keyup(function(e){
        if (e.keyCode == 13) {           // Enter
            $(this).hide();
            $("#frmSearch").submit();
        } else if (e.keyCode == 27) {    // Esc
            $(this).hide();
            $("#keyword").focus();
        }
    })
    .dblclick(function(){
        $(this).hide();
        $("#frmSearch").submit();
    });

Suggestセレクトボックスの選択項目が変更された場合は、選択されたキーワードを検索テキストボックスに設定します。

.change(function(){
    $("#keyword").val($(this).val());
})

Enterキー(keyCodeは13)が押下された場合は、Suggestを非表示にし、検索を実行します。

if (e.keyCode == 13) {// Enter
    $(this).hide();
    $("#frmSearch").submit();
}

escキー(keyCodeは27)が押下された場合は、Suggestを非表示にし、検索テキストボックスにフォーカスを移します。

} else if (e.keyCode == 27) {    // Esc
    $(this).hide();
    $("#keyword").focus();
}

Suggestセレクトボックスがダブルクリックされた場合は、Suggestを非表示にし、検索を実行します。

.dblclick(function(){
    $(this).hide();
    $("#frmSearch").submit();
});

初期処理

ページが読み込まれたときには、Suggestのselect要素を非表示にしますリスト6⁠。

リスト6 初期処理
// --- 初期処理 ---
// ページロード時は suggest を隠す
$("#suggest").hide();

Suggestの実装(2)

Suggestの実行

Suggest処理を実行し、Suggestを表示しますリスト7⁠。

リスト7 Suggestの実行
// --- suggest実行 ---
suggest: function(force) {
    this.stop_suggest();

    if (force) this.preinput = null;
    this.tid = setTimeout(function(){yt.do_suggest()}, Youtube.SUGGEST_TIME);
},

まず、実行中のSuggest処理を停止します。

this.stop_suggest();

引数forceがtrueであれば、前回のSuggest検索文字列にnullを代入しSuggestが必ず実行されるようにします。 preinputは、Youtubeコンストラクタ関数のprototype プロパティに定義されています。

if (force) this.preinput = null;

Suggest処理を実行します。 ただ、キーが押されるたびにYoutubeへリクエストを送ることは効率的ではないので、500ミリ秒後に実行されるようにし、 検索テキストボックスに入力された文字列は、yt.do_suggest()メソッドの内部から取得します。 そして、yt.do_suggest()メソッドでは、検索テキストボックスに入力された文字列に変更があった場合のみ検索処理を実行します。

this.tid = setTimeout(function(){yt.do_suggest()}, Youtube.SUGGEST_TIME);

Youtube.SUGGEST_TIMEは次のように定義されています。

Youtube.SUGGEST_TIME = 500;  // Suggestまでの時間(ミリ秒)

Suggestの停止

clearTimeoutを実行して、キューに登録されているSuggest処理があればそれをクリアしますリスト8⁠。

リスト8 Suggestの停止
// --- suggest停止 ---
stop_suggest: function() {
    clearTimeout(this.tid);
},

Suggest処理

検索テキストボックスに入力された文字列でビデオを検索し、ビデオに設定されているキーワードからSuggestを生成しますリスト9⁠。

リスト9 Suggest処理
// --- suggest処理 ---
do_suggest: function() {
    // Suggestが表示されていたら非表示にする。
    $("#suggest").hide();

    // 検索キーワードの処理
    var str = $("#keyword").val();
    if (str == null || str.length == 0) return;
    if (this.preinput == str) return;
    this.preinput = str;

    // ajax通信定義
    $.ajax({
        dataType: "jsonp",
        data: {
            "vq": str,
            "max-results": 10,  // 10件分を検索
            "alt":"json-in-script"
        },
        cache: true,
        url: "http://gdata.youtube.com/feeds/api/videos",
        success: function (data) {
            // 検索キーワードにマッチするデータがない
            if (data.feed.entry == null) return;

            var suggests = [];
            $("#suggest").empty();  // Suggestをクリア
            $.each(data.feed.entry, function(i, item){
                // ビデオに設定されているキーワードがない
                if (item.media$group.media$keywords == null) return true;
                // キーワードを配列に変換
                var keywords = item.media$group.media$keywords.$t.split(", ");
                if (keywords.length == 0) return true;
                // 各キーワードを判別
                $.each(keywords, function(n, keyword){
                    if ((keyword != str) &&                      // 検索キーワードと違う
                        (keyword.indexOf(str) == 0) &&           // 検索キーワードで始まる
                        ($.inArray(keyword, suggests) == -1)) {  // 既にSuggestに追加されていない
                        suggests.push(keyword);
                        $("#suggest").append($("<option/>").text(keyword));
                    }
                });
            });

            // Suggestが0件
            if (suggests.length == 0) return;
            // Suggestを表示
            $("#suggest")
                .show()
                [0].selectedIndex = -1;
        }
    });
}

前処理

まず、Suggestが表示されていたら非表示にします。

// Suggestが表示されていたら非表示にする。
$("#suggest").hide();

次に、検索テキストボックスに入力されている文字列を取得し、処理を行います。

var str = $("#keyword").val();

もし、検索文字列が空もしくは前回と同じである場合には、何も処理を行いません。

if (str == null || str.length == 0) return;
if (this.preinput == str) return;

最後に次回の検索文字列と比較するために、検索文字列を保存しておきます。

this.preinput = str;

ajax通信定義

基本的な構造は前回までのビデオ検索のものと変わりません。 よって、successコールバック関数のSuggest生成部分を抜粋して説明します。

各ビデオのエントリをイテレーションし、ビデオに設定されているキーワードを処理します。

$.each(data.feed.entry, function(i, item){

    ・・・

});

ビデオに設定されているキーワードがない場合は、media$keywordsプロパティ自体が存在しません。 よって次のようにしてキーワードが設定されているかどうかを判別し、キーワードがない場合には次のビデオのエントリを処理します。

// ビデオに設定されているキーワードがない
if (item.media$group.media$keywords == null) return true;

$.eachのコールバック関数がtrueを返すと、次のイテレーションを実行するのでしたね ⁠falseを返した場合は、$.eachの処理を終了します⁠⁠。

キーワードはカンマ+スペース「, 」で区切られて格納されています。扱いやすいように配列に変換します。

// キーワードを配列に変換
var keywords = item.media$group.media$keywords.$t.split(", ");
if (keywords.length == 0) return true;

キーワードの配列からSuggest文字列を抜き出します。 今回、Suggestの文字列は、検索テキストボックスの文字列で始まるキーワードとしました。

$.each(keywords, function(n, keyword){
    if ((keyword != str) &&                      // 検索キーワードと違う
        (keyword.indexOf(str) == 0) &&           // 検索キーワードで始まる
        ($.inArray(keyword, suggests) == -1)) {  // 既にSuggestに追加されていない
        suggests.push(keyword);
        $("#suggest").append($("<option/>").text(keyword));
    }
});

$.inArray(value, array)は、 配列arrayから、valueを検索し、配列内でのインデックス番号を返します。 valueが配列に存在しない場合は、-1 を返します。 ここでは、Suggest文字列の重複を避けるため、Suggest文字列配列suggestsに存在するかどうかを判別しています。

そして、Suggest文字列からoption要素を生成し、Suggestのselect要素の子要素に追加します。

最後に、Suggest文字列が1件以上あれば、Suggestを表示します。

// Suggestが0件
if (suggests.length == 0) return;
// Suggestを表示
$("#suggest")
    .show()
    [0].selectedIndex = -1;

以上で全ての実装が終了しました。サンプルを実行して動作を確認してみてください。

まとめ

今回はSuggestを実装してみました。 第3回までの内容が理解できていれば、思ったより簡単だったと思います。

この実装では、Suggest文字列をビデオに設定されたキーワードから生成しているため、Suggestが表示されるまでに少し時間がかかります。 検索件数を少なくしたり、Suggest文字列の切り出し処理を最適化すればもう少し速くなると思います(YoutubeがSuggestAPIを公開してくれればよいのですが…⁠⁠。

次回は、ユーザインターフェースライブラリjQuery UIを取り上げたいと思います。

おすすめ記事

記事・ニュース一覧