* @output wp-admin/js/widgets.js
/* global ajaxurl, isRtl, wpWidgets */
var $document = $( document );
* A closed Sidebar that gets a Widget dragged over it.
* Lookup of which widgets have had change events triggered.
chooser = $('.widgets-chooser'),
selectSidebar = chooser.find('.widgets-chooser-sidebars'),
sidebars = $('div.widgets-sortables'),
isRTL = !! ( 'undefined' !== typeof isRtl && isRtl );
// Handle the widgets containers in the right column.
$( '#widgets-right .sidebar-name' )
* Toggle the widgets containers when clicked and update the toggle
* button `aria-expanded` attribute value.
.on( 'click', function() {
$wrap = $this.closest( '.widgets-holder-wrap '),
$toggle = $this.find( '.handlediv' );
if ( $wrap.hasClass( 'closed' ) ) {
$wrap.removeClass( 'closed' );
$toggle.attr( 'aria-expanded', 'true' );
// Refresh the jQuery UI sortable items.
$this.parent().sortable( 'refresh' );
$wrap.addClass( 'closed' );
$toggle.attr( 'aria-expanded', 'false' );
// Update the admin menu "sticky" state.
$document.triggerHandler( 'wp-pin-menu' );
* Set the initial `aria-expanded` attribute value on the widgets
* containers toggle button. The first one is expanded by default.
.find( '.handlediv' ).each( function( index ) {
// jQuery equivalent of `continue` within an `each()` loop.
$( this ).attr( 'aria-expanded', 'false' );
// Show AYS dialog when there are unsaved widget changes.
$( window ).on( 'beforeunload.widgets', function( event ) {
var dirtyWidgetIds = [], unsavedWidgetsElements;
$.each( self.dirtyWidgets, function( widgetId, dirty ) {
dirtyWidgetIds.push( widgetId );
if ( 0 !== dirtyWidgetIds.length ) {
unsavedWidgetsElements = $( '#widgets-right' ).find( '.widget' ).filter( function() {
return -1 !== dirtyWidgetIds.indexOf( $( this ).prop( 'id' ).replace( /^widget-\d+_/, '' ) );
unsavedWidgetsElements.each( function() {
if ( ! $( this ).hasClass( 'open' ) ) {
$( this ).find( '.widget-title-action:first' ).trigger( 'click' );
// Bring the first unsaved widget into view and focus on the first tabbable field.
unsavedWidgetsElements.first().each( function() {
if ( this.scrollIntoViewIfNeeded ) {
this.scrollIntoViewIfNeeded();
$( this ).find( '.widget-inside :tabbable:first' ).trigger( 'focus' );
event.returnValue = wp.i18n.__( 'The changes you made will be lost if you navigate away from this page.' );
return event.returnValue;
// Handle the widgets containers in the left column.
$( '#widgets-left .sidebar-name' ).on( 'click', function() {
var $wrap = $( this ).closest( '.widgets-holder-wrap' );
.find( '.handlediv' ).attr( 'aria-expanded', ! $wrap.hasClass( 'closed' ) );
// Update the admin menu "sticky" state.
$document.triggerHandler( 'wp-pin-menu' );
$(document.body).on('click.widgets-toggle', function(e) {
var target = $(e.target), css = {},
widget, inside, targetWidth, widgetWidth, margin, saveButton, widgetId,
toggleBtn = target.closest( '.widget' ).find( '.widget-top button.widget-action' );
if ( target.parents('.widget-top').length && ! target.parents('#available-widgets').length ) {
widget = target.closest('div.widget');
inside = widget.children('.widget-inside');
targetWidth = parseInt( widget.find('input.widget-width').val(), 10 );
widgetWidth = widget.parent().width();
widgetId = inside.find( '.widget-id' ).val();
// Save button is initially disabled, but is enabled when a field is changed.
if ( ! widget.data( 'dirty-state-initialized' ) ) {
saveButton = inside.find( '.widget-control-save' );
saveButton.prop( 'disabled', true ).val( wp.i18n.__( 'Saved' ) );
inside.on( 'input change', function() {
self.dirtyWidgets[ widgetId ] = true;
widget.addClass( 'widget-dirty' );
saveButton.prop( 'disabled', false ).val( wp.i18n.__( 'Save' ) );
widget.data( 'dirty-state-initialized', true );
if ( inside.is(':hidden') ) {
if ( targetWidth > 250 && ( targetWidth + 30 > widgetWidth ) && widget.closest('div.widgets-sortables').length ) {
if ( widget.closest('div.widget-liquid-right').length ) {
margin = isRTL ? 'margin-right' : 'margin-left';
margin = isRTL ? 'margin-left' : 'margin-right';
css[ margin ] = widgetWidth - ( targetWidth + 30 ) + 'px';
* Don't change the order of attributes changes and animation:
* it's important for screen readers, see ticket #31476.
toggleBtn.attr( 'aria-expanded', 'true' );
inside.slideDown( 'fast', function() {
widget.addClass( 'open' );
* Don't change the order of attributes changes and animation:
* it's important for screen readers, see ticket #31476.
toggleBtn.attr( 'aria-expanded', 'false' );
inside.slideUp( 'fast', function() {
widget.attr( 'style', '' );
widget.removeClass( 'open' );
} else if ( target.hasClass('widget-control-save') ) {
wpWidgets.save( target.closest('div.widget'), 0, 1, 0 );
} else if ( target.hasClass('widget-control-remove') ) {
wpWidgets.save( target.closest('div.widget'), 1, 1, 0 );
} else if ( target.hasClass('widget-control-close') ) {
widget = target.closest('div.widget');
widget.removeClass( 'open' );
toggleBtn.attr( 'aria-expanded', 'false' );
wpWidgets.close( widget );
} else if ( target.attr( 'id' ) === 'inactive-widgets-control-remove' ) {
wpWidgets.removeInactiveWidgets();
sidebars.children('.widget').each( function() {
wpWidgets.appendTitle( this );
if ( $this.find( 'p.widget-error' ).length ) {
$this.find( '.widget-action' ).trigger( 'click' ).attr( 'aria-expanded', 'true' );
$('#widget-list').children('.widget').draggable({
connectToSortable: 'div.widgets-sortables',
handle: '> .widget-top > .widget-title',
start: function( event, ui ) {
var chooser = $(this).find('.widgets-chooser');
ui.helper.find('div.widget-description').hide();
// Hide the chooser and move it out of the widget.
$( '#wpbody-content' ).append( chooser.hide() );
// Delete the cloned chooser from the drag helper.
ui.helper.find('.widgets-chooser').remove();
self.clearWidgetSelection();
* Opens and closes previously closed Sidebars when Widgets are dragged over/out of them.
* Open Sidebar when a Widget gets dragged over it.
* @param {Object} event jQuery event object.
over: function( event ) {
var $wrap = $( event.target ).parent();
if ( wpWidgets.hoveredSidebar && ! $wrap.is( wpWidgets.hoveredSidebar ) ) {
// Close the previous Sidebar as the Widget has been dragged onto another Sidebar.
wpWidgets.closeSidebar( event );
if ( $wrap.hasClass( 'closed' ) ) {
wpWidgets.hoveredSidebar = $wrap;
.find( '.handlediv' ).attr( 'aria-expanded', 'true' );
$( this ).sortable( 'refresh' );
* Close Sidebar when the Widget gets dragged out of it.
* @param {Object} event jQuery event object.
if ( wpWidgets.hoveredSidebar ) {
wpWidgets.closeSidebar( event );
placeholder: 'widget-placeholder',
handle: '> .widget-top > .widget-title',
start: function( event, ui ) {
var height, $this = $(this),
inside = ui.item.children('.widget-inside');
if ( inside.css('display') === 'block' ) {
ui.item.removeClass('open');
ui.item.find( '.widget-top button.widget-action' ).attr( 'aria-expanded', 'false' );
$(this).sortable('refreshPositions');
if ( ! $wrap.hasClass('closed') ) {
// Lock all open sidebars min-height when starting to drag.
// Prevents jumping when dragging a widget from an open sidebar to a closed sidebar below.
height = ui.item.hasClass('ui-draggable') ? $this.height() : 1 + $this.height();
$this.css( 'min-height', height + 'px' );
stop: function( event, ui ) {
var addNew, widgetNumber, $sidebar, $children, child, item,
// Reset the var to hold a previously closed sidebar.
wpWidgets.hoveredSidebar = null;
if ( $widget.hasClass('deleting') ) {
wpWidgets.save( $widget, 1, 0, 1 ); // Delete widget.
addNew = $widget.find('input.add_new').val();
widgetNumber = $widget.find('input.multi_number').val();
$widget.attr( 'style', '' ).removeClass('ui-draggable');
if ( 'multi' === addNew ) {
$widget.html().replace( /<[^<>]+>/g, function( tag ) {
return tag.replace( /__i__|%i%/g, widgetNumber );
$widget.attr( 'id', id.replace( '__i__', widgetNumber ) );
$( 'div#' + id ).find( 'input.multi_number' ).val( widgetNumber );
} else if ( 'single' === addNew ) {
$widget.attr( 'id', 'new-' + id );
wpWidgets.save( $widget, 0, 0, 1 );
$widget.find('input.add_new').val('');
$document.trigger( 'widget-added', [ $widget ] );
$sidebar = $widget.parent();
if ( $sidebar.parent().hasClass('closed') ) {
.find( '.handlediv' ).attr( 'aria-expanded', 'true' );
$children = $sidebar.children('.widget');
// Make sure the dropped widget is at the top.
if ( $children.length > 1 ) {
child = $children.get(0);
if ( child.id && item.id && child.id !== item.id ) {
$( child ).before( $widget );
$widget.find( '.widget-action' ).trigger( 'click' );
wpWidgets.saveOrder( $sidebar.attr('id') );
$(this).parent().addClass( 'widget-hover' );
// Remove all min-height added on "start".
$(this).css( 'min-height', '' ).parent().removeClass( 'widget-hover' );
receive: function( event, ui ) {
var $sender = $( ui.sender );
// Don't add more widgets to orphaned sidebars.
if ( this.id.indexOf('orphaned_widgets') > -1 ) {
$sender.sortable('cancel');
// If the last widget was moved out of an orphaned sidebar, close and remove it.
if ( $sender.attr('id').indexOf('orphaned_widgets') > -1 && ! $sender.children('.widget').length ) {
$sender.parents('.orphan-sidebar').slideUp( 400, function(){ $(this).remove(); } );
}).sortable( 'option', 'connectWith', 'div.widgets-sortables' );
$('#available-widgets').droppable({
return $(o).parent().attr('id') !== 'widget-list';
ui.draggable.addClass('deleting');
$('#removing-widget').hide().children('span').empty();
ui.draggable.addClass('deleting');
$('div.widget-placeholder').hide();
if ( ui.draggable.hasClass('ui-sortable-helper') ) {
$('#removing-widget').show().children('span')
.html( ui.draggable.find( 'div.widget-title' ).children( 'h3' ).html() );
ui.draggable.removeClass('deleting');
$('div.widget-placeholder').show();
$('#removing-widget').hide().children('span').empty();
$( '#widgets-right .widgets-holder-wrap' ).each( function( index, element ) {
var $element = $( element ),
name = $element.find( '.sidebar-name h2' ).text() || '',
ariaLabel = $element.find( '.sidebar-name' ).data( 'add-to' ),
id = $element.find( '.widgets-sortables' ).attr( 'id' ),
button = $( '<button>', {
'class': 'widgets-chooser-button',
} ).text( name.toString().trim() );
li.addClass( 'widgets-chooser-selected' );
button.attr( 'aria-pressed', 'true' );
selectSidebar.append( li );
li.data( 'sidebarId', id );
$( '#available-widgets .widget .widget-top' ).on( 'click.widgets-chooser', function() {
var $widget = $( this ).closest( '.widget' ),
toggleButton = $( this ).find( '.widget-action' ),
chooserButtons = selectSidebar.find( '.widgets-chooser-button' );
if ( $widget.hasClass( 'widget-in-question' ) || $( '#widgets-left' ).hasClass( 'chooser' ) ) {
toggleButton.attr( 'aria-expanded', 'false' );
self.clearWidgetSelection();
$( '#widgets-left' ).addClass( 'chooser' );
// Add CSS class and insert the chooser after the widget description.
$widget.addClass( 'widget-in-question' ).children( '.widget-description' ).after( chooser );
// Open the chooser with a slide down animation.
chooser.slideDown( 300, function() {
// Update the toggle button aria-expanded attribute after previous DOM manipulations.
toggleButton.attr( 'aria-expanded', 'true' );
chooserButtons.on( 'click.widgets-chooser', function() {
selectSidebar.find( '.widgets-chooser-selected' ).removeClass( 'widgets-chooser-selected' );
chooserButtons.attr( 'aria-pressed', 'false' );
.attr( 'aria-pressed', 'true' )
.closest( 'li' ).addClass( 'widgets-chooser-selected' );
chooser.on( 'click.widgets-chooser', function( event ) {
var $target = $( event.target );
if ( $target.hasClass('button-primary') ) {
self.addWidget( chooser );
} else if ( $target.hasClass( 'widgets-chooser-cancel' ) ) {
}).on( 'keyup.widgets-chooser', function( event ) {
if ( event.which === $.ui.keyCode.ESCAPE ) {
saveOrder : function( sidebarId ) {
savewidgets: $('#_wpnonce_widgets').val(),
$( '#' + sidebarId ).find( '.spinner:first' ).addClass( 'is-active' );