Add-on SDK で始めるアドオン開発(Context-menu 編)

前回の記事:Add-on SDK で始めるアドオン開発(Content Script 編)

今回の記事から、 context-menu モジュールを利用した拡張機能の実装を例にして、 Add-on SDK によるアドオン開発方法を紹介して行こうと思います。

今回の記事では、前回 紹介した Content Script を、コンテクストメニューから実行し、現在のページの情報を取得してみます。
また、 simple-storage モジュールを用いて、取得した情報をアドオンのストレージに保存する方法も紹介します。

コンテクストメニューから JavaScript を実行

Add-on SDK では comtext-menu モジュールを用いて、簡単にコンテクストメニューの拡張を行うことができます。
まず最初は、現在開いているページで、 window.history オブジェクトの history.go() メソッドを用いて履歴リストを複数さかのぼる機能を、コンテクストメニューに追加してみようと思います。

Add-on SDK で新しいアドオンのパッケージを作成し、 main.js を次のように書き換えます。

const contextMenu = require("context-menu");

exports.main = function(){
    var oldPageItems = [];
    var maxItemLength =10;
    var menuLabel = 'Back to the...';
    var script = 'self.on("click", function (node, data) { ' +
                 '    history.go(data);' +
                 '});';
    for(var i = 0; i < maxItemLength; i++){
        oldPageItems.push(
            contextMenu.Item({
                label: (i + 1).toString() + '-previous page',
                data: -1 * (i + 1)
            }));
    }
    contextMenu.Menu({
        label: menuLabel,
        context: contextMenu.PageContext(),
        contentScript: script,
        items: oldPageItems
    });
}

context-menu モジュールでは、 Item クラスのインスタンスを作成することにより、新しいメニューアイテムをコンテクストメニューに追加することができます(12~15行目)。
さらに、Menu クラスを用いることにより、複数のメニューアイテムをまとめて、サブメニューに表示することができます(17~22行目)。

ここでは、1~10回分の履歴をさかのぼるメニューアイテムを作成し、一つのサブメニューにまとめています。
サブメニューのアイテムをクリックすることにより、メニューの contentScript プロパティに指定したスクリプトが実行されます。
このスクリプト(7~9行目)の中で引数となっている data は、クリックされたメニューアイテムの data プロパティに指定した値(14行目)となります。


動作例

また、メニューやアイテムのコンストラクタで context プロパティを指定することにより、コンテクストメニューが開かれるコンテクスト(文字列を選択しているとき、画像をクリックしたときなど)を指定できます。
詳細:Specifying Contexts

ページの情報を取得

先ほどは、10回分の履歴をさかのぼるメニューアイテムを常に生成しましたが、履歴に10件もページが登録されていない場合は、そのアイテムは無効となってしまいます。
そこで、現在閲覧しているページで実行される Content Script から history.length を取得し、履歴の数と同じだけのメニューアイテムを生成するように変更してみます。

const contextMenu = require("context-menu");
const PageMod = require("page-mod").PageMod;

exports.main = function(){
    var backMenu;
    var createNewBackMenu = function(length){
        if(backMenu !== undefined) backMenu.destroy();
        if(length > 0){
            var oldPageItems = [];
            var maxItemLength =length;
            var menuLabel = 'Back to the...';
            var script = 'self.on("click", function (node, data) { ' +
                         '    history.go(data);' +
                         '});';
            for(var i = 0; i < maxItemLength; i++){
                oldPageItems.push(
                    contextMenu.Item({
                        label: (i + 1).toString() + '-previous page',
                        data: -1 * (i + 1)
                    }));
            }
            backMenu = contextMenu.Menu({
                label: menuLabel,
                contentScript: script,
                items: oldPageItems
            });
        }
    }

    PageMod({
        include: ["*"],
        contentScript: 'self.postMessage(history.length);',
        onAttach: function onAttach(worker) {
            worker.on('message', function(length){
                createNewBackMenu(length - 1);
            });
        }
    });

}

ここでは、ユーザがページを遷移するたびにコンテクストメニューを更新する必要があるため、ページ遷移時に Content Script を実行できる page-mod モジュールを用いて、この機能を実装しました。

PageMod クラスのコンストラクタで include: [“*”] と指定することにより、任意のページで Content Script を実行することができます(31行目)。
PageMod の Content Script で history.length の値を取得し、その値を用いて createNewBackMenu() 関数を実行します(32~37行目)。
PageMod から Content Script とデータのやりとりをする場合には、 worker オブジェクトを利用します。
history.length の値は、現在のページを含んだ履歴数なので、 length – 1 個のメニューアイテムを生成します(35行目)。

また、新しいサブメニューを作成するときには、古いほうのサブメニューを削除しておかなければならないことに注意してください(7行目)。
そうしなければ、ページを遷移するたびに、新しいサブメニューが追加されてしまいます。


動作例

これにより、ページを遷移するたびにコンテクストメニューが更新され、履歴の数に応じたメニューアイテムが表示されます。

アドオンに情報を蓄積

上記のような History オブジェクトを使った方法では、履歴の URL やタイトルを直接取得することはできません。そこで、Add-on SDK の simple-storage モジュールを使って、ページのタイトル・URL・訪問時刻を記録し、その履歴情報を用いてコンテクストメニューを作成するようにしてみます。

main.js の前半部分に、次のようなコードを書き加えます。

const contextMenu = require("context-menu");
const storage = require("simple-storage").storage;
const PageMod = require("page-mod").PageMod;

exports.main = function(){
    var storageMenu;
    var maxItemLength = 10;
    var clearOldStorage = function(){
        storage.history = [];
    }
    var storeHistory = function(title, url){
        var time = new Date().toLocaleString();
        if (storage.history) {
            storage.history.unshift({
                title: title,
                url: url,
                time: time
            });
        } else{
            console.error('storage.history is undefined');
        }
    }

…

ここでは、 ストレージに履歴情報を蓄積する関数を定義しています(11~22行目)。
simple-storage モジュールを読み込むと、モジュールの storage オブジェクトに対して、通常の JavaScript オブジェクトと同様の方法で値を格納することができるようになります。

storage オブジェクトに格納した値は、 Firefox プロファイルフォルダの 「jetpack/拡張機能のプログラムID/simple-storage/storage.json」 へ記録され、永続的に保存されます。

続いて、先ほど定義した createNewBackMenu 関数を、次のように書き換えます。

…
    var createNewStorageMenu = function(length){
        if(storageMenu !== undefined) storageMenu.destroy();
        if(storage.history){
            var storagePageItems = [];
            var menuLabel = 'Move to the...';
            var script = 'self.on("click", function (node, data) { ' +
                         '    location.href = data;' +
                         '});';
            var storageMenuLength = Math.min(length, storage.history.length);
            for(var i = 0; i < storageMenuLength; i++){
                storagePageItems.push(
                    contextMenu.Item({
                        label: '[' + storage.history.time +'] ' + storage.history.title,
                        data : storage.history.url
                    }));
            }
            storageMenu = contextMenu.Menu({
                label: menuLabel,
                contentScript: script,
                items: storagePageItems
            });
        }
    }

…

新たに定義した createNewStorageMenu 関数は、引数 length を上限値として、 storage オブジェクトに格納された履歴情報をコンテクストメニューに追加します。

最後に、 PageMod を用いて、画面遷移のタイミングでページ情報を storage に格納し、コンテクストメニューを更新するように設定します。

…
    clearOldStorage();

    PageMod({
        include: ["*"],
        onAttach: function onAttach(worker) {
            storeHistory(worker.tab.title, worker.tab.url);
            createNewStorageMenu(maxItemLength);
        }
    });

}  // end of exports.main

ページのタイトルや URL の情報は、 worker オブジェクトのプロパティから参照できます。
したがって、今回は PageMod で Context Script を実行してページ情報を取得する必要はありません。


動作例

これによって、過去に閲覧したページのタイトルと訪問時刻をコンテクストメニューに表示し、メニューアイテムをクリックすることでそのページへ移動できる拡張機能が実装できました。

次回:アドオンのモジュール分割・ローカライズ

これまで紹介したアドオンの実装では、 Content Script を除いて、すべてのコードを main.js に記述してきました。
次回の記事では、アドオンのコードをモジュールに分割して、自作モジュールを作成する方法について紹介します。
それに合わせて、ローカライズのための properties ファイルを作成し、ラベルの文字列などを言語に応じて選択する方法を紹介する予定です。

次回の記事:Add-on SDK で始めるアドオン開発(自作モジュール編)