Unique post category/term

In WordPress, we can set multiple categories/terms for a post. This is true for both hierarchical taxonomies (ex: category) and non-hierarchical taxonomies (ex: post_tag). However, on a project I’ve worked on, the client wanted me to make the category metabox to use radio inputs (exactly like WordPress’ new post format feature) instead of checkboxes to prevent the editor from setting multiple categories for the post (s)he writes. This request forced me :) to come up with a solution. I was also inspired by Justin Tadlock’s idea on using a taxonomy for theme layout. Here’s my solution in hope it could be useful for others.

The result

Unique Taxonomy Metabox
Unique Taxonomy Metabox

The code

As usual, you can either add this code to your theme’s function.php file or create a new php file for it and place it inside wp-content/mu-plugins directory (preferred) so you can always have this feature no matter what theme you’re using.

<?php
function kc_unique_taxonomies() {
	$unique_taxonomies = apply_filters( 'kc_unique_taxonomies', array() );
	if ( empty($unique_taxonomies) )
		return;

	foreach ( $unique_taxonomies as $tax_name ) {
		$taxonomy = get_taxonomy( $tax_name );
		if ( !$taxonomy->hierarchical || !$taxonomy->show_ui || empty($taxonomy->object_type) )
			continue;

		foreach ( $taxonomy->object_type as $pt ) {
			# Remove default metabox
			remove_meta_box( "{$tax_name}div", $pt, 'side' );

			# Add our own
			add_meta_box( "unique-{$tax_name}-div", $taxonomy->labels->singular_name, 'kc_unique_taxonomies_metabox', $pt, 'side', 'low', array('taxonomy' => $tax_name) );
		}
	}
}
add_action( 'admin_menu', 'kc_unique_taxonomies' );


function kc_terms_radiolist( $post_id, $taxonomy, $echo = true ) {
	$terms = get_terms( $taxonomy, array('hide_empty' => false) );
	if ( empty($terms) )
		return;
	$name = ( $taxonomy == 'category' ) ? 'post_category' : "tax_input[{$taxonomy}]";

	$post_terms = get_the_terms( $post_id, $taxonomy );
	$nu_post_terms = array();
	if ( !empty($post_terms) ) {
		foreach ( $post_terms as $post_term )
			$nu_post_terms[] = $post_term->term_id;
	}

	$output = '';
	foreach ( $terms as $term ) {
		$output .= "<li class='selectit'>";
		$output .= "<label>";
		$output .= "<input type='radio' name='{$name}[]' value='{$term->term_id}' ".checked( in_array($term->term_id, $nu_post_terms), true, false )."/>";
		$output .= " {$term->name}</label>";
		$output .= "</li>\n";
	}

	if ( $echo )
		echo $output;
	else
		return $output;
}

function kc_unique_taxonomies_metabox( $post, $box ) {
	if ( !isset($box['args']) || !is_array($box['args']) )
		$args = array();
	else
		$args = $box['args'];

	$defaults = array('taxonomy' => 'category');
	extract( wp_parse_args($args, $defaults), EXTR_SKIP );
	$tax = get_taxonomy($taxonomy);

	?>
	<div id="taxonomy-<?php echo $taxonomy; ?>" class="categorydiv">
		<?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 kc_terms_radiolist( $post->ID, $taxonomy ) ?>
		</ul>
	<?php if ( !current_user_can($tax->cap->assign_terms) ) { ?>
		<p><em><?php _e('You cannot modify this taxonomy.'); ?></em></p>
	<?php } ?>
	<?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>
					<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 } ?>
	</div>
<?php } ?>

The explanation

The first function — kc_unique_taxonomies — will find if you have set a taxonomy (or more) to be unique. You will have to add the taxonomy IDs using the available kc_unique_taxonomies filter hook (we’ll talk about it shortly). If there’s a unique taxonomy found, it will then remove the taxonomy’s default metabox in the post editing page, and replace it with the new one.

Note:
This function will only search for hierarchical taxonomies, because I strongly believe that a post should be allowed to have multiple non-hierachical taxonomy terms (ex: tags). Also, non-public and orphan taxonomies (taxonomies that are not attached to a post type) will be ignored.

The second function — kc_terms_radiolist — will display the replacement metaboxes for each unique taxonomies that use radio inputs for the taxonomy’s terms. I basically just took WordPress default category metabox and adjusted some parts of it.

Usage

The last step is to actually set some taxonomies to be unique which you can either do from your theme or plugin. Just add this snippet of code:

function my_unique_taxonomies( $arr ) {
	$unique_taxonomies = array( 'category' );
	return array_merge( $arr, $unique_taxonomies );
}
add_filter( 'kc_unique_taxonomies', 'my_unique_taxonomies' );

You can add more taxonomies to the $unique_taxonomies array if you want.

Bugs, question? feel free to leave a comment below :)

5 thoughts on “Unique post category/term”

  1. The plugin does a great job for sure. However my need was really simple, a post should only attached to *one* category only, and the categories should not have children (they should be in the same level).

    I think I’ll stick with my solution for now but thanks for letting me know about the plugin, I may use it in some other projects :)

  2. Where is kc_terms_radiolist called?

    The kc_unique_taxonomies_metabox callback function is missing…

  3. Thanks for this hook. How would I do the same thing for Tags? I could definitely add a second taxonomy, but it will be great to use for tags. Hope you still monitoring this post or blog.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>