Home > Latest topics

Latest topics > 汎用的なアンドゥ・リドゥ機能を提供するoperationHistory.jsの基本的な使い方

宣伝1。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

宣伝2。Firefox Hacks Rebooted発売中。本書の1/3を使って、再起動不要なアドオンの作り方のテクニックや非同期処理の効率のいい書き方などを解説しています。既刊のFirefox 3 Hacks拡張機能開発チュートリアルと併せてどうぞ。

Firefox Hacks Rebooted ―Mozillaテクノロジ徹底活用テクニック
浅井 智也 池田 譲治 小山田 昌史 五味渕 大賀 下田 洋志 寺田 真 松澤 太郎
オライリージャパン

汎用的なアンドゥ・リドゥ機能を提供するoperationHistory.jsの基本的な使い方 - Jan 11, 2010

Undo Tab Operationsの核であるoperationHistory.jsは、タブに限らずいろんな操作に対してアンドゥ・リドゥを実装しやすくするための汎用のライブラリとして設計しています(一応)。こいつの使い方を、自分の頭の中の整理も兼ねて少しずつ解説していこうと思います。


  • 最新版はCOZMIX NGにあります。
  • 名前は「UI Operations History Manager」としています。長いので、以下「operationHistory」とだけ呼んでおきます。
  • ライセンスはMITライセンスとします。
  • Firefox 2以降でないと動きません。

読み込み方

まず、読み込みの方法。以下のようにJavaScriptのファイルを読み込ませるだけでOKです。

<script src="lib/operationHistory.js"
        type="application/javascript"/>

複数のアドオンでこのライブラリを読み込んでいる場合、最もリビジョンの新しい物が使われます。今後はAPIは変えないor後方互換を維持していくつもりなので、使う方はあんまり気にしないで使えるはずです。

operationHistoryの各機能には window['piro.sakura.ne.jp'].operationHistory でアクセスできます。以下の説明ではサンプルコードを短くするために、 OH という変数でこれを参照しているものとします。

var OH = window['piro.sakura.ne.jp']
               .operationHistory;

処理をやり直し可能にする

operationHistoryを使って任意の処理をアンドゥ・リドゥ可能にしたい時は、その処理を以下のように実行するようにします。

OH.doOperation(function() {
  // 任意の処理
});

doOperation()の引数に関数を渡すと、それがその場で実行されます。実行時のthisOH自身を指していますが、普通に分かりにくいんで、クロージャを使うなり何なりして好きなように書くといいと思います。

var MyAddon = {
  myFeature : function() {
    var self = this;
    OH.doOperation(function() {
      self.myInternalMethod();
    });
  },
  myInternalMethod : function() {
    // 何かの処理
  }
};

で、これだけだとまだアンドゥ・リドゥはできません。doOperation()に対して以下の引数をさらに指定してやる必要があります。

  • 履歴の名前(文字列、省略可能)
    • "MyAddonOperations" とかそんな感じで好きに名前を付けて下さい。省略すると、履歴の対象のウィンドウが指定されている場合は "global" 、そうでなければ "window" になります。
    • Undo Tab Operationsでは "TabbarOperations" という名前を指定してます。
  • 履歴の対象になるウィンドウ(nsIDOMChromeWindow、省略可能)
    • 拡張機能の名前空間で普通に見える所のwindowです。省略すると、ウィンドウ単位の履歴ではなく、クロスウィンドウな単一の履歴となります。が、動作が怪しいので今の所はウィンドウ単位での使い方だけ推奨しておきます。
  • 履歴エントリ(オブジェクト、必須)
    • オブジェクトリテラル、カスタムクラスなど、何でもOKです。詳しくは後述。

やり直し可能にしたい処理自体と合わせると、doOperation()は最大で4つまでの引数を取るという事ですね。引数は全部型が違う(関数、文字列、DOMWindow、オブジェクト)ので、doOperation()はそれらを受け取った後に、どれがどれなのかを自動的に判別します。なので引数はどの順番で指定しても構いません。

実際のアンドゥ・リドゥ処理を書く(関数を使うやり方)

実際のアンドゥ・リドゥ処理は、履歴エントリになるオブジェクトのプロパティとして関数で定義します。

var entry = {
  // 内部名
  name   : "undotab-addTab",
  // メニュー等に表示する「やり直す処理の名前」
  label  : "タブを開く",
  // アンドゥ時に実行される内容
  onUndo : function(aParams) {
    gBrowser.removeTab(this.tab);
  },
  // リドゥ時に実行される内容
  onRedo : function(aParams) {
    this.tab = gBrowser.addTab();
  },
  // 以下、任意のプロパティを好きなようにどうぞ
  tab : null
};

OH.doOperation(
  function() { // やり直し可能にする処理
    entry.tab = gBrowser.addTab();
  },
  'MyAddonOperations', // 履歴名
  window,              // 処理対象ウィンドウ
  entry                // 履歴エントリ
);

onUndoという名前のプロパティで関数を定義しておくとアンドゥ時にそれが実行されます。onRedoはリドゥの時に実行されます。どちらもthisは履歴エントリのオブジェクト自身になりますので、まあ見た通りで分かりやすいんじゃないかと思います。クロージャ使って書いても全然構いません。

namelabelは、文字列で好きなように名前を付けて下さい。定義しなくても使えますが、デバッグの時にはあると便利ですし、DOMイベントを使った記法(後で解説します)の時には無いと困ります。

アンドゥする・リドゥする

上記のようにして履歴に登録した処理は、以下のようにしてアンドゥ・リドゥできます。

// アンドゥ
OH.undo('MyAddonOperation', window);
// リドゥ
OH.redo('MyAddonOperation', window);

undo()redo()の引数には、doOperation()に対して指定したものと同じ履歴名と処理対象のウィンドウを渡します(こちらも引数の指定順は任意です)。ここではまだ「タブを開く操作」しか書いていませんが、同じ履歴名で「タブを閉じる操作」「タブを移動する操作」などに対してそれぞれアンドゥ・リドゥの処理を書いてやれば、線形にそれらをアンドゥ・リドゥできるようになります。

また、「戻る」「進む」のドロップダウンメニューで項目を指定してそこまで一気に飛ぶのと同じように、goToIndex()で履歴項目のインデックスを指定してそこまで一気にアンドゥする・リドゥする事もできます。

// 現在の位置を得る
var history = OH.getHistory('MyAddonOperation', window);
var current = history.index;
OH.goToIndex(current-3, 'MyAddonOperation', window);

getHistory()は、登録済みの履歴項目の全エントリを格納したオブジェクトを取得するメソッドです。それで取得したオブジェクトのindexプロパティで現在のフォーカス位置を得られるので、上の例ではそこから3つ手前に飛ぶ事になります。

要素やウィンドウに固有のIDを使う

ここまでの説明で既に疑問に思った人もいると思いますが、例えばこんな場合。

function NewTab() {
  var entry = {
    name   : "undotab-addTab",
    label  : "タブを開く",
    onUndo : function(aParams) {
      gBrowser.removeTab(this.tab);
    },
    onRedo : function(aParams) {
      this.tab = gBrowser.addTab();
      gBrowser.selectedTab = this.tab;
    },
    tab : null
  };
  OH.doOperation(
    function() {
      entry.tab = gBrowser.addTab();
      gBrowser.selectedTab = entry.tab;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

function MoveTab(aTab) {
  var entry = {
    name   : "undotab-moveTab",
    label  : "タブを移動する",
    onUndo : function(aParams) {
      gBrowser.moveTabTo(this.tab, this.oldPosition);
    },
    onRedo : function(aParams) {
      gBrowser.moveTabTo(this.tab, this.newPosition);
    },
    tab : null
  };
  OH.doOperation(
    function() {
      entry.tab = aTab;
      entry.oldPosition = aTab._tPos;
      gBrowser.moveTabTo(aTab, 3);
      entry.newPosition = aTab._tPos;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

NewTab()を実行→それで開かれたタブに対してMoveTab()を実行→アンドゥ→アンドゥ→リドゥ→リドゥ という順に操作すると、2つ目の履歴項目のリドゥ時にタブが見つからないせいでエラーになってしまいます。こうならないように、処理対象の要素は固有のIDなどで識別してやらないといけません。

operationHistoryにはそのために、要素に対して一意なIDを自動的に付与してそれを元に要素を検索する仕組みがあります。先の例を安全に書くと、以下のようになります。

function NewTab() {
  var entry = {
    name   : "undotab-addTab",
    label  : "タブを開く",
    onUndo : function(aParams) {
      var tab = OH.getElementById(this.tab,
                   gBrowser.mTabContainer);
      gBrowser.removeTab(tab);
    },
    onRedo : function(aParams) {
      var tab = gBrowser.addTab();
      OH.setElementId(tab, this.tab)
      gBrowser.selectedTab = tab;
    },
    tab : null
  };
  OH.doOperation(
    function() {
      var tab = gBrowser.addTab();
      entry.tab = OH.getElementId(tab);
      gBrowser.selectedTab = tab;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

function MoveTab(aTab) {
  var entry = {
    name   : "undotab-moveTab",
    label  : "タブを移動する",
    onUndo : function(aParams) {
      var tab = OH.getElementById(this.tab,
                   gBrowser.mTabContainer);
      gBrowser.moveTabTo(tab, this.oldPosition);
    },
    onRedo : function(aParams) {
      var tab = OH.getElementById(this.tab,
                   gBrowser.mTabContainer);
      gBrowser.moveTabTo(tab, this.newPosition);
    },
    tab : null
  };
  OH.doOperation(
    function() {
      entry.tab = OH.getElementId(aTab);
      entry.oldPosition = aTab._tPos;
      gBrowser.moveTabTo(aTab, 3);
      entry.newPosition = aTab._tPos;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

getElementId()は、要素に一意なIDが付いていなければ新しいIDを生成て設定した上でそのIDを、既にIDが付いていればその値を、文字列として返します。IDは普通のid属性ではなく別の属性名で保存されるので、通常の動作を破壊することはありません。

getElementById()は、そのID文字列をキーとして要素を検索するメソッドです。tabbrowserの場合はタブなどの内部の要素は普通のdocument.getElementById()等では取得できないのですが、getElementById()はID名の文字列以外に要素ノードを渡すと、その要素の子孫だけを検索するようになります。

ここでは、タブを開く操作のリドゥにおいてsetElementId()も使用しています。これは、既に生成されたID文字列を新しく復元された要素に付与することで、その要素を元の要素の代わりとして参照できるようにするためです。


とりあえず、まずはこの辺だけ解説しておきます。

→続き

分類:Mozilla > XUL, , , , , , , , 時刻:14:14 | Comments/Trackbacks (2) | Edit

Comments/Trackbacks

no title

どうせFirefox 2以降でないと動かないのなら、script要素に指定するMIME型はapplication/javascriptのほうがいいと思います。

Commented by nanto_vi at 2010/01/11 (Mon) 18:07:26

no title

そういわれてみるとそうでした……application/x-javascriptと書くのが癖になってました。
本文を修正しておきました。ありがとうございます。

Commented by Piro at 2010/01/12 (Tue) 09:03:37

TrackBack ping me at


の末尾に2014年1月19日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2010-01-11_operationhistory-basic.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。

Post a comment

writeback message: Ready to post a comment.

2014年1月19日時点の日本の首相のファミリーネーム(ひらがなで回答)

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき

オススメ

Mozilla Firefox ブラウザ無料ダウンロード