angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

customize-widgets.js (71666B)


      1 /**
      2  * @output wp-admin/js/customize-widgets.js
      3  */
      4 
      5 /* global _wpCustomizeWidgetsSettings */
      6 (function( wp, $ ){
      7 
      8 	if ( ! wp || ! wp.customize ) { return; }
      9 
     10 	// Set up our namespace...
     11 	var api = wp.customize,
     12 		l10n;
     13 
     14 	/**
     15 	 * @namespace wp.customize.Widgets
     16 	 */
     17 	api.Widgets = api.Widgets || {};
     18 	api.Widgets.savedWidgetIds = {};
     19 
     20 	// Link settings.
     21 	api.Widgets.data = _wpCustomizeWidgetsSettings || {};
     22 	l10n = api.Widgets.data.l10n;
     23 
     24 	/**
     25 	 * wp.customize.Widgets.WidgetModel
     26 	 *
     27 	 * A single widget model.
     28 	 *
     29 	 * @class    wp.customize.Widgets.WidgetModel
     30 	 * @augments Backbone.Model
     31 	 */
     32 	api.Widgets.WidgetModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.WidgetModel.prototype */{
     33 		id: null,
     34 		temp_id: null,
     35 		classname: null,
     36 		control_tpl: null,
     37 		description: null,
     38 		is_disabled: null,
     39 		is_multi: null,
     40 		multi_number: null,
     41 		name: null,
     42 		id_base: null,
     43 		transport: null,
     44 		params: [],
     45 		width: null,
     46 		height: null,
     47 		search_matched: true
     48 	});
     49 
     50 	/**
     51 	 * wp.customize.Widgets.WidgetCollection
     52 	 *
     53 	 * Collection for widget models.
     54 	 *
     55 	 * @class    wp.customize.Widgets.WidgetCollection
     56 	 * @augments Backbone.Collection
     57 	 */
     58 	api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{
     59 		model: api.Widgets.WidgetModel,
     60 
     61 		// Controls searching on the current widget collection
     62 		// and triggers an update event.
     63 		doSearch: function( value ) {
     64 
     65 			// Don't do anything if we've already done this search.
     66 			// Useful because the search handler fires multiple times per keystroke.
     67 			if ( this.terms === value ) {
     68 				return;
     69 			}
     70 
     71 			// Updates terms with the value passed.
     72 			this.terms = value;
     73 
     74 			// If we have terms, run a search...
     75 			if ( this.terms.length > 0 ) {
     76 				this.search( this.terms );
     77 			}
     78 
     79 			// If search is blank, set all the widgets as they matched the search to reset the views.
     80 			if ( this.terms === '' ) {
     81 				this.each( function ( widget ) {
     82 					widget.set( 'search_matched', true );
     83 				} );
     84 			}
     85 		},
     86 
     87 		// Performs a search within the collection.
     88 		// @uses RegExp
     89 		search: function( term ) {
     90 			var match, haystack;
     91 
     92 			// Escape the term string for RegExp meta characters.
     93 			term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
     94 
     95 			// Consider spaces as word delimiters and match the whole string
     96 			// so matching terms can be combined.
     97 			term = term.replace( / /g, ')(?=.*' );
     98 			match = new RegExp( '^(?=.*' + term + ').+', 'i' );
     99 
    100 			this.each( function ( data ) {
    101 				haystack = [ data.get( 'name' ), data.get( 'description' ) ].join( ' ' );
    102 				data.set( 'search_matched', match.test( haystack ) );
    103 			} );
    104 		}
    105 	});
    106 	api.Widgets.availableWidgets = new api.Widgets.WidgetCollection( api.Widgets.data.availableWidgets );
    107 
    108 	/**
    109 	 * wp.customize.Widgets.SidebarModel
    110 	 *
    111 	 * A single sidebar model.
    112 	 *
    113 	 * @class    wp.customize.Widgets.SidebarModel
    114 	 * @augments Backbone.Model
    115 	 */
    116 	api.Widgets.SidebarModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.SidebarModel.prototype */{
    117 		after_title: null,
    118 		after_widget: null,
    119 		before_title: null,
    120 		before_widget: null,
    121 		'class': null,
    122 		description: null,
    123 		id: null,
    124 		name: null,
    125 		is_rendered: false
    126 	});
    127 
    128 	/**
    129 	 * wp.customize.Widgets.SidebarCollection
    130 	 *
    131 	 * Collection for sidebar models.
    132 	 *
    133 	 * @class    wp.customize.Widgets.SidebarCollection
    134 	 * @augments Backbone.Collection
    135 	 */
    136 	api.Widgets.SidebarCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.SidebarCollection.prototype */{
    137 		model: api.Widgets.SidebarModel
    138 	});
    139 	api.Widgets.registeredSidebars = new api.Widgets.SidebarCollection( api.Widgets.data.registeredSidebars );
    140 
    141 	api.Widgets.AvailableWidgetsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Widgets.AvailableWidgetsPanelView.prototype */{
    142 
    143 		el: '#available-widgets',
    144 
    145 		events: {
    146 			'input #widgets-search': 'search',
    147 			'focus .widget-tpl' : 'focus',
    148 			'click .widget-tpl' : '_submit',
    149 			'keypress .widget-tpl' : '_submit',
    150 			'keydown' : 'keyboardAccessible'
    151 		},
    152 
    153 		// Cache current selected widget.
    154 		selected: null,
    155 
    156 		// Cache sidebar control which has opened panel.
    157 		currentSidebarControl: null,
    158 		$search: null,
    159 		$clearResults: null,
    160 		searchMatchesCount: null,
    161 
    162 		/**
    163 		 * View class for the available widgets panel.
    164 		 *
    165 		 * @constructs wp.customize.Widgets.AvailableWidgetsPanelView
    166 		 * @augments   wp.Backbone.View
    167 		 */
    168 		initialize: function() {
    169 			var self = this;
    170 
    171 			this.$search = $( '#widgets-search' );
    172 
    173 			this.$clearResults = this.$el.find( '.clear-results' );
    174 
    175 			_.bindAll( this, 'close' );
    176 
    177 			this.listenTo( this.collection, 'change', this.updateList );
    178 
    179 			this.updateList();
    180 
    181 			// Set the initial search count to the number of available widgets.
    182 			this.searchMatchesCount = this.collection.length;
    183 
    184 			/*
    185 			 * If the available widgets panel is open and the customize controls
    186 			 * are interacted with (i.e. available widgets panel is blurred) then
    187 			 * close the available widgets panel. Also close on back button click.
    188 			 */
    189 			$( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) {
    190 				var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
    191 				if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
    192 					self.close();
    193 				}
    194 			} );
    195 
    196 			// Clear the search results and trigger an `input` event to fire a new search.
    197 			this.$clearResults.on( 'click', function() {
    198 				self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' );
    199 			} );
    200 
    201 			// Close the panel if the URL in the preview changes.
    202 			api.previewer.bind( 'url', this.close );
    203 		},
    204 
    205 		/**
    206 		 * Performs a search and handles selected widget.
    207 		 */
    208 		search: _.debounce( function( event ) {
    209 			var firstVisible;
    210 
    211 			this.collection.doSearch( event.target.value );
    212 			// Update the search matches count.
    213 			this.updateSearchMatchesCount();
    214 			// Announce how many search results.
    215 			this.announceSearchMatches();
    216 
    217 			// Remove a widget from being selected if it is no longer visible.
    218 			if ( this.selected && ! this.selected.is( ':visible' ) ) {
    219 				this.selected.removeClass( 'selected' );
    220 				this.selected = null;
    221 			}
    222 
    223 			// If a widget was selected but the filter value has been cleared out, clear selection.
    224 			if ( this.selected && ! event.target.value ) {
    225 				this.selected.removeClass( 'selected' );
    226 				this.selected = null;
    227 			}
    228 
    229 			// If a filter has been entered and a widget hasn't been selected, select the first one shown.
    230 			if ( ! this.selected && event.target.value ) {
    231 				firstVisible = this.$el.find( '> .widget-tpl:visible:first' );
    232 				if ( firstVisible.length ) {
    233 					this.select( firstVisible );
    234 				}
    235 			}
    236 
    237 			// Toggle the clear search results button.
    238 			if ( '' !== event.target.value ) {
    239 				this.$clearResults.addClass( 'is-visible' );
    240 			} else if ( '' === event.target.value ) {
    241 				this.$clearResults.removeClass( 'is-visible' );
    242 			}
    243 
    244 			// Set a CSS class on the search container when there are no search results.
    245 			if ( ! this.searchMatchesCount ) {
    246 				this.$el.addClass( 'no-widgets-found' );
    247 			} else {
    248 				this.$el.removeClass( 'no-widgets-found' );
    249 			}
    250 		}, 500 ),
    251 
    252 		/**
    253 		 * Updates the count of the available widgets that have the `search_matched` attribute.
    254  		 */
    255 		updateSearchMatchesCount: function() {
    256 			this.searchMatchesCount = this.collection.where({ search_matched: true }).length;
    257 		},
    258 
    259 		/**
    260 		 * Sends a message to the aria-live region to announce how many search results.
    261 		 */
    262 		announceSearchMatches: function() {
    263 			var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ;
    264 
    265 			if ( ! this.searchMatchesCount ) {
    266 				message = l10n.noWidgetsFound;
    267 			}
    268 
    269 			wp.a11y.speak( message );
    270 		},
    271 
    272 		/**
    273 		 * Changes visibility of available widgets.
    274  		 */
    275 		updateList: function() {
    276 			this.collection.each( function( widget ) {
    277 				var widgetTpl = $( '#widget-tpl-' + widget.id );
    278 				widgetTpl.toggle( widget.get( 'search_matched' ) && ! widget.get( 'is_disabled' ) );
    279 				if ( widget.get( 'is_disabled' ) && widgetTpl.is( this.selected ) ) {
    280 					this.selected = null;
    281 				}
    282 			} );
    283 		},
    284 
    285 		/**
    286 		 * Highlights a widget.
    287  		 */
    288 		select: function( widgetTpl ) {
    289 			this.selected = $( widgetTpl );
    290 			this.selected.siblings( '.widget-tpl' ).removeClass( 'selected' );
    291 			this.selected.addClass( 'selected' );
    292 		},
    293 
    294 		/**
    295 		 * Highlights a widget on focus.
    296 		 */
    297 		focus: function( event ) {
    298 			this.select( $( event.currentTarget ) );
    299 		},
    300 
    301 		/**
    302 		 * Handles submit for keypress and click on widget.
    303 		 */
    304 		_submit: function( event ) {
    305 			// Only proceed with keypress if it is Enter or Spacebar.
    306 			if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
    307 				return;
    308 			}
    309 
    310 			this.submit( $( event.currentTarget ) );
    311 		},
    312 
    313 		/**
    314 		 * Adds a selected widget to the sidebar.
    315  		 */
    316 		submit: function( widgetTpl ) {
    317 			var widgetId, widget, widgetFormControl;
    318 
    319 			if ( ! widgetTpl ) {
    320 				widgetTpl = this.selected;
    321 			}
    322 
    323 			if ( ! widgetTpl || ! this.currentSidebarControl ) {
    324 				return;
    325 			}
    326 
    327 			this.select( widgetTpl );
    328 
    329 			widgetId = $( this.selected ).data( 'widget-id' );
    330 			widget = this.collection.findWhere( { id: widgetId } );
    331 			if ( ! widget ) {
    332 				return;
    333 			}
    334 
    335 			widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) );
    336 			if ( widgetFormControl ) {
    337 				widgetFormControl.focus();
    338 			}
    339 
    340 			this.close();
    341 		},
    342 
    343 		/**
    344 		 * Opens the panel.
    345 		 */
    346 		open: function( sidebarControl ) {
    347 			this.currentSidebarControl = sidebarControl;
    348 
    349 			// Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens.
    350 			_( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
    351 				if ( control.params.is_wide ) {
    352 					control.collapseForm();
    353 				}
    354 			} );
    355 
    356 			if ( api.section.has( 'publish_settings' ) ) {
    357 				api.section( 'publish_settings' ).collapse();
    358 			}
    359 
    360 			$( 'body' ).addClass( 'adding-widget' );
    361 
    362 			this.$el.find( '.selected' ).removeClass( 'selected' );
    363 
    364 			// Reset search.
    365 			this.collection.doSearch( '' );
    366 
    367 			if ( ! api.settings.browser.mobile ) {
    368 				this.$search.trigger( 'focus' );
    369 			}
    370 		},
    371 
    372 		/**
    373 		 * Closes the panel.
    374 		 */
    375 		close: function( options ) {
    376 			options = options || {};
    377 
    378 			if ( options.returnFocus && this.currentSidebarControl ) {
    379 				this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
    380 			}
    381 
    382 			this.currentSidebarControl = null;
    383 			this.selected = null;
    384 
    385 			$( 'body' ).removeClass( 'adding-widget' );
    386 
    387 			this.$search.val( '' ).trigger( 'input' );
    388 		},
    389 
    390 		/**
    391 		 * Adds keyboard accessiblity to the panel.
    392 		 */
    393 		keyboardAccessible: function( event ) {
    394 			var isEnter = ( event.which === 13 ),
    395 				isEsc = ( event.which === 27 ),
    396 				isDown = ( event.which === 40 ),
    397 				isUp = ( event.which === 38 ),
    398 				isTab = ( event.which === 9 ),
    399 				isShift = ( event.shiftKey ),
    400 				selected = null,
    401 				firstVisible = this.$el.find( '> .widget-tpl:visible:first' ),
    402 				lastVisible = this.$el.find( '> .widget-tpl:visible:last' ),
    403 				isSearchFocused = $( event.target ).is( this.$search ),
    404 				isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' );
    405 
    406 			if ( isDown || isUp ) {
    407 				if ( isDown ) {
    408 					if ( isSearchFocused ) {
    409 						selected = firstVisible;
    410 					} else if ( this.selected && this.selected.nextAll( '.widget-tpl:visible' ).length !== 0 ) {
    411 						selected = this.selected.nextAll( '.widget-tpl:visible:first' );
    412 					}
    413 				} else if ( isUp ) {
    414 					if ( isSearchFocused ) {
    415 						selected = lastVisible;
    416 					} else if ( this.selected && this.selected.prevAll( '.widget-tpl:visible' ).length !== 0 ) {
    417 						selected = this.selected.prevAll( '.widget-tpl:visible:first' );
    418 					}
    419 				}
    420 
    421 				this.select( selected );
    422 
    423 				if ( selected ) {
    424 					selected.trigger( 'focus' );
    425 				} else {
    426 					this.$search.trigger( 'focus' );
    427 				}
    428 
    429 				return;
    430 			}
    431 
    432 			// If enter pressed but nothing entered, don't do anything.
    433 			if ( isEnter && ! this.$search.val() ) {
    434 				return;
    435 			}
    436 
    437 			if ( isEnter ) {
    438 				this.submit();
    439 			} else if ( isEsc ) {
    440 				this.close( { returnFocus: true } );
    441 			}
    442 
    443 			if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
    444 				this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
    445 				event.preventDefault();
    446 			}
    447 		}
    448 	});
    449 
    450 	/**
    451 	 * Handlers for the widget-synced event, organized by widget ID base.
    452 	 * Other widgets may provide their own update handlers by adding
    453 	 * listeners for the widget-synced event.
    454 	 *
    455 	 * @alias    wp.customize.Widgets.formSyncHandlers
    456 	 */
    457 	api.Widgets.formSyncHandlers = {
    458 
    459 		/**
    460 		 * @param {jQuery.Event} e
    461 		 * @param {jQuery} widget
    462 		 * @param {string} newForm
    463 		 */
    464 		rss: function( e, widget, newForm ) {
    465 			var oldWidgetError = widget.find( '.widget-error:first' ),
    466 				newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' );
    467 
    468 			if ( oldWidgetError.length && newWidgetError.length ) {
    469 				oldWidgetError.replaceWith( newWidgetError );
    470 			} else if ( oldWidgetError.length ) {
    471 				oldWidgetError.remove();
    472 			} else if ( newWidgetError.length ) {
    473 				widget.find( '.widget-content:first' ).prepend( newWidgetError );
    474 			}
    475 		}
    476 	};
    477 
    478 	api.Widgets.WidgetControl = api.Control.extend(/** @lends wp.customize.Widgets.WidgetControl.prototype */{
    479 		defaultExpandedArguments: {
    480 			duration: 'fast',
    481 			completeCallback: $.noop
    482 		},
    483 
    484 		/**
    485 		 * wp.customize.Widgets.WidgetControl
    486 		 *
    487 		 * Customizer control for widgets.
    488 		 * Note that 'widget_form' must match the WP_Widget_Form_Customize_Control::$type
    489 		 *
    490 		 * @since 4.1.0
    491 		 *
    492 		 * @constructs wp.customize.Widgets.WidgetControl
    493 		 * @augments   wp.customize.Control
    494 		 */
    495 		initialize: function( id, options ) {
    496 			var control = this;
    497 
    498 			control.widgetControlEmbedded = false;
    499 			control.widgetContentEmbedded = false;
    500 			control.expanded = new api.Value( false );
    501 			control.expandedArgumentsQueue = [];
    502 			control.expanded.bind( function( expanded ) {
    503 				var args = control.expandedArgumentsQueue.shift();
    504 				args = $.extend( {}, control.defaultExpandedArguments, args );
    505 				control.onChangeExpanded( expanded, args );
    506 			});
    507 			control.altNotice = true;
    508 
    509 			api.Control.prototype.initialize.call( control, id, options );
    510 		},
    511 
    512 		/**
    513 		 * Set up the control.
    514 		 *
    515 		 * @since 3.9.0
    516 		 */
    517 		ready: function() {
    518 			var control = this;
    519 
    520 			/*
    521 			 * Embed a placeholder once the section is expanded. The full widget
    522 			 * form content will be embedded once the control itself is expanded,
    523 			 * and at this point the widget-added event will be triggered.
    524 			 */
    525 			if ( ! control.section() ) {
    526 				control.embedWidgetControl();
    527 			} else {
    528 				api.section( control.section(), function( section ) {
    529 					var onExpanded = function( isExpanded ) {
    530 						if ( isExpanded ) {
    531 							control.embedWidgetControl();
    532 							section.expanded.unbind( onExpanded );
    533 						}
    534 					};
    535 					if ( section.expanded() ) {
    536 						onExpanded( true );
    537 					} else {
    538 						section.expanded.bind( onExpanded );
    539 					}
    540 				} );
    541 			}
    542 		},
    543 
    544 		/**
    545 		 * Embed the .widget element inside the li container.
    546 		 *
    547 		 * @since 4.4.0
    548 		 */
    549 		embedWidgetControl: function() {
    550 			var control = this, widgetControl;
    551 
    552 			if ( control.widgetControlEmbedded ) {
    553 				return;
    554 			}
    555 			control.widgetControlEmbedded = true;
    556 
    557 			widgetControl = $( control.params.widget_control );
    558 			control.container.append( widgetControl );
    559 
    560 			control._setupModel();
    561 			control._setupWideWidget();
    562 			control._setupControlToggle();
    563 
    564 			control._setupWidgetTitle();
    565 			control._setupReorderUI();
    566 			control._setupHighlightEffects();
    567 			control._setupUpdateUI();
    568 			control._setupRemoveUI();
    569 		},
    570 
    571 		/**
    572 		 * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event.
    573 		 *
    574 		 * @since 4.4.0
    575 		 */
    576 		embedWidgetContent: function() {
    577 			var control = this, widgetContent;
    578 
    579 			control.embedWidgetControl();
    580 			if ( control.widgetContentEmbedded ) {
    581 				return;
    582 			}
    583 			control.widgetContentEmbedded = true;
    584 
    585 			// Update the notification container element now that the widget content has been embedded.
    586 			control.notifications.container = control.getNotificationsContainerElement();
    587 			control.notifications.render();
    588 
    589 			widgetContent = $( control.params.widget_content );
    590 			control.container.find( '.widget-content:first' ).append( widgetContent );
    591 
    592 			/*
    593 			 * Trigger widget-added event so that plugins can attach any event
    594 			 * listeners and dynamic UI elements.
    595 			 */
    596 			$( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] );
    597 
    598 		},
    599 
    600 		/**
    601 		 * Handle changes to the setting
    602 		 */
    603 		_setupModel: function() {
    604 			var self = this, rememberSavedWidgetId;
    605 
    606 			// Remember saved widgets so we know which to trash (move to inactive widgets sidebar).
    607 			rememberSavedWidgetId = function() {
    608 				api.Widgets.savedWidgetIds[self.params.widget_id] = true;
    609 			};
    610 			api.bind( 'ready', rememberSavedWidgetId );
    611 			api.bind( 'saved', rememberSavedWidgetId );
    612 
    613 			this._updateCount = 0;
    614 			this.isWidgetUpdating = false;
    615 			this.liveUpdateMode = true;
    616 
    617 			// Update widget whenever model changes.
    618 			this.setting.bind( function( to, from ) {
    619 				if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) {
    620 					self.updateWidget( { instance: to } );
    621 				}
    622 			} );
    623 		},
    624 
    625 		/**
    626 		 * Add special behaviors for wide widget controls
    627 		 */
    628 		_setupWideWidget: function() {
    629 			var self = this, $widgetInside, $widgetForm, $customizeSidebar,
    630 				$themeControlsContainer, positionWidget;
    631 
    632 			if ( ! this.params.is_wide || $( window ).width() <= 640 /* max-width breakpoint in customize-controls.css */ ) {
    633 				return;
    634 			}
    635 
    636 			$widgetInside = this.container.find( '.widget-inside' );
    637 			$widgetForm = $widgetInside.find( '> .form' );
    638 			$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
    639 			this.container.addClass( 'wide-widget-control' );
    640 
    641 			this.container.find( '.form:first' ).css( {
    642 				'max-width': this.params.width,
    643 				'min-height': this.params.height
    644 			} );
    645 
    646 			/**
    647 			 * Keep the widget-inside positioned so the top of fixed-positioned
    648 			 * element is at the same top position as the widget-top. When the
    649 			 * widget-top is scrolled out of view, keep the widget-top in view;
    650 			 * likewise, don't allow the widget to drop off the bottom of the window.
    651 			 * If a widget is too tall to fit in the window, don't let the height
    652 			 * exceed the window height so that the contents of the widget control
    653 			 * will become scrollable (overflow:auto).
    654 			 */
    655 			positionWidget = function() {
    656 				var offsetTop = self.container.offset().top,
    657 					windowHeight = $( window ).height(),
    658 					formHeight = $widgetForm.outerHeight(),
    659 					top;
    660 				$widgetInside.css( 'max-height', windowHeight );
    661 				top = Math.max(
    662 					0, // Prevent top from going off screen.
    663 					Math.min(
    664 						Math.max( offsetTop, 0 ), // Distance widget in panel is from top of screen.
    665 						windowHeight - formHeight // Flush up against bottom of screen.
    666 					)
    667 				);
    668 				$widgetInside.css( 'top', top );
    669 			};
    670 
    671 			$themeControlsContainer = $( '#customize-theme-controls' );
    672 			this.container.on( 'expand', function() {
    673 				positionWidget();
    674 				$customizeSidebar.on( 'scroll', positionWidget );
    675 				$( window ).on( 'resize', positionWidget );
    676 				$themeControlsContainer.on( 'expanded collapsed', positionWidget );
    677 			} );
    678 			this.container.on( 'collapsed', function() {
    679 				$customizeSidebar.off( 'scroll', positionWidget );
    680 				$( window ).off( 'resize', positionWidget );
    681 				$themeControlsContainer.off( 'expanded collapsed', positionWidget );
    682 			} );
    683 
    684 			// Reposition whenever a sidebar's widgets are changed.
    685 			api.each( function( setting ) {
    686 				if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
    687 					setting.bind( function() {
    688 						if ( self.container.hasClass( 'expanded' ) ) {
    689 							positionWidget();
    690 						}
    691 					} );
    692 				}
    693 			} );
    694 		},
    695 
    696 		/**
    697 		 * Show/hide the control when clicking on the form title, when clicking
    698 		 * the close button
    699 		 */
    700 		_setupControlToggle: function() {
    701 			var self = this, $closeBtn;
    702 
    703 			this.container.find( '.widget-top' ).on( 'click', function( e ) {
    704 				e.preventDefault();
    705 				var sidebarWidgetsControl = self.getSidebarWidgetsControl();
    706 				if ( sidebarWidgetsControl.isReordering ) {
    707 					return;
    708 				}
    709 				self.expanded( ! self.expanded() );
    710 			} );
    711 
    712 			$closeBtn = this.container.find( '.widget-control-close' );
    713 			$closeBtn.on( 'click', function() {
    714 				self.collapse();
    715 				self.container.find( '.widget-top .widget-action:first' ).focus(); // Keyboard accessibility.
    716 			} );
    717 		},
    718 
    719 		/**
    720 		 * Update the title of the form if a title field is entered
    721 		 */
    722 		_setupWidgetTitle: function() {
    723 			var self = this, updateTitle;
    724 
    725 			updateTitle = function() {
    726 				var title = self.setting().title,
    727 					inWidgetTitle = self.container.find( '.in-widget-title' );
    728 
    729 				if ( title ) {
    730 					inWidgetTitle.text( ': ' + title );
    731 				} else {
    732 					inWidgetTitle.text( '' );
    733 				}
    734 			};
    735 			this.setting.bind( updateTitle );
    736 			updateTitle();
    737 		},
    738 
    739 		/**
    740 		 * Set up the widget-reorder-nav
    741 		 */
    742 		_setupReorderUI: function() {
    743 			var self = this, selectSidebarItem, $moveWidgetArea,
    744 				$reorderNav, updateAvailableSidebars, template;
    745 
    746 			/**
    747 			 * select the provided sidebar list item in the move widget area
    748 			 *
    749 			 * @param {jQuery} li
    750 			 */
    751 			selectSidebarItem = function( li ) {
    752 				li.siblings( '.selected' ).removeClass( 'selected' );
    753 				li.addClass( 'selected' );
    754 				var isSelfSidebar = ( li.data( 'id' ) === self.params.sidebar_id );
    755 				self.container.find( '.move-widget-btn' ).prop( 'disabled', isSelfSidebar );
    756 			};
    757 
    758 			/**
    759 			 * Add the widget reordering elements to the widget control
    760 			 */
    761 			this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
    762 
    763 
    764 			template = _.template( api.Widgets.data.tpl.moveWidgetArea );
    765 			$moveWidgetArea = $( template( {
    766 					sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
    767 				} )
    768 			);
    769 			this.container.find( '.widget-top' ).after( $moveWidgetArea );
    770 
    771 			/**
    772 			 * Update available sidebars when their rendered state changes
    773 			 */
    774 			updateAvailableSidebars = function() {
    775 				var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem,
    776 					renderedSidebarCount = 0;
    777 
    778 				selfSidebarItem = $sidebarItems.filter( function(){
    779 					return $( this ).data( 'id' ) === self.params.sidebar_id;
    780 				} );
    781 
    782 				$sidebarItems.each( function() {
    783 					var li = $( this ),
    784 						sidebarId, sidebar, sidebarIsRendered;
    785 
    786 					sidebarId = li.data( 'id' );
    787 					sidebar = api.Widgets.registeredSidebars.get( sidebarId );
    788 					sidebarIsRendered = sidebar.get( 'is_rendered' );
    789 
    790 					li.toggle( sidebarIsRendered );
    791 
    792 					if ( sidebarIsRendered ) {
    793 						renderedSidebarCount += 1;
    794 					}
    795 
    796 					if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) {
    797 						selectSidebarItem( selfSidebarItem );
    798 					}
    799 				} );
    800 
    801 				if ( renderedSidebarCount > 1 ) {
    802 					self.container.find( '.move-widget' ).show();
    803 				} else {
    804 					self.container.find( '.move-widget' ).hide();
    805 				}
    806 			};
    807 
    808 			updateAvailableSidebars();
    809 			api.Widgets.registeredSidebars.on( 'change:is_rendered', updateAvailableSidebars );
    810 
    811 			/**
    812 			 * Handle clicks for up/down/move on the reorder nav
    813 			 */
    814 			$reorderNav = this.container.find( '.widget-reorder-nav' );
    815 			$reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() {
    816 				$( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' );
    817 			} ).on( 'click keypress', function( event ) {
    818 				if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
    819 					return;
    820 				}
    821 				$( this ).trigger( 'focus' );
    822 
    823 				if ( $( this ).is( '.move-widget' ) ) {
    824 					self.toggleWidgetMoveArea();
    825 				} else {
    826 					var isMoveDown = $( this ).is( '.move-widget-down' ),
    827 						isMoveUp = $( this ).is( '.move-widget-up' ),
    828 						i = self.getWidgetSidebarPosition();
    829 
    830 					if ( ( isMoveUp && i === 0 ) || ( isMoveDown && i === self.getSidebarWidgetsControl().setting().length - 1 ) ) {
    831 						return;
    832 					}
    833 
    834 					if ( isMoveUp ) {
    835 						self.moveUp();
    836 						wp.a11y.speak( l10n.widgetMovedUp );
    837 					} else {
    838 						self.moveDown();
    839 						wp.a11y.speak( l10n.widgetMovedDown );
    840 					}
    841 
    842 					$( this ).trigger( 'focus' ); // Re-focus after the container was moved.
    843 				}
    844 			} );
    845 
    846 			/**
    847 			 * Handle selecting a sidebar to move to
    848 			 */
    849 			this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) {
    850 				if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
    851 					return;
    852 				}
    853 				event.preventDefault();
    854 				selectSidebarItem( $( this ) );
    855 			} );
    856 
    857 			/**
    858 			 * Move widget to another sidebar
    859 			 */
    860 			this.container.find( '.move-widget-btn' ).click( function() {
    861 				self.getSidebarWidgetsControl().toggleReordering( false );
    862 
    863 				var oldSidebarId = self.params.sidebar_id,
    864 					newSidebarId = self.container.find( '.widget-area-select li.selected' ).data( 'id' ),
    865 					oldSidebarWidgetsSetting, newSidebarWidgetsSetting,
    866 					oldSidebarWidgetIds, newSidebarWidgetIds, i;
    867 
    868 				oldSidebarWidgetsSetting = api( 'sidebars_widgets[' + oldSidebarId + ']' );
    869 				newSidebarWidgetsSetting = api( 'sidebars_widgets[' + newSidebarId + ']' );
    870 				oldSidebarWidgetIds = Array.prototype.slice.call( oldSidebarWidgetsSetting() );
    871 				newSidebarWidgetIds = Array.prototype.slice.call( newSidebarWidgetsSetting() );
    872 
    873 				i = self.getWidgetSidebarPosition();
    874 				oldSidebarWidgetIds.splice( i, 1 );
    875 				newSidebarWidgetIds.push( self.params.widget_id );
    876 
    877 				oldSidebarWidgetsSetting( oldSidebarWidgetIds );
    878 				newSidebarWidgetsSetting( newSidebarWidgetIds );
    879 
    880 				self.focus();
    881 			} );
    882 		},
    883 
    884 		/**
    885 		 * Highlight widgets in preview when interacted with in the Customizer
    886 		 */
    887 		_setupHighlightEffects: function() {
    888 			var self = this;
    889 
    890 			// Highlight whenever hovering or clicking over the form.
    891 			this.container.on( 'mouseenter click', function() {
    892 				self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
    893 			} );
    894 
    895 			// Highlight when the setting is updated.
    896 			this.setting.bind( function() {
    897 				self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
    898 			} );
    899 		},
    900 
    901 		/**
    902 		 * Set up event handlers for widget updating
    903 		 */
    904 		_setupUpdateUI: function() {
    905 			var self = this, $widgetRoot, $widgetContent,
    906 				$saveBtn, updateWidgetDebounced, formSyncHandler;
    907 
    908 			$widgetRoot = this.container.find( '.widget:first' );
    909 			$widgetContent = $widgetRoot.find( '.widget-content:first' );
    910 
    911 			// Configure update button.
    912 			$saveBtn = this.container.find( '.widget-control-save' );
    913 			$saveBtn.val( l10n.saveBtnLabel );
    914 			$saveBtn.attr( 'title', l10n.saveBtnTooltip );
    915 			$saveBtn.removeClass( 'button-primary' );
    916 			$saveBtn.on( 'click', function( e ) {
    917 				e.preventDefault();
    918 				self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
    919 			} );
    920 
    921 			updateWidgetDebounced = _.debounce( function() {
    922 				self.updateWidget();
    923 			}, 250 );
    924 
    925 			// Trigger widget form update when hitting Enter within an input.
    926 			$widgetContent.on( 'keydown', 'input', function( e ) {
    927 				if ( 13 === e.which ) { // Enter.
    928 					e.preventDefault();
    929 					self.updateWidget( { ignoreActiveElement: true } );
    930 				}
    931 			} );
    932 
    933 			// Handle widgets that support live previews.
    934 			$widgetContent.on( 'change input propertychange', ':input', function( e ) {
    935 				if ( ! self.liveUpdateMode ) {
    936 					return;
    937 				}
    938 				if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
    939 					updateWidgetDebounced();
    940 				}
    941 			} );
    942 
    943 			// Remove loading indicators when the setting is saved and the preview updates.
    944 			this.setting.previewer.channel.bind( 'synced', function() {
    945 				self.container.removeClass( 'previewer-loading' );
    946 			} );
    947 
    948 			api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
    949 				if ( updatedWidgetId === self.params.widget_id ) {
    950 					self.container.removeClass( 'previewer-loading' );
    951 				}
    952 			} );
    953 
    954 			formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ];
    955 			if ( formSyncHandler ) {
    956 				$( document ).on( 'widget-synced', function( e, widget ) {
    957 					if ( $widgetRoot.is( widget ) ) {
    958 						formSyncHandler.apply( document, arguments );
    959 					}
    960 				} );
    961 			}
    962 		},
    963 
    964 		/**
    965 		 * Update widget control to indicate whether it is currently rendered.
    966 		 *
    967 		 * Overrides api.Control.toggle()
    968 		 *
    969 		 * @since 4.1.0
    970 		 *
    971 		 * @param {boolean}   active
    972 		 * @param {Object}    args
    973 		 * @param {function}  args.completeCallback
    974 		 */
    975 		onChangeActive: function ( active, args ) {
    976 			// Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments.
    977 			this.container.toggleClass( 'widget-rendered', active );
    978 			if ( args.completeCallback ) {
    979 				args.completeCallback();
    980 			}
    981 		},
    982 
    983 		/**
    984 		 * Set up event handlers for widget removal
    985 		 */
    986 		_setupRemoveUI: function() {
    987 			var self = this, $removeBtn, replaceDeleteWithRemove;
    988 
    989 			// Configure remove button.
    990 			$removeBtn = this.container.find( '.widget-control-remove' );
    991 			$removeBtn.on( 'click', function() {
    992 				// Find an adjacent element to add focus to when this widget goes away.
    993 				var $adjacentFocusTarget;
    994 				if ( self.container.next().is( '.customize-control-widget_form' ) ) {
    995 					$adjacentFocusTarget = self.container.next().find( '.widget-action:first' );
    996 				} else if ( self.container.prev().is( '.customize-control-widget_form' ) ) {
    997 					$adjacentFocusTarget = self.container.prev().find( '.widget-action:first' );
    998 				} else {
    999 					$adjacentFocusTarget = self.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
   1000 				}
   1001 
   1002 				self.container.slideUp( function() {
   1003 					var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ),
   1004 						sidebarWidgetIds, i;
   1005 
   1006 					if ( ! sidebarsWidgetsControl ) {
   1007 						return;
   1008 					}
   1009 
   1010 					sidebarWidgetIds = sidebarsWidgetsControl.setting().slice();
   1011 					i = _.indexOf( sidebarWidgetIds, self.params.widget_id );
   1012 					if ( -1 === i ) {
   1013 						return;
   1014 					}
   1015 
   1016 					sidebarWidgetIds.splice( i, 1 );
   1017 					sidebarsWidgetsControl.setting( sidebarWidgetIds );
   1018 
   1019 					$adjacentFocusTarget.focus(); // Keyboard accessibility.
   1020 				} );
   1021 			} );
   1022 
   1023 			replaceDeleteWithRemove = function() {
   1024 				$removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete".
   1025 				$removeBtn.attr( 'title', l10n.removeBtnTooltip );
   1026 			};
   1027 
   1028 			if ( this.params.is_new ) {
   1029 				api.bind( 'saved', replaceDeleteWithRemove );
   1030 			} else {
   1031 				replaceDeleteWithRemove();
   1032 			}
   1033 		},
   1034 
   1035 		/**
   1036 		 * Find all inputs in a widget container that should be considered when
   1037 		 * comparing the loaded form with the sanitized form, whose fields will
   1038 		 * be aligned to copy the sanitized over. The elements returned by this
   1039 		 * are passed into this._getInputsSignature(), and they are iterated
   1040 		 * over when copying sanitized values over to the form loaded.
   1041 		 *
   1042 		 * @param {jQuery} container element in which to look for inputs
   1043 		 * @return {jQuery} inputs
   1044 		 * @private
   1045 		 */
   1046 		_getInputs: function( container ) {
   1047 			return $( container ).find( ':input[name]' );
   1048 		},
   1049 
   1050 		/**
   1051 		 * Iterate over supplied inputs and create a signature string for all of them together.
   1052 		 * This string can be used to compare whether or not the form has all of the same fields.
   1053 		 *
   1054 		 * @param {jQuery} inputs
   1055 		 * @return {string}
   1056 		 * @private
   1057 		 */
   1058 		_getInputsSignature: function( inputs ) {
   1059 			var inputsSignatures = _( inputs ).map( function( input ) {
   1060 				var $input = $( input ), signatureParts;
   1061 
   1062 				if ( $input.is( ':checkbox, :radio' ) ) {
   1063 					signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ), $input.prop( 'value' ) ];
   1064 				} else {
   1065 					signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ) ];
   1066 				}
   1067 
   1068 				return signatureParts.join( ',' );
   1069 			} );
   1070 
   1071 			return inputsSignatures.join( ';' );
   1072 		},
   1073 
   1074 		/**
   1075 		 * Get the state for an input depending on its type.
   1076 		 *
   1077 		 * @param {jQuery|Element} input
   1078 		 * @return {string|boolean|Array|*}
   1079 		 * @private
   1080 		 */
   1081 		_getInputState: function( input ) {
   1082 			input = $( input );
   1083 			if ( input.is( ':radio, :checkbox' ) ) {
   1084 				return input.prop( 'checked' );
   1085 			} else if ( input.is( 'select[multiple]' ) ) {
   1086 				return input.find( 'option:selected' ).map( function () {
   1087 					return $( this ).val();
   1088 				} ).get();
   1089 			} else {
   1090 				return input.val();
   1091 			}
   1092 		},
   1093 
   1094 		/**
   1095 		 * Update an input's state based on its type.
   1096 		 *
   1097 		 * @param {jQuery|Element} input
   1098 		 * @param {string|boolean|Array|*} state
   1099 		 * @private
   1100 		 */
   1101 		_setInputState: function ( input, state ) {
   1102 			input = $( input );
   1103 			if ( input.is( ':radio, :checkbox' ) ) {
   1104 				input.prop( 'checked', state );
   1105 			} else if ( input.is( 'select[multiple]' ) ) {
   1106 				if ( ! Array.isArray( state ) ) {
   1107 					state = [];
   1108 				} else {
   1109 					// Make sure all state items are strings since the DOM value is a string.
   1110 					state = _.map( state, function ( value ) {
   1111 						return String( value );
   1112 					} );
   1113 				}
   1114 				input.find( 'option' ).each( function () {
   1115 					$( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) );
   1116 				} );
   1117 			} else {
   1118 				input.val( state );
   1119 			}
   1120 		},
   1121 
   1122 		/***********************************************************************
   1123 		 * Begin public API methods
   1124 		 **********************************************************************/
   1125 
   1126 		/**
   1127 		 * @return {wp.customize.controlConstructor.sidebar_widgets[]}
   1128 		 */
   1129 		getSidebarWidgetsControl: function() {
   1130 			var settingId, sidebarWidgetsControl;
   1131 
   1132 			settingId = 'sidebars_widgets[' + this.params.sidebar_id + ']';
   1133 			sidebarWidgetsControl = api.control( settingId );
   1134 
   1135 			if ( ! sidebarWidgetsControl ) {
   1136 				return;
   1137 			}
   1138 
   1139 			return sidebarWidgetsControl;
   1140 		},
   1141 
   1142 		/**
   1143 		 * Submit the widget form via Ajax and get back the updated instance,
   1144 		 * along with the new widget control form to render.
   1145 		 *
   1146 		 * @param {Object} [args]
   1147 		 * @param {Object|null} [args.instance=null]  When the model changes, the instance is sent here; otherwise, the inputs from the form are used
   1148 		 * @param {Function|null} [args.complete=null]  Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success.
   1149 		 * @param {boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element.
   1150 		 */
   1151 		updateWidget: function( args ) {
   1152 			var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
   1153 				updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
   1154 
   1155 			// The updateWidget logic requires that the form fields to be fully present.
   1156 			self.embedWidgetContent();
   1157 
   1158 			args = $.extend( {
   1159 				instance: null,
   1160 				complete: null,
   1161 				ignoreActiveElement: false
   1162 			}, args );
   1163 
   1164 			instanceOverride = args.instance;
   1165 			completeCallback = args.complete;
   1166 
   1167 			this._updateCount += 1;
   1168 			updateNumber = this._updateCount;
   1169 
   1170 			$widgetRoot = this.container.find( '.widget:first' );
   1171 			$widgetContent = $widgetRoot.find( '.widget-content:first' );
   1172 
   1173 			// Remove a previous error message.
   1174 			$widgetContent.find( '.widget-error' ).remove();
   1175 
   1176 			this.container.addClass( 'widget-form-loading' );
   1177 			this.container.addClass( 'previewer-loading' );
   1178 			processing = api.state( 'processing' );
   1179 			processing( processing() + 1 );
   1180 
   1181 			if ( ! this.liveUpdateMode ) {
   1182 				this.container.addClass( 'widget-form-disabled' );
   1183 			}
   1184 
   1185 			params = {};
   1186 			params.action = 'update-widget';
   1187 			params.wp_customize = 'on';
   1188 			params.nonce = api.settings.nonce['update-widget'];
   1189 			params.customize_theme = api.settings.theme.stylesheet;
   1190 			params.customized = wp.customize.previewer.query().customized;
   1191 
   1192 			data = $.param( params );
   1193 			$inputs = this._getInputs( $widgetContent );
   1194 
   1195 			/*
   1196 			 * Store the value we're submitting in data so that when the response comes back,
   1197 			 * we know if it got sanitized; if there is no difference in the sanitized value,
   1198 			 * then we do not need to touch the UI and mess up the user's ongoing editing.
   1199 			 */
   1200 			$inputs.each( function() {
   1201 				$( this ).data( 'state' + updateNumber, self._getInputState( this ) );
   1202 			} );
   1203 
   1204 			if ( instanceOverride ) {
   1205 				data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } );
   1206 			} else {
   1207 				data += '&' + $inputs.serialize();
   1208 			}
   1209 			data += '&' + $widgetContent.find( '~ :input' ).serialize();
   1210 
   1211 			if ( this._previousUpdateRequest ) {
   1212 				this._previousUpdateRequest.abort();
   1213 			}
   1214 			jqxhr = $.post( wp.ajax.settings.url, data );
   1215 			this._previousUpdateRequest = jqxhr;
   1216 
   1217 			jqxhr.done( function( r ) {
   1218 				var message, sanitizedForm,	$sanitizedInputs, hasSameInputsInResponse,
   1219 					isLiveUpdateAborted = false;
   1220 
   1221 				// Check if the user is logged out.
   1222 				if ( '0' === r ) {
   1223 					api.previewer.preview.iframe.hide();
   1224 					api.previewer.login().done( function() {
   1225 						self.updateWidget( args );
   1226 						api.previewer.preview.iframe.show();
   1227 					} );
   1228 					return;
   1229 				}
   1230 
   1231 				// Check for cheaters.
   1232 				if ( '-1' === r ) {
   1233 					api.previewer.cheatin();
   1234 					return;
   1235 				}
   1236 
   1237 				if ( r.success ) {
   1238 					sanitizedForm = $( '<div>' + r.data.form + '</div>' );
   1239 					$sanitizedInputs = self._getInputs( sanitizedForm );
   1240 					hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs );
   1241 
   1242 					// Restore live update mode if sanitized fields are now aligned with the existing fields.
   1243 					if ( hasSameInputsInResponse && ! self.liveUpdateMode ) {
   1244 						self.liveUpdateMode = true;
   1245 						self.container.removeClass( 'widget-form-disabled' );
   1246 						self.container.find( 'input[name="savewidget"]' ).hide();
   1247 					}
   1248 
   1249 					// Sync sanitized field states to existing fields if they are aligned.
   1250 					if ( hasSameInputsInResponse && self.liveUpdateMode ) {
   1251 						$inputs.each( function( i ) {
   1252 							var $input = $( this ),
   1253 								$sanitizedInput = $( $sanitizedInputs[i] ),
   1254 								submittedState, sanitizedState,	canUpdateState;
   1255 
   1256 							submittedState = $input.data( 'state' + updateNumber );
   1257 							sanitizedState = self._getInputState( $sanitizedInput );
   1258 							$input.data( 'sanitized', sanitizedState );
   1259 
   1260 							canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) );
   1261 							if ( canUpdateState ) {
   1262 								self._setInputState( $input, sanitizedState );
   1263 							}
   1264 						} );
   1265 
   1266 						$( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] );
   1267 
   1268 					// Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled.
   1269 					} else if ( self.liveUpdateMode ) {
   1270 						self.liveUpdateMode = false;
   1271 						self.container.find( 'input[name="savewidget"]' ).show();
   1272 						isLiveUpdateAborted = true;
   1273 
   1274 					// Otherwise, replace existing form with the sanitized form.
   1275 					} else {
   1276 						$widgetContent.html( r.data.form );
   1277 
   1278 						self.container.removeClass( 'widget-form-disabled' );
   1279 
   1280 						$( document ).trigger( 'widget-updated', [ $widgetRoot ] );
   1281 					}
   1282 
   1283 					/**
   1284 					 * If the old instance is identical to the new one, there is nothing new
   1285 					 * needing to be rendered, and so we can preempt the event for the
   1286 					 * preview finishing loading.
   1287 					 */
   1288 					isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
   1289 					if ( isChanged ) {
   1290 						self.isWidgetUpdating = true; // Suppress triggering another updateWidget.
   1291 						self.setting( r.data.instance );
   1292 						self.isWidgetUpdating = false;
   1293 					} else {
   1294 						// No change was made, so stop the spinner now instead of when the preview would updates.
   1295 						self.container.removeClass( 'previewer-loading' );
   1296 					}
   1297 
   1298 					if ( completeCallback ) {
   1299 						completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } );
   1300 					}
   1301 				} else {
   1302 					// General error message.
   1303 					message = l10n.error;
   1304 
   1305 					if ( r.data && r.data.message ) {
   1306 						message = r.data.message;
   1307 					}
   1308 
   1309 					if ( completeCallback ) {
   1310 						completeCallback.call( self, message );
   1311 					} else {
   1312 						$widgetContent.prepend( '<p class="widget-error"><strong>' + message + '</strong></p>' );
   1313 					}
   1314 				}
   1315 			} );
   1316 
   1317 			jqxhr.fail( function( jqXHR, textStatus ) {
   1318 				if ( completeCallback ) {
   1319 					completeCallback.call( self, textStatus );
   1320 				}
   1321 			} );
   1322 
   1323 			jqxhr.always( function() {
   1324 				self.container.removeClass( 'widget-form-loading' );
   1325 
   1326 				$inputs.each( function() {
   1327 					$( this ).removeData( 'state' + updateNumber );
   1328 				} );
   1329 
   1330 				processing( processing() - 1 );
   1331 			} );
   1332 		},
   1333 
   1334 		/**
   1335 		 * Expand the accordion section containing a control
   1336 		 */
   1337 		expandControlSection: function() {
   1338 			api.Control.prototype.expand.call( this );
   1339 		},
   1340 
   1341 		/**
   1342 		 * @since 4.1.0
   1343 		 *
   1344 		 * @param {Boolean} expanded
   1345 		 * @param {Object} [params]
   1346 		 * @return {Boolean} False if state already applied.
   1347 		 */
   1348 		_toggleExpanded: api.Section.prototype._toggleExpanded,
   1349 
   1350 		/**
   1351 		 * @since 4.1.0
   1352 		 *
   1353 		 * @param {Object} [params]
   1354 		 * @return {Boolean} False if already expanded.
   1355 		 */
   1356 		expand: api.Section.prototype.expand,
   1357 
   1358 		/**
   1359 		 * Expand the widget form control
   1360 		 *
   1361 		 * @deprecated 4.1.0 Use this.expand() instead.
   1362 		 */
   1363 		expandForm: function() {
   1364 			this.expand();
   1365 		},
   1366 
   1367 		/**
   1368 		 * @since 4.1.0
   1369 		 *
   1370 		 * @param {Object} [params]
   1371 		 * @return {Boolean} False if already collapsed.
   1372 		 */
   1373 		collapse: api.Section.prototype.collapse,
   1374 
   1375 		/**
   1376 		 * Collapse the widget form control
   1377 		 *
   1378 		 * @deprecated 4.1.0 Use this.collapse() instead.
   1379 		 */
   1380 		collapseForm: function() {
   1381 			this.collapse();
   1382 		},
   1383 
   1384 		/**
   1385 		 * Expand or collapse the widget control
   1386 		 *
   1387 		 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
   1388 		 *
   1389 		 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
   1390 		 */
   1391 		toggleForm: function( showOrHide ) {
   1392 			if ( typeof showOrHide === 'undefined' ) {
   1393 				showOrHide = ! this.expanded();
   1394 			}
   1395 			this.expanded( showOrHide );
   1396 		},
   1397 
   1398 		/**
   1399 		 * Respond to change in the expanded state.
   1400 		 *
   1401 		 * @param {boolean} expanded
   1402 		 * @param {Object} args  merged on top of this.defaultActiveArguments
   1403 		 */
   1404 		onChangeExpanded: function ( expanded, args ) {
   1405 			var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn;
   1406 
   1407 			self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
   1408 			if ( expanded ) {
   1409 				self.embedWidgetContent();
   1410 			}
   1411 
   1412 			// If the expanded state is unchanged only manipulate container expanded states.
   1413 			if ( args.unchanged ) {
   1414 				if ( expanded ) {
   1415 					api.Control.prototype.expand.call( self, {
   1416 						completeCallback:  args.completeCallback
   1417 					});
   1418 				}
   1419 				return;
   1420 			}
   1421 
   1422 			$widget = this.container.find( 'div.widget:first' );
   1423 			$inside = $widget.find( '.widget-inside:first' );
   1424 			$toggleBtn = this.container.find( '.widget-top button.widget-action' );
   1425 
   1426 			expandControl = function() {
   1427 
   1428 				// Close all other widget controls before expanding this one.
   1429 				api.control.each( function( otherControl ) {
   1430 					if ( self.params.type === otherControl.params.type && self !== otherControl ) {
   1431 						otherControl.collapse();
   1432 					}
   1433 				} );
   1434 
   1435 				complete = function() {
   1436 					self.container.removeClass( 'expanding' );
   1437 					self.container.addClass( 'expanded' );
   1438 					$widget.addClass( 'open' );
   1439 					$toggleBtn.attr( 'aria-expanded', 'true' );
   1440 					self.container.trigger( 'expanded' );
   1441 				};
   1442 				if ( args.completeCallback ) {
   1443 					prevComplete = complete;
   1444 					complete = function () {
   1445 						prevComplete();
   1446 						args.completeCallback();
   1447 					};
   1448 				}
   1449 
   1450 				if ( self.params.is_wide ) {
   1451 					$inside.fadeIn( args.duration, complete );
   1452 				} else {
   1453 					$inside.slideDown( args.duration, complete );
   1454 				}
   1455 
   1456 				self.container.trigger( 'expand' );
   1457 				self.container.addClass( 'expanding' );
   1458 			};
   1459 
   1460 			if ( expanded ) {
   1461 				if ( api.section.has( self.section() ) ) {
   1462 					api.section( self.section() ).expand( {
   1463 						completeCallback: expandControl
   1464 					} );
   1465 				} else {
   1466 					expandControl();
   1467 				}
   1468 			} else {
   1469 				complete = function() {
   1470 					self.container.removeClass( 'collapsing' );
   1471 					self.container.removeClass( 'expanded' );
   1472 					$widget.removeClass( 'open' );
   1473 					$toggleBtn.attr( 'aria-expanded', 'false' );
   1474 					self.container.trigger( 'collapsed' );
   1475 				};
   1476 				if ( args.completeCallback ) {
   1477 					prevComplete = complete;
   1478 					complete = function () {
   1479 						prevComplete();
   1480 						args.completeCallback();
   1481 					};
   1482 				}
   1483 
   1484 				self.container.trigger( 'collapse' );
   1485 				self.container.addClass( 'collapsing' );
   1486 
   1487 				if ( self.params.is_wide ) {
   1488 					$inside.fadeOut( args.duration, complete );
   1489 				} else {
   1490 					$inside.slideUp( args.duration, function() {
   1491 						$widget.css( { width:'', margin:'' } );
   1492 						complete();
   1493 					} );
   1494 				}
   1495 			}
   1496 		},
   1497 
   1498 		/**
   1499 		 * Get the position (index) of the widget in the containing sidebar
   1500 		 *
   1501 		 * @return {number}
   1502 		 */
   1503 		getWidgetSidebarPosition: function() {
   1504 			var sidebarWidgetIds, position;
   1505 
   1506 			sidebarWidgetIds = this.getSidebarWidgetsControl().setting();
   1507 			position = _.indexOf( sidebarWidgetIds, this.params.widget_id );
   1508 
   1509 			if ( position === -1 ) {
   1510 				return;
   1511 			}
   1512 
   1513 			return position;
   1514 		},
   1515 
   1516 		/**
   1517 		 * Move widget up one in the sidebar
   1518 		 */
   1519 		moveUp: function() {
   1520 			this._moveWidgetByOne( -1 );
   1521 		},
   1522 
   1523 		/**
   1524 		 * Move widget up one in the sidebar
   1525 		 */
   1526 		moveDown: function() {
   1527 			this._moveWidgetByOne( 1 );
   1528 		},
   1529 
   1530 		/**
   1531 		 * @private
   1532 		 *
   1533 		 * @param {number} offset 1|-1
   1534 		 */
   1535 		_moveWidgetByOne: function( offset ) {
   1536 			var i, sidebarWidgetsSetting, sidebarWidgetIds,	adjacentWidgetId;
   1537 
   1538 			i = this.getWidgetSidebarPosition();
   1539 
   1540 			sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting;
   1541 			sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // Clone.
   1542 			adjacentWidgetId = sidebarWidgetIds[i + offset];
   1543 			sidebarWidgetIds[i + offset] = this.params.widget_id;
   1544 			sidebarWidgetIds[i] = adjacentWidgetId;
   1545 
   1546 			sidebarWidgetsSetting( sidebarWidgetIds );
   1547 		},
   1548 
   1549 		/**
   1550 		 * Toggle visibility of the widget move area
   1551 		 *
   1552 		 * @param {boolean} [showOrHide]
   1553 		 */
   1554 		toggleWidgetMoveArea: function( showOrHide ) {
   1555 			var self = this, $moveWidgetArea;
   1556 
   1557 			$moveWidgetArea = this.container.find( '.move-widget-area' );
   1558 
   1559 			if ( typeof showOrHide === 'undefined' ) {
   1560 				showOrHide = ! $moveWidgetArea.hasClass( 'active' );
   1561 			}
   1562 
   1563 			if ( showOrHide ) {
   1564 				// Reset the selected sidebar.
   1565 				$moveWidgetArea.find( '.selected' ).removeClass( 'selected' );
   1566 
   1567 				$moveWidgetArea.find( 'li' ).filter( function() {
   1568 					return $( this ).data( 'id' ) === self.params.sidebar_id;
   1569 				} ).addClass( 'selected' );
   1570 
   1571 				this.container.find( '.move-widget-btn' ).prop( 'disabled', true );
   1572 			}
   1573 
   1574 			$moveWidgetArea.toggleClass( 'active', showOrHide );
   1575 		},
   1576 
   1577 		/**
   1578 		 * Highlight the widget control and section
   1579 		 */
   1580 		highlightSectionAndControl: function() {
   1581 			var $target;
   1582 
   1583 			if ( this.container.is( ':hidden' ) ) {
   1584 				$target = this.container.closest( '.control-section' );
   1585 			} else {
   1586 				$target = this.container;
   1587 			}
   1588 
   1589 			$( '.highlighted' ).removeClass( 'highlighted' );
   1590 			$target.addClass( 'highlighted' );
   1591 
   1592 			setTimeout( function() {
   1593 				$target.removeClass( 'highlighted' );
   1594 			}, 500 );
   1595 		}
   1596 	} );
   1597 
   1598 	/**
   1599 	 * wp.customize.Widgets.WidgetsPanel
   1600 	 *
   1601 	 * Customizer panel containing the widget area sections.
   1602 	 *
   1603 	 * @since 4.4.0
   1604 	 *
   1605 	 * @class    wp.customize.Widgets.WidgetsPanel
   1606 	 * @augments wp.customize.Panel
   1607 	 */
   1608 	api.Widgets.WidgetsPanel = api.Panel.extend(/** @lends wp.customize.Widgets.WigetsPanel.prototype */{
   1609 
   1610 		/**
   1611 		 * Add and manage the display of the no-rendered-areas notice.
   1612 		 *
   1613 		 * @since 4.4.0
   1614 		 */
   1615 		ready: function () {
   1616 			var panel = this;
   1617 
   1618 			api.Panel.prototype.ready.call( panel );
   1619 
   1620 			panel.deferred.embedded.done(function() {
   1621 				var panelMetaContainer, noticeContainer, updateNotice, getActiveSectionCount, shouldShowNotice;
   1622 				panelMetaContainer = panel.container.find( '.panel-meta' );
   1623 
   1624 				// @todo This should use the Notifications API introduced to panels. See <https://core.trac.wordpress.org/ticket/38794>.
   1625 				noticeContainer = $( '<div></div>', {
   1626 					'class': 'no-widget-areas-rendered-notice'
   1627 				});
   1628 				panelMetaContainer.append( noticeContainer );
   1629 
   1630 				/**
   1631 				 * Get the number of active sections in the panel.
   1632 				 *
   1633 				 * @return {number} Number of active sidebar sections.
   1634 				 */
   1635 				getActiveSectionCount = function() {
   1636 					return _.filter( panel.sections(), function( section ) {
   1637 						return 'sidebar' === section.params.type && section.active();
   1638 					} ).length;
   1639 				};
   1640 
   1641 				/**
   1642 				 * Determine whether or not the notice should be displayed.
   1643 				 *
   1644 				 * @return {boolean}
   1645 				 */
   1646 				shouldShowNotice = function() {
   1647 					var activeSectionCount = getActiveSectionCount();
   1648 					if ( 0 === activeSectionCount ) {
   1649 						return true;
   1650 					} else {
   1651 						return activeSectionCount !== api.Widgets.data.registeredSidebars.length;
   1652 					}
   1653 				};
   1654 
   1655 				/**
   1656 				 * Update the notice.
   1657 				 *
   1658 				 * @return {void}
   1659 				 */
   1660 				updateNotice = function() {
   1661 					var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount;
   1662 					noticeContainer.empty();
   1663 
   1664 					registeredAreaCount = api.Widgets.data.registeredSidebars.length;
   1665 					if ( activeSectionCount !== registeredAreaCount ) {
   1666 
   1667 						if ( 0 !== activeSectionCount ) {
   1668 							nonRenderedAreaCount = registeredAreaCount - activeSectionCount;
   1669 							someRenderedMessage = l10n.someAreasShown[ nonRenderedAreaCount ];
   1670 						} else {
   1671 							someRenderedMessage = l10n.noAreasShown;
   1672 						}
   1673 						if ( someRenderedMessage ) {
   1674 							noticeContainer.append( $( '<p></p>', {
   1675 								text: someRenderedMessage
   1676 							} ) );
   1677 						}
   1678 
   1679 						noticeContainer.append( $( '<p></p>', {
   1680 							text: l10n.navigatePreview
   1681 						} ) );
   1682 					}
   1683 				};
   1684 				updateNotice();
   1685 
   1686 				/*
   1687 				 * Set the initial visibility state for rendered notice.
   1688 				 * Update the visibility of the notice whenever a reflow happens.
   1689 				 */
   1690 				noticeContainer.toggle( shouldShowNotice() );
   1691 				api.previewer.deferred.active.done( function () {
   1692 					noticeContainer.toggle( shouldShowNotice() );
   1693 				});
   1694 				api.bind( 'pane-contents-reflowed', function() {
   1695 					var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0;
   1696 					updateNotice();
   1697 					if ( shouldShowNotice() ) {
   1698 						noticeContainer.slideDown( duration );
   1699 					} else {
   1700 						noticeContainer.slideUp( duration );
   1701 					}
   1702 				});
   1703 			});
   1704 		},
   1705 
   1706 		/**
   1707 		 * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas).
   1708 		 *
   1709 		 * This ensures that the widgets panel appears even when there are no
   1710 		 * sidebars displayed on the URL currently being previewed.
   1711 		 *
   1712 		 * @since 4.4.0
   1713 		 *
   1714 		 * @return {boolean}
   1715 		 */
   1716 		isContextuallyActive: function() {
   1717 			var panel = this;
   1718 			return panel.active();
   1719 		}
   1720 	});
   1721 
   1722 	/**
   1723 	 * wp.customize.Widgets.SidebarSection
   1724 	 *
   1725 	 * Customizer section representing a widget area widget
   1726 	 *
   1727 	 * @since 4.1.0
   1728 	 *
   1729 	 * @class    wp.customize.Widgets.SidebarSection
   1730 	 * @augments wp.customize.Section
   1731 	 */
   1732 	api.Widgets.SidebarSection = api.Section.extend(/** @lends wp.customize.Widgets.SidebarSection.prototype */{
   1733 
   1734 		/**
   1735 		 * Sync the section's active state back to the Backbone model's is_rendered attribute
   1736 		 *
   1737 		 * @since 4.1.0
   1738 		 */
   1739 		ready: function () {
   1740 			var section = this, registeredSidebar;
   1741 			api.Section.prototype.ready.call( this );
   1742 			registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId );
   1743 			section.active.bind( function ( active ) {
   1744 				registeredSidebar.set( 'is_rendered', active );
   1745 			});
   1746 			registeredSidebar.set( 'is_rendered', section.active() );
   1747 		}
   1748 	});
   1749 
   1750 	/**
   1751 	 * wp.customize.Widgets.SidebarControl
   1752 	 *
   1753 	 * Customizer control for widgets.
   1754 	 * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type
   1755 	 *
   1756 	 * @since 3.9.0
   1757 	 *
   1758 	 * @class    wp.customize.Widgets.SidebarControl
   1759 	 * @augments wp.customize.Control
   1760 	 */
   1761 	api.Widgets.SidebarControl = api.Control.extend(/** @lends wp.customize.Widgets.SidebarControl.prototype */{
   1762 
   1763 		/**
   1764 		 * Set up the control
   1765 		 */
   1766 		ready: function() {
   1767 			this.$controlSection = this.container.closest( '.control-section' );
   1768 			this.$sectionContent = this.container.closest( '.accordion-section-content' );
   1769 
   1770 			this._setupModel();
   1771 			this._setupSortable();
   1772 			this._setupAddition();
   1773 			this._applyCardinalOrderClassNames();
   1774 		},
   1775 
   1776 		/**
   1777 		 * Update ordering of widget control forms when the setting is updated
   1778 		 */
   1779 		_setupModel: function() {
   1780 			var self = this;
   1781 
   1782 			this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
   1783 				var widgetFormControls, removedWidgetIds, priority;
   1784 
   1785 				removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
   1786 
   1787 				// Filter out any persistent widget IDs for widgets which have been deactivated.
   1788 				newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) {
   1789 					var parsedWidgetId = parseWidgetId( newWidgetId );
   1790 
   1791 					return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } );
   1792 				} );
   1793 
   1794 				widgetFormControls = _( newWidgetIds ).map( function( widgetId ) {
   1795 					var widgetFormControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
   1796 
   1797 					if ( ! widgetFormControl ) {
   1798 						widgetFormControl = self.addWidget( widgetId );
   1799 					}
   1800 
   1801 					return widgetFormControl;
   1802 				} );
   1803 
   1804 				// Sort widget controls to their new positions.
   1805 				widgetFormControls.sort( function( a, b ) {
   1806 					var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
   1807 						bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
   1808 					return aIndex - bIndex;
   1809 				});
   1810 
   1811 				priority = 0;
   1812 				_( widgetFormControls ).each( function ( control ) {
   1813 					control.priority( priority );
   1814 					control.section( self.section() );
   1815 					priority += 1;
   1816 				});
   1817 				self.priority( priority ); // Make sure sidebar control remains at end.
   1818 
   1819 				// Re-sort widget form controls (including widgets form other sidebars newly moved here).
   1820 				self._applyCardinalOrderClassNames();
   1821 
   1822 				// If the widget was dragged into the sidebar, make sure the sidebar_id param is updated.
   1823 				_( widgetFormControls ).each( function( widgetFormControl ) {
   1824 					widgetFormControl.params.sidebar_id = self.params.sidebar_id;
   1825 				} );
   1826 
   1827 				// Cleanup after widget removal.
   1828 				_( removedWidgetIds ).each( function( removedWidgetId ) {
   1829 
   1830 					// Using setTimeout so that when moving a widget to another sidebar,
   1831 					// the other sidebars_widgets settings get a chance to update.
   1832 					setTimeout( function() {
   1833 						var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase,
   1834 							widget, isPresentInAnotherSidebar = false;
   1835 
   1836 						// Check if the widget is in another sidebar.
   1837 						api.each( function( otherSetting ) {
   1838 							if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
   1839 								return;
   1840 							}
   1841 
   1842 							var otherSidebarWidgets = otherSetting(), i;
   1843 
   1844 							i = _.indexOf( otherSidebarWidgets, removedWidgetId );
   1845 							if ( -1 !== i ) {
   1846 								isPresentInAnotherSidebar = true;
   1847 							}
   1848 						} );
   1849 
   1850 						// If the widget is present in another sidebar, abort!
   1851 						if ( isPresentInAnotherSidebar ) {
   1852 							return;
   1853 						}
   1854 
   1855 						removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId );
   1856 
   1857 						// Detect if widget control was dragged to another sidebar.
   1858 						wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] );
   1859 
   1860 						// Delete any widget form controls for removed widgets.
   1861 						if ( removedControl && ! wasDraggedToAnotherSidebar ) {
   1862 							api.control.remove( removedControl.id );
   1863 							removedControl.container.remove();
   1864 						}
   1865 
   1866 						// Move widget to inactive widgets sidebar (move it to Trash) if has been previously saved.
   1867 						// This prevents the inactive widgets sidebar from overflowing with throwaway widgets.
   1868 						if ( api.Widgets.savedWidgetIds[removedWidgetId] ) {
   1869 							inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
   1870 							inactiveWidgets.push( removedWidgetId );
   1871 							api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() );
   1872 						}
   1873 
   1874 						// Make old single widget available for adding again.
   1875 						removedIdBase = parseWidgetId( removedWidgetId ).id_base;
   1876 						widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } );
   1877 						if ( widget && ! widget.get( 'is_multi' ) ) {
   1878 							widget.set( 'is_disabled', false );
   1879 						}
   1880 					} );
   1881 
   1882 				} );
   1883 			} );
   1884 		},
   1885 
   1886 		/**
   1887 		 * Allow widgets in sidebar to be re-ordered, and for the order to be previewed
   1888 		 */
   1889 		_setupSortable: function() {
   1890 			var self = this;
   1891 
   1892 			this.isReordering = false;
   1893 
   1894 			/**
   1895 			 * Update widget order setting when controls are re-ordered
   1896 			 */
   1897 			this.$sectionContent.sortable( {
   1898 				items: '> .customize-control-widget_form',
   1899 				handle: '.widget-top',
   1900 				axis: 'y',
   1901 				tolerance: 'pointer',
   1902 				connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
   1903 				update: function() {
   1904 					var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds;
   1905 
   1906 					widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) {
   1907 						return $( '#' + widgetContainerId ).find( ':input[name=widget-id]' ).val();
   1908 					} );
   1909 
   1910 					self.setting( widgetIds );
   1911 				}
   1912 			} );
   1913 
   1914 			/**
   1915 			 * Expand other Customizer sidebar section when dragging a control widget over it,
   1916 			 * allowing the control to be dropped into another section
   1917 			 */
   1918 			this.$controlSection.find( '.accordion-section-title' ).droppable({
   1919 				accept: '.customize-control-widget_form',
   1920 				over: function() {
   1921 					var section = api.section( self.section.get() );
   1922 					section.expand({
   1923 						allowMultiple: true, // Prevent the section being dragged from to be collapsed.
   1924 						completeCallback: function () {
   1925 							// @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed.
   1926 							api.section.each( function ( otherSection ) {
   1927 								if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
   1928 									otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
   1929 								}
   1930 							} );
   1931 						}
   1932 					});
   1933 				}
   1934 			});
   1935 
   1936 			/**
   1937 			 * Keyboard-accessible reordering
   1938 			 */
   1939 			this.container.find( '.reorder-toggle' ).on( 'click', function() {
   1940 				self.toggleReordering( ! self.isReordering );
   1941 			} );
   1942 		},
   1943 
   1944 		/**
   1945 		 * Set up UI for adding a new widget
   1946 		 */
   1947 		_setupAddition: function() {
   1948 			var self = this;
   1949 
   1950 			this.container.find( '.add-new-widget' ).on( 'click', function() {
   1951 				var addNewWidgetBtn = $( this );
   1952 
   1953 				if ( self.$sectionContent.hasClass( 'reordering' ) ) {
   1954 					return;
   1955 				}
   1956 
   1957 				if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
   1958 					addNewWidgetBtn.attr( 'aria-expanded', 'true' );
   1959 					api.Widgets.availableWidgetsPanel.open( self );
   1960 				} else {
   1961 					addNewWidgetBtn.attr( 'aria-expanded', 'false' );
   1962 					api.Widgets.availableWidgetsPanel.close();
   1963 				}
   1964 			} );
   1965 		},
   1966 
   1967 		/**
   1968 		 * Add classes to the widget_form controls to assist with styling
   1969 		 */
   1970 		_applyCardinalOrderClassNames: function() {
   1971 			var widgetControls = [];
   1972 			_.each( this.setting(), function ( widgetId ) {
   1973 				var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
   1974 				if ( widgetControl ) {
   1975 					widgetControls.push( widgetControl );
   1976 				}
   1977 			});
   1978 
   1979 			if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) {
   1980 				this.container.find( '.reorder-toggle' ).hide();
   1981 				return;
   1982 			} else {
   1983 				this.container.find( '.reorder-toggle' ).show();
   1984 			}
   1985 
   1986 			$( widgetControls ).each( function () {
   1987 				$( this.container )
   1988 					.removeClass( 'first-widget' )
   1989 					.removeClass( 'last-widget' )
   1990 					.find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
   1991 			});
   1992 
   1993 			_.first( widgetControls ).container
   1994 				.addClass( 'first-widget' )
   1995 				.find( '.move-widget-up' ).prop( 'tabIndex', -1 );
   1996 
   1997 			_.last( widgetControls ).container
   1998 				.addClass( 'last-widget' )
   1999 				.find( '.move-widget-down' ).prop( 'tabIndex', -1 );
   2000 		},
   2001 
   2002 
   2003 		/***********************************************************************
   2004 		 * Begin public API methods
   2005 		 **********************************************************************/
   2006 
   2007 		/**
   2008 		 * Enable/disable the reordering UI
   2009 		 *
   2010 		 * @param {boolean} showOrHide to enable/disable reordering
   2011 		 *
   2012 		 * @todo We should have a reordering state instead and rename this to onChangeReordering
   2013 		 */
   2014 		toggleReordering: function( showOrHide ) {
   2015 			var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ),
   2016 				reorderBtn = this.container.find( '.reorder-toggle' ),
   2017 				widgetsTitle = this.$sectionContent.find( '.widget-title' );
   2018 
   2019 			showOrHide = Boolean( showOrHide );
   2020 
   2021 			if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
   2022 				return;
   2023 			}
   2024 
   2025 			this.isReordering = showOrHide;
   2026 			this.$sectionContent.toggleClass( 'reordering', showOrHide );
   2027 
   2028 			if ( showOrHide ) {
   2029 				_( this.getWidgetFormControls() ).each( function( formControl ) {
   2030 					formControl.collapse();
   2031 				} );
   2032 
   2033 				addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
   2034 				reorderBtn.attr( 'aria-label', l10n.reorderLabelOff );
   2035 				wp.a11y.speak( l10n.reorderModeOn );
   2036 				// Hide widget titles while reordering: title is already in the reorder controls.
   2037 				widgetsTitle.attr( 'aria-hidden', 'true' );
   2038 			} else {
   2039 				addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' );
   2040 				reorderBtn.attr( 'aria-label', l10n.reorderLabelOn );
   2041 				wp.a11y.speak( l10n.reorderModeOff );
   2042 				widgetsTitle.attr( 'aria-hidden', 'false' );
   2043 			}
   2044 		},
   2045 
   2046 		/**
   2047 		 * Get the widget_form Customize controls associated with the current sidebar.
   2048 		 *
   2049 		 * @since 3.9.0
   2050 		 * @return {wp.customize.controlConstructor.widget_form[]}
   2051 		 */
   2052 		getWidgetFormControls: function() {
   2053 			var formControls = [];
   2054 
   2055 			_( this.setting() ).each( function( widgetId ) {
   2056 				var settingId = widgetIdToSettingId( widgetId ),
   2057 					formControl = api.control( settingId );
   2058 				if ( formControl ) {
   2059 					formControls.push( formControl );
   2060 				}
   2061 			} );
   2062 
   2063 			return formControls;
   2064 		},
   2065 
   2066 		/**
   2067 		 * @param {string} widgetId or an id_base for adding a previously non-existing widget.
   2068 		 * @return {Object|false} widget_form control instance, or false on error.
   2069 		 */
   2070 		addWidget: function( widgetId ) {
   2071 			var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
   2072 				parsedWidgetId = parseWidgetId( widgetId ),
   2073 				widgetNumber = parsedWidgetId.number,
   2074 				widgetIdBase = parsedWidgetId.id_base,
   2075 				widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ),
   2076 				settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting;
   2077 
   2078 			if ( ! widget ) {
   2079 				return false;
   2080 			}
   2081 
   2082 			if ( widgetNumber && ! widget.get( 'is_multi' ) ) {
   2083 				return false;
   2084 			}
   2085 
   2086 			// Set up new multi widget.
   2087 			if ( widget.get( 'is_multi' ) && ! widgetNumber ) {
   2088 				widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
   2089 				widgetNumber = widget.get( 'multi_number' );
   2090 			}
   2091 
   2092 			controlHtml = $( '#widget-tpl-' + widget.get( 'id' ) ).html().trim();
   2093 			if ( widget.get( 'is_multi' ) ) {
   2094 				controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) {
   2095 					return m.replace( /__i__|%i%/g, widgetNumber );
   2096 				} );
   2097 			} else {
   2098 				widget.set( 'is_disabled', true ); // Prevent single widget from being added again now.
   2099 			}
   2100 
   2101 			$widget = $( controlHtml );
   2102 
   2103 			controlContainer = $( '<li/>' )
   2104 				.addClass( 'customize-control' )
   2105 				.addClass( 'customize-control-' + controlType )
   2106 				.append( $widget );
   2107 
   2108 			// Remove icon which is visible inside the panel.
   2109 			controlContainer.find( '> .widget-icon' ).remove();
   2110 
   2111 			if ( widget.get( 'is_multi' ) ) {
   2112 				controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
   2113 				controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
   2114 			}
   2115 
   2116 			widgetId = controlContainer.find( '[name="widget-id"]' ).val();
   2117 
   2118 			controlContainer.hide(); // To be slid-down below.
   2119 
   2120 			settingId = 'widget_' + widget.get( 'id_base' );
   2121 			if ( widget.get( 'is_multi' ) ) {
   2122 				settingId += '[' + widgetNumber + ']';
   2123 			}
   2124 			controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
   2125 
   2126 			// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget).
   2127 			isExistingWidget = api.has( settingId );
   2128 			if ( ! isExistingWidget ) {
   2129 				settingArgs = {
   2130 					transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh',
   2131 					previewer: this.setting.previewer
   2132 				};
   2133 				setting = api.create( settingId, settingId, '', settingArgs );
   2134 				setting.set( {} ); // Mark dirty, changing from '' to {}.
   2135 			}
   2136 
   2137 			controlConstructor = api.controlConstructor[controlType];
   2138 			widgetFormControl = new controlConstructor( settingId, {
   2139 				settings: {
   2140 					'default': settingId
   2141 				},
   2142 				content: controlContainer,
   2143 				sidebar_id: self.params.sidebar_id,
   2144 				widget_id: widgetId,
   2145 				widget_id_base: widget.get( 'id_base' ),
   2146 				type: controlType,
   2147 				is_new: ! isExistingWidget,
   2148 				width: widget.get( 'width' ),
   2149 				height: widget.get( 'height' ),
   2150 				is_wide: widget.get( 'is_wide' )
   2151 			} );
   2152 			api.control.add( widgetFormControl );
   2153 
   2154 			// Make sure widget is removed from the other sidebars.
   2155 			api.each( function( otherSetting ) {
   2156 				if ( otherSetting.id === self.setting.id ) {
   2157 					return;
   2158 				}
   2159 
   2160 				if ( 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) ) {
   2161 					return;
   2162 				}
   2163 
   2164 				var otherSidebarWidgets = otherSetting().slice(),
   2165 					i = _.indexOf( otherSidebarWidgets, widgetId );
   2166 
   2167 				if ( -1 !== i ) {
   2168 					otherSidebarWidgets.splice( i );
   2169 					otherSetting( otherSidebarWidgets );
   2170 				}
   2171 			} );
   2172 
   2173 			// Add widget to this sidebar.
   2174 			sidebarWidgets = this.setting().slice();
   2175 			if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) {
   2176 				sidebarWidgets.push( widgetId );
   2177 				this.setting( sidebarWidgets );
   2178 			}
   2179 
   2180 			controlContainer.slideDown( function() {
   2181 				if ( isExistingWidget ) {
   2182 					widgetFormControl.updateWidget( {
   2183 						instance: widgetFormControl.setting()
   2184 					} );
   2185 				}
   2186 			} );
   2187 
   2188 			return widgetFormControl;
   2189 		}
   2190 	} );
   2191 
   2192 	// Register models for custom panel, section, and control types.
   2193 	$.extend( api.panelConstructor, {
   2194 		widgets: api.Widgets.WidgetsPanel
   2195 	});
   2196 	$.extend( api.sectionConstructor, {
   2197 		sidebar: api.Widgets.SidebarSection
   2198 	});
   2199 	$.extend( api.controlConstructor, {
   2200 		widget_form: api.Widgets.WidgetControl,
   2201 		sidebar_widgets: api.Widgets.SidebarControl
   2202 	});
   2203 
   2204 	/**
   2205 	 * Init Customizer for widgets.
   2206 	 */
   2207 	api.bind( 'ready', function() {
   2208 		// Set up the widgets panel.
   2209 		api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({
   2210 			collection: api.Widgets.availableWidgets
   2211 		});
   2212 
   2213 		// Highlight widget control.
   2214 		api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
   2215 
   2216 		// Open and focus widget control.
   2217 		api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
   2218 	} );
   2219 
   2220 	/**
   2221 	 * Highlight a widget control.
   2222 	 *
   2223 	 * @param {string} widgetId
   2224 	 */
   2225 	api.Widgets.highlightWidgetFormControl = function( widgetId ) {
   2226 		var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
   2227 
   2228 		if ( control ) {
   2229 			control.highlightSectionAndControl();
   2230 		}
   2231 	},
   2232 
   2233 	/**
   2234 	 * Focus a widget control.
   2235 	 *
   2236 	 * @param {string} widgetId
   2237 	 */
   2238 	api.Widgets.focusWidgetFormControl = function( widgetId ) {
   2239 		var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
   2240 
   2241 		if ( control ) {
   2242 			control.focus();
   2243 		}
   2244 	},
   2245 
   2246 	/**
   2247 	 * Given a widget control, find the sidebar widgets control that contains it.
   2248 	 * @param {string} widgetId
   2249 	 * @return {Object|null}
   2250 	 */
   2251 	api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) {
   2252 		var foundControl = null;
   2253 
   2254 		// @todo This can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl().
   2255 		api.control.each( function( control ) {
   2256 			if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) {
   2257 				foundControl = control;
   2258 			}
   2259 		} );
   2260 
   2261 		return foundControl;
   2262 	};
   2263 
   2264 	/**
   2265 	 * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it.
   2266 	 *
   2267 	 * @param {string} widgetId
   2268 	 * @return {Object|null}
   2269 	 */
   2270 	api.Widgets.getWidgetFormControlForWidget = function( widgetId ) {
   2271 		var foundControl = null;
   2272 
   2273 		// @todo We can just use widgetIdToSettingId() here.
   2274 		api.control.each( function( control ) {
   2275 			if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) {
   2276 				foundControl = control;
   2277 			}
   2278 		} );
   2279 
   2280 		return foundControl;
   2281 	};
   2282 
   2283 	/**
   2284 	 * Initialize Edit Menu button in Nav Menu widget.
   2285 	 */
   2286 	$( document ).on( 'widget-added', function( event, widgetContainer ) {
   2287 		var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton;
   2288 		parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() );
   2289 		if ( 'nav_menu' !== parsedWidgetId.id_base ) {
   2290 			return;
   2291 		}
   2292 		widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' );
   2293 		if ( ! widgetControl ) {
   2294 			return;
   2295 		}
   2296 		navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' );
   2297 		editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' );
   2298 		if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) {
   2299 			return;
   2300 		}
   2301 		navMenuSelect.on( 'change', function() {
   2302 			if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) {
   2303 				editMenuButton.parent().show();
   2304 			} else {
   2305 				editMenuButton.parent().hide();
   2306 			}
   2307 		});
   2308 		editMenuButton.on( 'click', function() {
   2309 			var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' );
   2310 			if ( section ) {
   2311 				focusConstructWithBreadcrumb( section, widgetControl );
   2312 			}
   2313 		} );
   2314 	} );
   2315 
   2316 	/**
   2317 	 * Focus (expand) one construct and then focus on another construct after the first is collapsed.
   2318 	 *
   2319 	 * This overrides the back button to serve the purpose of breadcrumb navigation.
   2320 	 *
   2321 	 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus.
   2322 	 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus.
   2323 	 */
   2324 	function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) {
   2325 		focusConstruct.focus();
   2326 		function onceCollapsed( isExpanded ) {
   2327 			if ( ! isExpanded ) {
   2328 				focusConstruct.expanded.unbind( onceCollapsed );
   2329 				returnConstruct.focus();
   2330 			}
   2331 		}
   2332 		focusConstruct.expanded.bind( onceCollapsed );
   2333 	}
   2334 
   2335 	/**
   2336 	 * @param {string} widgetId
   2337 	 * @return {Object}
   2338 	 */
   2339 	function parseWidgetId( widgetId ) {
   2340 		var matches, parsed = {
   2341 			number: null,
   2342 			id_base: null
   2343 		};
   2344 
   2345 		matches = widgetId.match( /^(.+)-(\d+)$/ );
   2346 		if ( matches ) {
   2347 			parsed.id_base = matches[1];
   2348 			parsed.number = parseInt( matches[2], 10 );
   2349 		} else {
   2350 			// Likely an old single widget.
   2351 			parsed.id_base = widgetId;
   2352 		}
   2353 
   2354 		return parsed;
   2355 	}
   2356 
   2357 	/**
   2358 	 * @param {string} widgetId
   2359 	 * @return {string} settingId
   2360 	 */
   2361 	function widgetIdToSettingId( widgetId ) {
   2362 		var parsed = parseWidgetId( widgetId ), settingId;
   2363 
   2364 		settingId = 'widget_' + parsed.id_base;
   2365 		if ( parsed.number ) {
   2366 			settingId += '[' + parsed.number + ']';
   2367 		}
   2368 
   2369 		return settingId;
   2370 	}
   2371 
   2372 })( window.wp, jQuery );