Avoid hierarchical taxonomies to loose hierarchy

You surely ran into this sometime: you’ve a hierarchical taxonomy in WordPress like Categories, you check the child terms and leave the parent terms unchecked. Next time you save the post, the hierarchical structure is broken and child items are positioned on top of parent items, like the image above on the left. In this tutorial we’re going to see how to fix this for WordPress categories and any hierarchical custom taxonomy so that the terms are displayed like the right box in the image above.

To do this we can simply use WordPress’ own categories but to take it a bit further, we’re going to create a custom taxonomy and fix it there. It will be easy to translate this fix for every other hierarchical taxonomy that you might need, not just categories. So let’s create our hierarchical taxonomy using register_taxonomy and adding all the arguments, specially ‘hierarchical’ => true. You can add all the code in this tutorial in the functions.php file of your WordPress theme. If you want to use WordPress’ internal categories, skip this.

[php]
register_taxonomy( ‘related’,
‘page’,
array( ‘hierarchical’ => true,
//the rest of arguments
)
)
[/php]

This will a hierarchical custom taxonomy for pages. After our hierarchy is in place, create some terms nesting them in the usual parent-child hierarchy (if you’re using WordPress categories, go to your post and create some). After you’ve selected a few of them and updated your post, you will see that they’re no longer in the right hierarchy, but instead, those terms that were checked are now at the top, leaving a mess on your hierarchy (at least visually). We’re going to fix this. First, let’s remove the default custom taxonomies box.

[php]
add_action( ‘admin_head’, ‘remove_default_categories_box’ );
function remove_default_categories_box() {
remove_meta_box(‘relateddiv’, ‘page’, ‘side’);
}

[/php]

Now the custom taxonomies box is gone! We’re going to add our own now:

[php]
add_action(‘admin_menu’, ‘add_custom_categories_box’);
function add_custom_categories_box() {
add_meta_box(‘myrelateddiv’, ‘Related’, ‘ilc_post_related_meta_box’, ‘page’, ‘side’, ‘low’, array( ‘taxonomy’ => ‘related’ ));
}

[/php]

Notice that the function is simply adding a meta box for pages to assign terms from related taxonomy. Now we need to define this meta box which is actually very easy, since all we have to do is to copy and paste the post_categories_meta_box function from wp-adminmeta-boxes.php file. We’re going to rename the function to ilc_post_related_meta_box. The only thing we’re going to change is commented within the function body so scroll until you find the comment:

[php]
function ilc_post_related_meta_box( $post, $box ) {
$defaults = array(‘taxonomy’ => ‘related’);
if ( !isset($box[‘args’]) || !is_array($box[‘args’]) )
$args = array();
else
$args = $box[‘args’];
extract( wp_parse_args($args, $defaults), EXTR_SKIP );
$tax = get_taxonomy($taxonomy);

?>
<div id="taxonomy-<?php echo $taxonomy; ?>" class="categorydiv">
    <ul id="<?php echo $taxonomy; ?>-tabs" class="category-tabs">
        <li class="tabs"><a href="#<?php echo $taxonomy; ?>-all" tabindex="3"><?php echo $tax->labels->all_items; ?></a></li>
        <li class="hide-if-no-js"><a href="#<?php echo $taxonomy; ?>-pop" tabindex="3"><?php _e( 'Most Used' ); ?></a></li>
    </ul>

    <div id="<?php echo $taxonomy; ?>-pop" class="tabs-panel" style="display: none;">
        <ul id="<?php echo $taxonomy; ?>checklist-pop" class="categorychecklist form-no-clear" >
            <?php $popular_ids = wp_popular_terms_checklist($taxonomy); ?>
        </ul>
    </div>

    <div id="<?php echo $taxonomy; ?>-all" class="tabs-panel">
        <?php
        $name = ( $taxonomy == 'category' ) ? 'post_category' : 'tax_input[' . $taxonomy . ']';
        echo "<input type='hidden' name='{$name}[]' value='0' />"; // Allows for an empty term set to be sent. 0 is an invalid Term ID and will be ignored by empty() checks.
        ?>
        <ul id="<?php echo $taxonomy; ?>checklist" class="list:<?php echo $taxonomy?> categorychecklist form-no-clear">

<?php
//Hey! look here! this is the parameter that makes the difference!
//’checked_top’ => FALSE
wp_terms_checklist($post->ID, array( ‘taxonomy’ => $taxonomy, ‘popular_cats’ => $popular_ids, ‘checked_ontop’ => FALSE ) )
?>

        &lt;/ul&gt;
    &lt;/div&gt;
&lt;?php if ( current_user_can($tax-&gt;cap-&gt;edit_terms) ) : ?&gt;
        &lt;div id=&quot;&lt;?php echo $taxonomy; ?&gt;-adder&quot; class=&quot;wp-hidden-children&quot;&gt;
            &lt;h4&gt;
                &lt;a id=&quot;&lt;?php echo $taxonomy; ?&gt;-add-toggle&quot; href=&quot;#&lt;?php echo $taxonomy; ?&gt;-add&quot; class=&quot;hide-if-no-js&quot; tabindex=&quot;3&quot;&gt;
                    &lt;?php
                        /* translators: %s: add new taxonomy label */
                        printf( __( '+ %s' ), $tax-&gt;labels-&gt;add_new_item );
                    ?&gt;
                &lt;/a&gt;
            &lt;/h4&gt;
            &lt;p id=&quot;&lt;?php echo $taxonomy; ?&gt;-add&quot; class=&quot;category-add wp-hidden-child&quot;&gt;
                &lt;label class=&quot;screen-reader-text&quot; for=&quot;new&lt;?php echo $taxonomy; ?&gt;&quot;&gt;&lt;?php echo $tax-&gt;labels-&gt;add_new_item; ?&gt;&lt;/label&gt;
                &lt;input type=&quot;text&quot; name=&quot;new&lt;?php echo $taxonomy; ?&gt;&quot; id=&quot;new&lt;?php echo $taxonomy; ?&gt;&quot; class=&quot;form-required form-input-tip&quot; value=&quot;&lt;?php echo esc_attr( $tax-&gt;labels-&gt;new_item_name ); ?&gt;&quot; tabindex=&quot;3&quot; aria-required=&quot;true&quot;/&gt;
                &lt;label class=&quot;screen-reader-text&quot; for=&quot;new&lt;?php echo $taxonomy; ?&gt;_parent&quot;&gt;
                    &lt;?php echo $tax-&gt;labels-&gt;parent_item_colon; ?&gt;
                &lt;/label&gt;
                &lt;?php wp_dropdown_categories( array( 'taxonomy' =&gt; $taxonomy, 'hide_empty' =&gt; 0, 'name' =&gt; 'new'.$taxonomy.'_parent', 'orderby' =&gt; 'name', 'hierarchical' =&gt; 1, 'show_option_none' =&gt; '&amp;mdash; ' . $tax-&gt;labels-&gt;parent_item . ' &amp;mdash;', 'tab_index' =&gt; 3 ) ); ?&gt;
                &lt;input type=&quot;button&quot; id=&quot;&lt;?php echo $taxonomy; ?&gt;-add-submit&quot; class=&quot;add:&lt;?php echo $taxonomy ?&gt;checklist:&lt;?php echo $taxonomy ?&gt;-add button category-add-sumbit&quot; value=&quot;&lt;?php echo esc_attr( $tax-&gt;labels-&gt;add_new_item ); ?&gt;&quot; tabindex=&quot;3&quot; /&gt;
                &lt;?php wp_nonce_field( 'add-'.$taxonomy, '_ajax_nonce-add-'.$taxonomy, false ); ?&gt;
                &lt;span id=&quot;&lt;?php echo $taxonomy; ?&gt;-ajax-response&quot;&gt;&lt;/span&gt;
            &lt;/p&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
&lt;/div&gt;
&lt;?php

}

[/php]

And that’s all we need to do. If you now go to your post and check those hierarchical terms , they won’t lose the parenting. Again, although we used a custom taxonomies for the development, it also works for categories (since category is actually a custom taxonomy). Just replace all occurrences of related with category and you’re good to go! The following is the full code to prevent this issue for WordPress categories:

[php]
add_action( ‘admin_head’, ‘remove_default_categories_box’ );
add_action(‘admin_menu’, ‘add_custom_categories_box’);

function remove_default_categories_box() {
remove_meta_box(‘categorydiv’, ‘post’, ‘side’);
}
function add_custom_categories_box() {
add_meta_box(‘mycategorydiv’, ‘Categories’, ‘ilc_post_category_meta_box’, ‘post’, ‘side’, ‘low’, array( ‘taxonomy’ => ‘category’ ));
}

function ilc_post_category_meta_box( $post, $box ) {
$defaults = array(‘taxonomy’ => ‘category’);
if ( !isset($box[‘args’]) || !is_array($box[‘args’]) )
$args = array();
else
$args = $box[‘args’];
extract( wp_parse_args($args, $defaults), EXTR_SKIP );
$tax = get_taxonomy($taxonomy);

?&gt;
&lt;div id=&quot;taxonomy-&lt;?php echo $taxonomy; ?&gt;&quot; class=&quot;categorydiv&quot;&gt;
    &lt;ul id=&quot;&lt;?php echo $taxonomy; ?&gt;-tabs&quot; class=&quot;category-tabs&quot;&gt;
        &lt;li class=&quot;tabs&quot;&gt;&lt;a href=&quot;#&lt;?php echo $taxonomy; ?&gt;-all&quot; tabindex=&quot;3&quot;&gt;&lt;?php echo $tax-&gt;labels-&gt;all_items; ?&gt;&lt;/a&gt;&lt;/li&gt;
        &lt;li class=&quot;hide-if-no-js&quot;&gt;&lt;a href=&quot;#&lt;?php echo $taxonomy; ?&gt;-pop&quot; tabindex=&quot;3&quot;&gt;&lt;?php _e( 'Most Used' ); ?&gt;&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;

    &lt;div id=&quot;&lt;?php echo $taxonomy; ?&gt;-pop&quot; class=&quot;tabs-panel&quot; style=&quot;display: none;&quot;&gt;
        &lt;ul id=&quot;&lt;?php echo $taxonomy; ?&gt;checklist-pop&quot; class=&quot;categorychecklist form-no-clear&quot; &gt;
            &lt;?php $popular_ids = wp_popular_terms_checklist($taxonomy); ?&gt;
        &lt;/ul&gt;
    &lt;/div&gt;

    &lt;div id=&quot;&lt;?php echo $taxonomy; ?&gt;-all&quot; class=&quot;tabs-panel&quot;&gt;
        &lt;?php
        $name = ( $taxonomy == 'category' ) ? 'post_category' : 'tax_input[' . $taxonomy . ']';
        echo &quot;&lt;input type='hidden' name='{$name}[]' value='0' /&gt;&quot;; // Allows for an empty term set to be sent. 0 is an invalid Term ID and will be ignored by empty() checks.
        ?&gt;
        &lt;ul id=&quot;&lt;?php echo $taxonomy; ?&gt;checklist&quot; class=&quot;list:&lt;?php echo $taxonomy?&gt; categorychecklist form-no-clear&quot;&gt;

<?php
//Hey! look here! this is the parameter that makes the difference!
//’checked_top’ => FALSE
wp_terms_checklist($post->ID, array( ‘taxonomy’ => $taxonomy, ‘popular_cats’ => $popular_ids, ‘checked_ontop’ => FALSE ) )
?>
</ul>
</div>
<?php if ( current_user_can($tax->cap->edit_terms) ) : ?>
<div id="<?php echo $taxonomy; ?>-adder" class="wp-hidden-children">
<h4>
<a id="<?php echo $taxonomy; ?>-add-toggle" href="#<?php echo $taxonomy; ?>-add" class="hide-if-no-js" tabindex="3">
<?php
/* translators: %s: add new taxonomy label */
printf( __( ‘+ %s’ ), $tax->labels->add_new_item );
?>
</a>
</h4>
<p id="<?php echo $taxonomy; ?>-add" class="category-add wp-hidden-child">
<label class="screen-reader-text" for="new<?php echo $taxonomy; ?>"><?php echo $tax->labels->add_new_item; ?></label>
<input type="text" name="new<?php echo $taxonomy; ?>" id="new<?php echo $taxonomy; ?>" class="form-required form-input-tip" value="<?php echo esc_attr( $tax->labels->new_item_name ); ?>" tabindex="3" aria-required="true"/>
<label class="screen-reader-text" for="new<?php echo $taxonomy; ?>_parent">
<?php echo $tax->labels->parent_item_colon; ?>
</label>
<?php wp_dropdown_categories( array( ‘taxonomy’ => $taxonomy, ‘hide_empty’ => 0, ‘name’ => ‘new’.$taxonomy.’_parent’, ‘orderby’ => ‘name’, ‘hierarchical’ => 1, ‘show_option_none’ => ‘&mdash; ‘ . $tax->labels->parent_item . ‘ &mdash;’, ‘tab_index’ => 3 ) ); ?>
<input type="button" id="<?php echo $taxonomy; ?>-add-submit" class="add:<?php echo $taxonomy ?>checklist:<?php echo $taxonomy ?>-add button category-add-sumbit" value="<?php echo esc_attr( $tax->labels->add_new_item ); ?>" tabindex="3" />
<?php wp_nonce_field( ‘add-‘.$taxonomy, ‘_ajax_nonce-add-‘.$taxonomy, false ); ?>
<span id="<?php echo $taxonomy; ?>-ajax-response"></span>
</p>
</div>
<?php endif; ?>
</div>
<?php
}
[/php]

Hope it’s useful and see you next time.

15 thoughts on “Avoid hierarchical taxonomies to loose hierarchy”

  1. Great!

    Now I need something else. I need all parent taxonomies to be automatically included when selecting a child one. For instance, in your case, if you chose Related grandchild, I want both Related child and Related parent to be chosen as well. Is it possible?

  2. wonder why the heck that is the default WP behavior? nice save though. just used it on a custom tax for a custom post type.

  3. It’s definitely pretty strange. Can’t find any valid reason to break the hierarchy when children categories are selected. Maybe just for a faster appreciation?

  4. Can you do this with more than one box? I can manage to make it work with categories, and to make it work with my custom taxonomy, but if i try to do them both at the same time, I get a php error. Any ideas?

    1. ah yes, that plugin should do the trick! Although if you only need to fix the category and want to sell the theme or develop it for a client you might find easier to copy and paste the code in this page 😀

  5. alternative:
    function taxonomy_checklist_checked_ontop_filter ($args) { $args['checked_ontop'] = false; return $args; } add_filter('wp_terms_checklist_args','taxonomy_checklist_checked_ontop_filter');

Leave a Reply