階層を持つナビゲーションを作る際に、親メニューにマウスを載せて開閉というのはみかけますが、クリックで開閉させたい場合、親メニューを潰してしまうことになります。
そこで、Windowsのツリー表示みたいな感じで横に「+」「-」アイコンをおいてそこをクリックしたら下の階層を開くようなウィジェットを作ってみます。
本当は jQuery とかでなんとかしてみたかったのですが、 jQuery で後から追加した要素にごにょごにょするのは地味に面倒な上に自分の知識レベルだと小回りの効いたスクリプトがかけないのでそこで悩んでる間にウィジェットつくっちゃえ、とまぁそんなかんじです。
やりたいこと
要はこういうのがつくりたいんです。
- メニューの横にツリーを開閉する「+」「-」ボタン
- 現在のページがメニューの下の階層の方に存在する場合はそのメニューを開いておく
- 一番下の階層(子を持たないメニュー)は「+」でも「-」でもないボタン
画像でいうならば、こんなかんじでしょうか?
では次に実現するためには、どうすればいいのか、自動的にやってほしいことをやって欲しい順に書き出してみます。
- メニューが子を持っているかチェックする
- 子を持っている場合は「+」アイコンを、持っていない場合は横向き三角なり「→」なりのアイコンを表示させる
- アイコンを押した際に、自分の子に有る子メニューを表示させる。
(既に開いている場合は閉じさせる) - 状況に応じてアイコンを差し替える(開いてる時は「-」に変更させる)
- 現在開いているページがメニューの中にあるかをチェックし、それが下の方の階層にいる場合はその階層まで開く
うん。けっこうやること有るな…。
では順番にやっていきます。
リスト用のアイコンを用意する
適当に画像を用意します。素材集を使ってもいいですし、自力で頑張って作成しても構いません。
自分はこんなかんじに作りました。名前はなんでもいいと思いますがわかり易い名前で保存します。(名前を変えた場合は以下それにあわせて読み替えて下さい)
sidenavTree_off.gif | 子を持っているツリーが閉じている状態用 | |
sidenavTree_on.gif | 子を持っているツリーが開いている状態用 | |
sidenavTree_noChild.gif | 子を持っていないアイテム用 |
カスタムウィジェットを作る
今回は専用にカスタムメニューウィジェットを作ってメニューを書き出すときにアイコンを追加させますので、まずはウィジェットをつくります。
新しい PHP ファイルを作成し、こんなふうに書きます。
まぁ別に functions.phpに書いてもいいんですが…後々プラグインとして切り分けるかもしれないので別ファイルにしておくといいかもしれません。
プラグインにしない場合でも結構コードが長いんでただでさえ functions.php はカオスになりやすいわけですし、なるべく分けたほうがいいのかなとおもいます。個人的には。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
// カスタム「カスタムメニュー」ウィジェット class NGM_Widget_CatList extends WP_Widget { function NGM_Widget_CatList() { parent::WP_Widget( false, $name='カスタムメニュー(改)', array('description'=>'開閉ボタン付きのカスタムメニューを提供します')); } // 表画面 function widget($args,$instance) { extract($args); if ($instance['title']) { $title = apply_filters('widget_title',$instance['title']); echo $before_title.$instance['title'].$after_title; } if ($instance['nav_menu']) $nav_obj = !empty($instance['nav_menu']) ? wp_get_nav_menu_object($instance['nav_menu']) : false; if ($nav_obj) { echo $before_widget; $nav_cfg = array( 'before' => '', 'fallback_cb' => '', 'menu_class' => '', 'menu' => $nav_obj, 'walker' => new NGM_sNav_walker() ); wp_nav_menu($nav_cfg); echo $after_widget; } } // 管理画面・保存処理 function update($new_instance,$old_instance) { $instance = $old_instance; $instance['title'] = strip_tags($new_instance['title']); $instance['nav_menu'] = (int) $new_instance['nav_menu']; return $instance; } // 管理画面のウィジェットアイテム function form($instance) { $title = esc_attr($instance['title']); $get_navMenu = isset($instance['nav_menu']) ? $instance['nav_menu'] : ''; $navMenu = get_terms('nav_menu',array('hide_empty' => false)); echo '<p>'; echo '<label for="'.$this->get_field_id('title').'">タイトル : </label><br>'; echo '<input type="text" id="'.$this->get_field_id('title').'" name="'.$this->get_field_name('title').'" value="'.$title.'">'; echo '</p>'; echo '<p>'; echo '<label for="'.$this->get_field_id('nav_menu').'">メニューを選択 : </label>'; echo '<select id="'.$this->get_field_id('nav_menu').'" name="'.$this->get_field_name('nav_menu').'">'; foreach($navMenu as $item) { echo '<option value="'.$item->term_id.'"'.selected($instance['nav_menu'],$item->term_id,false).'>'.$item->name.'</option>'; } echo '</select>'; echo '</p>'; } } add_action('widgets_init',create_function('', 'return register_widget("NGM_Widget_CatList");')); |
このファイルを functions.php に結合させればとりあえず今作成したカスタムメニューを呼び出すためのウィジェットと管理画面が作成できます。
1 |
require_once('_customNav.class.php'); |
Walker Classを用意して表示内容を更にカスタマイズする
続いて、ウィジェットのなかで呼び出すメニューで使用する walker class を作成します。今作ったファイルに追記します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// カスタムメニュー用 walker class class NGM_sNav_walker extends Walker_Nav_Menu { function start_el(&$output,$item,$depth,$args) { global $wp_query; $indent = ($depth) ? str_repeat("\t",$depth):''; $css_dir = get_stylesheet_directory_uri(); $class_names = $value = ''; $classes = empty($item->classes) ? array() : (array) $item->classes; // 子持ち判定 $children = get_posts( array( 'post_type' => 'nav_menu_item', 'nopaging' => true, 'numberposts' => 1, 'meta_key' => '_menu_item_menu_item_parent', 'meta_value' => $item->ID ) ); if (empty($children)) { $classes[] = ' no_children'; } else { $classes[] = ' has_children'; } // 現在表示中のメニューかどうかを判定 if ($item->current) { $classes[] = ' current_item'; } // 現在表示中のメニューの親かどうかを判定 if ($item->current_item_ancestor) { $classes[] = ' current_item_ancestor'; } $class_names = join('',apply_filters('nav_menu_css_class',array_filter($classes),$item)); $class_names = ' class="'.esc_attr($class_names).'"'; if (empty($children)) { $output .= $indent . '<li id="menu-item-'. $item->ID . '"' . $value . $class_names .'><img class="sbMarker no_children" src="'.$css_dir.'/img/sidenavTree_noChild.gif" alt="" />'; } else { $output .= $indent . '<li id="menu-item-'. $item->ID . '"' . $value . $class_names .'><img class="sbMarker sbNavTgl has_children" src="'.$css_dir.'/img/sidenavTree_off.gif" alt="" />'; } $attributes = ! empty( $item->attr_title ) ? ' title="' . esc_attr( $item->attr_title ) .'"' : ''; $attributes .= ! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) .'"' : ''; $attributes .= ! empty( $item->xfn ) ? ' rel="' . esc_attr( $item->xfn ) .'"' : ''; $attributes .= ! empty( $item->url ) ? ' href="' . esc_attr( $item->url ) .'"' : ''; $item_output = $args->before; $item_output .= '<a'. $attributes .$class_names.'>'; $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after; $item_output .= '</a>'; $item_output .= $args->after; $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args ); } } |
jQuery で開閉動作などを作成する
最後に jQuery を書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
jQuery(document).ready(function($) { $('.sbNavTgl').click(function(){ var tgt_parent = $(this).parent('li'); if ($(tgt_parent).hasClass('open')) { $(tgt_parent) .removeClass('open') .children('ul') .hide(); $(this).attr('src',$(this).attr('src').replace('_on.','_off.')); } else { $(tgt_parent) .addClass('open') .children('ul') .show(); $(this).attr('src',$(this).attr('src').replace('_off.','_on.')); } }); $(function(){ var tgt = $('.current_item_ancestor'); $(tgt).addClass('open').children('ul').show(); $(tgt).find('.sbNavTgl').each(function(){ if ($(this).parent('li').hasClass('open')) { $(this).attr('src',$(this).attr('src').replace('_off.','_on.')); } }) }); }); |
なんか後半の方かなり強引になってしまってるんでもうちょいスマートな書き方ができればいいんですが…うーむ。
画像の差し替えは replace をつかって画像タグの src の値を書き換えています。これ自体は on off 2枚の画像を使用してハイライトするボタンを作る時なんかには結構見られる手法ですね。
あとは…たいしたことはやってないです。
クラスをチェックして、自動で開くのか、手動で開くのか…みたいなかんじです。
子があるかを判定する方法
メニューの情報も実は投稿です。ある種のカスタム投稿みたいなものなのかな?
nav_menu_item という投稿タイプで登録されています。
なので、そのメニューのカスタムフィールドを使って絞り込みで get_posts することで調べることができます。
_menu_item_menu_item_parent は名前から 現在のメニューの親に当たるメニューのIDだと予想できますので、ここに自分のメニューIDがいれば、自分を親とするメニューが居る=子メニューがあると判定できそうです。
1 2 3 4 5 6 7 8 9 |
$children = get_posts( array( 'post_type' => 'nav_menu_item', 'nopaging' => true, 'numberposts' => 1, 'meta_key' => '_menu_item_menu_item_parent', 'meta_value' => $item->ID ) ); |
子が1個でもあればOKなので、先ほどの条件にして1個だけ投稿を取得します。オブジェクトがかえってくれば子あり、そうでなければ($children が空ならば)子はいないというふうにできますね。
今回は子メニューがあるときと無いときで先頭のアイコンを変更しますので、
1 2 3 4 5 |
if (empty($children)) { $output .= $indent . '<li id="menu-item-'. $item->ID . '"' . $value . $class_names .'><img class="sbMarker no_children" src="'.$css_dir.'/img/sidenavTree_noChild.gif" alt="" />'; } else { $output .= $indent . '<li id="menu-item-'. $item->ID . '"' . $value . $class_names .'><img class="sbMarker sbNavTgl has_children" src="'.$css_dir.'/img/sidenavTree_off.gif" alt="" />'; } |
こういうふうにして出し分けました。
メニューの中に現在のページが含まれているかを調べる
walker class を作るときに受け取る $item のなかにはメニューのいろんな情報が格納されているようで、 custom walker をいじる時もここを参照したりしてますよね。なので、ここになんかヒントがないかと思い、おもむろに print_r($item); してみましたところ、ほんとに色々出てきました。
IDだったり、メニューの中に放り込まれる各種クラスに関する情報だったり…。
で。見つけました。
[current]と、[current_item_ancestor]、そして[current_item_praent]というものがいまして、メニューごとに 「1」または「(空)」というふうになっていました。
もうだいたいわかると思いますが、
current | 現在の投稿へのリンクをもつメニューのとき「1(true)」 |
current_item_ancestor | 現在の投稿まで繋がる「先祖」のとき「1」 (ancestorは先祖の意) |
current_item_parent | 現在の投稿の「親」のとき「1」 |
つまり、$item->current_item_ancestor を調べてあげることで、親の親までまとめてクラスを与えられます。
あとは jQuery を駆使してごにょごにょすることで普段は閉じているけど、ツリーの中に今表示中のページが有る時はそこまでのツリーは開く
という作りたかった動きがつくれるようになりました。
コメントする