|

Add Custom fields to Quick Edit and Bulk Edit

The next version (3.4) of Contextual Related Posts will see a brand new feature to allow a user to bulk edit posts, pages or custom post types to add the manual related posts. Plugin users will also be able to bulk select which posts they want to exclude from the related posts lists. Before this, users would need to set these one at a time by editing the individual post.

There are limited up-to-date examples and I did turn to GitHub CoPilot several times for code. With the amount of trial and error I did over the past few days, I felt it would be a good idea to post a tutorial that should hopefully help other WordPress developers and plugin authors.

I will assume you already have an existing plugin or are comfortable adding code to your theme’s functions.php. However, I’d strongly advise creating a functional plugin at a minimum. If you use Advanced Custom Fields, you’ll find value in the code below that allows the user to bulk edit custom fields.

In this tutorial, I have configured two ACF fields called related_posts, a comma-separated field, and exclude_this_post, a True/False field.

Here’s what we will be creating today. The image below links to a video in my YouTube channel.

Step 1: Add a custom admin column to the post screens

We will start by adding a single column to our post-listing screens. Our code will apply this for all public custom post types. WordPress users can hide the columns they don’t need. While you can add a column per field, we’ll keep it neat by displaying all our data in a single column.

We do this by hooking into the manage_{$post_type}_posts_columns1 filter to add the new column. And then, populate it using the manage_{$post->post_type}_posts_custom_column2 action.

<?php
/**
 * Class to add Bulk Edit functionality.
 */
class Bulk_Edit {

	/**
	 * CRP_Bulk_Edit constructor.
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'add_custom_columns' ), 99 );
	}

	/**
	 * Add custom columns to the posts list table.
	 */
	public function add_custom_columns() {
		// Get all post types present on the site.
		$post_types = get_post_types( array( 'public' => true ) );

		// For each post type, add the bulk edit functionality and the columns.
		foreach ( $post_types as $post_type ) {
			add_filter( 'manage_' . $post_type . '_posts_columns', array( $this, 'add_admin_columns' ) );
			add_action( 'manage_' . $post_type . '_posts_custom_column', array( $this, 'populate_custom_columns' ), 10, 2 );
		}
	}

}

Step 2: Add a custom box for Quick Edit and Bulk Edit screens

We will now add the additional fields to the Quick Edit and Bulk Edit boxes. We can do this using a single function to hook into bulk_edit_custom_box and quick_edit_custom_box.

Add these two lines to the __construct method of our class.

add_action( 'bulk_edit_custom_box', array( $this, 'quick_edit_custom_box' ) );
add_action( 'quick_edit_custom_box', array( $this, 'quick_edit_custom_box' ) );

And then add the below code as a new method of our class.

/**
 * Add custom field to quick edit screen.
 *
 * @param string $column_name The name of the column.
 */
public function quick_edit_custom_box( $column_name ) {

	switch ( $column_name ) {
		case 'wz_tutorials_columns':
			if ( current_filter() === 'quick_edit_custom_box' ) {
				wp_nonce_field( 'wz_tutorials_quick_edit_nonce', 'wz_tutorials_quick_edit_nonce' );
			} else {
				wp_nonce_field( 'wz_tutorials_bulk_edit_nonce', 'wz_tutorials_bulk_edit_nonce' );
			}
			?>
			<fieldset class="inline-edit-col-left inline-edit-wz_tutorials">
				<div class="inline-edit-col column-<?php echo esc_attr( $column_name ); ?>">
					<label class="inline-edit-group">
						<?php esc_html_e( 'Related Posts', 'wz-tutorials' ); ?>
						<?php
						if ( current_filter() === 'bulk_edit_custom_box' ) {
							' ' . esc_html_e( '(0 to clear the related posts)', 'wz-tutorials' );
						}
						?>
						<input type="text" name="wz_tutorials_related_posts" class="widefat" value="">
					</label>
					<label class="inline-edit-group">
						<?php if ( current_filter() === 'quick_edit_custom_box' ) { ?>
							<input type="checkbox" name="wz_tutorials_exclude_this_post"><?php esc_html_e( 'Exclude this post from related posts', 'wz-tutorials' ); ?>								
						<?php } else { ?>
							<?php esc_html_e( 'Exclude from related posts', 'wz-tutorials' ); ?>
							<select name="wz_tutorials_exclude_this_post">
								<option value="-1"><?php esc_html_e( '&mdash; No Change &mdash;' ); ?></option>
								<option value="1"><?php esc_html_e( 'Exclude' ); ?></option>
								<option value="0"><?php esc_html_e( 'Include' ); ?></option>
							</select>
						<?php } ?>
					</label>
				</div>
			</fieldset>
			<?php
			break;
	}
}

In the code above, you’ll see that we check which filter is called using current_filter() and then display the relevant nonce. We also use it to check and display a checkbox for exclude_this_post in the Quick Edit screen and a dropdown in the Bulk Edit screen.

Step 3: Populate the Quick Edit fields

We’ll need some JavaScript to populate the fields in the Quick Edit box. You can create a new JavaScript file – I called mine bulk-edit.js.

jQuery(document).ready(function ($) {

    // we create a copy of the WP inline edit post function
    const wp_inline_edit = inlineEditPost.edit;

    // and then we overwrite the function with our own code
    inlineEditPost.edit = function (post_id) {

        // "call" the original WP edit function
        // we don't want to leave WordPress hanging
        wp_inline_edit.apply(this, arguments);

        // now we take care of our business

        // get the post ID from the argument
        if (typeof (post_id) == 'object') { // if it is object, get the ID number
            post_id = parseInt(this.getId(post_id));
        }

        if (post_id > 0) {
            // define the edit row
            const edit_row = $('#edit-' + post_id);
            const post_row = $('#post-' + post_id);

            // get the data
            const related_posts = $('.wz_tutorials_related_posts', post_row).text();
            const exclude_this_post = 1 == $('.wz_tutorials_exclude_this_post', post_row).val() ? true : false;

            // populate the data
            $(':input[name="wz_tutorials_related_posts"]', edit_row).val(related_posts);
            $(':input[name="wz_tutorials_exclude_this_post"]', edit_row).prop('checked', exclude_this_post);
        }
    };
});

In the above code, we fetch the status of our fields from the classes – one a div and the other an input field created by our PHP code in Step 2. We then use that to set the content of the related posts field and check/uncheck the checkbox for the exclude_this_post.

You will need to enqueue the js file using admin_enqueue_scripts. See the code below:

/**
 * Enqueue scripts and styles.
 *
 * @param string $hook The current admin page.
 */
public function enqueue_scripts( $hook ) {
	if ( 'edit.php' !== $hook ) {
		return;
	}

	$file_prefix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';

	wp_enqueue_script(
		'wz-tutorials-bulk-edit',
		WZ_TUTORIALS_PLUGIN_URL . 'includes/admin/js/bulk-edit' . $file_prefix . '.js',
		array( 'jquery' ),
		WZ_TUTORIALS_VERSION,
		true
	);
	wp_localize_script(
		'wz-tutorials-bulk-edit',
		'wz_tutorials_bulk_edit',
		array(
			'nonce' => wp_create_nonce( 'wz_tutorials_bulk_edit_nonce' ),
		)
	);
}

Step 4: Save the Quick Edit fields

Now that we’ve populated our fields in the Quick Edit mode, we will tell WordPress to save them when the user updates the post. For that, we will hook into the save_post action. Similar to Step 3, add the add_action code into your __construct and the save_post_meta() function as a new method for the Bulk_Edit class.

add_action( 'save_post', array( $this, 'save_post_meta' ) );

/**
 * Save custom field data.
 *
 * @param int $post_id The post ID.
 */
public function save_post_meta( $post_id ) {
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return;
	}
	if ( ! isset( $_REQUEST['wz_tutorials_quick_edit_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['wz_tutorials_quick_edit_nonce'] ) ), 'wz_tutorials_quick_edit_nonce' ) ) {
		return;
	}

	if ( isset( $_REQUEST['wz_tutorials_related_posts'] ) ) {
		$related_posts = wp_parse_id_list( sanitize_text_field( wp_unslash( $_REQUEST['wz_tutorials_related_posts'] ) ) );

		// Remove any posts that are not published.
		foreach ( $related_posts as $key => $value ) {
			if ( 'publish' !== get_post_status( $value ) ) {
				unset( $related_posts[ $key ] );
			}
		}
		$related_posts = implode( ',', $related_posts );

		// Update the ACF field.
		if ( ! empty( $related_posts ) ) {
			update_field( 'related_posts', $related_posts, $post_id );
		} else {
			delete_field( 'related_posts', $post_id );
		}
	}

	if ( isset( $_REQUEST['wz_tutorials_exclude_this_post'] ) ) {
		// Update the ACF field.
		update_field( 'exclude_this_post', 1, $post_id );
	} else {
		// Delete the ACF field.
		delete_field( 'exclude_this_post', $post_id );
	}
}

When the user hits the Update button, the two fields will be updated and saved to the database.

Step 5: Save the Bulk Edit fields

This is more complicated vs the Quick Edit because we need to use Ajax to save the fields. Well, at least complicated for me.

Edit the bulk-edit.js file to add the following code before the closing brackets i.e. the last line of our previously created js file.

$('#bulk_edit').on('click', function (event) {
    const bulk_row = $('#bulk-edit');

    // Get the selected post ids that are being edited.
    const post_ids = [];

    // Get the data.
    const related_posts = $(':input[name="wz_tutorials_related_posts"]', bulk_row).val();
    const exclude_this_post = $('select[name="wz_tutorials_exclude_this_post"]', bulk_row).val();

    // Get post IDs from the bulk_edit ID. .ntdelbutton is the class that holds the post ID.
    bulk_row.find('#bulk-titles-list .ntdelbutton').each(function () {
        post_ids.push($(this).attr('id').replace(/^(_)/i, ''));
    });
    // Convert all post_ids to integer.
    post_ids.map(function (value, index, array) {
        array[index] = parseInt(value);
    });

    // Save the data.
    $.ajax({
        url: ajaxurl, // this is a variable that WordPress has already defined for us
        type: 'POST',
        async: false,
        cache: false,
        data: {
            action: 'wz_tutorials_save_bulk_edit', // this is the name of our WP AJAX function that we'll set up next
            post_ids: post_ids, // and these are the 2 parameters we're passing to our function
            related_posts: related_posts,
            exclude_this_post: exclude_this_post,
            wz_tutorials_bulk_edit_nonce: wz_tutorials_bulk_edit.nonce
        }
    });
});

I’ve left comments above. In summary, we fetch the post IDs and the values of the related_posts and exclude_this_post fields. We then send this data to the wz_tutorials_save_bulk_edit ajax action that we will create using the code below.

As above, add the add_action code into your __construct and the save_bulk_edit() function as a new method for the Bulk_Edit class.

add_action( 'wp_ajax_wz_tutorials_save_bulk_edit', array( $this, 'save_bulk_edit' ) );

/**
 * Save bulk edit data.
 */
public function save_bulk_edit() {
	// Security check.
	check_ajax_referer( 'wz_tutorials_bulk_edit_nonce', 'wz_tutorials_bulk_edit_nonce' );

	// Get the post IDs.
	$post_ids = isset( $_POST['post_ids'] ) ? wp_parse_id_list( wp_unslash( $_POST['post_ids'] ) ) : array();

	// Get the related posts. If the field is set to 0, then clear the related posts.
	if ( isset( $_POST['related_posts'] ) ) {
		$related_posts_array = wp_parse_id_list( wp_unslash( $_POST['related_posts'] ) );

		if ( ! empty( $related_posts_array ) ) {
			if ( 1 === count( $related_posts_array ) && 0 === $related_posts_array[0] ) {
				$related_posts = 0;
			} else {
				// Remove any posts that are not published.
				foreach ( $related_posts_array as $key => $value ) {
					if ( 'publish' !== get_post_status( $value ) ) {
						unset( $related_posts_array[ $key ] );
					}
				}
				$related_posts = implode( ',', $related_posts_array );
			}
		}
	}

	// Get the exclude this post value.
	if ( isset( $_POST['exclude_this_post'] ) && -1 !== (int) $_POST['exclude_this_post'] ) {
		$exclude_this_post = intval( wp_unslash( $_POST['exclude_this_post'] ) );
	}

	// Now we can start saving.
	foreach ( $post_ids as $post_id ) {
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			continue;
		}
		if ( isset( $related_posts ) ) {
			( 0 !== $related_posts ) ? update_field( 'related_posts', $related_posts, $post_id ) : delete_field( 'related_posts', $post_id );
		}
		if ( isset( $exclude_this_post ) ) {
			$exclude_this_post ? update_field( 'exclude_this_post', $exclude_this_post, $post_id ) : delete_field( 'exclude_this_post', $post_id );
		}
	}

	wp_send_json_success();
}

In the above function, if the related posts field is set to 0, then the ACF field is cleared out. Else we parse the field, check if the posts are published and then save it. It’s easier with the Exclude this post field as it is a true/false field. If set to true, we save the ACF field. If false, we delete the field.

View the demo / Get the code

You can view the above code in action in our demo site created using InstaWP. Click the button below to spin up a new instance and check out the post listings. You can also sign up and get a 30-day free trial of the Professional Plan using our affiliate link below.

References and footnotes

  1. manage_post_type_posts_columns filter ↩︎
  2. manage_{$post->post_type}_posts_custom_column action ↩︎

Leave a Reply

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