Symfony+Ajaxで「戻る」を実現する

Ajaxを使ってフォームのフィールドの変化に応じてdivの中身を入れ替えるような処理の場合、ブラウザの「戻る」機能を正常に使うことができなくなります。

フォントサイズの変更や「戻る」「進む」などのブラウザが標準で搭載している機能が使えなくなることはユーザの自由を奪ってしまうことになり、ユーザビリティがよくありません。


いろいろ調べてみたところ、「みかログ: Ajaxと戻るボタン・ブックマーク」というサイトにて対処法を発見しました。

ポイントとしては、

  • IE以外ではlocation.hashで各オペレーションに対して異なるhashを生成
  • IEでは隠しIFRAMEにlocation.hash生成

というあたりらしいです。

Javascriptを直接記述する場合は上記サイトのjQueryプラグインで事足りると思いますが、私の場合Symfonyを使用していたため、その中のヘルパ関数であるobserve_fieldを使っていたため、なんとかこれを前述の機能に対応させることができないか、半日かけて考えました。

希望としては前述のobserve_fieldをオーバーライドできるのがもっともよいのですが、これはphpのグローバル関数なのでそれは不可能。そこで、別名の同じインターフェイスの関数を定義(observe_field_wh)することにしました。

具体的な方法

まず、デフォルトのobserve_fieldを元にするため、それをコピーしてきます。内部では別の関数、_build_observerを呼んでいるので、これも別名(_build_observer_wh)を定義します。

また、prototype.jsをヘッダに追加しているので、今回の履歴対応させるためのjavascriptのファイルも同様に追加しておきます。

  /**
   * observe_field with history
   */
  function observe_field_wh($field_id, $options = array())
  {
    sfContext::getInstance()->getResponse()->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
    sfContext::getInstance()->getResponse()->addJavascript('ajax_history');

    if (isset($options['frequency']) && $options['frequency'] > 0)
    {
      return _build_observer_wh('Form.Element.Observer', $field_id, $options);
    }
    else
    {
      return _build_observer_wh('Form.Element.EventObserver', $field_id, $options);
    }
  }

次に、上記関数で呼んでいる _build_observer_wh ですが、これは元の_build_observerではForm.Element.Observerの引数として new Ajax.Updater() をしているだけです。

これを改造して、イベントが発生したときにlocation.hashの書き換えと new Ajax.Updater() の両方を行うことができるようにします。

まず、Form.Element.Observerの引数に直接入っている Ajax.Updater を関数の中に入れ引数から外へ出してやります。ここで定義した関数は、hashの書き換え時にcallbackさせます。このとき、もしobserve_field_whを同一ページで何度も使用した場合に関数名が重複しないように、関数名に乱数をくっつけています(現在 time() になっていますが、mt_rand() などを使うように書き換えた方がいいと思います・・)。

次に、Form.Element.Observerの引数としてhashを書き換える関数(別途JSファイルで定義)と先ほどのUpdaterを記述しておきます。

IE対策としてJavascriptコード出力時に同時にIFRAMEも出力しておきます。

  /**
   * _build_observer with history
   */
  function _build_observer_wh($klass, $name, $options = array())
  {
    if (!isset($options['with']) && $options['update'])
    {
      $options['with'] = 'value';
    }

    $callback = remote_function($options);

	$updator = "_ajax_updator_" . time();

    $javascript  = " function ".$updator.'(value) {'.$callback.'} history_callback_function = '.$updator.'; ';
   
    $javascript .= 'new '.$klass.'("'.$name.'", ';
    if (isset($options['frequency']) && $options['frequency'] > 0)
    {
      $javascript .= $options['frequency'].", ";
    }
    $javascript .= "function(element, value) {change_location('$name', value); ";
    $javascript .= $updator.'(value)});';

    return javascript_tag($javascript).'<iframe id="symfony_ajax_history_iframe" style="display: none;" width="0" height="0"></iframe>';
  }

JavascriptHistoryHelper.php

Javascript側のコードでは、ページロード完了時に現在のlocation.hashをバックアップしておき(IEの場合はIFRAMEのlocation.hash)、定期的にhashの変更のチェックを行うようタイマをセットします。

あとは、フィールド変更時にはJavascriptで、「戻る」「進む」時には履歴で、location.hashが変更されますので、定期的にチェックしているスクリプトがhashの変更を検地するとdivの中身を書き換える処理を実行することができます。

var isMSIE = /*@cc_on!@*/false;  // from http://dean.edwards.name/weblog/2007/03/sniff/
var symfony_current_hash;
var history_callback_function;
var observe_form_name;

function change_location(name, value)
{
	observe_form_name = name;
	var newhash = '#' + value;
	location.hash = newhash;
	symfony_current_hash = newhash;
	if(isMSIE)
	{
		updateIframeHash(newhash);
	}
}

function updateIframeHash(value)
{
	var ihistory = $('symfony_ajax_history_iframe');
	var iframe = ihistory.contentWindow.document;
	iframe.open();
	iframe.close();
	iframe.location.hash = value;
}

function history_check_updator()
{
	if(isMSIE)
	{
		var ihistory = $('symfony_ajax_history_iframe');
		var iframe = ihistory.contentWindow.document;
		var current_hash = iframe.location.hash;
		if(current_hash != symfony_current_hash)
		{
			location.hash = current_hash;
			symfony_current_hash = current_hash;
			var value = current_hash.replace(/^#/,'');
			history_callback_function(value);
		}
	}
	else
	{
		var current_hash = location.hash;
		if(current_hash != symfony_current_hash)
		{
			symfony_current_hash = current_hash;
			var value = current_hash.replace(/^#/,'');
			history_callback_function(value);
		}
	}
}

Event.observe(window, "load", function()
{
	symfony_current_hash = location.hash;
	if(isMSIE)
	{
		updateIframeHash(location.hash);
	}
	history_callback_function(location.hash.replace(/^#/,''));
	setInterval(history_check_updator, 100);
});

ajax_history.js

今日はこれを考えるので相当疲れました・・。

実はこれだけでは、もうちょっと不十分な点があるので、それについてはまた対応を考えていきたいと思っています。。

LINEで送る
Pocket

Symfony+Ajaxで「戻る」を実現する」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です