PageRenderTime 188ms CodeModel.GetById 38ms app.highlight 129ms RepoModel.GetById 1ms app.codeStats 1ms

/wp-admin/js/customize-widgets.js

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