3

I have some code that allows me to add custom fields to the quick edit and bulk edit sections of a custom post type. Here's what I'm using: https://github.com/bamadesigner/manage-wordpress-posts-using-bulk-edit-and-quick-edit

Everything is working great except I can't get it to save from Quick Edit. Bulk Edit works and saves just fine, it's just Quick Edit that won't save any changes. Below is what I believe to be the relevant code. Can anyone help by telling me what I'm doing wrong?

Updated code below in its entirety.

PHP for Quick Edit Plugin

<?php

/**
 * Plugin Name: Manage WordPress Posts Using Bulk Edit and Quick Edit
 * Description: This is the code for a tutorial WP Dreamer wrote about managing WordPress posts using bulk and quick edit.
 * Author: WP Dreamer
 * Author URI: http://wpdreamer.com/2012/03/manage-wordpress-posts-using-bulk-edit-and-quick-edit/
 */

/**
 * I decided to convert the tutorial to a plugin format
 * so I could easily monitor it on my development environment
 * and store it on GitHub.
 *
 * With that said, you could easily take this code and paste it
 * into your theme's functions.php file. There is, however,
 * an included javascript file so be sure to check the
 * manage_wp_posts_be_qe_enqueue_admin_scripts()
 * function to confirm you're enqueueing the right javascript file.
 *
 * Also, after a few requests for custom field examples other than
 * text boxes, I updated the tutorial to include a select dropdown
 * and a radio button.
 *
 * Custom Fields:
 * 'Release Date - input text
 * 'Coming Soon' - input radio
 * 'Film Rating' - select dropdown
 *
 * If you find any issues with the tutorial, or code, please let me know. Thanks!
 */

/**
 * Since Bulk Edit and Quick Edit hooks are triggered by custom columns,
 * you must first add custom columns for the fields you wish to add, which are setup by
 * 'filtering' the column information.
 *
 * There are 3 different column filters: 'manage_pages_columns' for pages,
 * 'manage_posts_columns' which covers ALL post types (including custom post types),
 * and 'manage_{$post_type_name}_posts_columns' which only covers, you guessed it,
 * the columns for the defined $post_type_name.
 *
 * The 'manage_pages_columns' and 'manage_{$post_type_name}_posts_columns' filters only
 * pass $columns (an array), which is the column info, as an argument, but 'manage_posts_columns'
 * passes $columns and $post_type (a string).
 *
 * Note: Don't forget that it's a WordPress filter so you HAVE to return the first argument that's
 * passed to the function, in this case $columns. And for filters that pass more than 1 argument,
 * you have to specify the number of accepted arguments in your add_filter() declaration,
 * following the priority argument.
 *
 */
add_filter( 'manage_posts_columns', 'manage_wp_posts_be_qe_manage_posts_columns', 10, 2 );
function manage_wp_posts_be_qe_manage_posts_columns( $columns, $post_type ) {

    /**
     * The first example adds our new columns at the end.
     * Notice that we're specifying a post type because our function covers ALL post types.
     *
     * Uncomment this code if you want to add your column at the end
     */
    if ( $post_type == 'products' ) {
        $columns[ 'field_products_sale' ] = 'Sale Icon';
        $columns[ 'field_products_description_under_button' ] = 'Sale Description';
    }

    return $columns;

    /**
     * The second example adds our new column after the �Title� column.
     * Notice that we're specifying a post type because our function covers ALL post types.
     */
    switch ( $post_type ) {

        case 'products':

            // building a new array of column data
            $new_columns = array();

            foreach( $columns as $key => $value ) {

                // default-ly add every original column
                $new_columns[ $key ] = $value;

                /**
                 * If currently adding the title column,
                 * follow immediately with our custom columns.
                 */
                if ( $key == 'title' ) {
                    $new_columns[ 'field_products_sale' ] = 'Sale Icone';
                    $new_columns[ 'field_products_description_under_button' ] = 'Description';
                }

            }

            return $new_columns;

    }

    return $columns;

}

/**
 * The following filter allows you to make your column(s) sortable.
 *
 * The 'edit-products' section of the filter name is the custom part
 * of the filter name, which tells WordPress you want this to run
 * on the main 'products' custom post type edit screen. So, e.g., if
 * your custom post type's name was 'books', then the filter name
 * would be 'manage_edit-books_sortable_columns'.
 *
 * Don't forget that filters must ALWAYS return a value.
 */
add_filter( 'manage_edit-products_sortable_columns', 'manage_wp_posts_be_qe_manage_sortable_columns' );
function manage_wp_posts_be_qe_manage_sortable_columns( $sortable_columns ) {

    /**
     * In order to make a column sortable, add the
     * column data to the $sortable_columns array.
     *
     * I want to make my 'Release Date' column
     * sortable so the array indexes (the 'field_products_sale_column'
     * value between the []) need to match from
     * where we added the column in the
     * manage_wp_posts_be_qe_manage_posts_columns()
     * function.
     *
     * The array value (after the =) should be set to
     * identify the data that is going to be sorted,
     * i.e. what will be placed in the URL when it's sorted.
     * Since my release date is a custom field, I just
     * use the custom field name, 'field_products_sale'.
     *
     * When the column is clicked, the URL will look like this:
     * http://mywebsite.com/wp-admin/edit.php?post_type=products&orderby=field_products_sale&order=asc
     */
    $sortable_columns[ 'field_products_sale' ] = 'field_products_sale';
    $sortable_columns[ 'field_products_description_under_button' ] = 'field_products_description_under_button';

    return $sortable_columns;

}

/**
 * Now that we have a column, we need to fill our column with data.
 * The filters to populate your custom column are pretty similar to the ones
 * that added your column: 'manage_pages_custom_column', 'manage_posts_custom_column',
 * and 'manage_{$post_type_name}_posts_custom_column'. All three pass the same
 * 2 arguments: $column_name (a string) and the $post_id (an integer).
 *
 * Our custom column data is post meta so it will be a pretty simple case of retrieving
 * the post meta with the meta key 'field_products_sale'.
 *
 * Note that we are wrapping our post meta in a div with an id of �field_products_sale-� plus the post id.
 * This will come in handy when we are populating our �Quick Edit� row.
 */
add_action( 'manage_posts_custom_column', 'manage_wp_posts_be_qe_manage_posts_custom_column', 10, 2 );
function manage_wp_posts_be_qe_manage_posts_custom_column( $column_name, $post_id ) {

    switch( $column_name ) {

        case 'field_products_sale':

            echo '<span class="produce-sale ' . get_post_meta( $post_id, 'products_sale', true ) . '"></span><div id="field_products_sale-' . $post_id . '">' . get_post_meta( $post_id, 'products_sale', true ) . '</div>';
            break;

        case 'field_products_description_under_button':

            echo '<div id="products_description_under_button-' . $post_id . '">' . get_post_meta( $post_id, 'products_description_under_button', true ) . '</div>';
            break;

    }

}

/**
 * Just because we've made the column sortable doesn't
 * mean the posts will sort by our column data. That's where
 * this next 2 filters come into play.
 *
 * If your sort data is simple, i.e. alphabetically or numerically,
 * then 'pre_get_posts' is the filter to use. This filter lets you
 * change up the query before it's run.
 *
 * If your orderby data is more complicated, like our release date
 * which is a date string stored in a custom field, then check out
 * the 'posts_clauses' filter example used below.
 *
 * In the example below, when the main query is trying to order by
 * the 'film_rating', it's a simple alphabetical sorting by a custom
 * field so we're telling the query to set our 'meta_key' which is
 * 'film_rating' and that we want to order by the query by the
 * custom field's meta_value, e.g. PG, PG-13, R, etc.
 *
 * Check out http://codex.wordpress.org/Class_Reference/WP_Query
 * for more info on WP Query parameters.
 */
add_action( 'pre_get_posts', 'manage_wp_posts_be_qe_pre_get_posts', 1 );
function manage_wp_posts_be_qe_pre_get_posts( $query ) {

    /**
     * We only want our code to run in the main WP query
     * AND if an orderby query variable is designated.
     */
    if ( $query->is_main_query() && ( $orderby = $query->get( 'orderby' ) ) ) {

        switch( $orderby ) {

            // If we're ordering by 'film_rating'
            case 'field_products_sale':

                // set our query's meta_key, which is used for custom fields
                $query->set( 'meta_key', 'products_sale' );

                /**
                 * Tell the query to order by our custom field/meta_key's
                 * value, in this case: PG, PG-13, R, etc.
                 *
                 * If your meta value are numbers, change
                 * 'meta_value' to 'meta_value_num'.
                 */
                $query->set( 'orderby', 'meta_value' );

                break;

            // If we're ordering by 'film_rating'
            case 'field_products_description_under_button':

                // set our query's meta_key, which is used for custom fields
                $query->set( 'meta_key', 'products_description_under_button' );

                /**
                 * Tell the query to order by our custom field/meta_key's
                 * value, in this case: PG, PG-13, R, etc.
                 *
                 * If your meta value are numbers, change
                 * 'meta_value' to 'meta_value_num'.
                 */
                $query->set( 'orderby', 'meta_value' );

                break;

        }

    }

}

/**
 * Just because we've made the column sortable doesn't
 * mean the posts will sort by our column data. That's where
 * the filter above, 'pre_get_posts', and the filter below,
 * 'posts_clauses', come into play.
 *
 * If your sort data is simple, i.e. alphabetically or numerically,
 * then check out the 'pre_get_posts' filter used above.
 *
 * If your orderby data is more complicated, like combining
 * several values or a date string stored in a custom field,
 * then the 'posts_clauses' filter used below is for you.
 * The 'posts_clauses' filter allows you to manually tweak
 * the query clauses in order to sort the posts by your
 * custom column data.
 *
 * The reason more complicated sorts will not with the
 * "out of the box" WP Query is because the WP Query orderby
 * parameter will only order alphabetically and numerically.
 *
 * Usually I would recommend simply using the 'pre_get_posts'
 * and altering the WP Query itself but because our custom
 * field is a date, we have to manually set the query to
 * order our posts by a date.
 */
add_filter( 'posts_clauses', 'manage_wp_posts_be_qe_posts_clauses', 1, 2 );
function manage_wp_posts_be_qe_posts_clauses( $pieces, $query ) {
    global $wpdb;

    /**
     * We only want our code to run in the main WP query
     * AND if an orderby query variable is designated.
     */
    if ( $query->is_main_query() && ( $orderby = $query->get( 'orderby' ) ) ) {

        // Get the order query variable - ASC or DESC
        $order = strtoupper( $query->get( 'order' ) );

        // Make sure the order setting qualifies. If not, set default as ASC
        if ( ! in_array( $order, array( 'ASC', 'DESC' ) ) )
            $order = 'ASC';

        switch( $orderby ) {

            // If we're ordering by field_products_sale
            case 'field_products_sale':

                /**
                 * We have to join the postmeta table to include
                 * our release date in the query.
                 */
                $pieces[ 'join' ] .= " LEFT JOIN $wpdb->postmeta wp_rd ON wp_rd.post_id = {$wpdb->posts}.ID AND wp_rd.meta_key = 'field_products_sale'";

                // Then tell the query to order by our date
                $pieces[ 'orderby' ] = "STR_TO_DATE( wp_rd.meta_value,'%m/%d/%Y' ) $order, " . $pieces[ 'orderby' ];

                break;

            // If we're ordering by field_products_sale
            case 'field_products_description_under_button':

                /**
                 * We have to join the postmeta table to include
                 * our release date in the query.
                 */
                $pieces[ 'join' ] .= " LEFT JOIN $wpdb->postmeta wp_rd ON wp_rd.post_id = {$wpdb->posts}.ID AND wp_rd.meta_key = 'field_products_description_under_button'";

                // Then tell the query to order by our date
                $pieces[ 'orderby' ] = "STR_TO_DATE( wp_rd.meta_value,'%m/%d/%Y' ) $order, " . $pieces[ 'orderby' ];

                break;

        }

    }

    return $pieces;

}

/**
 * Now that you have your custom column, it's bulk/quick edit showtime!
 * The filters are 'bulk_edit_custom_box' and 'quick_edit_custom_box'. Both filters
 * pass the same 2 arguments: the $column_name (a string) and the $post_type (a string).
 *
 * Your data's form fields will obviously vary so customize at will. For this example,
 * we're using an input. Also take note of the css classes on the <fieldset> and <div>.
 * There are a few other options like 'inline-edit-col-left' and 'inline-edit-col-center'
 * for the fieldset and 'inline-edit-col' for the div. I recommend studying the WordPress
 * bulk and quick edit HTML to see the best way to layout your custom fields.
 */
 add_action( 'bulk_edit_custom_box', 'manage_wp_posts_be_qe_bulk_quick_edit_custom_box', 10, 2 );
 add_action( 'quick_edit_custom_box', 'manage_wp_posts_be_qe_bulk_quick_edit_custom_box', 10, 2 );
 function manage_wp_posts_be_qe_bulk_quick_edit_custom_box( $column_name, $post_type ) {

    switch ( $post_type ) {

        case 'products':

            switch( $column_name ) {

                case 'field_products_sale':

                    ?><fieldset class="inline-edit-col-left">
                        <div class="inline-edit-col">
                            <label>
                                <span class="title">Products Sale</span>
                                <span class="input-text-wrap">
                                    <input type="text" value="" name="field_products_sale">
                                </span>
                            </label>
                        </div>
                    </fieldset><?php
                    break;

                case 'field_products_description_under_button':

                    ?><fieldset class="inline-edit-col-left">
                        <div class="inline-edit-col">
                            <label>
                                <span class="title">Sale Description</span>
                                <span class="input-text-wrap">
                                    <input type="text" value="" name="field_products_description_under_button">
                                </span>
                            </label>
                        </div>
                    </fieldset><?php
                    break;

            }

            break;

    }

 }

/**
 * When you click 'Quick Edit', you may have noticed that your form fields are not populated.
 * WordPress adds one 'Quick Edit' row which moves around for each post so the information cannot
 * be pre-populated. It has to be populated with JavaScript on a per-post 'click Quick Edit' basis.
 *
 * WordPress has an inline edit post function that populates all of their default quick edit fields
 * so we want to hook into this function, in a sense, to make sure our JavaScript code is run when
 * needed. We will 'copy' the WP function, 'overwrite' the WP function so we're hooked in, 'call'
 * the original WP function (via our copy) so WordPress is not left hanging, and then run our code.
 *
 * Remember where we wrapped our column data in a <div> in Step 2? This is where it comes in handy,
 * allowing our Javascript to retrieve the data by the <div>'s element ID to populate our form field.
 * There are other methods to retrieve your data that involve AJAX but this route is the simplest.
 *
 * Don't forget to enqueue your script and make sure it's dependent on WordPress's 'inline-edit-post' file.
 * Since we'll be using the jQuery library, we need to make sure 'jquery' is loaded as well.
 *
 * I have provided several scenarios for where you've placed this code. Simply uncomment the scenario
 * you're using. For all scenarios, make sure your javascript file is in the same folder as your code.
 */
add_action( 'admin_print_scripts-edit.php', 'manage_wp_posts_be_qe_enqueue_admin_scripts' );
function manage_wp_posts_be_qe_enqueue_admin_scripts() {

    // if code is in theme functions.php file
    //wp_enqueue_script( 'manage-wp-posts-using-bulk-quick-edit', trailingslashit( get_bloginfo( 'stylesheet_directory' ) ) . 'bulk_quick_edit.js', array( 'jquery', 'inline-edit-post' ), '', true );

    // if using code as plugin
    wp_enqueue_script( 'manage-wp-posts-using-bulk-quick-edit', trailingslashit( plugin_dir_url( __FILE__ ) ) . 'bulk_quick_edit.js', array( 'jquery', 'inline-edit-post' ), '', true );

}

/**
 * Saving your 'Quick Edit' data is exactly like saving custom data
 * when editing a post, using the 'save_post' hook. With that said,
 * you may have already set this up. If you're not sure, and your
 * 'Quick Edit' data is not saving, odds are you need to hook into
 * the 'save_post' action.
 *
 * The 'save_post' action passes 2 arguments: the $post_id (an integer)
 * and the $post information (an object).
 */
add_action( 'save_post', 'save_quick_edit_custom_fields', 10, 2 );
function save_quick_edit_custom_fields( $post_id, $post ) {

    // pointless if $_POST is empty (this happens on bulk edit)
    if ( empty( $_POST ) )
        return $post_id;

    // verify quick edit nonce
    if ( isset( $_POST[ '_inline_edit' ] ) && ! wp_verify_nonce( $_POST[ '_inline_edit' ], 'inlineeditnonce' ) )
        return $post_id;

    // don't save for autosave
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
        return $post_id;

    // dont save for revisions
    if ( isset( $post->post_type ) && $post->post_type == 'revision' )
        return $post_id;

    switch( $post->post_type ) {

        case 'products':

            /**
             * Because this action is run in several places, checking for the array key
             * keeps WordPress from editing data that wasn't in the form, i.e. if you had
             * this post meta on your "Quick Edit" but didn't have it on the "Edit Post" screen.
             */
            $custom_fields = array( 'products_sale', 'products_description_under_button' );

            foreach( $custom_fields as $field ) {

                if ( array_key_exists( $field, $_POST ) )
                    update_post_meta( $post_id, $field, $_POST[ $field ] );

            }

            break;

    }

}

/**
 * Saving the 'Bulk Edit' data is a little trickier because we have
 * to get JavaScript involved. WordPress saves their bulk edit data
 * via AJAX so, guess what, so do we.
 *
 * Your javascript will run an AJAX function to save your data.
 * This is the WordPress AJAX function that will handle and save your data.
 */
add_action( 'wp_ajax_manage_wp_posts_using_bulk_quick_save_bulk_edit', 'manage_wp_posts_using_bulk_quick_save_bulk_edit' );
function manage_wp_posts_using_bulk_quick_save_bulk_edit() {

    // we need the post IDs
    $post_ids = ( isset( $_POST[ 'post_ids' ] ) && !empty( $_POST[ 'post_ids' ] ) ) ? $_POST[ 'post_ids' ] : NULL;

    // if we have post IDs
    if ( ! empty( $post_ids ) && is_array( $post_ids ) ) {

        // get the custom fields
        $custom_fields = array( 'products_sale', 'products_description_under_button' );

        foreach( $custom_fields as $field ) {

            // if it has a value, doesn't update if empty on bulk
            if ( isset( $_POST[ $field ] ) && !empty( $_POST[ $field ] ) ) {

                // update for each post ID
                foreach( $post_ids as $post_id ) {
                    update_post_meta( $post_id, $field, $_POST[ $field ] );
                }

            }

        }

    }

}

?>

JavaScript for Quick Edit Plugin

(function($) {

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

    // and then we overwrite the function with our own code
    inlineEditPost.edit = function( 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
        var $post_id = 0;
        if ( typeof( id ) == 'object' )
            $post_id = parseInt( this.getId( id ) );

        if ( $post_id > 0 ) {

            // define the edit row
            var $edit_row = $( '#edit-' + $post_id );

            // get the release date
            var $products_sale = $( '#field_products_sale-' + $post_id ).text();
            var $products_description_under_button = $( '#products_description_under_button-' + $post_id ).text();

            // set the release date
            $edit_row.find( 'input[name="field_products_sale"]' ).val( $products_sale );
            $edit_row.find( 'input[name="field_products_description_under_button"]' ).val( $products_description_under_button );

        }

    };

    $( '#bulk_edit' ).live( 'click', function() {

        // define the bulk edit row
        var $bulk_row = $( '#bulk-edit' );

        // get the selected post ids that are being edited
        var $post_ids = new Array();
        $bulk_row.find( '#bulk-titles' ).children().each( function() {
            $post_ids.push( $( this ).attr( 'id' ).replace( /^(ttle)/i, '' ) );
        });

        // get the custom fields
        var $products_sale = $bulk_row.find( 'input[name="field_products_sale"]' ).val();
        var $products_description_under_button = $bulk_row.find( 'input[name="field_products_description_under_button"]' ).val();

        // 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: 'manage_wp_posts_using_bulk_quick_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
                products_sale: $products_sale,
                products_description_under_button: $products_description_under_button
            }
        });

    });
        console.log("new version 1");

    $( 'button.save' ).live( 'click', function() {

        console.log("uh oh!");

        // define the bulk edit row
        var $quick_edit_row = $( '.inline-edit-row' );

        // get the selected post ids that are being edited
        var $post_ids = $quick_edit_row.attr( 'id' ).replace( /^(edit-)/i, '' );

        // get the custom fields
        var $products_sale = $quick_edit_row.find( 'input[name="field_products_sale"]' ).val();
        var $products_description_under_button = $quick_edit_row.find( 'input[name="field_products_description_under_button"]' ).val();

        // 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: 'manage_wp_posts_using_bulk_quick_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
                products_sale: $products_sale,
                products_description_under_button: $products_description_under_button
            }
        });

    });

})(jQuery);

PHP for my Custom Post Type

    <?php
    add_action( 'init', 'grav_custom_post_type_products' );
    function grav_custom_post_type_products()
    {
        $single_label = 'Product';
        $plural_label = $single_label.'s';
        $name = strtolower(str_replace(' ', '-', $plural_label));
        $slug = $name;

        register_post_type($name, array(
          'label' => $plural_label,
          'description' => '',
          'public' => true,
          'publicly_queryable'  => true,
          'show_ui' => true,
          'show_in_menu' => true,
          'capability_type' => 'post',
          'map_meta_cap' => true,
          'hierarchical' => false,
          'rewrite' => array('with_front' => false, 'slug' => $slug),
          'query_var' => true,
          'exclude_from_search' => false,
          'can_export'          => true,
          'has_archive'         => true,
          'menu_icon'           => 'dashicons-star-empty',
          'supports' => array('title','editor','excerpt','thumbnail'),
          'labels' => array (
              'name' => $plural_label,
              'singular_name' => $single_label,
              'menu_name' => $plural_label,
              'add_new' => 'Add '.$single_label,
              'add_new_item' => 'Add New '.$single_label,
              'edit' => 'Edit',
              'edit_item' => 'Edit '.$single_label,
              'new_item' => 'New '.$single_label,
              'view' => 'View '.$single_label,
              'view_item' => 'View '.$single_label,
              'search_items' => 'Search '.$plural_label,
              'not_found' => 'No '.$plural_label.' Found',
              'not_found_in_trash' => 'No '.$plural_label.' Found in Trash',
              'parent' => 'Parent '.$single_label,
        )));
        register_taxonomy(
            $name.'_categories',
            $name,
            array(
                'labels' => array (
                      'name' => $plural_label.' Categories',
                      'singular_name' => 'Category',
                      'menu_name' => $plural_label.' Categories',
                      'add_new' => 'Add '.$plural_label,
                      'add_new_item' => 'Add New Category',
                      'edit' => 'Edit',
                      'edit_item' => 'Edit Category',
                      'new_item' => 'New Category',
                      'view' => 'View Category',
                      'view_item' => 'View Category',
                      'search_items' => 'Search Categories',
                ),
                'hierarchical' => true,
                'rewrite' => array('with_front' => false, 'slug' => $name.'-categories' )
            )
        );
    }
    ?>

PHP for products_sale Custom Field

array (
                'key' => 'field_products_sale',
                'label' => 'Sale',
                'name' => 'products_sale',
                'type' => 'text',
                'instructions' => '',
                'required' => 0,
                'conditional_logic' => 0,
                'wrapper' => array (
                    'width' => '',
                    'class' => 'icomoon-select',
                    'id' => '',
                ),
                'default_value' => '',
                'placeholder' => '',
                'formatting' => 'none',       // none | html
                'prepend' => '',
                'append' => '',
                'maxlength' => '',
                'readonly' => 0,
                'disabled' => 0,
            )
2
  • I posted a working example of the demo. If that doesn't help, please post your full source code. I'm thinking that your issue may be rooted in portions of the code not included in your original post -- particularly the JS. Sep 6, 2016 at 5:31
  • I've updated my post to include my custom post type code as well as my custom field code.
    – jkupczak
    Sep 6, 2016 at 21:05

1 Answer 1

7
+50

I downloaded and activated the code referenced on GitHub, then added custom post type registration code for movies to match the original example.

I noticed that the code on GitHub had a bug where the switch statement in manage_wp_posts_be_qe_bulk_quick_edit_custom_box() checks for release_date, coming_soon, and film_rating instead of release_date_column, coming_soon_column, and film_rating_column, so I fixed those issues. After that, the example worked for me; both quick edit and bulk edit save the values expected.

enter image description here

I pasted the tweaked PHP as well as the JS from the example below.

<?php

/**
 * Plugin Name: Manage WordPress Posts Using Bulk Edit and Quick Edit
 * Description: This is the code for a tutorial WP Dreamer wrote about managing WordPress posts using bulk and quick edit.
 * Author: WP Dreamer
 * Author URI: http://wpdreamer.com/2012/03/manage-wordpress-posts-using-bulk-edit-and-quick-edit/
 */

/**
 * I decided to convert the tutorial to a plugin format
 * so I could easily monitor it on my development environment
 * and store it on GitHub.
 *
 * With that said, you could easily take this code and paste it
 * into your theme's functions.php file. There is, however,
 * an included javascript file so be sure to check the
 * manage_wp_posts_be_qe_enqueue_admin_scripts()
 * function to confirm you're enqueueing the right javascript file.
 *
 * Also, after a few requests for custom field examples other than
 * text boxes, I updated the tutorial to include a select dropdown
 * and a radio button.
 *
 * Custom Fields:
 * 'Release Date - input text
 * 'Coming Soon' - input radio
 * 'Film Rating' - select dropdown
 *
 * If you find any issues with the tutorial, or code, please let me know. Thanks!
 */

/**
 * Since Bulk Edit and Quick Edit hooks are triggered by custom columns,
 * you must first add custom columns for the fields you wish to add, which are setup by
 * 'filtering' the column information.
 *
 * There are 3 different column filters: 'manage_pages_columns' for pages,
 * 'manage_posts_columns' which covers ALL post types (including custom post types),
 * and 'manage_{$post_type_name}_posts_columns' which only covers, you guessed it,
 * the columns for the defined $post_type_name.
 *
 * The 'manage_pages_columns' and 'manage_{$post_type_name}_posts_columns' filters only
 * pass $columns (an array), which is the column info, as an argument, but 'manage_posts_columns'
 * passes $columns and $post_type (a string).
 *
 * Note: Don't forget that it's a WordPress filter so you HAVE to return the first argument that's
 * passed to the function, in this case $columns. And for filters that pass more than 1 argument,
 * you have to specify the number of accepted arguments in your add_filter() declaration,
 * following the priority argument.
 *
 */
add_filter( 'manage_posts_columns', 'manage_wp_posts_be_qe_manage_posts_columns', 10, 2 );
function manage_wp_posts_be_qe_manage_posts_columns( $columns, $post_type ) {

    /**
     * The first example adds our new columns at the end.
     * Notice that we're specifying a post type because our function covers ALL post types.
     *
     * Uncomment this code if you want to add your column at the end
     */
    /*if ( $post_type == 'movies' ) {
        $columns[ 'release_date' ] = 'Release Date';
        $columns[ 'coming_soon' ] = 'Coming Soon';
        $columns[ 'film_rating' ] = 'Film Rating';
    }

    return $columns;*/

    /**
     * The second example adds our new column after the ÒTitleÓ column.
     * Notice that we're specifying a post type because our function covers ALL post types.
     */
    switch ( $post_type ) {

        case 'movies':

            // building a new array of column data
            $new_columns = array();

            foreach( $columns as $key => $value ) {

                // default-ly add every original column
                $new_columns[ $key ] = $value;

                /**
                 * If currently adding the title column,
                 * follow immediately with our custom columns.
                 */
                if ( $key == 'title' ) {
                    $new_columns[ 'release_date_column' ] = 'Release Date';
                    $new_columns[ 'coming_soon_column' ] = 'Coming Soon';
                    $new_columns[ 'film_rating_column' ] = 'Film Rating';
                }

            }

            return $new_columns;

    }

    return $columns;

}

/**
 * The following filter allows you to make your column(s) sortable.
 *
 * The 'edit-movies' section of the filter name is the custom part
 * of the filter name, which tells WordPress you want this to run
 * on the main 'movies' custom post type edit screen. So, e.g., if
 * your custom post type's name was 'books', then the filter name
 * would be 'manage_edit-books_sortable_columns'.
 *
 * Don't forget that filters must ALWAYS return a value.
 */
add_filter( 'manage_edit-movies_sortable_columns', 'manage_wp_posts_be_qe_manage_sortable_columns' );
function manage_wp_posts_be_qe_manage_sortable_columns( $sortable_columns ) {

    /**
     * In order to make a column sortable, add the
     * column data to the $sortable_columns array.
     *
     * I want to make my 'Release Date' column
     * sortable so the array indexes (the 'release_date_column'
     * value between the []) need to match from
     * where we added the column in the
     * manage_wp_posts_be_qe_manage_posts_columns()
     * function.
     *
     * The array value (after the =) should be set to
     * identify the data that is going to be sorted,
     * i.e. what will be placed in the URL when it's sorted.
     * Since my release date is a custom field, I just
     * use the custom field name, 'release_date'.
     *
     * When the column is clicked, the URL will look like this:
     * http://mywebsite.com/wp-admin/edit.php?post_type=movies&orderby=release_date&order=asc
     */
    $sortable_columns[ 'release_date_column' ] = 'release_date';

    // Let's also make the film rating column sortable
    $sortable_columns[ 'film_rating_column' ] = 'film_rating';

    return $sortable_columns;

}

/**
 * Now that we have a column, we need to fill our column with data.
 * The filters to populate your custom column are pretty similar to the ones
 * that added your column: 'manage_pages_custom_column', 'manage_posts_custom_column',
 * and 'manage_{$post_type_name}_posts_custom_column'. All three pass the same
 * 2 arguments: $column_name (a string) and the $post_id (an integer).
 *
 * Our custom column data is post meta so it will be a pretty simple case of retrieving
 * the post meta with the meta key 'release_date'.
 *
 * Note that we are wrapping our post meta in a div with an id of Òrelease_date-Ó plus the post id.
 * This will come in handy when we are populating our ÒQuick EditÓ row.
 */
add_action( 'manage_posts_custom_column', 'manage_wp_posts_be_qe_manage_posts_custom_column', 10, 2 );
function manage_wp_posts_be_qe_manage_posts_custom_column( $column_name, $post_id ) {

    switch( $column_name ) {

        case 'release_date_column':

            echo '<div id="release_date-' . $post_id . '">' . get_post_meta( $post_id, 'release_date', true ) . '</div>';
            break;

        case 'coming_soon_column':

            echo '<div id="coming_soon-' . $post_id . '">' . get_post_meta( $post_id, 'coming_soon', true ) . '</div>';
            break;

        case 'film_rating_column':

            echo '<div id="film_rating-' . $post_id . '">' . get_post_meta( $post_id, 'film_rating', true ) . '</div>';
            break;

    }

}

/**
 * Just because we've made the column sortable doesn't
 * mean the posts will sort by our column data. That's where
 * this next 2 filters come into play.
 * 
 * If your sort data is simple, i.e. alphabetically or numerically,
 * then 'pre_get_posts' is the filter to use. This filter lets you
 * change up the query before it's run.
 *
 * If your orderby data is more complicated, like our release date
 * which is a date string stored in a custom field, then check out
 * the 'posts_clauses' filter example used below.
 *
 * In the example below, when the main query is trying to order by
 * the 'film_rating', it's a simple alphabetical sorting by a custom
 * field so we're telling the query to set our 'meta_key' which is
 * 'film_rating' and that we want to order by the query by the
 * custom field's meta_value, e.g. PG, PG-13, R, etc.
 *
 * Check out http://codex.wordpress.org/Class_Reference/WP_Query
 * for more info on WP Query parameters.
 */
add_action( 'pre_get_posts', 'manage_wp_posts_be_qe_pre_get_posts', 1 );
function manage_wp_posts_be_qe_pre_get_posts( $query ) {

    /**
     * We only want our code to run in the main WP query
     * AND if an orderby query variable is designated.
     */
    if ( $query->is_main_query() && ( $orderby = $query->get( 'orderby' ) ) ) {

        switch( $orderby ) {

            // If we're ordering by 'film_rating'
            case 'film_rating':

                // set our query's meta_key, which is used for custom fields
                $query->set( 'meta_key', 'film_rating' );

                /**
                 * Tell the query to order by our custom field/meta_key's
                 * value, in this case: PG, PG-13, R, etc.
                 *
                 * If your meta value are numbers, change
                 * 'meta_value' to 'meta_value_num'.
                 */
                $query->set( 'orderby', 'meta_value' );

                break;

        }

    }

}

/**
 * Just because we've made the column sortable doesn't
 * mean the posts will sort by our column data. That's where
 * the filter above, 'pre_get_posts', and the filter below,
 * 'posts_clauses', come into play.
 *
 * If your sort data is simple, i.e. alphabetically or numerically,
 * then check out the 'pre_get_posts' filter used above.
 *
 * If your orderby data is more complicated, like combining
 * several values or a date string stored in a custom field,
 * then the 'posts_clauses' filter used below is for you.
 * The 'posts_clauses' filter allows you to manually tweak
 * the query clauses in order to sort the posts by your
 * custom column data.
 *
 * The reason more complicated sorts will not with the
 * "out of the box" WP Query is because the WP Query orderby
 * parameter will only order alphabetically and numerically.
 *
 * Usually I would recommend simply using the 'pre_get_posts'
 * and altering the WP Query itself but because our custom
 * field is a date, we have to manually set the query to
 * order our posts by a date.
 */
add_filter( 'posts_clauses', 'manage_wp_posts_be_qe_posts_clauses', 1, 2 );
function manage_wp_posts_be_qe_posts_clauses( $pieces, $query ) {
    global $wpdb;

    /**
     * We only want our code to run in the main WP query
     * AND if an orderby query variable is designated.
     */
    if ( $query->is_main_query() && ( $orderby = $query->get( 'orderby' ) ) ) {

        // Get the order query variable - ASC or DESC
        $order = strtoupper( $query->get( 'order' ) );

        // Make sure the order setting qualifies. If not, set default as ASC
        if ( ! in_array( $order, array( 'ASC', 'DESC' ) ) )
            $order = 'ASC';

        switch( $orderby ) {

            // If we're ordering by release_date
            case 'release_date':

                /**
                 * We have to join the postmeta table to include
                 * our release date in the query.
                 */
                $pieces[ 'join' ] .= " LEFT JOIN $wpdb->postmeta wp_rd ON wp_rd.post_id = {$wpdb->posts}.ID AND wp_rd.meta_key = 'release_date'";

                // Then tell the query to order by our date
                $pieces[ 'orderby' ] = "STR_TO_DATE( wp_rd.meta_value,'%m/%d/%Y' ) $order, " . $pieces[ 'orderby' ];

                break;

        }

    }

    return $pieces;

}

/**
 * Now that you have your custom column, it's bulk/quick edit showtime!
 * The filters are 'bulk_edit_custom_box' and 'quick_edit_custom_box'. Both filters
 * pass the same 2 arguments: the $column_name (a string) and the $post_type (a string).
 *
 * Your data's form fields will obviously vary so customize at will. For this example,
 * we're using an input. Also take note of the css classes on the <fieldset> and <div>.
 * There are a few other options like 'inline-edit-col-left' and 'inline-edit-col-center'
 * for the fieldset and 'inline-edit-col' for the div. I recommend studying the WordPress
 * bulk and quick edit HTML to see the best way to layout your custom fields.
 */
add_action( 'bulk_edit_custom_box', 'manage_wp_posts_be_qe_bulk_quick_edit_custom_box', 10, 2 );
add_action( 'quick_edit_custom_box', 'manage_wp_posts_be_qe_bulk_quick_edit_custom_box', 10, 2 );
function manage_wp_posts_be_qe_bulk_quick_edit_custom_box( $column_name, $post_type ) {

    switch ( $post_type ) {

        case 'movies':

            switch( $column_name ) {

                case 'release_date_column':
                    ?><fieldset class="inline-edit-col-left">
                        <div class="inline-edit-col">
                            <label>
                                <span class="title">Release Date</span>
                                <span class="input-text-wrap">
                                    <input type="text" value="" name="release_date">
                                </span>
                            </label>
                        </div>
                    </fieldset><?php
                    break;

                case 'coming_soon_column':

                    ?><fieldset class="inline-edit-col-left">
                        <div class="inline-edit-col">
                            <label>
                                <span class="title">Coming Soon</span>
                                <span class="input-text-wrap">
                                    <label style="display:inline;">
                                        <input type="radio" name="coming_soon" value="Yes" /> Yes
                                    </label>&nbsp;&nbsp;
                                    <label style="display:inline;">
                                        <input type="radio" name="coming_soon" value="No" /> No
                                    </label>
                                </span>
                            </label>
                        </div>
                    </fieldset><?php
                    break;

                case 'film_rating_column':

                    ?><fieldset class="inline-edit-col-left">
                        <div class="inline-edit-col">
                            <label>
                                <span class="title">Film rating</span>
                                <span class="input-text-wrap">
                                    <select name="film_rating">
                                        <option value="">Rating</option>
                                        <option value="G">G</option>
                                        <option value="PG">PG</option>
                                        <option value="PG-13">PG-13</option>
                                        <option value="R">R</option>
                                        <option value="NC-17">NC-17</option>
                                        <option value="X">X</option>
                                        <option value="GP">GP</option>
                                        <option value="M">M</option>
                                        <option value="M/PG">M/PG</option>
                                    </select>
                                </span>
                            </label>
                        </div>
                    </fieldset><?php
                    break;

            }

            break;

    }

}

/**
 * When you click 'Quick Edit', you may have noticed that your form fields are not populated.
 * WordPress adds one 'Quick Edit' row which moves around for each post so the information cannot
 * be pre-populated. It has to be populated with JavaScript on a per-post 'click Quick Edit' basis.
 *
 * WordPress has an inline edit post function that populates all of their default quick edit fields
 * so we want to hook into this function, in a sense, to make sure our JavaScript code is run when
 * needed. We will 'copy' the WP function, 'overwrite' the WP function so we're hooked in, 'call'
 * the original WP function (via our copy) so WordPress is not left hanging, and then run our code.
 *
 * Remember where we wrapped our column data in a <div> in Step 2? This is where it comes in handy,
 * allowing our Javascript to retrieve the data by the <div>'s element ID to populate our form field.
 * There are other methods to retrieve your data that involve AJAX but this route is the simplest.
 *
 * Don't forget to enqueue your script and make sure it's dependent on WordPress's 'inline-edit-post' file.
 * Since we'll be using the jQuery library, we need to make sure 'jquery' is loaded as well.
 *
 * I have provided several scenarios for where you've placed this code. Simply uncomment the scenario
 * you're using. For all scenarios, make sure your javascript file is in the same folder as your code.
 */
add_action( 'admin_print_scripts-edit.php', 'manage_wp_posts_be_qe_enqueue_admin_scripts' );
function manage_wp_posts_be_qe_enqueue_admin_scripts() {

    // if code is in theme functions.php file
    //wp_enqueue_script( 'manage-wp-posts-using-bulk-quick-edit', trailingslashit( get_bloginfo( 'stylesheet_directory' ) ) . 'bulk_quick_edit.js', array( 'jquery', 'inline-edit-post' ), '', true );

    // if using code as plugin
    wp_enqueue_script( 'manage-wp-posts-using-bulk-quick-edit', trailingslashit( plugin_dir_url( __FILE__ ) ) . 'bulk_quick_edit.js', array( 'jquery', 'inline-edit-post' ), '', true );

}

/**
 * Saving your 'Quick Edit' data is exactly like saving custom data
 * when editing a post, using the 'save_post' hook. With that said,
 * you may have already set this up. If you're not sure, and your
 * 'Quick Edit' data is not saving, odds are you need to hook into
 * the 'save_post' action.
 *
 * The 'save_post' action passes 2 arguments: the $post_id (an integer)
 * and the $post information (an object).
 */
add_action( 'save_post', 'manage_wp_posts_be_qe_save_post', 10, 2 );
function manage_wp_posts_be_qe_save_post( $post_id, $post ) {

    // pointless if $_POST is empty (this happens on bulk edit)
    if ( empty( $_POST ) )
        return $post_id;

    // verify quick edit nonce
    if ( isset( $_POST[ '_inline_edit' ] ) && ! wp_verify_nonce( $_POST[ '_inline_edit' ], 'inlineeditnonce' ) )
        return $post_id;

    // don't save for autosave
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
        return $post_id;

    // dont save for revisions
    if ( isset( $post->post_type ) && $post->post_type == 'revision' )
        return $post_id;

    switch( $post->post_type ) {

        case 'movies':

            /**
             * Because this action is run in several places, checking for the array key
             * keeps WordPress from editing data that wasn't in the form, i.e. if you had
             * this post meta on your "Quick Edit" but didn't have it on the "Edit Post" screen.
             */
            $custom_fields = array( 'release_date', 'coming_soon', 'film_rating' );

            foreach( $custom_fields as $field ) {

                if ( array_key_exists( $field, $_POST ) )
                    update_post_meta( $post_id, $field, $_POST[ $field ] );

            }

            break;

    }

}

/**
 * Saving the 'Bulk Edit' data is a little trickier because we have
 * to get JavaScript involved. WordPress saves their bulk edit data
 * via AJAX so, guess what, so do we.
 *
 * Your javascript will run an AJAX function to save your data.
 * This is the WordPress AJAX function that will handle and save your data.
 */
add_action( 'wp_ajax_manage_wp_posts_using_bulk_quick_save_bulk_edit', 'manage_wp_posts_using_bulk_quick_save_bulk_edit' );
function manage_wp_posts_using_bulk_quick_save_bulk_edit() {

    // we need the post IDs
    $post_ids = ( isset( $_POST[ 'post_ids' ] ) && !empty( $_POST[ 'post_ids' ] ) ) ? $_POST[ 'post_ids' ] : NULL;

    // if we have post IDs
    if ( ! empty( $post_ids ) && is_array( $post_ids ) ) {

        // get the custom fields
        $custom_fields = array( 'release_date', 'coming_soon', 'film_rating' );

        foreach( $custom_fields as $field ) {

            // if it has a value, doesn't update if empty on bulk
            if ( isset( $_POST[ $field ] ) && !empty( $_POST[ $field ] ) ) {

                // update for each post ID
                foreach( $post_ids as $post_id ) {
                    update_post_meta( $post_id, $field, $_POST[ $field ] );
                }

            }

        }

    }
}


// Register 'movies' cpt for this demo.
add_action( 'init', 'wpse237805_register_movies' );
function wpse237805_register_movies() {
    $labels = array(
        "name" => __( 'Movies', 'textdomain' ),
        "singular_name" => __( 'Movie', 'textdomain' ),
    );

    $args = array(
        "label" => __( 'Movies', 'textdomain' ),
        "labels" => $labels,
        "description" => "",
        "public" => true,
        "publicly_queryable" => true,
        "show_ui" => true,
        "show_in_rest" => false,
        "rest_base" => "",
        "has_archive" => false,
        "show_in_menu" => true,
                "exclude_from_search" => false,
        "capability_type" => "post",
        "map_meta_cap" => true,
        "hierarchical" => false,
        "rewrite" => array( "slug" => "movies", "with_front" => true ),
        "query_var" => true,

        "supports" => array( "title", "editor", "custom-fields" ),
    );

    register_post_type( "movies", $args );
}

bulk_quick_edit.js:

(function($) {

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

    // and then we overwrite the function with our own code
    inlineEditPost.edit = function( 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
        var $post_id = 0;
        if ( typeof( id ) == 'object' )
            $post_id = parseInt( this.getId( id ) );

        if ( $post_id > 0 ) {

            // define the edit row
            var $edit_row = $( '#edit-' + $post_id );

            // get the release date
            var $release_date = $( '#release_date-' + $post_id ).text();

            // set the release date
            $edit_row.find( 'input[name="release_date"]' ).val( $release_date );

            // get the release date
            var $coming_soon = $( '#coming_soon-' + $post_id ).text();

            // set the film rating
            $edit_row.find( 'input[name="coming_soon"][value="' + $coming_soon + '"]' ).prop( 'checked', true );

            // get the film rating
            var $film_rating = $( '#film_rating-' + $post_id ).text();

            // set the film rating
            $edit_row.find( 'select[name="film_rating"]' ).val( $film_rating );

        }

    };

    $( '#bulk_edit' ).live( 'click', function() {

        // define the bulk edit row
        var $bulk_row = $( '#bulk-edit' );

        // get the selected post ids that are being edited
        var $post_ids = new Array();
        $bulk_row.find( '#bulk-titles' ).children().each( function() {
            $post_ids.push( $( this ).attr( 'id' ).replace( /^(ttle)/i, '' ) );
        });

        // get the custom fields
        var $release_date = $bulk_row.find( 'input[name="release_date"]' ).val();
        var $coming_soon = $bulk_row.find( 'input[name="coming_soon"]:checked' ).val();
        var $film_rating = $bulk_row.find( 'select[name="film_rating"]' ).val();

        // 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: 'manage_wp_posts_using_bulk_quick_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
                release_date: $release_date,
                coming_soon: $coming_soon,
                film_rating: $film_rating
            }
        });

    });

})(jQuery);
7
  • Thank you for the extensive reply. Unfortunately this isn't working for me. I tried adding '_column' everywhere you did, but the only result is that the fields disappeared from Quick Edit completely. I've updated my original post with my FULL original code. And here's a link to a pastebin of the modified code based on your suggestion: pastebin.com/kG68Pjz7
    – jkupczak
    Sep 6, 2016 at 20:14
  • I fired up a local fresh install of WordPress and tried your code and it works perfectly for me. I'm not sure yet where my code goes wrong. I've updated my original post with my custom post type info and my custom field info.
    – jkupczak
    Sep 6, 2016 at 21:06
  • I'm all set. Instead of fixing my version I just settled on using yours and modifying it to fit my various fields. Everything works now. Thank you for the help!
    – jkupczak
    Sep 7, 2016 at 1:32
  • myscript.js?ver=1:47 Uncaught TypeError: $(...).live is not a function Apr 8, 2021 at 8:59
  • @ZaheerAbbas This answer contains some older jquery code. I think you can resolve the error you're getting by installing the jQuery Migrate Helper plugin. Even better, update the old code using .live to .on. (Untested) change $( '#bulk_edit' ).live( 'click', function() { to $( 'body' ).on( 'click', '#bulk_edit', function( e ) { Apr 8, 2021 at 13:30

Your Answer

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

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