9

My real problem is a bit complex, so I'll try here to abstract it and keep it simple.

I'm working on a custom app based on WordPress. I registered a custom post type, let's call it "people" where I store information about... people.

The CPT supports only post title and post content default fields, but there are some metaboxes to store person informations (think my app as an address book).

So there is a metabox to store personal info, one to store social networks info, another to store work-related info, i.e. if that person is to me a customer, a supplier, if we have credits or debits...

I simplified here, but there are a consistent amount of metaboxes, let's say 12.

My problem is that, some people for which I want to store info are just random contacts, and I want to store only personal info, other are friends and I want to store personal info and social networks info, other are customers or suppliers and I want to store work-related info.

If when editing a post I hide (via screen option menu) or close any metabox I don't need, when I open another post where I need them I have to show or open them again. That because metaboxes position/status/order are saved on per-user basis as user metadata.

If you imagine in some posts I need 2 metaboxes, in some 10 and in some 5, you understand that's annoying because keeping all them shown/open make the edit screen low accessible (scrollbar seems endless), and sometimes the info I look for is at the end of the page after a bunch of metaboxes with no info...

Question:

Is possible to save metaboxes position/status/order on a per-post basis for a specific post type?


PS: I know some js/jQuery can solve the issue, but if possible I would avoid javascript solutions.

8

The Main Problem:

The main problem here is that in the closing-, hiding- and ordering- ajax calls, there's no post ID sent with the payload. Here are two form data examples:

1) action:closed-postboxes
closed:formatdiv,tagsdiv-post_tag,trackbacksdiv,authordiv
hidden:slugdiv
closedpostboxesnonce:8723ee108f
page:post

2) action:meta-box-order
_ajax_nonce:b6b48d2d16
page_columns:2
page:post
order[side]:submitdiv,formatdiv,categorydiv,tagsdiv-post_tag,postimagediv
order[normal]:postexcerpt,postcustom,trackbacksdiv,commentsdiv,authordiv
order[advanced]:

We could get around this by using another custom ajax call.

We could of course just hook into the save_post hook and modify the data each time the post is saved. But that's not the normal UI experience, so that's not consider here

There's another non-elegant solution available with PHP, described here below:

A Non Javascript Solution:

The question is where to store the data? As user meta data, post meta data or maybe in a custom table?

Here we store it as user meta data and take the closing of post meta boxes as an example.

When the closedpostboxes_post meta value is updated, we save it into the closedpostboxes_post_{post_id} meta value as well.

Then we hijack the fetching of closedpostboxes_post to override it with the corresponding meta value based on user id and post id.

a) Updating during the closed-postboxes ajax action:

We can fetch the post ID, through the wp_get_referer() and then use the handy url_to_postid() function. I first knew about this "funny" function after reading the answer from @s_ha_dum, few months ago ;-) Unfortunately the function doesn't recognize ?post=123 GET variables, but we can do a little trick by just changing it to p=123 to get around it.

We can hook into updated_user_meta, that's fired just after the user meta data for closedpostboxes_post has been updated:

add_action( 'updated_user_meta',                           
    function ( $meta_id, $object_id, $meta_key, $_meta_value )
    {
        $post_id = url_to_postid( str_replace( 'post=', 'p=', wp_get_referer() ) );
        if( 'closedpostboxes_post' === $meta_key && $post_id > 0 )
            update_user_meta( 
                $object_id, 
                'closedpostboxes_post_' . $post_id, 
                $_meta_value 
            );
    }
, 10, 4 );

b) Fetching data:

We can hook into the get_user_option_closedpostboxes_post hook to modify the data fetched from the closedpostboxes_post user meta:

add_filter( 'get_user_option_closedpostboxes_post',
    function ( $result, $option, $user )
    {
        $post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT );
        $newresult = get_user_option( 'closedpostboxes_post_'. $post_id , $user->ID );
        return ( $newresult ) ? $newresult : $result;
    }
, 10, 3 );

We might also want to think about the case where there's no post based closedpostboxes_post_{post_id} available. So it will use the last saved settings from closedpostboxes_post. Maybe you would want to have it all open or all closed, in that default case. It would be easy to modify this behaviour.

For other custom post types we can use the corresponding closedpostboxes_{post_type} hook.

Same should be possible for the ordering and hiding of metaboxes with the metaboxhidden_{post_type} and meta-box-order_{post_data} user meta.

ps: sorry for this too long weekend answer, since they should always be short & jolly ;-)

  • Great +1. N/P for long answer, I wouldn't expect short ones. To be honest I didn't expect any on weekend :) Two things I liked very much: 1st the idea to store data on per-user and per-post basis: my idea was to store in post meta, but in that way all users will have same status. 2nd the idea to use 'get_user_option_*_post' to make WP recognize custom data. Only think I don't like too much is the usage of wp_get_referer that really on $_SERVER var that's not really reliable but I think I have an idea to overcome the "main problem" ;) – gmazzap Sep 13 '14 at 21:29
  • Thanks, I guess it depends on the number of users and posts where it would be best to store the data. Maybe this data should have some TTL and be erased e.g. once a month? Yes I agree with you regarding the wp_get_referer() method, that's why I called it a non-elegant PHP solution ;-) I first thought about storing the current post id for each user, but that doesn't work if a user is editing two or more posts in the browser. Look forward hearing about your idea on the "main problem" Enjoy the weekend ;-) – birgire Sep 13 '14 at 22:04
  • After 43 days an upvote remeber me to answer this. Thanks again for your answer. – gmazzap Oct 26 '14 at 9:58
6

As pointed out by birgire in his answer, WordPress uses AJAX to update metaboxes status and data passed in the AJAX request does not include post id, and that makes it hard to update boxes status on a per-post basis.

Once I found the AJAX action used by WordPress is 'closed-postboxes', I searched for this string in the admin js folder to find how WordPress makes the AJAX request.

I found it happens on postbox.js at line #118.

It looks like so:

save_state : function(page) {
  var closed = $('.postbox').filter('.closed').map(function() {
      return this.id;
    }).get().join(',');
  var hidden = $('.postbox').filter(':hidden').map(function() {
      return this.id;
    }).get().join(',');
  $.post(ajaxurl, {
    action: 'closed-postboxes',
    closed: closed,
    hidden: hidden,
    closedpostboxesnonce: jQuery('#closedpostboxesnonce').val(),
    page: page
  });
}

Essentially, WordPress looks at DOM items with class 'postbox' and class 'closed' and creates a comma separated list of their IDs. The same is done for hidden DOM items with class 'postbox'.

So, my thought was: I can create a fake metabox that has the right classes and that is hidden, setting its id to contain post ID, and in this way I can retrieve it in an AJAX request.

This is what I've done:

add_action( 'dbx_post_sidebar', function() {
    global $post;
    if ( $post->post_type === 'mycpt' ) {
        $id = $post->ID;
        $f = '<span id="fakebox_pid_%d" class="postbox closed" style="display:none;"></span>';
        printf( $f, $id );
    }
});

In this way I created a metabox that is always closed and always hidden, so WordPress will send its ID in as $_POST var in the AJAX request, and once fake box id contains post ID in a predictable way, I am able to recognize the post.

After that I looked at how WordPress performs the AJAX task.

In admin-ajax.php at line 72, WordPress hooks 'wp_ajax_closed-postboxes' with priority 1.

So, to act before WordPress, I could hook same action with priority 0.

add_action( 'wp_ajax_closed-postboxes', function() {

    // check if we are in right post type: WordPress passes it in 'page' post var
    $page = filter_input( INPUT_POST, 'page', FILTER_SANITIZE_STRING );
    if ( $page !== 'mycpt' ) return;

    // get post data
    $data = filter_input_array( INPUT_POST, array(
        'closed' => array( 'filter' => FILTER_SANITIZE_STRING ),
        'hidden' => array( 'filter' => FILTER_SANITIZE_STRING )
    ) );

    // search among closed boxes for the "fake" one, and return if not found
    $look_for_fake = array_filter( explode( ',', $data[ 'closed' ] ), function( $id ) {
         return strpos( $id, 'fakebox_pid_' ) === 0;
    } );
    if ( empty( $look_for_fake ) ) return;

    $post_id = str_replace( 'fakebox_pid_', '', $look_for_fake[0] );
    $user_id = get_current_user_id();

    // remove fake id from values
    $closed = implode(',', array_diff( explode(',', $data['closed'] ), $look_for_fake ) );
    $hidden = implode(',', array_diff( explode(',', $data['hidden'] ), $look_for_fake ) );

    // save metabox status on a per-post and per-user basis in a post meta
    update_post_meta( $post_id, "_mycpt_closed_boxes_{user_id}", $closed );
    update_post_meta( $post_id, "_mycpt_hidden_boxes_{user_id}", $hidden );

}, 0 );

Having data saved in a post meta made it possible to filter get_user_option_closedpostboxes_mycpt and get_user_option_metaboxhidden_mycpt (both variations of the get_user_option_{$option} filter) to force WordPress load options from post meta:

add_filter( 'get_user_option_closedpostboxes_mycpt', function ( $result, $key, $user ) {
    global $post;
    $meta = get_post_meta( $post->ID, "_mycpt_closed_boxes_{$user->ID}", TRUE );
    if ( ! empty( $meta ) ) {
        $result = $meta;
    }
    return $result;
}, 10, 3 );

and

add_filter( 'get_user_option_metaboxhidden_mycpt', function ( $result, $key, $user ) {
    global $post;
    $meta = get_post_meta( $post->ID, "_mycpt_hidden_boxes_{$user->ID}", TRUE );
    if ( ! empty( $meta ) ) {
        $result = $meta;
    }
    return $result;
}, 10, 3 );
  • What a great idea using a hidden metabox with the relevant info +1 – birgire Oct 26 '14 at 10:13
  • thanks @birgire and thanks again for your A, the idea of saving data on both per-user and per-post basis is all yours :) – gmazzap Oct 26 '14 at 10:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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