Yaks

Prototype.js 1.6 + Safariで onbeforeunloadに Event.observeを使うと確認ダイアログが表示されない はてなブックマーク - Prototype.js 1.6 + Safariで onbeforeunloadに Event.observeを使うと確認ダイアログが表示されない

久々に技術系のお話。

DOM(window)のイベントに onbeforeunloadというものがあります。 これは今開いているブラウザのウィンドウ(タブ)が閉じられる(または別のページに遷移、再読み込みされる)直前に発生するイベントで、編集中の内容があるかどうかなどをチェックして、実際にウィンドウを閉じてよいかを確認するダイアログを表示することができます。(例えば、Gmailでメールを編集中に再読み込みなどを行うと表示されるダイアログがこのイベントを使っています。)

このイベントは IE6以降、Firefox、Safari3以降でサポートされているようです。(Operaでは動作しませんでした。) ですが Safari3.1.1で、その確認ダイアログが表示されないという報告があり、色々と調べてみました。

報告された環境では prototype.jsの最新版1.6.0.2を使っており、以下のように Event.observeでイベントハンドラを追加していました。(説明を簡易にするため、実際のコードとは変えています。)

Event.observe( window, 'beforeunload', function( aEvent ){
	aEvent.returnValue = 'ほんとに閉じてよいですか?';
} );

そしてこの部分を Safariで動かしてみると、なるほど確かにダイアログが表示されません。(Mac、Windowsどちらも)
ためしに alertを置いてやるときちんと呼び出されるので、どうやらイベントハンドラ自体は動作しているようでした。

で、調べてみると以下のような記事を見つけました。

上記の結果、以下のようにすれば IE 6.0/7.0、Firefox 2.0.0.5、Safari 3.0.2 (beta) でもうまくいくことがわかりました。

Event.observe(window, 'beforeunload', function(e) {
    // イベントをキャンセルする場合は、なにも返さない。
    return e.returnValue = '<任意のメッセージ>';
});

(prototype.js の Event.observe を使った onbeforeunload について - cl.pocari.org)

早速上記のように書き換えて実行してみると... うまくいきません。

上記ページにも書かれている通り、window.onbeforeunloadに直接イベントハンドラを代入すると動作しましたが、 イベントハンドラを上書きしてしまうなどあまりよろしくない方法なので最後の手段として取っておき、まずは動かない原因を探ってみることにしました。

そこでまずは問題を切り分けるため、まずは Event.observeを使わずに addEventListenerを直接叩いてみることにしました。

window.addEventListener( 'beforeunload', function( aEvent ) {
    return aEvent.returnValue = 'ほんとに閉じてよいですか?';
}, false );

...動作しました。

つまり、prototype.jsの Event.observeが原因である可能性が高まりました。

というわけで prototype.jsのソースを眺めてみると、以下の部分を見つけました。

  (3866行目から)
  function createWrapper(element, eventName, handler) {
    var id = getEventID(element);
    var c = getWrappersForEventName(id, eventName);
    if (c.pluck("handler").include(handler)) return false;

    var wrapper = function(event) {
      if (!Event || !Event.extend ||
        (event.eventName && event.eventName != eventName))
          return false;

      Event.extend(event);
      handler.call(element, event); ←ここ
    };

    wrapper.handler = handler;
    c.push(wrapper);
    return wrapper;
  }

以前のバージョン(1.5.x)ではイベントハンドラ関数をキャッシュしていたものの、関数自体は Event.observeで指定した関数が直接セットされていました。

ですが 現在の最新版である 1.6.xでは、eventオブジェクトの拡張や重複のチェックのためのラッパー関数を作成し、その中から実際のイベントハンドラ関数を呼び出すようになりました。

そのラッパー関数を作成しているのが上の Event.createWrapperです。(内部関数なので通常は使用しません。)そして、実際のイベントハンドラ関数を呼び出しているのが上記の矢印(←ここ)の部分なのですが、よく見るとイベントハンドラ関数の呼び出しに returnがありません。つまり、イベントハンドラ内で returnで値を返しても無視されてしまっています。

そして safariでは onbeforeunloadの確認メッセージは returnで返す必要があるものの上記の部分のせいで伝わらず、確認ダイアログが表示されないということのようです。 ためしに太字の部分に returnを追加してやると正しく動作しました。

ただ、フレームワークを直接いじるのもいやなので、とりあえずは Safariの場合のみ window.addEventListenerを直接呼び出すようにしました。

もし同様の処理を検討している場合には、ちょっと注意が必要かもしれません。

# ...ちなみにこれは prototype.jsと Safariどちらの問題になるのか...
# 一応 prototype.jsと WebKitの不具合管理システムを見てみましたが、それらしきものは挙がっていないようでした。

であ、また。