Menu Family: Setting Menu Configuration Properties At Runtime

このExample(YUIのページへのリンクはこちら)を、実際に動かし見て、戸惑ってしまった。

サンプル自体は、非常に単純なもので、MenuItemのコンフィグが変化するイベント(configChanged)と、そのハンドラを仕掛け、メニューの表示からその後の操作までのコンフィグの変化を捉えるというものだ。

戸惑ってしまったのは、やはり、イベントの扱いである。
configChangedというカスタムイベントと、それを発火するメソッドを持つのは、以前にもでてきた、MenuItemにあるcfg(YAHOO.util.Configのインスタンス)である。

まずは、サンプルの動きをみる。YUIのExampleでは、MenuItemのコンフィグの変化だけを追うようになっているが、このcfg属性はMenuインスタンスにも存在して、外部からアクセスできるので、Menuのコンフィグの動きもおうようにした。
また、YUIのExampleではLoggerに書かせているが、ここでは画面に表示させるようにした。
初期画面は以下。このタイミングで、コンフィグがセットされるので、いくつかのコンフィグの変化が表示される。赤がMenuのコンフィグの変化、黒がMenuItemのコンフィグの変化である。

以下がメニューを表示させたときの動き。Menuの動きは青線で囲み、MenuItemの動きは緑線で囲んでみた。


これを見てあきらかなのだが、取得できそうなのにundefinedとなってしまうものがある。
特に、緑でくくった部分では、肝心のindexが取れない。
プログラムミスかと思って、YUIのExampleページを確認したが、やはり、undefinedとなってしまっている。
この原因は、イベントハンドラーが、MenuItemオブジェクトscopeで動いていないことに起因するようで、(定石どおりに)thisでMenuItemのインスタンスが取得できない。
ソース(下に示す)は、ほぼ、「Menu Family: Listening For Menu Events」と同じもであるが、違いは、先の例が、MenuとMenuItemオブジェクトにイベントとハンドラをsubscribeしていたのに対して、configChangedというイベントが、YAHOO.util.Configのイベントであるために

   Menu(もしくは、MenItem)のインスタンス.cfg.subscribe("configChanged",ハンドラ);

とやっている点である。また、YAHOO.util.Configのソースを追ってみても、今ひとつ、ピンとこない。
とりあえず、属性名と値は取得できそうであるが、なによりも、ハンドラのスコープがMenuItemでないということは、定石に反する。

YAHOO.util.Configには、owerというプロパティーがあって、そこにMenuやMenuItemのような、コンフィグを依頼するインスタンスが保管されている(toString()でowerが表示できる)。だが、このプロパティーは隠蔽されていて、アクセッサーが定義されていない(これも何か意味あんのかな)。

このように、一つのハンドラにMenuItemのconfigChangedイベントをハンドルさせる(手っ取りはやい方法として考えられるもの)には、

  1. 「Menu(もしくは、MenItem)のインスタンス.cfg.subscribe("configChanged",ハンドラ)」のタイミングで、この「Menu(もしくは、MenItem)のインスタンス」から、indexを引っ張り出して、別に用意したカスタムイベントとハンドラに渡してしまう。
  2. Configをカスタマイズして、owerのgetterを定義してしまい、ハンドラの引数として渡してしまう。

というようなやり方かもしれない。
いずれにしても、上で書いたsubscribeの方法ではうまくいかない。

時間がかかりそうなのと、configChangedというイベントがあまり使いそうにないイベント(実際には、「Handling Menu Click Events」の応用で十分と思われる)なので、ここら辺でやめにするが、面倒な仕様であることには違いないと思う。

コードの誤りなどあったら、ご指摘いただければうれしい。

以下に上のサンプルコードの全文を示す。

<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<TITLE>Ajax_Sampling</TITLE>
<style type="text/css">
body {
	margin:0;
	padding:0;
}
</style>
<link rel="stylesheet" type="text/css" href="scripts/yui/fonts/fonts-min.css" />
<link rel="stylesheet" type="text/css" href="scripts/yui/menu/assets/skins/sam/menu.css" />
<script type="text/javascript" src="scripts/yui/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript" src="scripts/yui/container/container_core-min.js"></script>
<script type="text/javascript" src="scripts/yui/menu/menu-min.js"></script>

<style type="text/css" id="defaultstyle">
#container {
	margin: 2px;
	padding: 3px;
	width: 400px;
	height: auto;
	border:1px dashed #999999;
}
div.yuimenu a.yuimenuitemlabel-disabled:visited {
    color: #A6A6A6;
}


</style>
<script type="text/javascript">
//モジュールパターンで実装する。
YAHOO.namespace("EGP");

YAHOO.EGP.ScriptMenu = function() {

	var oMenu;
		Dom = YAHOO.util.Dom;
	
	// 第1引数はCustomイベントの種類、第2引数はconfig属性の名前と指定値の配列
	// thisはMenuItemインスタンス
	var onMenuItemConfigChange = function (p_sType, p_aArgs) {

        var sPropertyName = p_aArgs[0][0],
	        sPropertyValue = p_aArgs[0][1];
        
        var log = "<hr> Index: " + this.index + ", " +
			"Group Index: " + this.groupIndex + ", " +
			"config属性: " + sPropertyName + ", " +
			"設定値: " + sPropertyValue + "<br/>";
		Dom.get('log').innerHTML += log;
	};

	// 第1引数はCustomイベントの種類、第2引数はconfig属性の名前と指定値の配列
	// thisはMenuItemインスタンス
	var onMenuConfigChange = function (p_sType, p_aArgs) {

        var sPropertyName = p_aArgs[0][0],
	        sPropertyValue = p_aArgs[0][1];

        var log ='<hr><font color="red"> id: ' + this.id + ', ' +
			'config属性: ' + sPropertyName + ', ' +
			'設定値: ' + sPropertyValue + '</font><br/>';
	  Dom.get('log').innerHTML += log;
	};

	return{

		init: function() {
		   // Muneのインスタンス生成
		   // コンストラクタの第1引数は、menuを構成するdivのid(この場合ない)、
		   // 第2引数は、属性を表すオブジェクトリテラル
		   // 属性はMenuのAPIドキュメント
		   // http://developer.yahoo.com/yui/docs/YAHOO.widget.Menu.html
   		
	   		oMenu = new YAHOO.widget.Menu("basicmenu", 
   			   				{ xy: [300,65] }
			);

			// MenuのitemAddedイベントで発火(MenuItem一つずつ発火する模様)。
			// 追加したMenuItemをp_aArgsで受け取って、イベントハンドラを仕掛ける。
			// イベントハンドラの第2引数には、発生させたMenuItemインスタンスがはいる。
			oMenu.subscribe("itemAdded", 
	    	   	    function (p_sType, p_aArgs) {
	            	        var oMenuItem = p_aArgs[0];

			        // MenuItemにCustomイベントハンドラーを仕掛ける。
			        // Customイベントの場合、cfgにsubscribeする。
	            	        oMenuItem.cfg.subscribe("configChanged", 
	    	            	    onMenuItemConfigChange);
			    }
        	       );

		// MenuItemを生成してMenuに追加するが、ここでは、コンフィグ属性
		// を設定しない。
	        oMenu.addItems([
                    "selectedを指定",
                    "Disabledを指定",
                    "URLにリンク",
                    "チェック済みを指定"
               ]);

		// MenuにCustomイベントハンドラーを仕掛ける。
        	oMenu.cfg.subscribe("configChanged", 
	            	onMenuConfigChange);
			
		// 引数はrenderingしたメニューのターゲット
   		oMenu.render("rendertarget");

		// Menuのコンフィグプロパティーを変更している。
		oMenu.cfg.setProperty("keepopen", true);

		// MenuItemを取り出して、コンフィグプロパティーを変更している。
		// これにより、"configChanged"というCustomイベントが発生する。
		oMenu.getItem(0).cfg.setProperty("selected", true);
   	        oMenu.getItem(1).cfg.setProperty("disabled", true);
   	        oMenu.getItem(2).cfg.setProperty("url", "http://www.yahoo.com");
   	        oMenu.getItem(3).cfg.setProperty("checked", true);
   	        
		// Bottunのクリックイベントにメニューの表示(show)を仕掛ける。
		// EventのAPIはhttp://developer.yahoo.com/yui/docs/YAHOO.util.Event.html
		// 第4引数は、ハンドラーへの引数、第5引数は実行scope.
   		YAHOO.util.Event.addListener(
   		  				"menubutton", 
   		   				"click", 
   		   				oMenu.show, 
   		   				null, 
   		   				oMenu
   		);
	    }
	};
}();

//DOMが完全にloadされたら、サンプルを初期化する。
YAHOO.util.Event.onDOMReady(
	YAHOO.EGP.ScriptMenu.init
);

</script>
</HEAD>

<!-- class=" yui-skin-sam"の指定が必要 -->
<BODY class="yui-skin-sam">
<div id="container">
<p>
メニューを生成後に、Config属性を変更します。<br/>
この変更イベント("configChanged")を捕らえて、以下に表示します。
</p>
<button id="menubutton" type="button">Menuを表示</button>
<div id="rendertarget"></div>
<div id="log"></div>
</div>
</BODY>
</HTML>