機能を他のアドオンからキャンセルできるようにする

このエントリは、一つ前のカスタムイベントの通知方法の紹介を踏まえた内容になっています。先に前のエントリを読んでからご覧になる事をお勧めします。

さて、前のエントリでは closeTabs() 関数が呼ばれた時にそれを通知する方法を解説しました。しかし前のエントリに書いた方法だけだと、以下のような制限があります。

  • イベントを通知する側から検知する側へ、一方通行でしか情報を送れない。

例えば、何か時間のかかる処理をするアドオンを開発していたとしましょう。

gBrowser.addEventListener(
  'ClosingTabs',
  function (aEvent) {
    // お、複数のタブが閉じられようとしているぞ!
    if (aEvent.tabs.some(function(aTab) {
          return aTab.hasAttribute('wait-for-a-while');
        })) {
      // 処理中のタブがある!
      // これは今閉じられると困るなあ。
      // でも、「やめて!」と伝える方法がない><
    }
  },
  false
);

このように、処理の中止を訴えたくてもイベントを発行した側である closeTabs() に対して値を返したりメッセージを送ったりできないので、指をくわえて見ていることしかできません。

こういうニーズに備えて、事前にキャンセルができるようなタイミングでカスタムイベントを通知する時は、キャンセルできるイベントとして通知するようにするとよいでしょう。

// イベント通知側
function closeTabs(aTabs) {
  var event = document.createEvent('Events');
  // 好きなイベント名を付ける。
  // この時、第3引数が true だと、キャンセルできるイベントになる。
  event.initEvent('ClosingTabs', true, true);
  event.tabs = aTabs;
  gBrowser.dispatchEvent(event);
  // イベントがキャンセルされていたら、処理を中断する。
  if (event.getPreventDefault())
    return;

  ...
}

// イベント検知側
gBrowser.addEventListener(
  'ClosingTabs',
  function (aEvent) {
    // お、複数のタブが閉じられようとしているぞ!
    if (aEvent.tabs.some(function(aTab) {
          return aTab.hasAttribute('wait-for-a-while');
        })) {
      // 処理中のタブがあるからやめて!
      aEvent.preventDefault();
    }
  },
  false
);

イベントを検知する側でイベントオブジェクトの preventDefault() メソッドを呼ぶと、getPreventDefault() の戻り値が false から true に変わります。なので、これを見ればイベント通知側からも、イベントがキャンセルされたかどうかを判別できるというわけです。

ただし、Firefox 3.6 以前では UIEvents として生成・初期化したイベントでなければ getPreventDefault() が使えないという問題があります。Firefox 3.5 以前でも正常に動作するようにするには、以下のようなハックを行う必要があります。

function closeTabs(aTabs) {
  var event = document.createEvent('Events');
  event.initEvent('ClosingTabs', true, true);
  event.tabs = aTabs;
  gBrowser.dispatchEvent(event);
  ensureEventCancelable(event); // ←ここで問題を解消
  if (event.getPreventDefault())
    return;

  ...
}

function ensureEventCancelable(aEvent) {
  // Gecko 1.9.2 ( Firefox 3.6 )以降なら何もしない
  if (aEvent.getPreventDefault) return;

  // 元の関数を保存
  aEvent.__original__preventDefault = aEvent.preventDefault;
  aEvent.__canceled = false;
  aEvent.preventDefault = function() {
    this.__original__preventDefault();
    // キャンセルされたかどうかのフラグを独自に立てる
    this.__canceled = true;
  };
  aEvent.getPreventDefault = function() {
    return this.__canceled;
  };
}

……というこのハックを見るとバレバレなのですが、実はイベントオブジェクトのプロパティを介しても情報のやりとりはできます。ただ、preventDefault() はリンクのクリックによるページ遷移をキャンセルするといった場合でも使われる、W3C の DOM Level2 Events の仕様にも含まれる広く知られたメソッドなので、それと同じ方法でイベントをキャンセルできるようにしておいた方が、イベントを検知するアドオンを作る人にとっては分かりやすいでしょう。

nsIObserverService など他の方法を使うと、クロスウィンドウでイベントを通知するなどのより高度なこともできます。とりあえず、まずはウィンドウ内で完結するイベントの仕組みから始めてみて、限界を感じたら別の方法を検討してみてください。