: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
control.expandedArgumentsQueue = [];
control.expanded.bind( function( expanded ) {
var args = control.expandedArgumentsQueue.shift();
args = $.extend( {}, control.defaultExpandedArguments, args );
control.onChangeExpanded( expanded, args );
control.altNotice = true;
api.Control.prototype.initialize.call( control, id, options );
* Embed a placeholder once the section is expanded. The full widget
* form content will be embedded once the control itself is expanded,
* and at this point the widget-added event will be triggered.
if ( ! control.section() ) {
control.embedWidgetControl();
api.section( control.section(), function( section ) {
var onExpanded = function( isExpanded ) {
control.embedWidgetControl();
section.expanded.unbind( onExpanded );
if ( section.expanded() ) {
section.expanded.bind( onExpanded );
* Embed the .widget element inside the li container.
embedWidgetControl: function() {
var control = this, widgetControl;
if ( control.widgetControlEmbedded ) {
control.widgetControlEmbedded = true;
widgetControl = $( control.params.widget_control );
control.container.append( widgetControl );
control._setupWideWidget();
control._setupControlToggle();
control._setupWidgetTitle();
control._setupReorderUI();
control._setupHighlightEffects();
control._setupUpdateUI();
control._setupRemoveUI();
* Embed the actual widget form inside of .widget-content and finally trigger the widget-added event.
embedWidgetContent: function() {
var control = this, widgetContent;
control.embedWidgetControl();
if ( control.widgetContentEmbedded ) {
control.widgetContentEmbedded = true;
// Update the notification container element now that the widget content has been embedded.
control.notifications.container = control.getNotificationsContainerElement();
control.notifications.render();
widgetContent = $( control.params.widget_content );
control.container.find( '.widget-content:first' ).append( widgetContent );
* Trigger widget-added event so that plugins can attach any event
* listeners and dynamic UI elements.
$( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] );
* Handle changes to the setting
_setupModel: function() {
var self = this, rememberSavedWidgetId;
// Remember saved widgets so we know which to trash (move to inactive widgets sidebar).
rememberSavedWidgetId = function() {
api.Widgets.savedWidgetIds[self.params.widget_id] = true;
api.bind( 'ready', rememberSavedWidgetId );
api.bind( 'saved', rememberSavedWidgetId );
this.isWidgetUpdating = false;
this.liveUpdateMode = true;
// Update widget whenever model changes.
this.setting.bind( function( to, from ) {
if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) {
self.updateWidget( { instance: to } );
* Add special behaviors for wide widget controls
_setupWideWidget: function() {
var self = this, $widgetInside, $widgetForm, $customizeSidebar,
$themeControlsContainer, positionWidget;
if ( ! this.params.is_wide || $( window ).width() <= 640 /* max-width breakpoint in customize-controls.css */ ) {
$widgetInside = this.container.find( '.widget-inside' );
$widgetForm = $widgetInside.find( '> .form' );
$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
this.container.addClass( 'wide-widget-control' );
this.container.find( '.form:first' ).css( {
'max-width': this.params.width,
'min-height': this.params.height
* Keep the widget-inside positioned so the top of fixed-positioned
* element is at the same top position as the widget-top. When the
* widget-top is scrolled out of view, keep the widget-top in view;
* likewise, don't allow the widget to drop off the bottom of the window.
* If a widget is too tall to fit in the window, don't let the height
* exceed the window height so that the contents of the widget control
* will become scrollable (overflow:auto).
positionWidget = function() {
var offsetTop = self.container.offset().top,
windowHeight = $( window ).height(),
formHeight = $widgetForm.outerHeight(),
$widgetInside.css( 'max-height', windowHeight );
0, // Prevent top from going off screen.
Math.max( offsetTop, 0 ), // Distance widget in panel is from top of screen.
windowHeight - formHeight // Flush up against bottom of screen.
$widgetInside.css( 'top', top );
$themeControlsContainer = $( '#customize-theme-controls' );
this.container.on( 'expand', function() {
$customizeSidebar.on( 'scroll', positionWidget );
$( window ).on( 'resize', positionWidget );
$themeControlsContainer.on( 'expanded collapsed', positionWidget );
this.container.on( 'collapsed', function() {
$customizeSidebar.off( 'scroll', positionWidget );
$( window ).off( 'resize', positionWidget );
$themeControlsContainer.off( 'expanded collapsed', positionWidget );
// Reposition whenever a sidebar's widgets are changed.
api.each( function( setting ) {
if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
setting.bind( function() {
if ( self.container.hasClass( 'expanded' ) ) {
* Show/hide the control when clicking on the form title, when clicking
_setupControlToggle: function() {
var self = this, $closeBtn;
this.container.find( '.widget-top' ).on( 'click', function( e ) {
var sidebarWidgetsControl = self.getSidebarWidgetsControl();
if ( sidebarWidgetsControl.isReordering ) {
self.expanded( ! self.expanded() );
$closeBtn = this.container.find( '.widget-control-close' );
$closeBtn.on( 'click', function() {
self.container.find( '.widget-top .widget-action:first' ).focus(); // Keyboard accessibility.
* Update the title of the form if a title field is entered
_setupWidgetTitle: function() {
var self = this, updateTitle;
updateTitle = function() {
var title = self.setting().title,
inWidgetTitle = self.container.find( '.in-widget-title' );
inWidgetTitle.text( ': ' + title );
inWidgetTitle.text( '' );
this.setting.bind( updateTitle );
* Set up the widget-reorder-nav
_setupReorderUI: function() {
var self = this, selectSidebarItem, $moveWidgetArea,
$reorderNav, updateAvailableSidebars, template;
* select the provided sidebar list item in the move widget area
selectSidebarItem = function( li ) {
li.siblings( '.selected' ).removeClass( 'selected' );
li.addClass( 'selected' );
var isSelfSidebar = ( li.data( 'id' ) === self.params.sidebar_id );
self.container.find( '.move-widget-btn' ).prop( 'disabled', isSelfSidebar );
* Add the widget reordering elements to the widget control
this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
template = _.template( api.Widgets.data.tpl.moveWidgetArea );
$moveWidgetArea = $( template( {
sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
this.container.find( '.widget-top' ).after( $moveWidgetArea );
* Update available sidebars when their rendered state changes
updateAvailableSidebars = function() {
var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem,
renderedSidebarCount = 0;
selfSidebarItem = $sidebarItems.filter( function(){
return $( this ).data( 'id' ) === self.params.sidebar_id;
$sidebarItems.each( function() {
sidebarId, sidebar, sidebarIsRendered;
sidebarId = li.data( 'id' );
sidebar = api.Widgets.registeredSidebars.get( sidebarId );
sidebarIsRendered = sidebar.get( 'is_rendered' );
li.toggle( sidebarIsRendered );
if ( sidebarIsRendered ) {
renderedSidebarCount += 1;
if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) {
selectSidebarItem( selfSidebarItem );
if ( renderedSidebarCount > 1 ) {
self.container.find( '.move-widget' ).show();
self.container.find( '.move-widget' ).hide();
updateAvailableSidebars();
api.Widgets.registeredSidebars.on( 'change:is_rendered', updateAvailableSidebars );
* Handle clicks for up/down/move on the reorder nav
$reorderNav = this.container.find( '.widget-reorder-nav' );
$reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() {
$( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' );
} ).on( 'click keypress', function( event ) {
if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
$( this ).trigger( 'focus' );
if ( $( this ).is( '.move-widget' ) ) {
self.toggleWidgetMoveArea();
var isMoveDown = $( this ).is( '.move-widget-down' ),
isMoveUp = $( this ).is( '.move-widget-up' ),
i = self.getWidgetSidebarPosition();
if ( ( isMoveUp && i === 0 ) || ( isMoveDown && i === self.getSidebarWidgetsControl().setting().length - 1 ) ) {
wp.a11y.speak( l10n.widgetMovedUp );
wp.a11y.speak( l10n.widgetMovedDown );
$( this ).trigger( 'focus' ); // Re-focus after the container was moved.
* Handle selecting a sidebar to move to
this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) {
if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
selectSidebarItem( $( this ) );
* Move widget to another sidebar
this.container.find( '.move-widget-btn' ).click( function() {
self.getSidebarWidgetsControl().toggleReordering( false );
var oldSidebarId = self.params.sidebar_id,
newSidebarId = self.container.find( '.widget-area-select li.selected' ).data( 'id' ),
oldSidebarWidgetsSetting, newSidebarWidgetsSetting,
oldSidebarWidgetIds, newSidebarWidgetIds, i;
oldSidebarWidgetsSetting = api( 'sidebars_widgets[' + oldSidebarId + ']' );
newSidebarWidgetsSetting = api( 'sidebars_widgets[' + newSidebarId + ']' );
oldSidebarWidgetIds = Array.prototype.slice.call( oldSidebarWidgetsSetting() );
newSidebarWidgetIds = Array.prototype.slice.call( newSidebarWidgetsSetting() );
i = self.getWidgetSidebarPosition();
oldSidebarWidgetIds.splice( i, 1 );
newSidebarWidgetIds.push( self.params.widget_id );
oldSidebarWidgetsSetting( oldSidebarWidgetIds );
newSidebarWidgetsSetting( newSidebarWidgetIds );
* Highlight widgets in preview when interacted with in the Customizer
_setupHighlightEffects: function() {
// Highlight whenever hovering or clicking over the form.
this.container.on( 'mouseenter click', function() {
self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
// Highlight when the setting is updated.
this.setting.bind( function() {
self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
* Set up event handlers for widget updating
_setupUpdateUI: function() {
var self = this, $widgetRoot, $widgetContent,
$saveBtn, updateWidgetDebounced, formSyncHandler;
$widgetRoot = this.container.find( '.widget:first' );
$widgetContent = $widgetRoot.find( '.widget-content:first' );
// Configure update button.
$saveBtn = this.container.find( '.widget-control-save' );
$saveBtn.val( l10n.saveBtnLabel );
$saveBtn.attr( 'title', l10n.saveBtnTooltip );
$saveBtn.removeClass( 'button-primary' );
$saveBtn.on( 'click', function( e ) {
self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
updateWidgetDebounced = _.debounce( function() {
// Trigger widget form update when hitting Enter within an input.
$widgetContent.on( 'keydown', 'input', function( e ) {
if ( 13 === e.which ) { // Enter.
self.updateWidget( { ignoreActiveElement: true } );
// Handle widgets that support live previews.
$widgetContent.on( 'change input propertychange', ':input', function( e ) {
if ( ! self.liveUpdateMode ) {
if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
// Remove loading indicators when the setting is saved and the preview updates.
this.setting.previewer.channel.bind( 'synced', function() {
self.container.removeClass( 'previewer-loading' );
api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
if ( updatedWidgetId === self.params.widget_id ) {
self.container.removeClass( 'previewer-loading' );
formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ];
$( document ).on( 'widget-synced', function( e, widget ) {
if ( $widgetRoot.is( widget ) ) {
formSyncHandler.apply( document, arguments );
* Update widget control to indicate whether it is currently rendered.
* Overrides api.Control.toggle()
* @param {boolean} active
* @param {function} args.completeCallback
onChangeActive: function ( active, args ) {
// Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments.
this.container.toggleClass( 'widget-rendered', active );
if ( args.completeCallback ) {
* Set up event handlers for widget removal
_setupRemoveUI: function() {
var self = this, $removeBtn, replaceDeleteWithRemove;
// Configure remove button.
$removeBtn = this.container.find( '.widget-control-remove' );
$removeBtn.on( 'click', function() {
// Find an adjacent element to add focus to when this widget goes away.
var $adjacentFocusTarget;
if ( self.container.next().is( '.customize-control-widget_form' ) ) {
$adjacentFocusTarget = self.container.next().find( '.widget-action:first' );
} else if ( self.container.prev().is( '.customize-control-widget_form' ) ) {
$adjacentFocusTarget = self.container.prev().find( '.widget-action:first' );
$adjacentFocusTarget = self.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );