Making Custom Content
Management Disappear
into the WordPress Admin

Helen Hou-Sandí
WordCamp NYC 2012

About Me

User Experience Engineer with 10up, LLC


@helenhousandi on Twitter


WordPress Core Contributor *

* Check out the credits screen.
This dashed border tells us … that I am a developer, not a designer.
mdawaffe, WCSF 2011

* To be fair, there were color issues with the display

Decisions, not Options.

Design/UI to pay attention to:

WordPress.org UI Style Guide

Custom Content Types and
Post Meta

Post Type Icons

// Admin menu icon
register_post_type( 'event', array(
	...
	'menu_icon' => get_template_directory_uri() . '/images/icon-16.png';
));

// Screen icon
add_action( 'admin_head', 'hhs_event_screen_icon' );
function hhs_event_screen_icon() {
	$post_type = get_current_screen()->post_type;

	if ( 'event' != $post_type )
		return;
?>
<style type="text/css">
.icon32.icon32-posts-event {
	background: url(<?php echo get_template_directory_uri(); ?>/images/icon-32.png) !important;
}
</style>
<?php
}

Better Post Type Icons

// Don't set anything for the menu_icon arg in register_post_type()

add_action( 'admin_head', 'hhs_event_admin_menu_icon' );
function hhs_event_admin_menu_icon() {
?>
<style type="text/css">
#adminmenu #menu-posts-event div.wp-menu-image{
	background: transparent url(<?php echo get_template_directory_uri(); ?>/images/icon-16.png) no-repeat 6px -17px;
}
#adminmenu #menu-posts-event:hover div.wp-menu-image,
#adminmenu #menu-posts-event.wp-has-current-submenu div.wp-menu-image {
	background-position: 6px 7px;
</style>
<?php
}

Less is More

register_post_type( 'slide', array(
	...
	'show_in_menu' => 'themes.php',
	'show_in_nav_menus' => false,
	'supports' => array( 'thumbnail' )
) );

Add New / Edit screen, before

Featured Image metabox

add_action( 'add_meta_boxes_slide',  'hhs_slide_add_meta_boxes' );
add_filter( 'admin_post_thumbnail_html', 'hhs_slide_post_thumbnail_html' );

function hhs_slide_add_meta_boxes() {
	remove_meta_box( 'postimagediv', 'slide', 'side' );

	add_meta_box( 'postimagediv', 'Slide Image', 'post_thumbnail_meta_box', 'slide', 'normal', 'high' );
}

function hhs_slide_post_thumbnail_html( $output ) {
	global $post_type;

	// beware of translated admin
	if ( ! empty ( $post_type ) && 'slide' == $post_type ) {
		$output = str_replace( 'Set featured image', 'Select / Upload a slide image', $output );
		$output = str_replace( 'Remove featured image', 'Remove slide image', $output );
	}

	return $output;
}

Note: does not work via Ajax. See Trac ticket #20891

List table, before

List table, after

List table columns and row actions

add_filter( 'manage_edit-slide_columns', 'hhs_slide_edit_columns' );
add_action( 'manage_slide_posts_custom_column',  'hhs_slide_custom_columns' );

function hhs_slide_edit_columns( $columns ) {
	$columns = array(
		'cb' => '<input type="checkbox" />',
		'thumbnail' => 'Slide',
	);

	return $columns;
}

function hhs_slide_custom_columns( $column ) {
	global $post;
	switch ($column) {
		case 'thumbnail' :
			if ( has_post_thumbnail( $post->ID ) ) { // the current post has a thumbnail
				the_post_thumbnail( $post->ID );
			}
			else { // the current post lacks a thumbnail
				?>
				No image
				<?php
			}

			// add row_action links for Edit and Trash because there's no title column
			$post_type_object = get_post_type_object( $post->post_type );
			$can_edit_post = current_user_can( $post_type_object->cap->edit_post, $post->ID );
			$always_visible = false; // change to true to make it always show instead of on hover
			$actions = array();

			if ( $can_edit_post && 'trash' != $post->post_status ) {
				$actions['edit'] = '<a href="' . get_edit_post_link( $post->ID, true ) . '" title="' . esc_attr( __( 'Edit this item' ) ) . '">' . __( 'Edit' ) . '</a>';
			}
			if ( current_user_can( $post_type_object->cap->delete_post, $post->ID ) ) {
				if ( 'trash' == $post->post_status )
					$actions['untrash'] = "<a title='" . esc_attr( __( 'Restore this item from the Trash' ) ) . "' href='" . wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&amp;action=untrash', $post->ID ) ), 'untrash-' . $post->post_type . '_' . $post->ID ) . "'>" . __( 'Restore' ) . "</a>";
				elseif ( EMPTY_TRASH_DAYS )
					$actions['trash'] = "<a class='submitdelete' title='" . esc_attr( __( 'Move this item to the Trash' ) ) . "' href='" . get_delete_post_link( $post->ID ) . "'>" . __( 'Trash' ) . "</a>";
				if ( 'trash' == $post->post_status || !EMPTY_TRASH_DAYS )
					$actions['delete'] = "<a class='submitdelete' title='" . esc_attr( __( 'Delete this item permanently' ) ) . "' href='" . get_delete_post_link( $post->ID, '', true ) . "'>" . __( 'Delete Permanently' ) . "</a>";
			}

			$action_count = count( $actions );
			$i = 0;
			$out = '<div class="' . ( $always_visible ? 'row-actions-visible' : 'row-actions' ) . '">';
			foreach ( $actions as $action => $link ) {
				++$i;
				( $i == $action_count ) ? $sep = '' : $sep = ' | ';
				$out .= "<span class='$action'>$link$sep</span>";
			}
			$out .= '</div>';

			echo $out;
			break;
	}
}

jQuery UI Example: Datepicker

Datepicker in a metabox

// can also use callback in register_post_type() instead
register_post_type( 'event', array(
		...
		'register_meta_box_cb' => 'hhs_event_add_meta_boxes' ),
	)
);

function hhs_event_add_meta_boxes( $post ) {
	add_meta_box( 'hhs-event-date', 'Event Information', 'hhs_event_datepicker_meta_box', 'event', 'side', 'default' );
}

function hhs_event_datepicker_meta_box( $post ) {
	$event_date = get_post_meta( $post->ID, 'hhs_event_date', true );

	// mid-page enqueueing came in 3.3
	wp_enqueue_script( 'jquery-ui-datepicker' );

	wp_nonce_field( plugin_basename(__FILE__), 'hhs_eventmeta_nonce' );
	?>
	<script type="text/javascript">
	jQuery(document).ready(function($) {
		$('.datepicker').datepicker({
			dateFormat : 'yy-mm-dd'
		});
	});
	</script>

	<p><label for="hhs_event_date">Event Date</label><br />
	<input type="text" class="datepicker" name="hhs_event_date" id="hhs_event_date"<?php if( $event_date ) echo ' value="' . esc_attr( $event_date) . '"'; ?> /></p>
	<?php
}

List table with custom sortable column

Add columns to the list table

add_filter( 'manage_edit-events_columns', 'hhs_event_edit_columns' );
add_action( 'manage_event_posts_custom_column',  'hhs_event_custom_columns' );

function hhs_event_edit_columns( $columns ) {
	// relabel title column
	$columns['title'] = 'Event Title';

	// remove published date column
	unset( $columns['date'] );

	// append new columns
	$columns['event-date'] = 'Event Date';

	return $columns;
}

function hhs_event_custom_columns( $column ) {
	global $post;
	$event_date = get_post_meta( $post->ID, 'hhs_event_date', true );

	switch ( $column ) {
		case 'event-date' :
			// escape as appropriate
			// Idea: Use get_option( 'date_format' ) to format the date instead :)
			echo esc_html( $event_date );
			break;
	}
}

Make a column sortable

add_filter( 'manage_edit-event_sortable_columns', 'hhs_event_sortable_columns' );
add_action( 'load-edit.php', 'hhs_event_edit_load' );

function hhs_event_sortable_columns( $columns ) {
	$columns['event-date'] = 'event-date';
	return $columns;
}

function hhs_event_edit_load() {
	add_filter( 'request', 'hhs_event_sort' );
}

function hhs_event_sort( $vars ) {
	// Check post type and for the orderby request
	if ( isset( $vars['post_type'] ) && 'event' == $vars['post_type'] &&
	     isset( $vars['orderby'] ) && 'event-date' == $vars['orderby'] ) {

		// Merge in with the rest of the query vars
		$vars = array_merge(
			$vars,
			array(
				'meta_key' => 'hhs_event_date',
				'orderby' => 'meta_value'
			)
		);
	}

	return $vars;
}

Multiple Post Thumbnails: Before

Using regular post thumbnails and the Multiple Post Thumbnails plugin to add two more sizes/orientations

Multiple Post Thumbnails: After

Images in a metabox

function hhs_bmpt_metabox( $post ) {
	wp_nonce_field( basename(__FILE__), 'hhs_bmpt_nonce' );

	$id = get_post_meta( $post->ID, '_thumbnail_id', true );
	$img = false;
	if ( $id )
		$img = wp_get_attachment_image_src( $id, 'post-thumbnail' );
	?>
	<div class="item" data-size="post-thumbnail">
		<input type="hidden" name="_thumbnail_id" value="<?php if ( $id ) echo esc_attr( $id ); ?>" />
		<h4>Post Thumbnail</h4>
		<div class="image">
			<?php if ( $img ) :?>
			<img src="<?php echo esc_url( $img[0] ); ?>" />
			<?php else : ?>
			<img src="" class="screen-reader-text" />
			<?php endif; ?>
		</div>
		<p>
			<a href="#" class="upload-image">Upload / select image</a><br />
			<a href="#" class="remove-image<?php if ( ! $img ) echo '  screen-reader-text'; ?>">Remove image</a>
		</p>
		<p class="desc">This is the regular post thumbnail.</p>
	</div>
	<?php
}

Using the Media Library / Thickbox

JavaScript on the editing screen

jQuery(document).ready(function($){
	$('.upload-image').on('click', function(e){
		e.preventDefault();

		var item = $(this).closest('.item'),
		    post_id = $('input#post_ID').val(),
		    preview_size = item.data('size');

		window.image_item = item;

		tb_show('Select image', 'media-upload.php?post_id=' + post_id + '&type=image&meta_type=image&meta_preview_size=' + preview_size + 'TB_iframe=1');
	});

	$('.remove-image').on('click', function(e){
		e.preventDefault();

		var $this = $(this),
		    div = $this.closest('.item');

		div.find('input').attr('value', '');
		div.find('img').attr('src', '').addClass('screen-reader-text');

		$this.addClass('screen-reader-text');
	});
});

Clean up the Media Library Thickbox UI

<?php
// Insert some CSS and JS in the head of the Thickbox to simplify
add_action( 'admin_head-media-upload-popup', 'hhs_bmpt_thickbox_head' );
function hhs_bmpt_thickbox_head() {
	// generally hide the post thumbnail selection link
	if ( ! isset( $_GET['meta_type'] ) || 'image' !== $_GET['meta_type'] ) :
		?>
		<style type="text/css">
			tr.submit .wp-post-thumbnail {
				display: none;
			}
		</style>
		<?php
	// hide those links and a bunch of other things
	else :
		$preview_size = isset( $arr_postinfo['meta_preview_size'] ) ? sanitize_key( $arr_postinfo['meta_preview_size'] ) : 'post-thumbnail';
		?>
		<style type="text/css">
			#media-upload-header #sidemenu li#tab-type_url,
			#gallery-settings,
			#gallery-form table.widefat thead,
			#gallery-form .menu_order,
			#sort-buttons,
			.ml-submit,
			tr.url,
			tr.align,
			tr.image_alt,
			tr.image-size,
			tr.post_title,
			tr.post_excerpt,
			tr.post_content,
			tr.image_alt p,
			table thead input.button,
			table thead img.imgedit-wait-spin,
			tr.submit a.wp-post-thumbnail {
				display: none !important;
			}
		</style>
		<script type="text/javascript">
		jQuery(document).ready(function($){
			$('#media-items').bind('DOMNodeInserted',function(){
				$('input[value="Insert into Post"]').each(function(){
					$(this).attr('value','Select Image');
				});
			}).trigger('DOMNodeInserted');

			$('form#filter').each(function(){
				$(this).append('<input type="hidden" name="meta_preview_size" value="<?php echo esc_js( $preview_size ); ?>" />');
				$(this).append('<input type="hidden" name="meta_type" value="image" />');
			});
		});
		</script>
		<?php
	endif;
}

Inserting an Image from the Thickbox

// Inject JS to show the image thumbnail and set the hidden input value
add_filter( 'media_send_to_editor', 'hhs_bmpt_media_send_to_editor', 15, 2 );
function hhs_bmpt_media_send_to_editor( $html, $id ) {
	if( ! isset( $arr_postinfo['meta_type'] ) || $arr_postinfo['meta_type'] !== 'image' )
		return $html;

	else {
		$preview_size = ( ! empty( $arr_postinfo['meta_preview_size'] ) ) ? sanitize_key( $arr_postinfo['meta_preview_size'] ) : 'post-thumbnail';

		$file_src = wp_get_attachment_image_src( $id, $preview_size );
		$file_src = $file_src[0];
		?>
		<script type="text/javascript">
			self.parent.image_item.find('input').val('<?php echo esc_js( $id ); ?>');
		 	self.parent.image_item.find('img').attr('src','<?php echo esc_js( $file_src ); ?>');
		 	self.parent.image_item.find('img').removeClass('screen-reader-text');
            self.parent.image_item.find('.remove-image').removeClass('screen-reader-text');

		 	self.parent.image_item = null;
		 	self.parent.tb_remove();
		</script>
		<?php
		exit;
	}
}

Static front page metabox

add_action( 'add_meta_boxes', 'hhs_add_meta_boxes', 10, 2 );
function hhs_add_meta_boxes( $post_type, $post ) {
	$front_page = get_option( 'page_on_front' );

	if ( $post->ID === $front_page ) {
		add_meta_box( 'hhs_front_page_meta', 'Home Page Content', 'hhs_front_page_meta', 'page', 'normal', 'high' );

		// remove the editor
		remove_post_type_support( 'page', 'editor' );
	}
}
Note: Only appears when a front page has already been selected.

Chosen for multi-selects and taxonomies

Better place for status-related meta

post_submitbox_misc_actions

<?php
add_action( 'post_submitbox_misc_actions', 'hhs_post_submitbox_misc_actions' );
function hhs_post_submitbox_misc_actions() {
	// restrict post type if needed
	if ( 'post' != get_post_type() )
		return;

	wp_nonce_field( 'hhs_post_submitbox_nonce', plugin_basename(__FILE__) );

	// Inline styles needed pre-3.4 - see WCPHX 2012 slides
	// But you're using the latest, right?
?>
	<div class="misc-pub-section">
		<input type="checkbox" name="remove_claim_link" id="remove_claim_link" value="1" <?php checked( get_post_meta( get_the_ID(), 'remove_claim_link', true ) ); ?> />
		<label for="remove_claim_link">Claimed</label>
	</div>
	<div class="misc-pub-section">
		<input type="checkbox" name="is_featured" id="is_featured" value="1" <?php checked( get_post_meta( get_the_ID(), 'is_featured', true ) ); ?> />
		<label for="is_featured">Featured</label>
	</div>
	<div class="misc-pub-section">
		<input type="checkbox" name="is_premium" id="is_premium" value="1" <?php checked( get_post_meta( get_the_ID(), 'is_premium', true ) ); ?> />
		<label for="is_premium">Premium</label>
	</div>
<?php
}

Default Content

// These are actual text, not placeholders - will save if not removed
// Also: default_excerpt
add_filter( 'default_title', 'hhs_event_default_title', 10, 2 );
function hhs_event_default_title( $title, $post ) {
	if ( 'event' == $post->post_type )
		$title = 'Event Title';

	return $title;
}
add_filter( 'default_content', 'hhs_event_default_content', 10, 2 );
function hhs_event_default_content( $content, $post ) {
	if ( 'event' == $post->post_type )
		$content = 'Event description';

	return $content;
}

// This is a placeholder
add_filter( 'enter_title_here', 'hhs_event_enter_title_here', 10, 2 );
function hhs_event_enter_title_here( $placeholder, $post ) {
	if ( 'event' == $post->post_type )
		$placeholder = 'Event Title';

	return $placeholder;
}

More things you can do

Plugins of Note

Related Presentations

Thank you! (Questions?)


Slides online at
http://slides.helenhousandi.com/wcnyc2012.html