I wanted to created a ‘navigation tree’ of all the categories (or specific defined categories on my site). For example, all the ‘Gallery’ pages have on the right-hand side of the screen a list of each of the ‘gallery posts’ which are then subdivided into ‘moon’, ‘planets’ etc.
The best category widget that I found out there was the excellent ‘category posts’ widget designed by James Lao.
There were, however, inevitably a couple of amends that I wanted to make to JL’s widget and I thought I might as well document them here as this seems to be a fairly common issue for bloggers. The administrator interface for the end-result can be seen on the right.
The site set-up
The widget I’ve created (well, adapted from JL) requires a couple of features of the site and the way it handles categorisation. These features are necessary for some of the extra functionality of this widget and were already part of my site set-up. They are as follows:
(1) The site has a strict hierarchy so that any one post cannot be in a category of the same ‘level’. To give an example, this post is in the ‘Blogging’ category. It cannot therefore be in either the ‘Gallery’, ‘Equipement’ or ‘CapeMirror’ categories. Together these form the 4 ‘top / parent categories’.
(2) The order in which the categories of any level will be displayed is defined by their description. This was because I wanted to be able to do a ‘full site map’ which would show all posts in all categories, but I wanted to be able to define which was the first category: gallery, then capemirror, then equipment, then blogging etc. So the ‘category description’ (which in my case is always a 3 digit number) cannot be displayed anywhere on the site. This use of the category description is clumsy but it means I retain total control of the ordering of all categories and sub-categories.
Amends to WordPress files
I also had to make a few changes to the WordPress files – which is why this widget could never go for general release. These changes implemented the ‘order by description’ and also ensured that the breadcrumb function (the ‘filing system’ above which starts “Location: home / blogging”) ordered the categories correctly.
(1) To make sure that the categories are always ordered by their description I edited wp-includes/category-template.php as follows, first, in function ‘get_the_category’ change as follows:
if ( !empty($categories) ) // usort($categories, '_usort_terms_by_name'); usort($categories, '_usort_terms_by_description');
(2) In the same category-template.php file I created a new ‘_usort_terms_by_description’ function:
function _usort_terms_by_description($a, $b) { return strcmp($a->description, $b->description); }
(3) Then I changed the category.php file to ensure that all other functions order by category description:
usort($categories, '_usort_terms_by_description'); return $categories;
Having done that, the site is ready for the widget!
The Widget
The full file is here. But this is a step-through of the widget:
(1) The introductory comments that WordPress uses when it displays the widget to the user:
/* Plugin Name: Cape Ealing Navigation Widget Plugin URI: https://capeealing.com/2009/01/wordpress-navigation-tree/ Description: This widget creates a 'tree' of all posts in (i) all categories, (ii) one user-define category, or (iii) the category of the post on the main page. Author: Ben Version: 1.0 Author URI: https://capeealing.com/ */
(2) The last function runs first, it basically makes sure this widget is registered:
/* add_action( 'widgets_init', 'ce_nav_tree_register' ) This last action runs first and ensures the function 'ce_nav_tree_register' runs whenever the 'widgets_init' is run. */ add_action( 'widgets_init', 'ce_nav_tree_register' );
(3) A further registration function, the one that was just referred to:’ce_nav_tree_register’.
/* ce_nav_tree_register() This function registers each instance of our widget on startup. Want to acknowledge Milan Petrovic's excellent tutorial on this found here: http://wp.gdragon.info/2008/07/06/create-multi-instances-widget/ and the commenting this script by James Lao in his navigation tree widget: http://jameslao.com/2008/04/18/category-posts-widget-13/ */ function ce_nav_tree_register() { // First check to see if there is a 'widget_ce_nav_tree' in the options table of the database. // If not, creat an empty $options array. if ( !$options = get_option('widget_ce_nav_tree') ) $options = array(); // Populate various easy reference variables $widget_ops = array('classname' => 'ce_tree', 'description' => __('Widget that creates navigation trees for Cape Ealing.')); $control_ops = array('id_base' => 'ce-tree'); $name = __('Cape Ealing Tree'); // Set $registered as false (this is checked later to prevent running more than once). $registered = false; // Return each of the keys of the array '$options' foreach ( array_keys($options) as $o ) { // Checking whether a particular 'test' variable has been set, if not then continue if ( !isset($options[$o]['title']) ) continue; // $id should look like {$id_base}-{$o}. Never translate an ID. $id = "ce-tree-$o"; // now register $registered = true; wp_register_sidebar_widget( $id, $name, 'ce_nav_tree_widget', $widget_ops, array( 'number' => $o ) ); wp_register_widget_control( $id, $name, 'ce_nav_tree_control', $control_ops, array( 'number' => $o ) ); } // End foreach // If not registered, then register with a generic template if ( !$registered ) { wp_register_sidebar_widget( 'ce-tree-1', $name, 'ce_nav_tree_widget', $widget_ops, array( 'number' => -1 ) ); wp_register_widget_control( 'ce-tree-1', $name, 'ce_nav_tree_control', $control_ops, array( 'number' => -1 ) ); } }
(4) Next up (although it appears earlier in the widget) the administrator control panel – see the image above.
/* ce_nav_tree_control( $widget_args = 1 ) This is the section for the WordPress control panel (admin panel) form. This is where the widget settings are defined and then stored in the database. Want to acknowledge Milan Petrovic's excellent tutorial on this found here: http://wp.gdragon.info/2008/07/06/create-multi-instances-widget/ and the commenting this script by James Lao in his navigation tree widget: http://jameslao.com/2008/04/18/category-posts-widget-13/ */ function ce_nav_tree_control( $widget_args = 1 ) { // start-up procedures. Unchanged across all 'multiple instances' widgets global $wp_registered_widgets; static $updated = false; // whether been updated yet. if ( is_numeric($widget_args) ) $widget_args = array( 'number' => $widget_args ); $widget_args = wp_parse_args( $widget_args, array( 'number' => -1 ) ); extract( $widget_args, EXTR_SKIP ); // extracts widget variables and their values (eg creates $number) $options = get_option('widget_ce_nav_tree');// get the correct details for all instances of the widget if ( !is_array($options) ) // if there are no details then create an empty array $options = array(); if ( !$updated && !empty($_POST['sidebar']) ) { // Not updated, post form not empty. So need to update. $sidebar = (string) $_POST['sidebar']; // Tells us what sidebar to put the data in $sidebars_widgets = wp_get_sidebars_widgets(); if ( isset($sidebars_widgets[$sidebar]) ) $this_sidebar =& $sidebars_widgets[$sidebar]; else $this_sidebar = array(); foreach ( $this_sidebar as $_widget_id ) { // Remove all widgets from the sidebar. Added in again later. if ( 'ce_nav_tree_widget' == $wp_registered_widgets[$_widget_id]['callback'] && isset($wp_registered_widgets[$_widget_id]['params'][0]['number']) ) { $widget_number = $wp_registered_widgets[$_widget_id]['params'][0]['number']; if ( !in_array( "ce-tree-$widget_number", $_POST['widget-id'] ) ) // the widget has been removed. "many-$widget_number" is "{id_base}-{widget_number} unset($options[$widget_number]); } } // Write the data for each instance of the widget foreach ( (array) $_POST['ce-tree'] as $widget_number => $ce_tree_instance ) { $title = $ce_tree_instance['title']; $options[$widget_number] = array( 'title' => $title, 'type' => $ce_tree_instance['type'], 'cat' => (int) $ce_tree_instance['cat'], 'post_orderby' => $ce_tree_instance['post_orderby'], 'post_order' => $ce_tree_instance['post_order']); } update_option('widget_ce_nav_tree', $options); // update the particular key in the options database $updated = true; // So that we don't go through this more than once } // Having completed start-up, now show the form. // Populate with default values if necessary. if ( -1 == $number ) { $title = 'Show all posts'; $type = 'All'; $cat = ''; $post_orderby = ''; $post_order = ''; $number = '%i%'; } else { $title = attribute_escape($options[$number]['title']); $type = $options[$number]['type']; $cat = (int) $options[$number]['cat']; $post_orderby = $options[$number]['post_orderby']; $post_order = $options[$number]['post_order']; } // Echo out the form. // Note: WordPress includes the $title in the heading for the control box. // The form has inputs with names like widget-many[$number][something] so that all data for that instance of // the widget are stored in one $_POST variable: $_POST['widget-many'][$number] // Work out which option should be pre-selected in form switch ($type) { case 'AllPosts': $sel1="Selected"; break; case 'ByPost': $sel2="Selected"; break; default: $sel3="Selected"; break; } if ($post_orderby == "title") $sel6 = "Selected"; if ($post_order == "DESC") $sel7 = "Selected"; ?> <p> <label for="ce-tree-title-<?php echo $number; ?>"> <?php _e( 'Title:' ); ?> <input class="widefat" id="ce-tree-title-<?php echo $number; ?>" name="ce-tree[<?php echo $number; ?>][title]" type="text" value="<?php echo $title; ?>" /> </label> </p> <p></p> <p>Select type of Cape Ealing navigation tree.</p> <p>'All categories' or 'By post category' will override any particular category selected below.</p> <p> <label><?php _e( 'Selection type:' ); ?><br /> <select id="ce-tree-type-<?php echo $number; ?>" name="ce-tree[<?php echo $number; ?>][type]" value="<?php echo $type; ?>" > <option <?php echo $sel1; ?> value="AllPosts" >All categories</option> <option <?php echo $sel2; ?> value="ByPost" >By post category</option> <option <?php echo $sel3; ?> value="ByCat">One category</option> </select> </label> </p> <p> <label> <?php _e( 'This one category only:' ); ?><br /> <?php wp_dropdown_categories( array( 'name' => 'ce-tree[' . $number . '][cat]', 'selected' => $cat ) ); ?> </label> </p> <p> <label><?php _e( 'Order posts by:' ); ?><br /> <select id="ce-tree-type-<?php echo $number; ?>" name="ce-tree[<?php echo $number; ?>][post_orderby]" value="<?php echo $post_orderby; ?>" > <option value="date" >Date</option> <option <?php echo $sel6; ?> value="title" >Title</option> </select> </label> <label> <select id="ce-tree-type-<?php echo $number; ?>" name="ce-tree[<?php echo $number; ?>][post_order]" value="<?php echo $post_order; ?>" > <option value="ASC" >Ascending</option> <option <?php echo $sel7; ?> value="DESC" >Descending</option> </select> </label> </p> <p></p> <!--<input type="hidden" name="ce-tree[<?php echo $number; ?>][submit]" value="1" />--> <?php }
(5) Now time for the actual widget itself. This first section finds out whether the widget is to show all categories or just one.
/* ce_tree_start($TreeType, $Tree_cat_orderby, $Tree_cat_order, $Tree_post_orderby, $Tree-post_order) function which initialises the widget depending on whether to show: i. all posts, ii. the posts of the parent category of the post displayed, or iii. a particular category. */ function ce_tree_start($TreeType, $Tree_post_orderby, $Tree_post_order){ // What type of Tree is in use (All posts, By post category, By set category). switch ($TreeType){ case 'AllPosts': // All posts selected. $i=0; // set the counter // get the top categories foreach (get_categories() as $all_cat) { if (!get_category($all_cat->category_parent)) { $top_cat[$i] = $all_cat->cat_ID; $i++; } } $i=0; // reset the counter // the top loop: runs for each each top parent category foreach ($top_cat as $a_top_cat){ ce_tree_main ($a_top_cat, $Tree_post_orderby, $Tree_post_order); } // end for each break; case 'ByPost': // Get posts according to the top category of the post $i=0; // Reset the counter // get the categories of this particular post foreach (get_the_category() as $all_post_cat) { if (!get_category($all_post_cat->category_parent)) { $top_post_cat[$i] = $all_post_cat->cat_ID; $i++; } } $i=0; // reset the counter // finds the top category from those categories foreach ($top_post_cat as $a_top_post_cat){ ce_tree_main ($a_top_post_cat, $Tree_post_orderby, $Tree_post_order); } // end for each break; default: // Pass the category ID ce_tree_main($TreeType, $Tree_post_orderby, $Tree_post_order); break; } // end switch }
(6) This main function (ce_tree_main) runs to find all the posts in a particular category but note it moves over to find ‘child categories’ if there are any (see the next function).
/* ce_tree_main ($tree_cat, $cetree_cat_orderby, $cetree_cat_order, $cetree_post_orderby, $cetree_post_order) This is the main 'post selection' function. Having selected the posts it runs the ce_get_posts to display them */ function ce_tree_main ($tree_cat, $cetree_post_orderby, $cetree_post_order){ // show the top category with link echo '<h4 align=center><a href="' .get_category_link($tree_cat) . '">' .(get_cat_name($tree_cat)). '</a></h4>'; echo '<p></p>'; $this_post_ID = get_the_ID(); // get the id of this particular post $level = 0; // set the level (0=for the top category, 1=first sub category etc) $breadcrumb[$level]=$tree_cat; // the first item on the breadcrumb = the parent category $excluded[$level] = $tree_cat.","; // the string of excluded categories must first include the top category $iteration[$level+1]=0; // set the iteration no. for the next level down (ie the first sub-category) // for each parent category run this loop which looks for subcategories // the loop runs the function 'find_child' giving it the current parent ($breadcrumb[$level]) // and how many times it has looked at that level already ($iteration[$level+1]) do { $child = find_child($breadcrumb[$level], $iteration[$level+1], $level); switch ($child) { case '[no kids]': // Valid category with no children so get the posts $cat_posts = get_posts('numberposts=-1&category='.$breadcrumb[$level].'&orderby='.$cetree_post_orderby.'&order='.$cetree_post_order.''); ce_get_posts($cat_posts, $this_post_ID, $level); $level--; // Now need to go 'up one' (level -1) $iteration[$level+1]++; // and 'along one' (iteration+1). break; case '[end iteration]': // no more cats at this level, show the post within this category but not in sub categories. $exc_posts = get_posts('category='.$excluded[$level].'&numberposts=-1&orderby='.$cetree_post_orderby.'&order='.$cetree_post_order.''); ce_get_posts($exc_posts, $this_post_ID, $excluded_from_level[$level]); $level--; // need to go up one level $iteration[$level+1]++; // and along one. break; case '[grandchildcat]': // the cat found is not a direct sub category so ignore it, move along in iteration $iteration[$level+1]++; break; default: // valid category id been returned, try next level down $excluded[$level] .= " -".$child.","; // a category to be excluded $level++; $iteration[$level+1] = 0; $breadcrumb[$level]=$child; $excluded[$level] = $breadcrumb[$level].","; // start next level exclusion string $excluded_from_level[$level] = $level; break; } // end switch echo '<p></p>'; // insert a line break }while ($level>-1); // do while not trying to get to level above the parent level } // end function ce_tree_main
(7) Find the children! In other words, get any sub-categories of the current category. This can drill down as many times as necessary (I think).
/* find_child ($child_of, $iteration, $level2) The heart of the widget. Used by the ce_tree_main function. */ function find_child($child_of, $iteration, $level2){ // get all the children $all_children = get_categories('child_of='.$child_of.''); // check to see if any children present at all if (empty($all_children)) { $child = "[no kids]"; return $child; } // if there are children, check that they continue through the iteration $child = $all_children[$iteration]->cat_ID; // check to see if actually got a child or gone 'too far' in the iteration if ($child == ""){ $child = "[end iteration]"; return $child; } // Only report child categorieswho are direct descendants if ($all_children[$iteration]->category_parent == $child_of) { // indent as necessary $i=0; $depthstring = ' '; while ($i<$level2){ $depthstring .= ' '; $i++; } // end while echo '<h4><a href="' .get_category_link($child) . '">' .$depthstring.$all_children[$iteration]->cat_name. '</a></h4>'; return $child; } // end if else { $child = "[grandchildcat]"; return $child; } // end if } // end function
(8) Finally, the last function which actually displays each post found within a particular category and indents each ‘level’ as it goes along.
/* function ce_get_posts ($cat_posts, $this_post_ID, $postlevel) Passed 3 variables: an array of the posts to be displayed as a list, the ID of the current post, and which level it's on (for indenting) Prints out the name of each post (hyperlinked unless it matches the current post). */ function ce_get_posts($cat_posts, $this_post_ID, $postlevel) { // indent as necessary $i=0; $depthstring = ''; while ($i<$postlevel){ $depthstring .= ' '; $i++; } $postcount_value =1; // set counter to 1 // loop through each posts and report its name foreach($cat_posts as $post) { setup_postdata($post); $list_post_ID = $post->ID; // get the post ID echo '<ul class="cat-recent-list">'; if (is_single() && $list_post_ID == $this_post_ID) echo $depthstring.$postcount_value.'. '.$post->post_title; // if this post = current post don't hyperlink else echo $depthstring.'<a href="' . get_permalink($post) . '">'.$postcount_value.'. '.$post->post_title.'</a>'; echo '</ul>'; $postcount_value++; } // end for each echo '<p></p>'; } // end function
Enough.
Post-script wp 2.9
After the upgrade to WordpPress 2.9 (I jumped from 2.6) I had to amend the following line (which appears twice) in the code above as follows:
// next line amended for wordpress 2.9 // if (!get_category($all_cat->category_parent)) { if ($all_cat->category_parent == 0) {
For the current complete widget, see here.
Ben
post a comment...
you must be logged in to post a comment.