5

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?

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.

5 Answers 5

3
+50

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

1
  • Can this be done even if taxonomies are defined via a plugin and not manually?
    – pax
    Oct 6, 2014 at 20:59
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('');
        }
    });
});
1

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

8
  • Exactly what i would have done(remove metabox -> add a custom one). +1
    – t31os
    Aug 16, 2011 at 17:31
  • @Bainternet The tag interface is rendered perfectly (autofills existing terms, tag cloud is correct, etc), but when I save the post, tags are not saved and all tags that were previously tagged are removed. Ideas?
    – supertrue
    Aug 17, 2011 at 5:01
  • no, i need to see your setup, but it's working fine on my end
    – Bainternet
    Aug 17, 2011 at 5:44
  • I think it must be something in the code; I tried this on a fresh WP install, and the same thing happens. Is there a missing instance of TAXONOMY_NAME or something? Here's the code: gist.github.com/1151032 —that uses a custom taxonomy, but I also tried with the category taxonomy.
    – supertrue
    Aug 17, 2011 at 7:45
  • i updated my answer
    – Bainternet
    Aug 17, 2011 at 8:13
1

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 <[email protected]>
 * 
 * @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).

0

I think we can add a hidden input that retrieves the selected tag's term_id via jQuery (AJAX) and save it as the post's term by using the function wp_set_object_terms.

I've posted another question relevant to this problem: WP native tag suggest metabox. How does it process tag id?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.