Webアプリを公開しよう! Chrome Web Store/Apps入門

第7回Webアプリを作ろう#4─⁠─Omnibox、Context Menus

gihyo.jp/dev/serial/01/chrome-web-store/0006">前回はWebアプリの設定と保存を通して、Options PageとWeb Storageについて詳細を解説しました。今回は引き続きOdometerに機能を追加する形で、Context MenusとOmniboxについて解説していきたいと思います。

検索ボックスとコンテキストメニューの拡張

これまでは、Odometerを使うためにWebアプリを起動して画面から住所を検索し、目的地を入力していました。わざわざWebアプリを起動しなくても、もっと素早く住所を検索したいとか、ほかのWebサイトにある住所を直接検索したいといった要望があるかもしれません。これらを実現するための仕組みとして、Chromeの検索ボックス(アドレスバー)を拡張するOmniboxやWebサイト内でのコンテキストメニュー(右クリックメニュー)を拡張するContext Menusがあります。今回はこの2つを使って、検索ボックスとコンテキストメニューから素早くOdometerを呼び出し、住所検索を行えるように修正します。

図1 検索ボックス
図1 検索ボックス
図2 コンテキストメニュー
図2 コンテキストメニュー

Webアプリの構成

今回は、Webアプリを構成するファイルに追加はありません。odometer.jsとbackground.js、そしてmanifest.jsonを変更して機能を追加しています。

ディレクトリ図
manifest.json
{
  "name": "Odometer",
  "description": "距離計",
  "version": "0.4",
  "app": {
    "launch": {
      "local_path": "main.html"
    }
  },
  "icons": {
    "16": "icon_16.png",
    "48": "icon_48.png",
    "128": "icon_128.png"
  },
  "omnibox": {
    "keyword": "Odometer"
},
  "options_page": "options.html",
  "background_page": "background.html",
  "permissions": [
    "geolocation",
    "notifications",
    "background",
    "contextMenus",
    "tabs"
  ]
}

検索ボックスを拡張するための指定として、新たに "omnibox" という項目を追加しています。その中の "keyword" は、検索ボックスからOdometerを呼び出すためのトリガーとなる最初のキーワードを指定します。ここでは "Odometer" を指定していますが、大文字と小文字は区別しません。詳細は後述します。

コンテキストメニューを拡張するためには、"permissons"に "contextMenus" を追加します。また、検索ボックスやコンテキストメニューからOdometerを呼び出すために "tabs" も合わせて追加しています。

Omnibox

Omniboxは、Chromeの検索ボックス(アドレスバー)を拡張するための仕組みです。みなさん普段はこの検索ボックスを使ってGoogleなどで検索をしているかと思います。Omniboxでは、この検索ボックスからキーワードを検索エンジンではなくWebアプリへ渡すことができるようになります。また、キーワードを入力する際のサジェストなどもカスタマイズすることができます。

Omniboxの使い方

先に実際のOmniboxの使い方を説明しましょう。検索ボックスには、検索エンジンによる検索のほかに、お気に入りや履歴などからもサジェストが表示されます。その中でWebアプリのアイコンが付いたサジェストが、そのWebアプリを呼び出すためのものになっています。

  1. 検索ボックスにマニフェストファイルで指定したキーワードを入力する

    図3 検索ボックスにキーワードを入力
    図3 検索ボックスにキーワードを入力
  2. Webアプリのアイコンが表示されたサジェストを選択する

    図4 Webアプリのサジェストを選択
    図4 Webアプリのサジェストを選択
    図5 Webアプリに渡すキーワード
    図5 Webアプリに渡すキーワード
  3. エンターキーで入力完了とWebアプリの呼び出し

  4. 続けてWebアプリに渡すキーワードを入力

    図6 Webアプリの呼び出し
    図6 Webアプリの呼び出し

OmniboxからのWebアプリの呼び出し

今回は、マニフェストファイルで指定した"Odometer"という文字列を検索ボックスに入力すると、その後に続くキーワードをパラメーターとしてOdometerを呼び出すことになります。パラメーターを住所としてOdometerで検索するようにしてみましょう。検索ボックスの設定は非常に簡単で、各種イベントを登録するだけです。ここでは、background.js内でchrome.omnibox.onInputEnteredイベントを登録しています。

// 検索ボックス設定
chrome.omnibox.onInputEntered.addListener(
    function(text){
        searchOnApp(text);
    }
);

onInputEnteredイベントは、検索ボックスでキーワードを入力し、エンターキーを押した際のイベントになります。このイベントには入力されたキーワードを引数に持つコールバックを指定します。入力されたキーワードをsearchOnAppメソッドで検索して表示しています。searchOnAppメソッドは今回作成したもので、Odometerが既に起動していればそのタブで検索を行い、まだ起動していなければ新たにOdometerを起動してから検索を行っています。Omniboxとは直接関係はありませんが詳しくみていきましょう。

background.js(Odometerで検索)
// アプリのメイン画面URL
var APP_URL = chrome.extension.getURL('main.html');

/*
 * アプリ上で住所検索を行います
 */
function searchOnApp(text){
    selectOrCreateTab(APP_URL, function(tab){
        
        var view = getView(APP_URL);
        if ( !view ) {
            return;
        }
        
        if ( tab.status === 'loading') {
            view.searchText = text;
        } else {
            view.searchByText(text);
        }
    });
}

/*
 * 指定されたURLのタブがあれば選択し、なければ作成します
 */
function selectOrCreateTab(url, callback){
    chrome.tabs.getAllInWindow(null, function(tabs){
        for ( var i = 0, len = tabs.length; i < len; i++ ) {
            if ( tabs[i].url == url ) {
                chrome.tabs.update(tabs[i].id, { selected: true }, callback);
                return;
            }
        }
        chrome.tabs.create({ url: url }, callback);
    });
}

/*
 * アプリ内の指定されたURLのWindowオブジェクトを取得します
 */
function getView(url){
    var views = chrome.extension.getViews();
    for ( var i = 0, len = views.length; i < len; i++ ) {
        if ( views[i].location.href === url ) {
            return views[i];
        }
    }
}

selectOrCreateTab メソッドでOdometerのタブを選択、もしくは作成をします。chrome.tabs.getAllInWindowメソッドを使って指定のURLを持つタブが存在するかチェックし、既に存在する場合chrome.tabs.updateメソッドを使ってそのタブを選択しています。chrome.tabs.updateメソッドはURLだけでなく、その他のステータスを変更する場合にも利用することができます。Odometerのタブが存在しなければchrome.tabs.createメソッドを使ってタブを作成しています。

Odometerで検索するには、chrome.extension.getViewsメソッドを使ってタブのWindowオブジェクトを取得し、グローバルに定義されているsearchByTextメソッド呼び出しています。ポイントは、新たに作成したタブのstatusをチェックし、"loading"となっていた場合はすぐには検索を実行せず、対象のWindowオブジェクトへsearchText変数を定義していることです。表示側はsearchText変数が定義されていれば準備ができ次第、検索を実行します。若干スマートではないかもしれませんが、とりあえず今回はこの方法で実装しました。

odometer.js
//初期表示
//住所検索の呼び出しがあれば検索実行
if ( searchText ) {
    searchByText(searchText);
    searchText = '';
}

~省略~

/*
 * 住所検索(テキスト指定)
 */
function searchByText(text){
    document.getElementById('address').value = text;
    search();
}

Omniboxのサジェスト

今回は利用していませんが、Omniboxのサジェストの設定方法についても説明します。デフォルトのサジェストを設定するには、chrome.omnibox.setDefaultSuggestionメソッドを使います。また.キーワード入力中のサジェストはchrome.omnibox.onInputChangedイベントのコールバックの中で設定します。サジェストは、contentとdescriptionを持つオブジェクトで構成されます。contentは実際にサジェストするキーワードで、descriptionは検索ボックスに候補として表示される内容になります。サンプルを以下に示します。

サジェストのサンプルコード
// デフォルトのサジェスト
chrome.omnibox.setDefaultSuggestion({
    content: "default",
    description: "デフォルト"
});

// 入力中のサジェスト
chrome.omnibox.onInputChanged.addListener(
    function(text, suggest){
        suggest([
            { content: 'search ' + text, description: '住所検索=> ' + text }
        ]);
    }
);
図7 検索ボックスのサジェスト
図7 検索ボックスのサジェスト
表1 chrome.omnibox
メソッド/プロパティ説明
setDefaultSuggestion(suggest)デフォルトのサジェストを設定する
表2 chrome.omnibox(イベント)
イベント説明
onInputCancelledキーワードの入力がキャンセルイベント
onInputChangedキーワードの入力変更イベント
コールバックにキーワードとサジェストを引数に持つ。function(text, suggest){…}
onInputEnteredキーワードの入力完了イベント
コールバックにキーワードを引数に持つ。function(text){…}
onInputStartedキーワードの入力開始イベント

Context Menus

Context Menusは、Chromeのコンテキストメニュー(右クリックメニュー)を拡張するための仕組みです。通常であれば、コンテキストメニューにはフォーカスした対象に合わせたメニューが表示されています。例えばテキストを選択している場合であれば、⁠コピー」「Googleで検索⁠⁠、⁠要素を検証」などのメニューが表示されています。Context Menusでは、これらのメニューに加えてWebアプリ独自のメニューを追加します。

メニューの作成

今回は、選択したテキストを「住所検索」するメニューを追加します。メニューを作成するには、chrome.contextMenus.createメソッドを利用します。引数にとるオブジェクトでメニューの内容を細かく指定していきます。メニューがクリックされた場合のコールバックもこのオブジェクトのonclickプロパティに登録します。

background.js
// コンテキストメニュー作成
chrome.contextMenus.create({
    title: '住所検索',
    contexts: ['selection'],
    onclick: function(info, tab){
        var text = info.selectionText;
        searchOnApp(text);
    }
});

titleでメニューの表示名を設定し、contextsでフォーカスした対象がどういったものの場合にメニューを表示するかどうかを設定します。ここでは、テキストを選択している場合に表示される"selection"を指定しています。contextsにはそのほかにもリンク要素に対して表示される"link"や画像に対して表示される"image"などがあります。contextsの設定によってonclick時のコールバックに返されるオブジェクトの内容が変わります。contextsが"selection"の場合、selectionTextプロパティから選択したテキストを取得することができます。

さまざまなメニューと階層化

単なるラベルのメニュー以外にも、チェックボックスやラジオボタン、セパレーターなどを作成することができます。また、chrome.contextMenus.createメソッドの戻り値であるメニューIDを使って階層化することもできます。以下にサンプルコードを示します。

各種メニューのサンプルコード
var parentId = chrome.contextMenus.create({
    title: 'サンプルメニュー'
});

// チェックボックス
chrome.contextMenus.create({
    type: 'checkbox',
    title: 'チェックボックス',
    parentId: parentId
});

// セパレーター
chrome.contextMenus.create({
    type: 'separator',
    parentId: parentId
});

// ラジオボタン
chrome.contextMenus.create({
    type: 'radio',
    title: 'ラジオボタン',
    parentId: parentId
});
図8 さまざまなメニュー
図8 さまざまなメニュー
表3 chrome.contextMenus
メソッド/プロパティ説明
create(createProperties, callback)メニューを作成する
remove(menuItemId, callback)メニューを削除する
removeAll(callback)メニューをすべて削除する
update(id, updateProperties, callback)メニューを更新する
表4 createProperties/updateProperties
メソッド/プロパティ説明
typeメニューのタイプ
normal: ラベル(デフォルト)
checkbox: チェックボックス
radio: ラジオボタン
separator: セパレーター
titleメニューの表示名
checkedtypeがcheckboxまたはradioの場合のチェック状態
contextsメニューを表示するフォーカスの対象を配列で指定する
all: すべて
page: ページ(デフォルト)
frame: iframe内
selection: テキスト選択
link: リンク
editable: 入力可能フォーム
image: 画像
video: 動画
audio: 音声
onclickクリック時のコールバック
function(info, tab){…}
infoは、下記のOnClickDataを参照
documentUrlPatternsメニューを表示するURLパターン
targetUrlPatternscontextsがimage、video、audioの場合にメニューを表示する対象のURLパターン
表5 OnClickData
メソッド/プロパティ説明
menuItemIdメニューID
parentMenuItemId親メニューID
mediaTypeメディアタイプ
contexts: image/video/audio
linkUrlリンクURL
contexts: link
srcUrlソースURL
contexts: image/video/audio
pageUrlページURL
contexts: page
frameUrlフレームURL
contexts: frame
selectionText選択テキスト
contexts: selection
editable編集可能かどうか
contexts: editable

※:contextsは同時に複数マッチする場合があります。

まとめ

これで、Odometerを検索ボックスやコンテキストメニューから呼び出すことができるようになりました。検索ボックスやコンテキストメニューは、単にWebアプリを呼び出す導線というだけでなく、インターフェースとしてさまざまなことに応用できるので、是非とも何か面白い使い方を考えてみてください。

今回は、OmniboxとContext Menusの詳細を解説しました。次回も引き続きさまざまなAPIを解説していきたいと思います。

crxファイルはzipファイルですので、右クリックからのダウンロード後に拡張子をzipに変えていただければ中身を参照できます。

おすすめ記事

記事・ニュース一覧