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