[WP] Underscoresでカスタムテーマを作る:ナビゲーションメニュー改造編

Underscores で魔改造したメモその2。
ナビゲーションメニューを改造しようそうしよう。

  • Compass導入編
  • ナビゲーションメニュー改造編 ←今ここ

マークアップがてら何も考えず昔の手法でメニュー作ってたんだけど、

<ul class="menu">
    <li><a class="btn" href="<?php echo get_category_link( 1 ); ?>">News</a></li>
    <li><a class="btn" href="<?php echo get_permalink(8) ?>">About</a></li>
    <li><a class="btn" href="<?php echo get_permalink(12) ?>">Gallary</a></li>
    <li><a class="btn" href="<?php echo get_category_link( 2 ); ?>">Blog</a></li>
</ul>

今のWordpressにはメニューを管理ページで操作出来る機能があるじゃないですか。
ということを思い出したので、対応しておくことにした。

ナビゲーションメニューの登録

テーマがカスタムメニュー機能に対応していれば、管理画面の「外観」内にある『メニュー』という項目であれこれ操作ができる。
Underscoresはデフォルトで対応状態になってます。

その設定は functions.php の78行目付近にある register_nav_menusです。

/**
 * This theme uses wp_nav_menu() in one location.
 */
register_nav_menus( array(
	'main_navigation' => __( 'Main Navigation', 'my' ),
) );

この関数に渡す配列は '位置'=>'名前' の形式で、設定した数だけ「位置の管理」に表示される。

位置の管理画面

「メニューを編集」で表示したいものをメニュー構造に設定してメニューを作成する。

メニューの編集画面

あとはテンプレートの表示したい所で wp_nav_menu タグを使うだけ。

<?php wp_nav_menu(array('theme_location'  => 'main_navigation')) ?>

デフォルトソース

くっそ便利…

カスタムウォーカーの使用

wp_nav_menuにパラメータを渡せばそれなりにカスタムできる。
これについては公式のWikiにあらかた書いてあるのでそっちを見てください。

で、このパラメータだけだとリストタグとアンカータグには殆ど何も出来ないのだが、最後らへんに書いてある walker というパラメータに Walker_Nav_Menu クラスのインスタンスを渡すと魔改造ができるのでした。
それについてもWikiの終盤に「カスタムウォーカー機能を使用する」というタイトルで書いてあります。

試しにWikiにあるサンプルソースをfunctions.phpにコピペして

class themeslug_walker_nav_menu extends Walker_Nav_Menu {
  
// add classes to ul sub-menus
function start_lvl( &$output, $depth ) {
    // depth dependent classes
    $indent = ( $depth > 0  ? str_repeat( "\t", $depth ) : '' ); // code indent
    $display_depth = ( $depth + 1); // because it counts the first submenu as 0
    $classes = array(
        'sub-menu',
        ( $display_depth % 2  ? 'menu-odd' : 'menu-even' ),
        ( $display_depth >=2 ? 'sub-sub-menu' : '' ),
        'menu-depth-' . $display_depth
        );
    $class_names = implode( ' ', $classes );
  
    // build html
    $output .= "\n" . $indent . '<ul class="' . $class_names . '">' . "\n";
}
  
// add main/sub classes to li's and links
 function start_el( &$output, $item, $depth, $args ) {
    global $wp_query;
    $indent = ( $depth > 0 ? str_repeat( "\t", $depth ) : '' ); // code indent
  
    // depth dependent classes
    $depth_classes = array(
        ( $depth == 0 ? 'main-menu-item' : 'sub-menu-item' ),
        ( $depth >=2 ? 'sub-sub-menu-item' : '' ),
        ( $depth % 2 ? 'menu-item-odd' : 'menu-item-even' ),
        'menu-item-depth-' . $depth
    );
    $depth_class_names = esc_attr( implode( ' ', $depth_classes ) );
  
    // passed classes
    $classes = empty( $item->classes ) ? array() : (array) $item->classes;
    $class_names = esc_attr( implode( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item ) ) );
  
    // build html
    $output .= $indent . '<li id="nav-menu-item-'. $item->ID . '" class="' . $depth_class_names . ' ' . $class_names . '">';
  
    // link attributes
    $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        ) .'"' : '';
    $attributes .= ' class="menu-link ' . ( $depth > 0 ? 'sub-menu-link' : 'main-menu-link' ) . '"';
  
    $item_output = sprintf( '%1$s<a%2$s>%3$s%4$s%5$s</a>%6$s',
        $args->before,
        $attributes,
        $args->link_before,
        apply_filters( 'the_title', $item->title, $item->ID ),
        $args->link_after,
        $args->after
    );
  
    // build html
    $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
}

wp_nav_menuのパラメータで渡してみると…

<?php wp_nav_menu(array('theme_location'  => 'main_navigation', 'walker' => new themeslug_walker_nav_menu)) ?>

排出されるソースが書き変わる。

wp-underscores-2-4

これだとBEM化もできるわけで…

<?php
wp_nav_menu( array(
	'theme_location'  => 'main_navigation',
	'container'       => 'nav',
	'container_class' => 'nav',
	'container_id' => 'page-nav',
	'menu_id'         => '',
	'items_wrap'      => '<ul class="%2$s">%3$s</ul>',
	'walker' => new bem_walker_nav_menu
) );
?>

<?php
class bem_walker_nav_menu extends Walker_Nav_Menu {
  
	// add main/sub classes to li's and links
	 function start_el( &$output, $item, $depth, $args ) {
		global $wp_query;
		$indent = ( $depth > 0 ? str_repeat( "\t", $depth ) : '' ); // code indent
	  
		// build html
		$output .= $indent . '<li class="menu__item menu__item--' . $item->ID . '">';
	  
		// link attributes
		$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        ) .'"' : '';
		$attributes .= ' class="menu__link ' . ( $depth > 0 ? 'menu__link--sub' : 'menu__link--main' ) . '"';
	  
		$item_output = sprintf( '%1$s<a%2$s>%3$s%4$s%5$s</a>%6$s',
			$args->before,
			$attributes,
			$args->link_before,
			apply_filters( 'the_title', $item->title, $item->ID ),
			$args->link_after,
			$args->after
		);
	  
		// build html
		$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
	}
}
?>

wp-underscores-2-5

カスタムが捗りますなあ。

新着があったらNEW付ける

ミーティングでこんな会話をした。
「たとえばNewsに新しい投稿があったら、メニューにNew!アイコンを付けられる?」
「Wordpressならできますよ」

やっべ、安請け合いだったかな…って思ってたけど、カスタムウォーカーで一気に解決だ!!!


<? php
wp_nav_menu(array('walker' => new addnew_walker_nav_menu) );
?>

<? php
class addnew_walker_nav_menu extends Walker_Nav_Menu {


	//編集日時と現在時刻の差分チェック
	function get_interval_days( $item ) {
		
		$param = 'numberposts=1&orderby=modified&order=DESC';
		
		switch ( $item->object ) {
			case 'category':
				$param .= '&category=' . $item->object_id;
			break;
			
			case 'page':
				$param .= '&post_type=page&post_parent='. $item->object_id;
			break;
		}
		
		$post = get_posts($param);

		if ( !count($post) ) {
		    return -1;
		}
		
		$timezone = new DateTimeZone('Asia/Tokyo');
		
		$post_modified_date = date_create( $post[0]->post_modified, $timezone );
		$today_date = date_create('now', $timezone);
		$interval = $today_date->diff($post_modified_date);
		
		return $interval->days;
	}
	  
	// add main/sub classes to li's and links
	 function start_el( &$output, $item, $depth, $args ) {
		global $wp_query;
		$indent = ( $depth > 0 ? str_repeat( "\t", $depth ) : '' ); // code indent
	  
		// passed classes
		$classes = empty( $item->classes ) ? array() : (array) $item->classes;
		$class_names = esc_attr( implode( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item ) ) );
		
		// build html
		$output .= $indent . '<li id="menu-item-'. $item->ID . '" class="' . $class_names . ' menu-item-'. $item->ID . '">';

		$interval =  $this->get_interval_days($item);
		
	        //編集3日以内の記事があったらNEWつける
		if (  $interval >= 0 && $interval <= 3  ) {
			$output .= '<i class="icon icon-new" title="New!"></i>';
		}
	  
		// link attributes
		$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 = sprintf( '%1$s<a%2$s>%3$s%4$s%5$s</a>%6$s',
			$args->before,
			$attributes,
			$args->link_before,
			apply_filters( 'the_title', $item->title, $item->ID ),
			$args->link_after,
			$args->after
		);
	  
		// build html
		$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
	}
}
?>

このカスタムウォーカーを使うと、3日以内に更新された記事があるカテゴリーか、
3日以内に更新された子ページを持つ固定ページのメニューリストの中にNEWアイコンのタグが入る。

wp-underscores-2-6

投稿日時の差分チェックは get_interval_days 関数でやってて、
PHP5.5らしく DateTimeDateIntervalDateTimeZone を使ってナウく仕上げてみた\(^o^)/

	//編集日時と現在時刻の差分チェック
	function get_interval_days( $item ) {
		
		//編集日時の最新順に1件だけ( ゚д゚)ホスィ
		$param = 'numberposts=1&orderby=modified&order=DESC';
		
		switch ( $item->object ) {
			//カテゴリーだったら
			case 'category':
				//カテゴリーID指定
				$param .= '&category=' . $item->object_id;
			break;
			//固定ページだったら
			case 'page':
				//子ページだけ得る
				$param .= '&post_type=page&post_parent='. $item->object_id;
			break;
		}
		
		//上で作ったパラメータをget_postsに投げて投稿オブジェクトを得る
		$post = get_posts($param);

		//投稿がなかった
		if ( !count($post) ) {
		    return -1;
		}
		
		//タイムゾーンを東京に設定
		$timezone = new DateTimeZone('Asia/Tokyo');
		
		//編集日時の DateTime インスタンスを作る
		$post_modified_date = date_create( $post[0]->post_modified, $timezone );
		//現在時刻の DateTime インスタンスを作る
		$today_date = date_create('now', $timezone);
		//差分の DateInterval インスタンスを得る
		$interval = $today_date->diff($post_modified_date);
		//何日差があるか返す
		return $interval->days;
	}

この関数はそのカテゴリーor子ページで1番新しい投稿が編集されてから何日経過しているか、を返すので
カスタムウォーカー内で適当な日数でチェックしてtrueならタグ入れるようにすればNewがつく。

		$interval =  $this->get_interval_days($item);
		
	     //編集3日以内の記事があったらNEWつける
		if (  $interval >= 0 && $interval <= 3  ) {
			$output .= '<i class="icon icon-new" title="New!"></i>';
		}

なおget_postsはデータベースにクエリを投げつけてデータを得る割と乱暴な関数なので、実行回数が多いとかなりの負荷を生み出す。
商用サイト風なナビゲーションの対象が少ないサイトには向いているが、
普通のブログ形式で普通にカテゴリーとか羅列しているサイトでの使用はオススメできない。

関数の中身をちょっと変更すればループ内でも使えます。

カテゴリー内の投稿数表示

カテゴリーリストの変更はwp_list_categoriesでWalker_Category使うっていう手があるんだけど、
親カテゴリーの表示は別に用意しなきゃだめだったことと、書き出されるHTMLにULを含める事がWalkerだけでは出来なかったので、
wp_nav_menuを使ってカウント表示させることにした。

子カテゴリータブ

メニューの設定で表示したいカテゴリーを1つずつ追加する。

設定画面

カスタムウォーカー

<?php

class Category_walker_nav_menu extends Walker_Nav_Menu {
    	  
	// add main/sub classes to li's and links
	 function start_el( &$output, $item, $depth, $args ) {
		global $wp_query;
         
		$indent = ( $depth > 0 ? str_repeat( "\t", $depth ) : '' ); // code indent
        $count = '';
       
         
        if ($item->object === 'category') {
            $category = get_category($item->object_id);
            $count = ' (' . number_format_i18n( $category->count ) . ')';
            $current = $item->current ? ' active' : '';
        }
		
		// build html
		$output .= $indent . '<li id="tab-'. $item->ID . '" class="' . $current . '">';
	  
		// link attributes
		$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        ) .'"' : '';
		$attributes .= ' class="btn"';
		
	  
		$item_output = sprintf( '%1$s<a%2$s>%3$s%4$s%5$s%6$s</a>%7$s',
			$args->before,
			$attributes,
			$args->link_before,
			apply_filters( 'the_title', $item->title, $item->ID ),
            $count,
			$args->link_after,
			$args->after
		);
         
	  
		// build html
		$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
	}
}