* Contains the postboxes logic, opening and closing postboxes, reordering and saving
* the state and ordering to the database.
* @output wp-admin/js/postbox.js
/* global ajaxurl, postboxes */
var $document = $( document ),
* This object contains all function to handle the behaviour of the post boxes. The post boxes are the boxes you see
* around the content on the edit page.
* Handles a click on either the postbox heading or the postbox open/close icon.
* Opens or closes the postbox. Expects `this` to equal the clicked element.
* Calls postboxes.pbshow if the postbox has been opened, calls postboxes.pbhide
* if the postbox has been closed.
* @fires postboxes#postbox-toggled
handle_click : function () {
p = $el.closest( '.postbox' ),
if ( 'dashboard_browser_nag' === id ) {
p.toggleClass( 'closed' );
ariaExpandedValue = ! p.hasClass( 'closed' );
if ( $el.hasClass( 'handlediv' ) ) {
// The handle button was clicked.
$el.attr( 'aria-expanded', ariaExpandedValue );
// The handle heading was clicked.
$el.closest( '.postbox' ).find( 'button.handlediv' )
.attr( 'aria-expanded', ariaExpandedValue );
if ( postboxes.page !== 'press-this' ) {
postboxes.save_state( postboxes.page );
if ( !p.hasClass('closed') && typeof postboxes.pbshow === 'function' ) {
} else if ( p.hasClass('closed') && typeof postboxes.pbhide === 'function' ) {
* Fires when a postbox has been opened or closed.
* Contains a jQuery object with the relevant postbox element.
* @event postboxes#postbox-toggled
$document.trigger( 'postbox-toggled', p );
* Handles clicks on the move up/down buttons.
handleOrder: function() {
postbox = button.closest( '.postbox' ),
postboxId = postbox.attr( 'id' ),
postboxesWithinSortables = postbox.closest( '.meta-box-sortables' ).find( '.postbox:visible' ),
postboxesWithinSortablesCount = postboxesWithinSortables.length,
postboxWithinSortablesIndex = postboxesWithinSortables.index( postbox ),
firstOrLastPositionMessage;
if ( 'dashboard_browser_nag' === postboxId ) {
// If on the first or last position, do nothing and send an audible message to screen reader users.
if ( 'true' === button.attr( 'aria-disabled' ) ) {
firstOrLastPositionMessage = button.hasClass( 'handle-order-higher' ) ?
__( 'The box is on the first position' ) :
__( 'The box is on the last position' );
wp.a11y.speak( firstOrLastPositionMessage );
if ( button.hasClass( 'handle-order-higher' ) ) {
// If the box is first within a sortable area, move it to the previous sortable area.
if ( 0 === postboxWithinSortablesIndex ) {
postboxes.handleOrderBetweenSortables( 'previous', button, postbox );
postbox.prevAll( '.postbox:visible' ).eq( 0 ).before( postbox );
button.trigger( 'focus' );
postboxes.updateOrderButtonsProperties();
postboxes.save_order( postboxes.page );
if ( button.hasClass( 'handle-order-lower' ) ) {
// If the box is last within a sortable area, move it to the next sortable area.
if ( postboxWithinSortablesIndex + 1 === postboxesWithinSortablesCount ) {
postboxes.handleOrderBetweenSortables( 'next', button, postbox );
postbox.nextAll( '.postbox:visible' ).eq( 0 ).after( postbox );
button.trigger( 'focus' );
postboxes.updateOrderButtonsProperties();
postboxes.save_order( postboxes.page );
* Moves postboxes between the sortables areas.
* @param {string} position The "previous" or "next" sortables area.
* @param {Object} button The jQuery object representing the button that was clicked.
* @param {Object} postbox The jQuery object representing the postbox to be moved.
handleOrderBetweenSortables: function( position, button, postbox ) {
var closestSortablesId = button.closest( '.meta-box-sortables' ).attr( 'id' ),
// Get the list of sortables within the page.
$( '.meta-box-sortables:visible' ).each( function() {
sortablesIds.push( $( this ).attr( 'id' ) );
// Return if there's only one visible sortables area, e.g. in the block editor page.
if ( 1 === sortablesIds.length ) {
// Find the index of the current sortables area within all the sortable areas.
sortablesIndex = $.inArray( closestSortablesId, sortablesIds );
// Detach the postbox to be moved.
detachedPostbox = postbox.detach();
// Move the detached postbox to its new position.
if ( 'previous' === position ) {
$( detachedPostbox ).appendTo( '#' + sortablesIds[ sortablesIndex - 1 ] );
if ( 'next' === position ) {
$( detachedPostbox ).prependTo( '#' + sortablesIds[ sortablesIndex + 1 ] );
postboxes.updateOrderButtonsProperties();
postboxes.save_order( postboxes.page );
* Update the move buttons properties depending on the postbox position.
updateOrderButtonsProperties: function() {
var firstSortablesId = $( '.meta-box-sortables:visible:first' ).attr( 'id' ),
lastSortablesId = $( '.meta-box-sortables:visible:last' ).attr( 'id' ),
firstPostbox = $( '.postbox:visible:first' ),
lastPostbox = $( '.postbox:visible:last' ),
firstPostboxId = firstPostbox.attr( 'id' ),
lastPostboxId = lastPostbox.attr( 'id' ),
firstPostboxSortablesId = firstPostbox.closest( '.meta-box-sortables' ).attr( 'id' ),
lastPostboxSortablesId = lastPostbox.closest( '.meta-box-sortables' ).attr( 'id' ),
moveUpButtons = $( '.handle-order-higher' ),
moveDownButtons = $( '.handle-order-lower' );
// Enable all buttons as a reset first.
.attr( 'aria-disabled', 'false' )
.removeClass( 'hidden' );
.attr( 'aria-disabled', 'false' )
.removeClass( 'hidden' );
// When there's only one "sortables" area (e.g. in the block editor) and only one visible postbox, hide the buttons.
if ( firstSortablesId === lastSortablesId && firstPostboxId === lastPostboxId ) {
moveUpButtons.addClass( 'hidden' );
moveDownButtons.addClass( 'hidden' );
// Set an aria-disabled=true attribute on the first visible "move" buttons.
if ( firstSortablesId === firstPostboxSortablesId ) {
$( firstPostbox ).find( '.handle-order-higher' ).attr( 'aria-disabled', 'true' );
// Set an aria-disabled=true attribute on the last visible "move" buttons.
if ( lastSortablesId === lastPostboxSortablesId ) {
$( '.postbox:visible .handle-order-lower' ).last().attr( 'aria-disabled', 'true' );
* Adds event handlers to all postboxes and screen option on the current page.
* @param {string} page The page we are currently on.
* @param {Function} args.pbshow A callback that is called when a postbox opens.
* @param {Function} args.pbhide A callback that is called when a postbox closes.
add_postbox_toggles : function (page, args) {
var $handles = $( '.postbox .hndle, .postbox .handlediv' ),
$orderButtons = $( '.postbox .handle-order-higher, .postbox .handle-order-lower' );
$handles.on( 'click.postboxes', this.handle_click );
// Handle the order of the postboxes.
$orderButtons.on( 'click.postboxes', this.handleOrder );
$('.postbox .hndle a').on( 'click', function(e) {
* Event handler for the postbox dismiss button. After clicking the button
* the postbox will be hidden.
* As of WordPress 5.5, this is only used for the browser update nag.
$( '.postbox a.dismiss' ).on( 'click.postboxes', function( e ) {
var hide_id = $(this).parents('.postbox').attr('id') + '-hide';
$( '#' + hide_id ).prop('checked', false).triggerHandler('click');
* Hides the postbox element
* Event handler for the screen options checkboxes. When a checkbox is
* clicked this function will hide or show the relevant postboxes.
* @fires postboxes#postbox-toggled
$('.hide-postbox-tog').on('click.postboxes', function() {
$postbox = $( '#' + boxId );
if ( $el.prop( 'checked' ) ) {
if ( typeof postboxes.pbshow === 'function' ) {
postboxes.pbshow( boxId );
if ( typeof postboxes.pbhide === 'function' ) {
postboxes.pbhide( boxId );
postboxes.save_state( page );
* @see postboxes.handle_click
$document.trigger( 'postbox-toggled', $postbox );
* Changes the amount of columns based on the layout preferences.
$('.columns-prefs input[type="radio"]').on('click.postboxes', function(){
var n = parseInt($(this).val(), 10);
postboxes.save_order( page );
* Initializes all the postboxes, mainly their sortable behaviour.
* @param {string} page The page we are currently on.
* @param {Object} [args={}] The arguments for the postbox initializer.
* @param {Function} args.pbshow A callback that is called when a postbox opens.
* @param {Function} args.pbhide A callback that is called when a postbox
init : function(page, args) {
var isMobile = $( document.body ).hasClass( 'mobile' ),
$handleButtons = $( '.postbox .handlediv' );
$.extend( this, args || {} );
$('.meta-box-sortables').sortable({
placeholder: 'sortable-placeholder',
connectWith: '.meta-box-sortables',
delay: ( isMobile ? 200 : 0 ),
forcePlaceholderSize: true,
helper: function( event, element ) {
/* `helper: 'clone'` is equivalent to `return element.clone();`
* Cloning a checked radio and then inserting that clone next to the original
* radio unchecks the original radio (since only one of the two can be checked).
* We get around this by renaming the helper's inputs' name attributes so that,
* when the helper is inserted into the DOM for the sortable, no radios are
* duplicated, and no original radio gets unchecked.
.attr( 'name', function( i, currentName ) {
return 'sort_' + parseInt( Math.random() * 100000, 10 ).toString() + '_' + currentName;
$( 'body' ).addClass( 'is-dragging-metaboxes' );
// Refresh the cached positions of all the sortable items so that the min-height set while dragging works.
$( '.meta-box-sortables' ).sortable( 'refreshPositions' );
$( 'body' ).removeClass( 'is-dragging-metaboxes' );
if ( $el.find( '#dashboard_browser_nag' ).is( ':visible' ) && 'dashboard_browser_nag' != this.firstChild.id ) {
postboxes.updateOrderButtonsProperties();
postboxes.save_order(page);
receive: function(e,ui) {
if ( 'dashboard_browser_nag' == ui.item[0].id )
$(ui.sender).sortable('cancel');
$document.trigger( 'postbox-moved', ui.item );
$(document.body).on('orientationchange.postboxes', function(){ postboxes._pb_change(); });
// Update the "move" buttons properties.
this.updateOrderButtonsProperties();
$document.on( 'postbox-toggled', this.updateOrderButtonsProperties );
// Set the handle buttons `aria-expanded` attribute initial value on page load.
$handleButtons.each( function () {
$el.attr( 'aria-expanded', ! $el.closest( '.postbox' ).hasClass( 'closed' ) );
* Saves the state of the postboxes to the server.
* It sends two lists, one with all the closed postboxes, one with all the
* @param {string} page The page we are currently on.
save_state : function(page) {
// Return on the nav-menus.php screen, see #35112.
if ( 'nav-menus' === page ) {
closed = $( '.postbox' ).filter( '.closed' ).map( function() { return this.id; } ).get().join( ',' );
hidden = $( '.postbox' ).filter( ':hidden' ).map( function() { return this.id; } ).get().join( ',' );
action: 'closed-postboxes',
closedpostboxesnonce: jQuery('#closedpostboxesnonce').val(),
* Saves the order of the postboxes to the server.
* Sends a list of all postboxes inside a sortable area to the server.
* @param {string} page The page we are currently on.
save_order : function(page) {
var postVars, page_columns = $('.columns-prefs input:checked').val() || 0;
action: 'meta-box-order',
_ajax_nonce: $('#meta-box-order-nonce').val(),
page_columns: page_columns,
$('.meta-box-sortables').each( function() {
postVars[ 'order[' + this.id.split( '-' )[0] + ']' ] = $( this ).sortable( 'toArray' ).join( ',' );