WordPress/wp-admin/js/widgets.js
Sergey Biryukov 76718fbea3 Widgets: Prevent currently active wide widget controls from being partially covered by another widget's controls.
The currently active widget controls should always be displayed on top.

Props audrasjb, fervillz, rinkuyadav999, jaydeep23290, ashokrd2013, melchoyce, pento, westonruter, SergeyBiryukov.
Fixes #42001.
Built from https://develop.svn.wordpress.org/trunk@47263


git-svn-id: http://core.svn.wordpress.org/trunk@47063 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2020-02-11 15:26:09 +00:00

761 lines
22 KiB
JavaScript

/**
* @output wp-admin/js/widgets.js
*/
/* global ajaxurl, isRtl, wpWidgets */
(function($) {
var $document = $( document );
window.wpWidgets = {
/**
* A closed Sidebar that gets a Widget dragged over it.
*
* @var {element|null}
*/
hoveredSidebar: null,
/**
* Translations.
*
* Exported from PHP in wp_default_scripts().
*
* @var {object}
*/
l10n: {
save: '{save}',
saved: '{saved}',
saveAlert: '{saveAlert}',
widgetAdded: '{widgetAdded}'
},
/**
* Lookup of which widgets have had change events triggered.
*
* @var {object}
*/
dirtyWidgets: {},
init : function() {
var rem, the_id,
self = this,
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.
*/
.click( function() {
var $this = $( this ),
$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' );
} else {
$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 ) {
if ( 0 === index ) {
// jQuery equivalent of `continue` within an `each()` loop.
return;
}
$( 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 ) {
if ( 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' ).click();
}
});
// Bring the first unsaved widget into view and focus on the first tabbable field.
unsavedWidgetsElements.first().each( function() {
if ( this.scrollIntoViewIfNeeded ) {
this.scrollIntoViewIfNeeded();
} else {
this.scrollIntoView();
}
$( this ).find( '.widget-inside :tabbable:first' ).focus();
} );
event.returnValue = wpWidgets.l10n.saveAlert;
return event.returnValue;
}
});
// Handle the widgets containers in the left column.
$( '#widgets-left .sidebar-name' ).click( function() {
var $wrap = $( this ).closest( '.widgets-holder-wrap' );
$wrap
.toggleClass( 'closed' )
.find( '.handlediv' ).attr( 'aria-expanded', ! $wrap.hasClass( 'closed' ) );
// Update the admin menu "sticky" state.
$document.triggerHandler( 'wp-pin-menu' );
});
$(document.body).bind('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( wpWidgets.l10n.saved );
inside.on( 'input change', function() {
self.dirtyWidgets[ widgetId ] = true;
widget.addClass( 'widget-dirty' );
saveButton.prop( 'disabled', false ).val( wpWidgets.l10n.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';
} else {
margin = isRTL ? 'margin-left' : 'margin-right';
}
css[ margin ] = widgetWidth - ( targetWidth + 30 ) + 'px';
widget.css( css );
}
/*
* 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' );
});
} else {
/*
* 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 );
e.preventDefault();
} 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();
e.preventDefault();
}
});
sidebars.children('.widget').each( function() {
var $this = $(this);
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',
distance: 2,
helper: 'clone',
zIndex: 101,
containment: '#wpwrap',
refreshPositions: true,
start: function( event, ui ) {
var chooser = $(this).find('.widgets-chooser');
ui.helper.find('div.widget-description').hide();
the_id = this.id;
if ( chooser.length ) {
// 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();
}
},
stop: function() {
if ( rem ) {
$(rem).hide();
}
rem = '';
}
});
/**
* Opens and closes previously closed Sidebars when Widgets are dragged over/out of them.
*/
sidebars.droppable( {
tolerance: 'intersect',
/**
* Open Sidebar when a Widget gets dragged over it.
*
* @ignore
*
* @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;
$wrap
.removeClass( 'closed' )
.find( '.handlediv' ).attr( 'aria-expanded', 'true' );
}
$( this ).sortable( 'refresh' );
},
/**
* Close Sidebar when the Widget gets dragged out of it.
*
* @ignore
*
* @param {object} event jQuery event object.
*/
out: function( event ) {
if ( wpWidgets.hoveredSidebar ) {
wpWidgets.closeSidebar( event );
}
}
} );
sidebars.sortable({
placeholder: 'widget-placeholder',
items: '> .widget',
handle: '> .widget-top > .widget-title',
cursor: 'move',
distance: 2,
containment: '#wpwrap',
tolerance: 'pointer',
refreshPositions: true,
start: function( event, ui ) {
var height, $this = $(this),
$wrap = $this.parent(),
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' );
inside.hide();
$(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,
$widget = ui.item,
id = the_id;
// Reset the var to hold a previously closed sidebar.
wpWidgets.hoveredSidebar = null;
if ( $widget.hasClass('deleting') ) {
wpWidgets.save( $widget, 1, 0, 1 ); // Delete widget.
$widget.remove();
return;
}
addNew = $widget.find('input.add_new').val();
widgetNumber = $widget.find('input.multi_number').val();
$widget.attr( 'style', '' ).removeClass('ui-draggable');
the_id = '';
if ( addNew ) {
if ( 'multi' === addNew ) {
$widget.html(
$widget.html().replace( /<[^<>]+>/g, function( tag ) {
return tag.replace( /__i__|%i%/g, widgetNumber );
})
);
$widget.attr( 'id', id.replace( '__i__', widgetNumber ) );
widgetNumber++;
$( 'div#' + id ).find( 'input.multi_number' ).val( widgetNumber );
} else if ( 'single' === addNew ) {
$widget.attr( 'id', 'new-' + id );
rem = 'div#' + 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') ) {
$sidebar.parent()
.removeClass( '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);
item = $widget.get(0);
if ( child.id && item.id && child.id !== item.id ) {
$( child ).before( $widget );
}
}
}
if ( addNew ) {
$widget.find( '.widget-action' ).trigger( 'click' );
} else {
wpWidgets.saveOrder( $sidebar.attr('id') );
}
},
activate: function() {
$(this).parent().addClass( 'widget-hover' );
},
deactivate: function() {
// 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');
return;
}
// 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({
tolerance: 'pointer',
accept: function(o){
return $(o).parent().attr('id') !== 'widget-list';
},
drop: function(e,ui) {
ui.draggable.addClass('deleting');
$('#removing-widget').hide().children('span').empty();
},
over: function(e,ui) {
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() );
}
},
out: function(e,ui) {
ui.draggable.removeClass('deleting');
$('div.widget-placeholder').show();
$('#removing-widget').hide().children('span').empty();
}
});
// Area Chooser.
$( '#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' ),
li = $( '<li>' ),
button = $( '<button>', {
type: 'button',
'aria-pressed': 'false',
'class': 'widgets-chooser-button',
'aria-label': ariaLabel
} ).text( $.trim( name ) );
li.append( button );
if ( index === 0 ) {
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.closeChooser();
} else {
// Open the chooser.
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' );
$( this )
.attr( 'aria-pressed', 'true' )
.closest( 'li' ).addClass( 'widgets-chooser-selected' );
} );
}
});
// Add event handlers.
chooser.on( 'click.widgets-chooser', function( event ) {
var $target = $( event.target );
if ( $target.hasClass('button-primary') ) {
self.addWidget( chooser );
self.closeChooser();
} else if ( $target.hasClass( 'widgets-chooser-cancel' ) ) {
self.closeChooser();
}
}).on( 'keyup.widgets-chooser', function( event ) {
if ( event.which === $.ui.keyCode.ESCAPE ) {
self.closeChooser();
}
});
},
saveOrder : function( sidebarId ) {
var data = {
action: 'widgets-order',
savewidgets: $('#_wpnonce_widgets').val(),
sidebars: []
};
if ( sidebarId ) {
$( '#' + sidebarId ).find( '.spinner:first' ).addClass( 'is-active' );
}
$('div.widgets-sortables').each( function() {
if ( $(this).sortable ) {
data['sidebars[' + $(this).attr('id') + ']'] = $(this).sortable('toArray').join(',');
}
});
$.post( ajaxurl, data, function() {
$( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length );
$( '.spinner' ).removeClass( 'is-active' );
});
},
save : function( widget, del, animate, order ) {
var self = this, data, a,
sidebarId = widget.closest( 'div.widgets-sortables' ).attr( 'id' ),
form = widget.find( 'form' ),
isAdd = widget.find( 'input.add_new' ).val();
if ( ! del && ! isAdd && form.prop( 'checkValidity' ) && ! form[0].checkValidity() ) {
return;
}
data = form.serialize();
widget = $(widget);
$( '.spinner', widget ).addClass( 'is-active' );
a = {
action: 'save-widget',
savewidgets: $('#_wpnonce_widgets').val(),
sidebar: sidebarId
};
if ( del ) {
a.delete_widget = 1;
}
data += '&' + $.param(a);
$.post( ajaxurl, data, function(r) {
var id = $('input.widget-id', widget).val();
if ( del ) {
if ( ! $('input.widget_number', widget).val() ) {
$('#available-widgets').find('input.widget-id').each(function(){
if ( $(this).val() === id ) {
$(this).closest('div.widget').show();
}
});
}
if ( animate ) {
order = 0;
widget.slideUp( 'fast', function() {
$( this ).remove();
wpWidgets.saveOrder();
delete self.dirtyWidgets[ id ];
});
} else {
widget.remove();
delete self.dirtyWidgets[ id ];
if ( sidebarId === 'wp_inactive_widgets' ) {
$( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length );
}
}
} else {
$( '.spinner' ).removeClass( 'is-active' );
if ( r && r.length > 2 ) {
$( 'div.widget-content', widget ).html( r );
wpWidgets.appendTitle( widget );
// Re-disable the save button.
widget.find( '.widget-control-save' ).prop( 'disabled', true ).val( wpWidgets.l10n.saved );
widget.removeClass( 'widget-dirty' );
// Clear the dirty flag from the widget.
delete self.dirtyWidgets[ id ];
$document.trigger( 'widget-updated', [ widget ] );
if ( sidebarId === 'wp_inactive_widgets' ) {
$( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length );
}
}
}
if ( order ) {
wpWidgets.saveOrder();
}
});
},
removeInactiveWidgets : function() {
var $element = $( '.remove-inactive-widgets' ), self = this, a, data;
$( '.spinner', $element ).addClass( 'is-active' );
a = {
action : 'delete-inactive-widgets',
removeinactivewidgets : $( '#_wpnonce_remove_inactive_widgets' ).val()
};
data = $.param( a );
$.post( ajaxurl, data, function() {
$( '#wp_inactive_widgets .widget' ).each(function() {
var $widget = $( this );
delete self.dirtyWidgets[ $widget.find( 'input.widget-id' ).val() ];
$widget.remove();
});
$( '#inactive-widgets-control-remove' ).prop( 'disabled', true );
$( '.spinner', $element ).removeClass( 'is-active' );
} );
},
appendTitle : function(widget) {
var title = $('input[id*="-title"]', widget).val() || '';
if ( title ) {
title = ': ' + title.replace(/<[^<>]+>/g, '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
$(widget).children('.widget-top').children('.widget-title').children()
.children('.in-widget-title').html(title);
},
close : function(widget) {
widget.children('.widget-inside').slideUp('fast', function() {
widget.attr( 'style', '' )
.find( '.widget-top button.widget-action' )
.attr( 'aria-expanded', 'false' )
.focus();
});
},
addWidget: function( chooser ) {
var widget, widgetId, add, n, viewportTop, viewportBottom, sidebarBounds,
sidebarId = chooser.find( '.widgets-chooser-selected' ).data('sidebarId'),
sidebar = $( '#' + sidebarId );
widget = $('#available-widgets').find('.widget-in-question').clone();
widgetId = widget.attr('id');
add = widget.find( 'input.add_new' ).val();
n = widget.find( 'input.multi_number' ).val();
// Remove the cloned chooser from the widget.
widget.find('.widgets-chooser').remove();
if ( 'multi' === add ) {
widget.html(
widget.html().replace( /<[^<>]+>/g, function(m) {
return m.replace( /__i__|%i%/g, n );
})
);
widget.attr( 'id', widgetId.replace( '__i__', n ) );
n++;
$( '#' + widgetId ).find('input.multi_number').val(n);
} else if ( 'single' === add ) {
widget.attr( 'id', 'new-' + widgetId );
$( '#' + widgetId ).hide();
}
// Open the widgets container.
sidebar.closest( '.widgets-holder-wrap' )
.removeClass( 'closed' )
.find( '.handlediv' ).attr( 'aria-expanded', 'true' );
sidebar.append( widget );
sidebar.sortable('refresh');
wpWidgets.save( widget, 0, 0, 1 );
// No longer "new" widget.
widget.find( 'input.add_new' ).val('');
$document.trigger( 'widget-added', [ widget ] );
/*
* Check if any part of the sidebar is visible in the viewport. If it is, don't scroll.
* Otherwise, scroll up to so the sidebar is in view.
*
* We do this by comparing the top and bottom, of the sidebar so see if they are within
* the bounds of the viewport.
*/
viewportTop = $(window).scrollTop();
viewportBottom = viewportTop + $(window).height();
sidebarBounds = sidebar.offset();
sidebarBounds.bottom = sidebarBounds.top + sidebar.outerHeight();
if ( viewportTop > sidebarBounds.bottom || viewportBottom < sidebarBounds.top ) {
$( 'html, body' ).animate({
scrollTop: sidebarBounds.top - 130
}, 200 );
}
window.setTimeout( function() {
// Cannot use a callback in the animation above as it fires twice,
// have to queue this "by hand".
widget.find( '.widget-title' ).trigger('click');
// At the end of the animation, announce the widget has been added.
window.wp.a11y.speak( wpWidgets.l10n.widgetAdded, 'assertive' );
}, 250 );
},
closeChooser: function() {
var self = this,
widgetInQuestion = $( '#available-widgets .widget-in-question' );
$( '.widgets-chooser' ).slideUp( 200, function() {
$( '#wpbody-content' ).append( this );
self.clearWidgetSelection();
// Move focus back to the toggle button.
widgetInQuestion.find( '.widget-action' ).attr( 'aria-expanded', 'false' ).focus();
});
},
clearWidgetSelection: function() {
$( '#widgets-left' ).removeClass( 'chooser' );
$( '.widget-in-question' ).removeClass( 'widget-in-question' );
},
/**
* Closes a Sidebar that was previously closed, but opened by dragging a Widget over it.
*
* Used when a Widget gets dragged in/out of the Sidebar and never dropped.
*
* @param {object} event jQuery event object.
*/
closeSidebar: function( event ) {
this.hoveredSidebar
.addClass( 'closed' )
.find( '.handlediv' ).attr( 'aria-expanded', 'false' );
$( event.target ).css( 'min-height', '' );
this.hoveredSidebar = null;
}
};
$document.ready( function(){ wpWidgets.init(); } );
})(jQuery);