Use tag interface for hierarchical taxonomy?

Custom taxonomies are great. I registered a bunch of new taxonomies and wrote an importer to import our hierarchical taxonomy into WordPress a la XML. The problem is one taxonomy has about 1,100 terms and browsing a checklist of 1,100 things is cruel and unusual punishment.

Is there any way to have a hierarchical taxonomy but use the Tag interface (search box with auto complete) instead?

Read More

Update: this code from Bainternet’s answer gets most of the way there
(adds tag interface for the specified taxonomy, with working
autocomplete and correctly populated “most used” tag cloud), but terms
are not saved on post save. If the post had terms before, they will
be deleted on save. So I am still looking for an answer. (This same code saves terms just fine if the taxonomy is registered with hierarchichalset to false, but the point of the question is to use the tag interface on a hierarchical taxonomy.)

//remove default metabox
//change TAXONOMY_NAME to your taxonomy name
add_action( 'admin_menu' , 'remove_post_custom_fields' );
function remove_post_custom_fields() {
    remove_meta_box( 'issuediv' , 'post' , 'normal' ); 
}



//add our custom meta box
add_action( 'add_meta_boxes', 'my_add_custom_box' );

 function my_add_custom_box() {
    add_meta_box( 
//      'myplugin_sectionid',
        'tagsdiv-issue',
        __( 'New and Improved Issue Tags', 'textdomain' ),
        'tags_like_custom_tax',
        'post' 
    );
 }

 //call back function to display the metabox
 //change TAXONOMY_NAME to your taxonomy name 
 function tags_like_custom_tax(){
     $tax_name = 'issue';
     global $post;
     $taxonomy = get_taxonomy($tax_name);
     $disabled = !current_user_can($taxonomy->cap->assign_terms) ? 'disabled="disabled"' : '';
     ?>
     <div class="tagsdiv" id="<?php echo $tax_name; ?>">
        <div class="jaxtag">
            <div class="nojs-tags hide-if-js">
                <p><?php echo $taxonomy->labels->add_or_remove_items; ?></p>
                <textarea name="<?php echo "tax_input[$tax_name]"; ?>" rows="3" cols="20" class="the-tags" id="tax-input-<?php echo $tax_name; ?>" <?php echo $disabled; ?>><?php echo get_terms_to_edit( $post->ID, $tax_name ); // textarea_escaped by esc_attr() ?></textarea>
            </div>
            <?php if ( current_user_can($taxonomy->cap->assign_terms) ) { ?>
            <div class="ajaxtag hide-if-no-js">
                <label class="screen-reader-text" for="new-tag-<?php echo $tax_name; ?>"><?php echo $taxonomy->labels->name; ?></label>
                <div class="taghint"><?php echo $taxonomy->labels->add_new_item; ?></div>
                <p><input type="text" id="new-tag-<?php echo $tax_name; ?>" name="newtag[<?php echo $tax_name; ?>]" class="newtag form-input-tip" size="16" autocomplete="off" value="" />
                <input type="button" class="button tagadd" value="<?php esc_attr_e('Add'); ?>" tabindex="3" /></p>
            </div>
            <p class="howto"><?php echo esc_attr( $taxonomy->labels->separate_items_with_commas ); ?></p>
            <?php } ?>
        </div>
        <div class="tagchecklist"></div>
    </div>
          <?php if ( current_user_can($taxonomy->cap->assign_terms) ) { ?>
            <p class="hide-if-no-js"><a href="#titlediv" class="tagcloud-link" id="link-<?php echo $tax_name; ?>"><?php echo $taxonomy->labels->choose_from_most_used; ?></a></p>
          <?php } 
}

The original question is borrowed from the WordPress forum post here.

Related posts

Leave a Reply

5 comments

  1. Here’s how I did it. Just add a conditional that checks if the page being loaded is an admin page or not. If it is an admin page, set hierarchical to false, otherwise set hierarchical to true. Like so:

    $args = array( 
        'hierarchical' => true,
        'labels' => $labels,
        'show_ui' => true,
        'query_var' => true,
        'rewrite' => array( 
              'slug' => 'genre'
        ), 
     )
    
    if( is_admin() ) {
        $args['hierarchical'] = false;
    }
    
    register_taxonomy('genre', array('book'), $args);
    

    That should give you the idea. The downside to this is you can’t add parent relationships to terms using the admin interface. You could get more specific in the is_admin() conditional such as looking to see if the request contains post-new.php or post.php…

  2. Terms from a hierarchical taxonomy can only be added if WP gets their term_ids on save (look at the way the default metabox for category is working to get the idea), because in a hierarchical taxonomy there can be multiple identical term names if these terms are in different branches of the hierarchy tree.

    With non-hierarchical terms, this is not possible. As was pointed out here, when adding terms from a non-hierarchical taxonomy, their ids are retrieved by term_exists() via the term names, which obviously can only work if you have unique term names. So I don’t think kingkool68’s answer is working in more complex taxonomy situations.

    So I made this work by using a combination of the tag-style UI and the “inner workings” of a hierarchical taxonomy metabox. I’ve based this on Bainternet’s example. My custom taxonomy here is “coverages”, which can be added to posts, attachments and pages.

    PHP

    //remove default metabox
    add_action('admin_menu', function() {
        remove_meta_box('coveragesdiv', ['post', 'attachment', 'page'], 'normal');
    });
    
    //add our custom meta box
    add_action('add_meta_boxes', function() {
        add_meta_box(
            'tagsdiv-coverages',
            __('Coverage', 'textdomain'),
            'coveragesMetaboxShow',
            ['post', 'attachment', 'page'],
            'side'
        );
    });
    
    //enqueue js for custom autosuggest/remove terms
    add_action('admin_enqueue_scripts', function() {
        $screen = get_current_screen();
    
        if ($screen->base == 'post' && (in_array($screen->post_type, ['post', 'headlines', 'attachment', 'page']))) {
            wp_enqueue_script('coveragesMetaBoxJS', plugin_dir_url(__FILE__) . '../js/admin/coveragesMetaBox.js', ['jquery', 'jquery-ui-autocomplete']);
            wp_localize_script('coveragesMetaBoxJS', 'coveragesMetaBoxAjax', ['url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('coveragesMetaBoxNonce')]);  
        }
    });
    
    //show metabox
    function coveragesMetaboxShow() {
        global $post;
        $tax_name = 'coverages';
        $taxonomy = get_taxonomy($tax_name);
        $bCanAssignTerms = current_user_can($taxonomy->cap->assign_terms);
        $aryTerms = get_the_terms($post->ID, $tax_name);
        ?>
        <style type="text/css">
            .tagchecklist.coverages             { margin-bottom:0; }
            .tagchecklist.coverages.disabled    { margin-left:0; }
            .tagchecklist.coverages input       { display:none; }   
        </style>
        <div class="tagsdiv" id="<?php echo $tax_name; ?>">
            <?php if ($bCanAssignTerms) { ?>
                <label class="screen-reader-text" for="new-tag-<?php echo $tax_name; ?>"><?php echo $tax_name; ?></label>           
                <p>
                    <input type="text" id="new-tag-<?php echo $tax_name; ?>" class="newtag form-input-tip" autocomplete="off" value="" style="width:100%; float:none;">
                </p>
                <!-- needed so WP deletes all term relations on save if all terms where removed from UI -->
                <input type="hidden" name="tax_input[<?php echo $tax_name; ?>][]" value="0">
            <?php } ?>      
            <ul class="tagchecklist <?php echo $tax_name; if (!$bCanAssignTerms) { echo ' disabled'; } ?>" role="list">         
            <?php
                foreach($aryTerms AS $term) {
                    echo '<li id="tax'.$term->term_id.'"><input value="'.$term->term_id.'" name="tax_input['.$tax_name.'][]" type="checkbox" checked>';
                    if ($bCanAssignTerms) {
                        echo '<button type="button" class="ntdelbutton"><span class="remove-tag-icon" aria-hidden="true"></span></button>';
                    }
                    else {
                        echo '&bull;';
                    }
                    echo '&nbsp;'.$term->name.'</li>';          
                }
            ?>          
            </ul>
        </div>  
        <?php
    }
    
    //custom autosuggest search; based on WP core https://developer.wordpress.org/reference/functions/wp_ajax_ajax_tag_search/
    //no suitable hooks/filters there :(
    add_action('wp_ajax_coveragesTagSearch', function() {
        if (isset($_GET['nonce']) && wp_verify_nonce($_GET['nonce'], 'coveragesMetaBoxNonce')) {
             if ( ! isset( $_GET['tax'] ) ) {
                  wp_die( 0 );
             }
    
             $taxonomy = sanitize_key( $_GET['tax'] );
             $tax = get_taxonomy( $taxonomy );
    
             if ( ! current_user_can( $tax->cap->assign_terms ) ) {
                  wp_die( -1 );
             }
    
             $s = wp_unslash( $_GET['term'] );
    
             $comma = _x( ',', 'tag delimiter' );
             if ( ',' !== $comma )
                  $s = str_replace( $comma, ',', $s );
             if ( false !== strpos( $s, ',' ) ) {
                  $s = explode( ',', $s );
                  $s = $s[count( $s ) - 1];
             }
             $s = trim( $s ); 
             $term_search_min_chars = 2; 
             if ( ( $term_search_min_chars == 0 ) || ( strlen( $s ) < $term_search_min_chars ) ){
                  wp_die();
             }
    
             $results = get_terms( $taxonomy, array( 'name__like' => $s, 'fields' => 'id=>name', 'hide_empty' => false, 'number' => 20 ) );  
             //change result format from associative array to array of objects; needed in jQuery.autocomplete's select event to get the term_id
             $aryResults = [];
             foreach ($results AS $term_id=>$term_name) {
                 $objTerm = new stdClass;
                 $objTerm->id = $term_id;
                 $objTerm->value = $term_name;
                 $aryResults[] = $objTerm;
             }
             echo json_encode($aryResults, JSON_NUMERIC_CHECK);
        }
        wp_die();
    }); 
    

    JS

    jQuery(function() {
        //remove term from UI
        jQuery('.tagchecklist.coverages .ntdelbutton').on('click', function() {
            jQuery(this).parent().remove();
        });
    
        //custom term autocomplete
        jQuery('#new-tag-coverages').autocomplete({
            minLength:2,
            source:coveragesMetaBoxAjax.url + '?action=coveragesTagSearch&tax=coverages&nonce=' + coveragesMetaBoxAjax.nonce,
            select:function(event, ui) {
                jQuery('.tagchecklist.coverages').append('<li id="tax' + ui.item.id + '"><input value="' + ui.item.id + '" name="tax_input[coverages][]" type="checkbox" checked><button type="button" class="ntdelbutton"><span class="remove-tag-icon" aria-hidden="true"></span></button>&nbsp;' + ui.item.value + '</li>');
                //when selecting a term with the mouse from the autosuggest list, the close-event is triggered *after* the target value is set :(
                window.setTimeout(function() { jQuery(event.target).val(''); }, 100);
            },      
            close:function(event, ui) {
                jQuery(event.srcElement).val('');
            }
        });
    });
    
  3. The only way i have found is to remove the default metabox and create your own, here is the code i have used:

    //remove default metabox
    //change TAXONOMY_NAME to your taxonomy name
    add_action( 'admin_menu' , 'remove_post_custom_fields' );
    function remove_post_custom_fields() {
        remove_meta_box( 'TAXONOMY_NAMEdiv' , 'post' , 'normal' ); 
    }
    
    
    
    //add our custom meta box
    add_action( 'add_meta_boxes', 'my_add_custom_box' );
    
     function my_add_custom_box() {
        add_meta_box( 
            'myplugin_sectionid',
            __( 'My Taxonomy Section Title', 'textdomain' ),
            'tags_like_custom_tax',
            'post' 
        );
     }
    
     //call back function to display the metabox
     //change TAXONOMY_NAME to your taxonomy name 
     function tags_like_custom_tax(){
         $tax_name = 'TAXONOMY_NAME';
         global $post;
         $taxonomy = get_taxonomy($tax_name);
         $disabled = !current_user_can($taxonomy->cap->assign_terms) ? 'disabled="disabled"' : '';
         ?>
         <div class="tagsdiv" id="<?php echo $tax_name; ?>">
            <div class="jaxtag">
                <div class="nojs-tags hide-if-js">
                    <p><?php echo $taxonomy->labels->add_or_remove_items; ?></p>
                    <textarea name="<?php echo "tax_input[$tax_name]"; ?>" rows="3" cols="20" class="the-tags" id="tax-input-<?php echo $tax_name; ?>" <?php echo $disabled; ?>><?php echo get_terms_to_edit( $post->ID, $tax_name ); // textarea_escaped by esc_attr() ?></textarea>
                </div>
                <?php if ( current_user_can($taxonomy->cap->assign_terms) ) { ?>
                <div class="ajaxtag hide-if-no-js">
                    <label class="screen-reader-text" for="new-tag-<?php echo $tax_name; ?>"><?php echo $box['title']; ?></label>
                    <div class="taghint"><?php echo $taxonomy->labels->add_new_item; ?></div>
                    <p><input type="text" id="new-tag-<?php echo $tax_name; ?>" name="newtag[<?php echo $tax_name; ?>]" class="newtag form-input-tip" size="16" autocomplete="off" value="" />
                    <input type="button" class="button tagadd" value="<?php esc_attr_e('Add'); ?>" tabindex="3" /></p>
                </div>
                <p class="howto"><?php echo esc_attr( $taxonomy->labels->separate_items_with_commas ); ?></p>
                <?php } ?>
            </div>
            <div class="tagchecklist"></div>
        </div>
              <?php if ( current_user_can($taxonomy->cap->assign_terms) ) { ?>
                <p class="hide-if-no-js"><a href="#titlediv" class="tagcloud-link" id="link-<?php echo $tax_name; ?>"><?php echo $taxonomy->labels->choose_from_most_used; ?></a></p>
              <?php } 
    }
    

    As for saving, you don’t need to worry about it, WordPress does that for you.

    Update, I just tested it with categories and it works fine:

    enter image description here

  4. I find solution, how to use the tag interface on a hierarchical taxonomy.

    For the first, we need to create is_edit_page() function:

    /**
     * is_edit_page 
     * function to check if the current page is a post edit page
     * 
     * @author Ohad Raz <admin@bainternet.info>
     * 
     * @param  string  $new_edit what page to check for accepts new - new post page ,edit - edit post page, null for either
     * @return boolean
     */
    function is_edit_page($new_edit = null){
        global $pagenow;
        //make sure we are on the backend
        if (!is_admin()) return false;
    
    
        if($new_edit == "edit")
            return in_array( $pagenow, array( 'post.php',  ) );
        elseif($new_edit == "new") //check for new post page
            return in_array( $pagenow, array( 'post-new.php' ) );
        else //check for either new or edit
            return in_array( $pagenow, array( 'post.php', 'post-new.php' ) );
    }
    

    * code taken from here.

    Now you can use code prodived by kingkool68:

    $args = array(
        //...
        'hierarchical' => true,
        //...
    );
    if( is_edit_page() ) {
        $args['hierarchical'] = false;
    }
    

    It saves the hierarchical structure, but show taxonomies on edit page like tags (comma separated).