Webアプリケーションを作るのに欠かせない部品についてちょっと考えてみます。以前カレンダーヘルパーを作りましたが,これは必須な機能であるワケじゃありません。オマケ的なものです。ほぼすべてのWebアプリケーションに必要と思われるのは,メニューの機能です。企業のサイトやYahoo!などのポータルを見ても,画面のヘッダの後などにはタブ形式のメニューがあり,画面の左側にはナビゲーション形式のメニューがあります。これらを私のアプリケーションでも使えるようになりたいということで研究してみました。
メニューのhtml的な構造
カラフルだったりクールだったりするタブ形式に見えているメニューも,ナビゲーションに表示されているメニューも,現在のメニューのトレンドでは<ul><li>タグで表記されています。cssでスタイルが設定されていなければ次のような表示になります。
- メニュー1
- メニュー2
- メニュー3
このそれぞれの<li>タグの中身に<a>タグでリンクをつけたものがメニューとなります。
<ul> <li><a href="/hoge/menu1">メニュー1</a></li> <li><a href="/hoge/menu2">メニュー2</a></li> <li><a href="/hoge/menu3">メニュー3</a></li> </ul>
そのリンクをAjaxLinkにしたならば,Ajaxを使ったメニューのできあがりです。ごらんのようにhtmlベースでは非常に簡単なものです。ただこれだけだと上記の例のようにそっけないメニューですから,クールなメニューにするにはこれに色々とスパイスを加えていく必要があります。
CSS
最初のスパイスはcssです。cssによってこれらのメニューを格好良く見せます。メニュー全体のデザインを決めるためには,ulタグにidまたはclass属性を設定して,その属性に対してcssを定義します。cssを設定することで,タブ形式のように画面の横方向に広がるメニューにすることもできます。html側では,class属性などを指定するだけで,他の部分はすべてcssにお任せします。
またメニューとして利用する場合には,どのメニュー項目がアクティブであるかを画面上で表現(メニューの色を変えるとかタブの形を変えるとか)しなければなりません。この場合もアクティブなメニュー項目のliタグにclass属性を指定することで対応できます。
<ul class="tab_menu"> <li><a href="/hoge/menu1">メニュー1</a></li> <li class="active"><a href="/hoge/menu2">メニュー2</a></li> <li><a href="/hoge/menu3">メニュー3</a></li> </ul>
一行目ではulタグにtab_menuというclass属性を指定しています。またメニュー2のliタグにactiveというclass属性をつけて,このメニューが選択されていることを示しています。このhtmlに対してcssを定義します。
ul.tab_menu { margin: 0; padding: 0; list-style-type: none; } ul.tab_menu li { margin: 0; display: inline; } ul.tab_menu li a { border: 1px solid #cccccc; margin: 0; padding: 5px 10px; text-align: center; text-decoration: none; float: left; background-color: #cccccc; } ul.tab_menu li a:hover { border-color: #ffffcc; } ul.tab_menu li.active a { border-color: #ffffcc; background-color: #ffffcc; }
tab_menuというクラスのulタグとそれに従属するタグに対してスタイルを設定しています。このcssでは,さほどクールなものにはなりませんが,必要最低限の装飾を定義しています。
- 4行目ではリストの先頭に表示される中黒を表示させないようにしています。
- 8行目ではタブ形式メニューと言うことで,リストを横方向に表示させています。
- 10~18行目,個々のメニュー項目の書式は,liタグの部分ではなくその内部のaタグに対して設定しています。
- 19~20行目のブロックは個々のメニュー項目にマウスカーソルが重なったときに境界線色を変更しています。
- 22~25で設定していいるのは,liに対してactiveというclass属性が設定されている場合の書式です。これにより選択状態のメニューの色が変わります。
これで,一応の見栄えを設定することができました。ここまでできれば背景画像などを用意することでクールなメニューに仕上げることは簡単です。しかし,選択されたメニュー項目はactiveというclass属性を設定されているものですが,メニューがクリックされたときに,li項目のclass属性を変更するしくみを作らないと,メニューをクリックしたのにメニューが切り替わらないことになります。それを実現するためにJavaScriptを使います。
JavaScript
メニューが選択された時点で,選択されているメニューが変更になります。通常のリンクの場合は,変更になった先のページでそのメニューのclass属性をactiveに設定してやればいいのですが,Ajaxで画面描画をしている場合にはメニュー部分のページ遷移は発生しませんので,ページ内でメニューの表示を変更してやる必要があります。そこでJavaScriptを使用します。次のようなスクリプトを書いてみました。
function changeMenu(obj){ var ul = obj.parentNode.parentNode; var lis = ul.childNodes; var i=0; for(i in lis){ lis[i].className = ''; } obj.parentNode.className = 'active'; }
このスクリプトをすべてのメニューの中の<a>タグのonclickから呼び出すようにします。
<li><a href="/hoge/menu1" onclick="changeMenu(this)">メニュー1</a></li>
前述のように,選択されているメニューのliタグにはactiveというclassを設定することになっています。
- 2行目,引数で渡されるobjは,<a>タグですので,その親の親が<ul>タグになります。
- 3行目,その子ノードがliタグです。その集合を得ます。
- 5~7行目,liタグのclass属性を一旦全てクリアします。
- 8行目,クリックされたメニューのliタグにacitiveというclass属性を設定します。
これで選択されたメニュー項目を変更することができるようになりました。
再びヘルパー作成
これまで,いろんなソースを引用しましたが,全部PHPではありませんね。(^_^; そろそろPHPのコードを書かなきゃいけません。
ここで作成方法を学んだメニューをCakePHPの中でそのまま利用することはできますが,せっかくですので,これをまたヘルパーにしたいと思います。ヘルパーにすることで非常に簡単に,またソースもわかりやすくメニューを構築することができるようになります。
クラス名 SvMenuHelper
メソッド menuInit
前述のメニュー項目を切り替えるためのスクリプトを記述したhtmlソースを返します。このメソッドが呼び出されていない状態では,menuメソッドやajaxMenuメソッドではメニュー項目を切り替える処理は出力されません。
引数
- $scriptName スクリプトの名前
メソッド menu
通常のリンクを持ったメニューのhtmlソースを返します。
引数
- $menuList メニュー表示配列。メニューをあらわす次の形式の配列を渡します。
array( array( 'title'=>'表示文字列', 'url'=>'リンク先URL', ) )
- $default デフォルトで選択状態になるメニューのindex番号
- $options <a>タグに設定するhtmlオプション
- $class ulタグに設定するclass属性名
- $confirm $html->linkの$confirmと同じ
- $escapeTitle $html->linkの$escapeTitleと同じ
通常のリンクを持ったメニューや,Ajaxリンクを持ったメニューを簡単利用できるようにします。
メソッド ajaxMenu
Ajaxリンクを持ったメニューのhtmlソースを返します。
引数
- $menuList メニュー表示配列。menuメソッドと同じ形式です。
- $default デフォルトで選択状態になるメニューのindex番号
- $class ulタグに設定するclass属性名
- $opttions ajaxのオプション
- $confirm $ajax->linkの$confirmと同じ
- $escapeTitle $ajax->linkの$escapeTitleと同じ
$optionsは$ajax->linkを使うときと同じように使います。例えば,ajaxMenuをクリックしたときにある<div>要素にupdateをかけるには,$optionsに'update'=>'target_div_id'を追加してコールします。
$defaultがちゃんとセットされている場合には,$defaultで指定したメニューがクリックされたのと同じ処理をページ表示時にも処理します。
SvMenuHelperの使用方法
SvMenuHelperを利用したviewのサンプルを下記に示します。
<?php ); ?> <div id="menu"> <div id="menu_inner"> <?php 'update'=>'main_inner' ); ?> </div><!-- menu_inner --> </div><!-- menu --> <div id="main"> <div id="main_inner"> </div><!-- main_inner --> </div><!-- main -->
- 2~8行目,メニュー表示配列を用意しています。
- 9行目,メニューの選択状態更新用のスクリプトを出力しています。
- 14~16行目,メニューがクリックされたときに更新されるdiv要素を"main_inner"であると指定しています。
- 17行目,Ajaxを利用したメニューを出力しています。
これにより出力されたhtmlのコードは次のようになります。(少々整形してあります)
<script type="text/javascript"> function changeMenu(obj){ var ul = obj.parentNode.parentNode; var lis = ul.childNodes; var i=0; for(i in lis){ lis[i].className = ''; } obj.parentNode.className = 'active'; } </script> <div id="menu"> <div id="menu_inner"> <script type="text/javascript">Event.observe(window, 'load', function(event) { new Ajax.Updater('main_inner','/cake/schedule/acal/menu/file', {asynchronous:true, evalScripts:true, requestHeaders:['X-Update', 'main_inner']}) }, false);</script> <ul class = "tab_menu"> <li class = "active"><a href="/cake/schedule/acal/menu/file" onclick="changeMenu(this); event.returnValue = false; return false;" id="link17663">ファイル</a><script type="text/javascript">Event.observe('link17663', 'click', function(event) { new Ajax.Updater('main_inner','/cake/schedule/acal/menu/file', {asynchronous:true, evalScripts:true, requestHeaders:['X-Update', 'main_inner']}) }, false);</script></li> <li><a href="/cake/schedule/acal/menu/edit" onclick="changeMenu(this); event.returnValue = false; return false;" id="link17100">編集</a><script type="text/javascript">Event.observe('link17100', 'click', function(event) { new Ajax.Updater('main_inner','/cake/schedule/acal/menu/edit', {asynchronous:true, evalScripts:true, requestHeaders:['X-Update', 'main_inner']}) }, false);</script></li> <li><a href="/cake/schedule/acal/menu/view" onclick="changeMenu(this); event.returnValue = false; return false;" id="link18709">表示</a><script type="text/javascript">Event.observe('link18709', 'click', function(event) { new Ajax.Updater('main_inner','/cake/schedule/acal/menu/view', {asynchronous:true, evalScripts:true, requestHeaders:['X-Update', 'main_inner']}) }, false);</script></li> <li><a href="/cake/schedule/acal/menu/search" onclick="changeMenu(this); event.returnValue = false; return false;" id="link13354">検索</a><script type="text/javascript">Event.observe('link13354', 'click', function(event) { new Ajax.Updater('main_inner','/cake/schedule/acal/menu/search', {asynchronous:true, evalScripts:true, requestHeaders:['X-Update', 'main_inner']}) }, false);</script></li> <li><a href="/cake/schedule/acal/menu/help" onclick="changeMenu(this); event.returnValue = false; return false;" id="link12827">ヘルプ</a><script type="text/javascript">Event.observe('link12827', 'click', function(event) { new Ajax.Updater('main_inner','/cake/schedule/acal/menu/help', {asynchronous:true, evalScripts:true, requestHeaders:['X-Update', 'main_inner']}) }, false);</script></li> </ul> </div><!-- menu_inner --> </div><!-- menu -->
14行目は,ページ表示時に$defaultでセットされたメニュー項目のurlでdiv項目が表示されるコードです。これがないとメニューが表示されてもdiv要素の中は空っぽな状態でページが表示されてしまいます。

表示されたメニューのスナップショットです。こんな感じになります。
SvMenuHelperのソース
<?php /** * SvMenuHelper * * メニュー表示ヘルパー * * @author Sunvisor */ class SvMenuHelper extends Helper { var $helpers = array('Html', 'Ajax', 'Javascript'); var $scriptName = null; var $activeClassName = 'active'; function menuInit($scritpName){ $this->scriptName = $scritpName; $ac = $this->activeClassName; $result = $this->Javascript->codeBlock( "\nfunction $scritpName(obj){\n". " var ul = obj.parentNode.parentNode;\n". " var lis = ul.childNodes;\n". " var i=0;\n". " for(i in lis){\n". " lis[i].className = '';\n". " }\n". " obj.parentNode.className = '$ac';\n". "} \n" )."\n"; return $result; } function menu($menuList, $default=0, $options=array(), $class = null, $confirm = null, $escapeTitle = true){ if(isset($this->scriptName)){ if (!isset($options['onclick'])) { $options['onclick'] = ''; } $options['onclick'] .= $this->scriptName."(this); "; } if(isset($class)) $c = ' class = "'.$class.'"'; else $c = ''; $result = "<ul$c>\n"; foreach($menuList as $index => $menuValue){ $t = $menuValue['title']; $u = $menuValue['url']; if($default==$index) $c = ' class = "'.$this->activeClassName.'"'; else $c = ''; $result .= " <li$c>"; $result .= $this->Html->link($t, $u, $options, $confirm, $escapeTitle); $result .= "</li>\n"; } $result .= "</ul>"; return $result; } function ajaxMenu($menuList, $default=0, $class = null, $options=array(), $confirm = null, $escapeTitle = true){ if(isset($this->scriptName)){ if (!isset($options['onclick'])) { $options['onclick'] = ''; } $options['onclick'] .= $this->scriptName."(this); "; } if(isset($menuList[$default])){ $opt = $options; $opt['url'] = $menuList[$default]['url']; echo $this->Javascript->event('window','load', $this->Ajax->remoteFunction($opt))."\n"; } if(isset($class)) $c = ' class = "'.$class.'"'; else $c = ''; $result = "<ul$c>\n"; foreach($menuList as $index => $menuValue){ $t = $menuValue['title']; $u = $menuValue['url']; // li%u306E%u30AF%u30E9%u30B9%u540D -> $c if($default==$index) $c = ' class = "'.$this->activeClassName.'"'; else $c = ''; $result .= " <li$c>"; $result .= $this->Ajax->link($t, $u, $options, $confirm, $escapeTitle); $result .= "</li>\n"; } $result .= "</ul>"; return $result; } } ?>