Menu Family: Adding A Context Menu To A TreeView

ひさびさに「実用的な」Exampleがでてきた(YUIのExampleのページへのリンクはこちら)。

内容はTreeViewのテキストノードに、ContextMenuを仕込み、ノードの追加、ラベルの編集、ノードの削除を行わせるというもの。
ContextMenuは、TreeView全体に対して1つのインスタンスを生成する、という戦略がとられていて、これは、先の例「Menu Family: Adding A Context Menu To A Table」と同様である。

以下は初期画面。

以下はノードを一つ選んで、右クリックをしたときの画面。ContextMenuが表示されている。
ここで、「子ノードの追加」、「ラベルの編集」を選ぶとラベル名を入力するためのwindowが表示される。

以下にJavascriptを含む、htmlファイルの全体を示す。
YUIのMenuをここまでやってきて、簡単なMenu機構を作るのであれば、MenuモジュールではなくTreeViewの方が見通しがよいように感じている。
以下のScriptも、比較的見通しのよいものとなっている。
特に、イベントのハンドリングは、コンストラクタへのonclick属性定義によるもので、この程度であれば、さほどの苦労なく拡張ができる。
Menu機構を考える上では「TreeView Control: Inline Editing of TreeView Node LabelsComments」と並んでよいサンプルであると思う。

<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/reset/reset.css" />
<link rel="stylesheet" type="text/css" href="scripts/yui/menu/assets/skins/sam/menu.css" />
<link rel="stylesheet" type="text/css" href="scripts/yui/treeview/assets/skins/sam/treeview.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>
<script type="text/javascript" src="scripts/yui/treeview/treeview.js"></script>

<style type="text/css" id="defaultstyle">

h1 { 
	font-weight: bold; 
	margin: 0 0 1em 0;
}

body {
	padding: 1em;
}

p, ul {
	margin: 1em 0;
}
            
p em,
#operainstructions li em {
	font-weight: bold;
}

#operainstructions {
	list-style-type: square;
	margin-left: 2em;
}

</style>

<script type="text/javascript">

YAHOO.util.Event.onContentReady("mytreeview", function () {

	var Dom = YAHOO.util.Dom;
	
    /*
    Map of YAHOO.widget.TextNode instances in the 
    TreeView instance.
	*/

	// oTextNodeMap[ラベルid]にTextNodeインスタンスを格納する
	var oTextNodeMap = {};

	// TreeViewインスタンスを生成
	var oTreeView = new YAHOO.widget.TreeView("mytreeview");

	var n, oTextNode;
	// 3個から7個のトップレベルのノードを作成する。
	for (n = 0; n < Math.floor((Math.random()*4) + 3); n++) {

   		oTextNode = new YAHOO.widget.TextNode("label-" + n, 
		   	oTreeView.getRoot(), false);
   
   		// oTextNodeMap[ラベルid]にTextNodeインスタンス
   		oTextNodeMap[oTextNode.labelElId] = oTextNode;

   		// ブランチの作成
   		buildRandomTextBranch(oTextNode);
	}

	// treeViewの描画
	oTreeView.draw();

	//ブランチの作成;6階層までのTreeViewを再帰的に作成する。
	function buildRandomTextBranch(p_oNode) {
   		var oTextNode,
       		i;
   		if (p_oNode.depth < 6) {
			// 乱数で、0から4個までのテキストノードを生成する
			for (i = 0; i < Math.floor(Math.random() * 4); i++) {
        		oTextNode = new YAHOO.widget.TextNode(p_oNode.label + 
                   		"-" + i, p_oNode, false);

           		oTextNodeMap[oTextNode.labelElId] = oTextNode;
           		buildRandomTextBranch(oTextNode);
       		}
   		}
	}


	//------------------ 以下、イベントの処理 ---------------------
	
	// contextmenu(DOM)イベントで、ContextMenuインスタンスを発生・表示させた
	// TextNodeインスタンスを保管する。
	var oCurrentTextNode = null;

	//oCurrentTextNode(上述)に新しいTextNodeを追加する。
	function addNode() {

   		var sLabel = window.prompt("新しいノードのラベルを入力: ", ""),
       		oChildNode;

   		if (sLabel && sLabel.length > 0) {
  			//新しいNodeの追加(展開しない)     
       		oChildNode = new YAHOO.widget.TextNode(sLabel, 
    	       oCurrentTextNode, false);

  			// oCurrentTextNode(上述)をリフレッシュして展開
       		oCurrentTextNode.refresh();
       		oCurrentTextNode.expand();
       
			//oTextNodeMapに新規保管
       		oTextNodeMap[oChildNode.labelElId] = oChildNode;
   		}
	}


	//oCurrentTextNode(上述)のラベルを変更する。
	function editNodeLabel() {

   		var sLabel = window.prompt("ラベル名を入力してください: ", 
		   		oCurrentTextNode.getLabelEl().innerHTML);

   		if (sLabel && sLabel.length > 0) {
       		oCurrentTextNode.getLabelEl().innerHTML = sLabel;
   		}
	}


	//oCurrentTextNode(上述)のラベルを削除する。
	function deleteNode() {

		//oTextNodeMapから除去
		delete oTextNodeMap[oCurrentTextNode.labelElId];

   		oTreeView.removeNode(oCurrentTextNode);
		//drawしなおし。
   		oTreeView.draw();
	}

	// contextmenuイベントのハンドラー
	// ContextMenuを表示させたTextNodeへの参照をoCurrentTextNodeにセットする。
	function onTriggerContextMenu(p_oEvent) {

		var oTarget = this.contextEventTarget;

   		// ygtvlabelはTExtNodeのラベルスタイル
   		// http://developer.yahoo.com/yui/docs/TextNode.js.html
   		var oTextNode = Dom.hasClass(oTarget, "ygtvlabel") ? 
   				oTarget : Dom.getAncestorByClassName(oTarget, "ygtvlabel");

		// TextNodeが見つかったら、それをoCurrentTextNodeにセット。
		// 見つからなかったら、キャンセル(ContextMenuは表示されない)。
   		if (oTextNode) {
       		oCurrentTextNode = oTextNodeMap[oTarget.id];
   		} else {
       		// Cancel the display of the ContextMenu instance.
       		this.cancel();
   		}
	}


	// TreeView全体に対して、ContextManuのインスタンスを生成する。
	var oContextMenu = new YAHOO.widget.ContextMenu("mytreecontextmenu", {
					trigger: "mytreeview",
					lazyload: true, 
					itemdata: [
						{ text: "子ノードの追加", 
							onclick: { fn: addNode } },
						{ text: "ラベルの変更", 
							onclick: { fn: editNodeLabel } },
						{ text: "ノードの削除", 
							onclick: { fn: deleteNode } }
						] 
				}
	);

	// ContextManuのインスタンスにcontextmenuイベントをlistenさせ、
	// ハンドラー(onTriggerContextMenu)を登録する。
	oContextMenu.subscribe("triggerContextMenu", 
			onTriggerContextMenu);

});

</script>
</HEAD>

<!-- class=" yui-skin-sam"の指定が必要 -->
<BODY class="yui-skin-sam">
<p>
TreeViewにコンテキストメニューを表示するサンプルです。
</p>
<div id="mytreeview"></div>

</BODY>
</HTML>