4

How can I add a new column to quick edit and put a custom field in it? I read this question but one of the links lead to an error 404. I need a new column for my custom meta key, "summary" and post types, "post" and "episode".

3 Answers 3

14

There are a few steps to create the custom quick edit box and custom column

  1. create a custom meta key (assumed that you have 1 already)
  2. add custom admin column title and data (assumed that you want to shows the custom meta key in the column, if not, you may also modify a bit of the logic to accomplish the same effect because the principal is the same)
  3. add custom quick edit box
  4. add save logic
  5. load script to modify original inline-edit-post function in order to support custom meta value
  6. prepare the script file

This example is going to add a quick edit input box in post type Page. The following code is proved to work by putting in functions.php .

1. create a custom meta key (assumed that you have 1 already)

I have prepared a custom meta key remark for post type page

2. add custom admin column title and data

for custom post type, you may use

Add column title

Add column content

The following is an example for post type Page as well as custom post type + default Post as an example for multiple post types setup.

// add custom column title for custom meta value
add_filter('manage_edit-page_columns', 'ws365150_add_custom_columns_title' );
function ws365150_add_custom_columns_title( $columns ) {
    $columns['page_remark'] = 'Remark'; // you may use __() later on for translation support
    
    return $columns;
}
    
// add custom column data with custom meta value
add_action('manage_pages_custom_column', 'ws365150_add_custom_column_data', 10, 2 );
function ws365150_add_custom_column_data( $column_name, $post_id ) {
    switch ( $column_name ) {
        case 'page_remark':
            echo get_post_meta( $post_id, 'remark', true );
            break;
        
        default:
            break;
    }
}

The following is an example for custom post type episode + default Post with meta key summary

// add custom column title for custom meta value
// 'manage_pages_columns' or 'manage_edit-post_columns' both works
add_filter('manage_posts_columns', 'ws365150_add_custom_columns_title_pt', 10, 2 );
function ws365150_add_custom_columns_title_pt( $columns, $post_type ) {
    switch ( $post_type ) {
        case 'post':
        case 'episode':
            $columns['ws365150_summary'] = 'Summary'; // you may use __() later on for translation support
            break;
        
        default:
            
            break;
    }
    
    return $columns;
}
    
// add custom column data with custom meta value for custom post types
add_action('manage_posts_custom_column', 'ws365150_add_custom_column_data_pt', 10, 2 );
function ws365150_add_custom_column_data_pt( $column_name, $post_id ) {
    switch ( $column_name ) {
        case 'ws365150_summary': // specified for this column assigned in the column title
            echo get_post_meta( $post_id, 'summary', true );
            break;
        
        default:
            break;
    }
}

3. add custom quick edit box

// for page
add_action( 'quick_edit_custom_box', 'ws365150_custom_edit_box', 10, 3 );
function ws365150_custom_edit_box( $column_name, $post_type, $taxonomy ) {
    global $post;

    switch ( $post_type ) {
        case 'page':

        if( $column_name === 'page_remark' ): // same column title as defined in previous step
        ?>
                <?php // echo get_post_meta( $post->ID, 'remark', true ); ?>
            <fieldset class="inline-edit-col-right" id="#edit-">
                <div class="inline-edit-col">
                    <label>
                        <span class="title">Remark</span>
                        <span class="input-text-wrap"><input type="text" name="remark" class="inline-edit-menu-order-input" value=""></span>
                    </label>
                </div>
            </fieldset>
            <?php
        endif;
            // echo 'custom page field';
            break;
        
        default:
            break;
    }
}

// for Post + custom post type
add_action( 'quick_edit_custom_box', 'ws365150_custom_edit_box_pt', 10, 3 );
function ws365150_custom_edit_box_pt( $column_name, $post_type, $taxonomy ) {
    global $post;

    switch ( $post_type ) {
        case 'post':
        case 'episode':

        if( $column_name === 'ws365150_summary' ): // same column title as defined in previous step
        ?>
                <?php // echo get_post_meta( $post->ID, 'remark', true ); ?>
            <fieldset class="inline-edit-col-right" id="#edit-">
                <div class="inline-edit-col">
                    <label>
                        <span class="title">Summary</span>
                        <span class="input-text-wrap"><input type="text" name="summary" class="inline-edit-menu-order-input" value=""></span>
                    </label>
                </div>
            </fieldset>
            <?php
        endif;
            // echo 'custom page field';
            break;
        
        default:
            break;
    }
}

4. add save logic

add_action( 'save_post', 'ws365150_update_custom_quickedit_box' );
function ws365150_update_custom_quickedit_box() {
    // any checking logic here, skip and keep it simple for simple illustration purpose (nonce, existing of $_POST['remark'], ajax save and so on
    
    // remark in Page
    if( isset( $_POST ) && isset( $_POST['remark'] ) ) { // where remark is defined in the <input name="remark">
        update_post_meta($_POST['post_ID'], 'remark', $_POST['remark']);
    }

    // summary in Post, custom post type
    if( isset( $_POST ) && isset( $_POST['summary'] ) ) { // where summary is defined in the <input name="summary">
        update_post_meta($_POST['post_ID'], 'summary', $_POST['summary']);
    }
    
    // for debugging purpose in inspector, not necessary, enable this will break the saving but could see the ajax return
    // wp_send_json_success( array(
    //  'message' => 'Save test!',
    //  'post_data' => $_POST,
    // ) );
    return; // finish the function call
}

5. load script to modify original inline-edit-post function

add_action( 'admin_enqueue_scripts', function( $page ) {

    // add page checking logic, this is a simple one, you may test post type and so on...
    if ( 'edit.php' != $page ) {
        return;
    }
    
    wp_enqueue_script( 'custom-quickedit-box', get_stylesheet_directory_uri() . '/ws365150_custom_quickedit_box.js', array( 'jquery', 'inline-edit-post' ) );
});

6. prepare the script file (ws365150_custom_quickedit_box.js, when test the above code, put in the theme folder)

( function( $, wp ) {
    // clone from original function in inline-post-edit.js for override
    // actually no need to create an alias object, however, create an alias could be a note and mark of override without forgetting for later maintenance
    window.customInlineEditPost = window.inlineEditPost;

    // function override: add custom meta value, the base is copied from the source
    customInlineEditPost.edit = function(id) {
            // console.log( 'custom edit' );
            var t = this, fields, editRow, rowData, status, pageOpt, pageLevel, nextPage, pageLoop = true, nextLevel, f, val, pw;
            t.revert();

            if ( typeof(id) === 'object' ) {
                id = t.getId(id);
            }

            fields = ['post_title', 'post_name', 'post_author', '_status', 'jj', 'mm', 'aa', 'hh', 'mn', 'ss', 'post_password', 'post_format', 'menu_order', 'page_template'];
            if ( t.type === 'page' ) {
                fields.push('post_parent');
            }

            // Add the new edit row with an extra blank row underneath to maintain zebra striping.
            editRow = $('#inline-edit').clone(true);
            $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '.widefat:first thead' ).length );

            $(t.what+id).removeClass('is-expanded').hide().after(editRow).after('<tr class="hidden"></tr>');

            // Populate fields in the quick edit window.
            rowData = $('#inline_'+id);
            if ( !$(':input[name="post_author"] option[value="' + $('.post_author', rowData).text() + '"]', editRow).val() ) {

                // The post author no longer has edit capabilities, so we need to add them to the list of authors.
                $(':input[name="post_author"]', editRow).prepend('<option value="' + $('.post_author', rowData).text() + '">' + $('#' + t.type + '-' + id + ' .author').text() + '</option>');
            }
            if ( $( ':input[name="post_author"] option', editRow ).length === 1 ) {
                $('label.inline-edit-author', editRow).hide();
            }

            // populate custom meta value
            if ( $( ':input[name="remark"]', editRow ).length === 1 ) {
                $( ':input[name="remark"]', editRow ).val( $('#post-' + id + ' .page_remark').text() );
            }

            if ( $( ':input[name="summary"]', editRow ).length === 1 ) {
                $( ':input[name="summary"]', editRow ).val( $('#post-' + id + ' .ws365150_summary').text() );
            }

            for ( f = 0; f < fields.length; f++ ) {
                val = $('.'+fields[f], rowData);

                /**
                 * Replaces the image for a Twemoji(Twitter emoji) with it's alternate text.
                 *
                 * @returns Alternate text from the image.
                 */
                val.find( 'img' ).replaceWith( function() { return this.alt; } );
                val = val.text();
                $(':input[name="' + fields[f] + '"]', editRow).val( val );
            }

            if ( $( '.comment_status', rowData ).text() === 'open' ) {
                $( 'input[name="comment_status"]', editRow ).prop( 'checked', true );
            }
            if ( $( '.ping_status', rowData ).text() === 'open' ) {
                $( 'input[name="ping_status"]', editRow ).prop( 'checked', true );
            }
            if ( $( '.sticky', rowData ).text() === 'sticky' ) {
                $( 'input[name="sticky"]', editRow ).prop( 'checked', true );
            }

            /**
             * Creates the select boxes for the categories.
             */
            $('.post_category', rowData).each(function(){
                var taxname,
                    term_ids = $(this).text();

                if ( term_ids ) {
                    taxname = $(this).attr('id').replace('_'+id, '');
                    $('ul.'+taxname+'-checklist :checkbox', editRow).val(term_ids.split(','));
                }
            });

            /**
             * Gets all the taxonomies for live auto-fill suggestions when typing the name
             * of a tag.
             */
            $('.tags_input', rowData).each(function(){
                var terms = $(this),
                    taxname = $(this).attr('id').replace('_' + id, ''),
                    textarea = $('textarea.tax_input_' + taxname, editRow),
                    comma = inlineEditL10n.comma;

                terms.find( 'img' ).replaceWith( function() { return this.alt; } );
                terms = terms.text();

                if ( terms ) {
                    if ( ',' !== comma ) {
                        terms = terms.replace(/,/g, comma);
                    }
                    textarea.val(terms);
                }

                textarea.wpTagsSuggest();
            });

            // Handle the post status.
            status = $('._status', rowData).text();
            if ( 'future' !== status ) {
                $('select[name="_status"] option[value="future"]', editRow).remove();
            }

            pw = $( '.inline-edit-password-input' ).prop( 'disabled', false );
            if ( 'private' === status ) {
                $('input[name="keep_private"]', editRow).prop('checked', true);
                pw.val( '' ).prop( 'disabled', true );
            }

            // Remove the current page and children from the parent dropdown.
            pageOpt = $('select[name="post_parent"] option[value="' + id + '"]', editRow);
            if ( pageOpt.length > 0 ) {
                pageLevel = pageOpt[0].className.split('-')[1];
                nextPage = pageOpt;
                while ( pageLoop ) {
                    nextPage = nextPage.next('option');
                    if ( nextPage.length === 0 ) {
                        break;
                    }

                    nextLevel = nextPage[0].className.split('-')[1];

                    if ( nextLevel <= pageLevel ) {
                        pageLoop = false;
                    } else {
                        nextPage.remove();
                        nextPage = pageOpt;
                    }
                }
                pageOpt.remove();
            }

            $(editRow).attr('id', 'edit-'+id).addClass('inline-editor').show();
            $('.ptitle', editRow).focus();

            return false;
    };
})( jQuery, window.wp );

The above is for quick edit using quick_edit_custom_box hook, and you may need to take care of bulk edit also. You may explore more by reading and using bulk_edit_custom_box.

12
  • Great! Could you edit the example for the post types post and episode and the custom meta key summary? Sorry, I do not like fiddling with the functions.php file, I could accidentally wreck WordPress. I have to know that there is a 100% chance the code will function.
    – Gusto
    May 4, 2020 at 22:12
  • I have added additional example code for post, episode with summary meta field with additional notes on column title and checking example when save. You may modify and add additional checking based on the content you needed. May 10, 2020 at 10:06
  • Wait a second, this code is not working and is just making my "All Pages" edit page blank.
    – Gusto
    May 12, 2020 at 20:47
  • The code is updated, please change add_filter('manage_posts_columns', 'ws365150_add_custom_columns_title_pt' ); to add_filter('manage_posts_columns', 'ws365150_add_custom_columns_title_pt', 10, 2 ); Please don't worry to hurry tick the answer before testing it out. May 12, 2020 at 23:15
  • Hm, when testing the above code, the new summary input is added to the post and episode post type's quick edit menu, but after clicking "update", the summary parameter is empty. Even if the summary parameter was already met (without using quick edit), the quick edit input still makes the parameter empty.
    – Gusto
    May 13, 2020 at 21:17
1

i didn't rewrite full js in step 6 i added just

(function($){
    lesson_orders={};
    if($('.post_type_page').length && $('.post_type_page').val()=='resource'){
        $('#the-list tr').each(function(){
            id=$(this).find('.check-column input').val();
            val=$(this).find('.lesson_order').text();
            lesson_orders['edit-'+id]=val;
        });
    }
    $(document).on('focus','.ptitle',function(){
        id =$(this).closest('.inline-edit-row').attr('id');
        $('#'+id+' input[name="lesson_order"]').val(lesson_orders[''+id]);
    }); 
})(jQuery);
0

This is what I've done in 2024 to add the meta field summary to post and episode post types as columns and editable via quick edit.

Add an admin column

First, define the columns with the manage_posts_columns filter.

/**
 * Add Summary Column to Posts and Episodes
 *
 * @param array $columns An associative array of column headings.
 * @param string $post_type The post type slug.
 *
 * @return array A possibly modified array of columns.
 */
function prefix_add_summary_column($columns, $post_type)
{
    if (in_array($post_type, array('post','episode')) {
        return array_merge($columns, array(
            'summary' => __('Summary')
        ));
    }
    return $columns;
}
add_filter('manage_posts_columns', 'prefix_add_summary_column', 10, 2);

Alternatively, you can use add_filter('manage_post_posts_columns', 'prefix_add_summary_column'); and add_filter('manage_episode_posts_columns', 'prefix_add_summary_column'); to target specific post types and remove the if-statement that checks for post type inside the function.

Next, define what is displayed in the content for your summary column using the manage_posts_custom_column action. For this example, it's just outputting the value of the summary meta field.

/**
 * Add Summary Column to Posts and Episodes
 *
 * @param string $column_name The name of the column to display.
 * @param int $post_id The post ID of the current post being displayed.
 *
 * @return void
 */
function prefix_display_summary_column($column_name, $post_id)
{
    if ($column_name === 'summary') {
        echo esc_html(get_post_meta($post_id,'summary',true));
    }
}
add_action('manage_posts_custom_column', 'prefix_display_summary_column', 10, 2);

Once again, you can instead use add_action('manage_post_posts_custom_column', 'prefix_display_summary_column', 10, 2); and add_action('manage_episode_posts_custom_column', 'prefix_display_summary_column', 10, 2); to apply the action to specific post types. The reason there is no if-statement checking for post types is because this column won't be found on any other post types due to the previous step.

Add quick edit field

Now that's enough to render the custom summary column and it's data. Next up is adding in the functionality for the quick edit. The first step here is adding it to the hidden inline data to store it on the frontend for retrieval later using the add_inline_data action like this:

/**
 * Add Summary to Inline Editor Data
 *
 * @param WP_Post $post The current post object.
 *
 * @return void
 */
function prefix_add_summary_inline($post)
{
    if (in_array($post->post_type, array('post','episode')) {
        printf(
            '<div class="summary">%s</div>',
            esc_html(get_post_meta($post->ID,'summary',true))
        );
    }
}
add_action('add_inline_data', 'prefix_add_summary_inline');

As of writing this, there isn't a post type specific action, so the if-statement checking it is needed. I'm using esc_html() here but use whichever escaping function is appropriate for your data.

Now to display the text field in the quick edit editor box using the quick_edit_custom_box action like this:

/**
 * Display Summary Editor Input Field
 *
 * @param string $column_name Name of the column to edit.
 * @param string $post_type The post type slug
 *
 * @return void
 */
function prefix_display_summary_field($column_name, $post_type)
{
    if ($column_name == 'summary' && in_array($post_type, array('post', 'episode')) {
        printf(
            '<fieldset class="inline-edit-col-right">
                <div class="inline-edit-col">
                    <div class="inline-edit-group wp-clearfix">
                        <label class="inline-edit-au-websites alignleft">
                            <span class="title">%s</span>
                            <span class="input-text-wrap">
                                <input type="text" name="%s" autocomplete="off">
                            </span>
                        </label>
                    </div>
                </div>
            </fieldset>',
            esc_html__('Summary'),
            esc_attr($column_name)
        );
    }
}
add_action('quick_edit_custom_box', 'prefix_display_summary_field', 10, 2);

Here we're checking for our custom column name and post type and outputting the HTML that's using similar markup to the other quick edit fields. Feel free to minify the HTML.

And finally, the last piece is to pre-fill the input field with the hidden data! Instead of cloning around with the editor script, here we simply listen for the focus trigger it pushes out when it opens, then we grab our value from the hidden field and set it (but only if our hidden value has any data and the current value of the editor field is empty)!

For this, I hook into the admin_enqueue_scripts action and utilize the wp_add_inline_script function and attach our custom JS to the inline editor script (inline-edit-post) that's already enqueued on the page. This way, our script will always show up after inline-edit-post.min.js is loaded, but only on our specified post types post and episode.

/**
 * Prepopulate Summary Edit Field with Post Value
 *
 * @param string $hook Hook suffix for the current admin page
 *
 * @return void
 */
function prefix_prefill_summary_field($hook) {
    if ($hook == 'edit.php' && in_array(get_post_type(), array('post', 'episode')) {
        wp_add_inline_script(
            'inline-edit-post', // Handle
            '(function($) {
                $(".ptitle").on("focus",function(e){
                    let id = parseInt($(e.target).closest(".quick-edit-row").attr("id").replace("edit-","")),
                        value = $("#inline_"+id+" .summary").text(),
                        $input = $("#edit-"+id+".quick-edit-row input[name=summary]");
                    if(value && !$input.val()) {
                        $input.val(value);
                    }
                });
            })(jQuery);'
        );
    }
}
add_action('admin_enqueue_scripts', 'prefix_prefill_summary_field');

This all works because it's extending what WordPress natively does, how it saves meta data and post data. That's why I don't have to write anything custom for a save_post action.

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.