How to add a custom field to quick edit

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.