]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-admin/js/customize-widgets.js
WordPress 4.4.1-scripts
[autoinstalls/wordpress.git] / wp-admin / js / customize-widgets.js
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: 'refresh',
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;
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                         $moveWidgetArea = $(
685                                 _.template( api.Widgets.data.tpl.moveWidgetArea, {
686                                         sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
687                                 } )
688                         );
689                         this.container.find( '.widget-top' ).after( $moveWidgetArea );
690
691                         /**
692                          * Update available sidebars when their rendered state changes
693                          */
694                         updateAvailableSidebars = function() {
695                                 var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem,
696                                         renderedSidebarCount = 0;
697
698                                 selfSidebarItem = $sidebarItems.filter( function(){
699                                         return $( this ).data( 'id' ) === self.params.sidebar_id;
700                                 } );
701
702                                 $sidebarItems.each( function() {
703                                         var li = $( this ),
704                                                 sidebarId, sidebar, sidebarIsRendered;
705
706                                         sidebarId = li.data( 'id' );
707                                         sidebar = api.Widgets.registeredSidebars.get( sidebarId );
708                                         sidebarIsRendered = sidebar.get( 'is_rendered' );
709
710                                         li.toggle( sidebarIsRendered );
711
712                                         if ( sidebarIsRendered ) {
713                                                 renderedSidebarCount += 1;
714                                         }
715
716                                         if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) {
717                                                 selectSidebarItem( selfSidebarItem );
718                                         }
719                                 } );
720
721                                 if ( renderedSidebarCount > 1 ) {
722                                         self.container.find( '.move-widget' ).show();
723                                 } else {
724                                         self.container.find( '.move-widget' ).hide();
725                                 }
726                         };
727
728                         updateAvailableSidebars();
729                         api.Widgets.registeredSidebars.on( 'change:is_rendered', updateAvailableSidebars );
730
731                         /**
732                          * Handle clicks for up/down/move on the reorder nav
733                          */
734                         $reorderNav = this.container.find( '.widget-reorder-nav' );
735                         $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() {
736                                 $( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' );
737                         } ).on( 'click keypress', function( event ) {
738                                 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
739                                         return;
740                                 }
741                                 $( this ).focus();
742
743                                 if ( $( this ).is( '.move-widget' ) ) {
744                                         self.toggleWidgetMoveArea();
745                                 } else {
746                                         var isMoveDown = $( this ).is( '.move-widget-down' ),
747                                                 isMoveUp = $( this ).is( '.move-widget-up' ),
748                                                 i = self.getWidgetSidebarPosition();
749
750                                         if ( ( isMoveUp && i === 0 ) || ( isMoveDown && i === self.getSidebarWidgetsControl().setting().length - 1 ) ) {
751                                                 return;
752                                         }
753
754                                         if ( isMoveUp ) {
755                                                 self.moveUp();
756                                                 wp.a11y.speak( l10n.widgetMovedUp );
757                                         } else {
758                                                 self.moveDown();
759                                                 wp.a11y.speak( l10n.widgetMovedDown );
760                                         }
761
762                                         $( this ).focus(); // re-focus after the container was moved
763                                 }
764                         } );
765
766                         /**
767                          * Handle selecting a sidebar to move to
768                          */
769                         this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) {
770                                 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
771                                         return;
772                                 }
773                                 event.preventDefault();
774                                 selectSidebarItem( $( this ) );
775                         } );
776
777                         /**
778                          * Move widget to another sidebar
779                          */
780                         this.container.find( '.move-widget-btn' ).click( function() {
781                                 self.getSidebarWidgetsControl().toggleReordering( false );
782
783                                 var oldSidebarId = self.params.sidebar_id,
784                                         newSidebarId = self.container.find( '.widget-area-select li.selected' ).data( 'id' ),
785                                         oldSidebarWidgetsSetting, newSidebarWidgetsSetting,
786                                         oldSidebarWidgetIds, newSidebarWidgetIds, i;
787
788                                 oldSidebarWidgetsSetting = api( 'sidebars_widgets[' + oldSidebarId + ']' );
789                                 newSidebarWidgetsSetting = api( 'sidebars_widgets[' + newSidebarId + ']' );
790                                 oldSidebarWidgetIds = Array.prototype.slice.call( oldSidebarWidgetsSetting() );
791                                 newSidebarWidgetIds = Array.prototype.slice.call( newSidebarWidgetsSetting() );
792
793                                 i = self.getWidgetSidebarPosition();
794                                 oldSidebarWidgetIds.splice( i, 1 );
795                                 newSidebarWidgetIds.push( self.params.widget_id );
796
797                                 oldSidebarWidgetsSetting( oldSidebarWidgetIds );
798                                 newSidebarWidgetsSetting( newSidebarWidgetIds );
799
800                                 self.focus();
801                         } );
802                 },
803
804                 /**
805                  * Highlight widgets in preview when interacted with in the Customizer
806                  */
807                 _setupHighlightEffects: function() {
808                         var self = this;
809
810                         // Highlight whenever hovering or clicking over the form
811                         this.container.on( 'mouseenter click', function() {
812                                 self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
813                         } );
814
815                         // Highlight when the setting is updated
816                         this.setting.bind( function() {
817                                 self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
818                         } );
819                 },
820
821                 /**
822                  * Set up event handlers for widget updating
823                  */
824                 _setupUpdateUI: function() {
825                         var self = this, $widgetRoot, $widgetContent,
826                                 $saveBtn, updateWidgetDebounced, formSyncHandler;
827
828                         $widgetRoot = this.container.find( '.widget:first' );
829                         $widgetContent = $widgetRoot.find( '.widget-content:first' );
830
831                         // Configure update button
832                         $saveBtn = this.container.find( '.widget-control-save' );
833                         $saveBtn.val( l10n.saveBtnLabel );
834                         $saveBtn.attr( 'title', l10n.saveBtnTooltip );
835                         $saveBtn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
836                         $saveBtn.on( 'click', function( e ) {
837                                 e.preventDefault();
838                                 self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
839                         } );
840
841                         updateWidgetDebounced = _.debounce( function() {
842                                 self.updateWidget();
843                         }, 250 );
844
845                         // Trigger widget form update when hitting Enter within an input
846                         $widgetContent.on( 'keydown', 'input', function( e ) {
847                                 if ( 13 === e.which ) { // Enter
848                                         e.preventDefault();
849                                         self.updateWidget( { ignoreActiveElement: true } );
850                                 }
851                         } );
852
853                         // Handle widgets that support live previews
854                         $widgetContent.on( 'change input propertychange', ':input', function( e ) {
855                                 if ( ! self.liveUpdateMode ) {
856                                         return;
857                                 }
858                                 if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
859                                         updateWidgetDebounced();
860                                 }
861                         } );
862
863                         // Remove loading indicators when the setting is saved and the preview updates
864                         this.setting.previewer.channel.bind( 'synced', function() {
865                                 self.container.removeClass( 'previewer-loading' );
866                         } );
867
868                         api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
869                                 if ( updatedWidgetId === self.params.widget_id ) {
870                                         self.container.removeClass( 'previewer-loading' );
871                                 }
872                         } );
873
874                         formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ];
875                         if ( formSyncHandler ) {
876                                 $( document ).on( 'widget-synced', function( e, widget ) {
877                                         if ( $widgetRoot.is( widget ) ) {
878                                                 formSyncHandler.apply( document, arguments );
879                                         }
880                                 } );
881                         }
882                 },
883
884                 /**
885                  * Update widget control to indicate whether it is currently rendered.
886                  *
887                  * Overrides api.Control.toggle()
888                  *
889                  * @since 4.1.0
890                  *
891                  * @param {Boolean}   active
892                  * @param {Object}    args
893                  * @param {Callback}  args.completeCallback
894                  */
895                 onChangeActive: function ( active, args ) {
896                         // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments
897                         this.container.toggleClass( 'widget-rendered', active );
898                         if ( args.completeCallback ) {
899                                 args.completeCallback();
900                         }
901                 },
902
903                 /**
904                  * Set up event handlers for widget removal
905                  */
906                 _setupRemoveUI: function() {
907                         var self = this, $removeBtn, replaceDeleteWithRemove;
908
909                         // Configure remove button
910                         $removeBtn = this.container.find( 'a.widget-control-remove' );
911                         $removeBtn.on( 'click', function( e ) {
912                                 e.preventDefault();
913
914                                 // Find an adjacent element to add focus to when this widget goes away
915                                 var $adjacentFocusTarget;
916                                 if ( self.container.next().is( '.customize-control-widget_form' ) ) {
917                                         $adjacentFocusTarget = self.container.next().find( '.widget-action:first' );
918                                 } else if ( self.container.prev().is( '.customize-control-widget_form' ) ) {
919                                         $adjacentFocusTarget = self.container.prev().find( '.widget-action:first' );
920                                 } else {
921                                         $adjacentFocusTarget = self.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
922                                 }
923
924                                 self.container.slideUp( function() {
925                                         var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ),
926                                                 sidebarWidgetIds, i;
927
928                                         if ( ! sidebarsWidgetsControl ) {
929                                                 return;
930                                         }
931
932                                         sidebarWidgetIds = sidebarsWidgetsControl.setting().slice();
933                                         i = _.indexOf( sidebarWidgetIds, self.params.widget_id );
934                                         if ( -1 === i ) {
935                                                 return;
936                                         }
937
938                                         sidebarWidgetIds.splice( i, 1 );
939                                         sidebarsWidgetsControl.setting( sidebarWidgetIds );
940
941                                         $adjacentFocusTarget.focus(); // keyboard accessibility
942                                 } );
943                         } );
944
945                         replaceDeleteWithRemove = function() {
946                                 $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Delete"
947                                 $removeBtn.attr( 'title', l10n.removeBtnTooltip );
948                         };
949
950                         if ( this.params.is_new ) {
951                                 api.bind( 'saved', replaceDeleteWithRemove );
952                         } else {
953                                 replaceDeleteWithRemove();
954                         }
955                 },
956
957                 /**
958                  * Find all inputs in a widget container that should be considered when
959                  * comparing the loaded form with the sanitized form, whose fields will
960                  * be aligned to copy the sanitized over. The elements returned by this
961                  * are passed into this._getInputsSignature(), and they are iterated
962                  * over when copying sanitized values over to the the form loaded.
963                  *
964                  * @param {jQuery} container element in which to look for inputs
965                  * @returns {jQuery} inputs
966                  * @private
967                  */
968                 _getInputs: function( container ) {
969                         return $( container ).find( ':input[name]' );
970                 },
971
972                 /**
973                  * Iterate over supplied inputs and create a signature string for all of them together.
974                  * This string can be used to compare whether or not the form has all of the same fields.
975                  *
976                  * @param {jQuery} inputs
977                  * @returns {string}
978                  * @private
979                  */
980                 _getInputsSignature: function( inputs ) {
981                         var inputsSignatures = _( inputs ).map( function( input ) {
982                                 var $input = $( input ), signatureParts;
983
984                                 if ( $input.is( ':checkbox, :radio' ) ) {
985                                         signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ), $input.prop( 'value' ) ];
986                                 } else {
987                                         signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ) ];
988                                 }
989
990                                 return signatureParts.join( ',' );
991                         } );
992
993                         return inputsSignatures.join( ';' );
994                 },
995
996                 /**
997                  * Get the state for an input depending on its type.
998                  *
999                  * @param {jQuery|Element} input
1000                  * @returns {string|boolean|array|*}
1001                  * @private
1002                  */
1003                 _getInputState: function( input ) {
1004                         input = $( input );
1005                         if ( input.is( ':radio, :checkbox' ) ) {
1006                                 return input.prop( 'checked' );
1007                         } else if ( input.is( 'select[multiple]' ) ) {
1008                                 return input.find( 'option:selected' ).map( function () {
1009                                         return $( this ).val();
1010                                 } ).get();
1011                         } else {
1012                                 return input.val();
1013                         }
1014                 },
1015
1016                 /**
1017                  * Update an input's state based on its type.
1018                  *
1019                  * @param {jQuery|Element} input
1020                  * @param {string|boolean|array|*} state
1021                  * @private
1022                  */
1023                 _setInputState: function ( input, state ) {
1024                         input = $( input );
1025                         if ( input.is( ':radio, :checkbox' ) ) {
1026                                 input.prop( 'checked', state );
1027                         } else if ( input.is( 'select[multiple]' ) ) {
1028                                 if ( ! $.isArray( state ) ) {
1029                                         state = [];
1030                                 } else {
1031                                         // Make sure all state items are strings since the DOM value is a string
1032                                         state = _.map( state, function ( value ) {
1033                                                 return String( value );
1034                                         } );
1035                                 }
1036                                 input.find( 'option' ).each( function () {
1037                                         $( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) );
1038                                 } );
1039                         } else {
1040                                 input.val( state );
1041                         }
1042                 },
1043
1044                 /***********************************************************************
1045                  * Begin public API methods
1046                  **********************************************************************/
1047
1048                 /**
1049                  * @return {wp.customize.controlConstructor.sidebar_widgets[]}
1050                  */
1051                 getSidebarWidgetsControl: function() {
1052                         var settingId, sidebarWidgetsControl;
1053
1054                         settingId = 'sidebars_widgets[' + this.params.sidebar_id + ']';
1055                         sidebarWidgetsControl = api.control( settingId );
1056
1057                         if ( ! sidebarWidgetsControl ) {
1058                                 return;
1059                         }
1060
1061                         return sidebarWidgetsControl;
1062                 },
1063
1064                 /**
1065                  * Submit the widget form via Ajax and get back the updated instance,
1066                  * along with the new widget control form to render.
1067                  *
1068                  * @param {object} [args]
1069                  * @param {Object|null} [args.instance=null]  When the model changes, the instance is sent here; otherwise, the inputs from the form are used
1070                  * @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.
1071                  * @param {Boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element.
1072                  */
1073                 updateWidget: function( args ) {
1074                         var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
1075                                 updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
1076
1077                         // The updateWidget logic requires that the form fields to be fully present.
1078                         self.embedWidgetContent();
1079
1080                         args = $.extend( {
1081                                 instance: null,
1082                                 complete: null,
1083                                 ignoreActiveElement: false
1084                         }, args );
1085
1086                         instanceOverride = args.instance;
1087                         completeCallback = args.complete;
1088
1089                         this._updateCount += 1;
1090                         updateNumber = this._updateCount;
1091
1092                         $widgetRoot = this.container.find( '.widget:first' );
1093                         $widgetContent = $widgetRoot.find( '.widget-content:first' );
1094
1095                         // Remove a previous error message
1096                         $widgetContent.find( '.widget-error' ).remove();
1097
1098                         this.container.addClass( 'widget-form-loading' );
1099                         this.container.addClass( 'previewer-loading' );
1100                         processing = api.state( 'processing' );
1101                         processing( processing() + 1 );
1102
1103                         if ( ! this.liveUpdateMode ) {
1104                                 this.container.addClass( 'widget-form-disabled' );
1105                         }
1106
1107                         params = {};
1108                         params.action = 'update-widget';
1109                         params.wp_customize = 'on';
1110                         params.nonce = api.Widgets.data.nonce;
1111                         params.theme = api.settings.theme.stylesheet;
1112                         params.customized = wp.customize.previewer.query().customized;
1113
1114                         data = $.param( params );
1115                         $inputs = this._getInputs( $widgetContent );
1116
1117                         // Store the value we're submitting in data so that when the response comes back,
1118                         // we know if it got sanitized; if there is no difference in the sanitized value,
1119                         // then we do not need to touch the UI and mess up the user's ongoing editing.
1120                         $inputs.each( function() {
1121                                 $( this ).data( 'state' + updateNumber, self._getInputState( this ) );
1122                         } );
1123
1124                         if ( instanceOverride ) {
1125                                 data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } );
1126                         } else {
1127                                 data += '&' + $inputs.serialize();
1128                         }
1129                         data += '&' + $widgetContent.find( '~ :input' ).serialize();
1130
1131                         if ( this._previousUpdateRequest ) {
1132                                 this._previousUpdateRequest.abort();
1133                         }
1134                         jqxhr = $.post( wp.ajax.settings.url, data );
1135                         this._previousUpdateRequest = jqxhr;
1136
1137                         jqxhr.done( function( r ) {
1138                                 var message, sanitizedForm,     $sanitizedInputs, hasSameInputsInResponse,
1139                                         isLiveUpdateAborted = false;
1140
1141                                 // Check if the user is logged out.
1142                                 if ( '0' === r ) {
1143                                         api.previewer.preview.iframe.hide();
1144                                         api.previewer.login().done( function() {
1145                                                 self.updateWidget( args );
1146                                                 api.previewer.preview.iframe.show();
1147                                         } );
1148                                         return;
1149                                 }
1150
1151                                 // Check for cheaters.
1152                                 if ( '-1' === r ) {
1153                                         api.previewer.cheatin();
1154                                         return;
1155                                 }
1156
1157                                 if ( r.success ) {
1158                                         sanitizedForm = $( '<div>' + r.data.form + '</div>' );
1159                                         $sanitizedInputs = self._getInputs( sanitizedForm );
1160                                         hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs );
1161
1162                                         // Restore live update mode if sanitized fields are now aligned with the existing fields
1163                                         if ( hasSameInputsInResponse && ! self.liveUpdateMode ) {
1164                                                 self.liveUpdateMode = true;
1165                                                 self.container.removeClass( 'widget-form-disabled' );
1166                                                 self.container.find( 'input[name="savewidget"]' ).hide();
1167                                         }
1168
1169                                         // Sync sanitized field states to existing fields if they are aligned
1170                                         if ( hasSameInputsInResponse && self.liveUpdateMode ) {
1171                                                 $inputs.each( function( i ) {
1172                                                         var $input = $( this ),
1173                                                                 $sanitizedInput = $( $sanitizedInputs[i] ),
1174                                                                 submittedState, sanitizedState, canUpdateState;
1175
1176                                                         submittedState = $input.data( 'state' + updateNumber );
1177                                                         sanitizedState = self._getInputState( $sanitizedInput );
1178                                                         $input.data( 'sanitized', sanitizedState );
1179
1180                                                         canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) );
1181                                                         if ( canUpdateState ) {
1182                                                                 self._setInputState( $input, sanitizedState );
1183                                                         }
1184                                                 } );
1185
1186                                                 $( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] );
1187
1188                                         // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled
1189                                         } else if ( self.liveUpdateMode ) {
1190                                                 self.liveUpdateMode = false;
1191                                                 self.container.find( 'input[name="savewidget"]' ).show();
1192                                                 isLiveUpdateAborted = true;
1193
1194                                         // Otherwise, replace existing form with the sanitized form
1195                                         } else {
1196                                                 $widgetContent.html( r.data.form );
1197
1198                                                 self.container.removeClass( 'widget-form-disabled' );
1199
1200                                                 $( document ).trigger( 'widget-updated', [ $widgetRoot ] );
1201                                         }
1202
1203                                         /**
1204                                          * If the old instance is identical to the new one, there is nothing new
1205                                          * needing to be rendered, and so we can preempt the event for the
1206                                          * preview finishing loading.
1207                                          */
1208                                         isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
1209                                         if ( isChanged ) {
1210                                                 self.isWidgetUpdating = true; // suppress triggering another updateWidget
1211                                                 self.setting( r.data.instance );
1212                                                 self.isWidgetUpdating = false;
1213                                         } else {
1214                                                 // no change was made, so stop the spinner now instead of when the preview would updates
1215                                                 self.container.removeClass( 'previewer-loading' );
1216                                         }
1217
1218                                         if ( completeCallback ) {
1219                                                 completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } );
1220                                         }
1221                                 } else {
1222                                         // General error message
1223                                         message = l10n.error;
1224
1225                                         if ( r.data && r.data.message ) {
1226                                                 message = r.data.message;
1227                                         }
1228
1229                                         if ( completeCallback ) {
1230                                                 completeCallback.call( self, message );
1231                                         } else {
1232                                                 $widgetContent.prepend( '<p class="widget-error"><strong>' + message + '</strong></p>' );
1233                                         }
1234                                 }
1235                         } );
1236
1237                         jqxhr.fail( function( jqXHR, textStatus ) {
1238                                 if ( completeCallback ) {
1239                                         completeCallback.call( self, textStatus );
1240                                 }
1241                         } );
1242
1243                         jqxhr.always( function() {
1244                                 self.container.removeClass( 'widget-form-loading' );
1245
1246                                 $inputs.each( function() {
1247                                         $( this ).removeData( 'state' + updateNumber );
1248                                 } );
1249
1250                                 processing( processing() - 1 );
1251                         } );
1252                 },
1253
1254                 /**
1255                  * Expand the accordion section containing a control
1256                  */
1257                 expandControlSection: function() {
1258                         api.Control.prototype.expand.call( this );
1259                 },
1260
1261                 /**
1262                  * @since 4.1.0
1263                  *
1264                  * @param {Boolean} expanded
1265                  * @param {Object} [params]
1266                  * @returns {Boolean} false if state already applied
1267                  */
1268                 _toggleExpanded: api.Section.prototype._toggleExpanded,
1269
1270                 /**
1271                  * @since 4.1.0
1272                  *
1273                  * @param {Object} [params]
1274                  * @returns {Boolean} false if already expanded
1275                  */
1276                 expand: api.Section.prototype.expand,
1277
1278                 /**
1279                  * Expand the widget form control
1280                  *
1281                  * @deprecated 4.1.0 Use this.expand() instead.
1282                  */
1283                 expandForm: function() {
1284                         this.expand();
1285                 },
1286
1287                 /**
1288                  * @since 4.1.0
1289                  *
1290                  * @param {Object} [params]
1291                  * @returns {Boolean} false if already collapsed
1292                  */
1293                 collapse: api.Section.prototype.collapse,
1294
1295                 /**
1296                  * Collapse the widget form control
1297                  *
1298                  * @deprecated 4.1.0 Use this.collapse() instead.
1299                  */
1300                 collapseForm: function() {
1301                         this.collapse();
1302                 },
1303
1304                 /**
1305                  * Expand or collapse the widget control
1306                  *
1307                  * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
1308                  *
1309                  * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
1310                  */
1311                 toggleForm: function( showOrHide ) {
1312                         if ( typeof showOrHide === 'undefined' ) {
1313                                 showOrHide = ! this.expanded();
1314                         }
1315                         this.expanded( showOrHide );
1316                 },
1317
1318                 /**
1319                  * Respond to change in the expanded state.
1320                  *
1321                  * @param {Boolean} expanded
1322                  * @param {Object} args  merged on top of this.defaultActiveArguments
1323                  */
1324                 onChangeExpanded: function ( expanded, args ) {
1325                         var self = this, $widget, $inside, complete, prevComplete;
1326
1327                         self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
1328                         if ( expanded ) {
1329                                 self.embedWidgetContent();
1330                         }
1331
1332                         // If the expanded state is unchanged only manipulate container expanded states
1333                         if ( args.unchanged ) {
1334                                 if ( expanded ) {
1335                                         api.Control.prototype.expand.call( self, {
1336                                                 completeCallback:  args.completeCallback
1337                                         });
1338                                 }
1339                                 return;
1340                         }
1341
1342                         $widget = this.container.find( 'div.widget:first' );
1343                         $inside = $widget.find( '.widget-inside:first' );
1344
1345                         if ( expanded ) {
1346
1347                                 if ( self.section() && api.section( self.section() ) ) {
1348                                         self.expandControlSection();
1349                                 }
1350
1351                                 // Close all other widget controls before expanding this one
1352                                 api.control.each( function( otherControl ) {
1353                                         if ( self.params.type === otherControl.params.type && self !== otherControl ) {
1354                                                 otherControl.collapse();
1355                                         }
1356                                 } );
1357
1358                                 complete = function() {
1359                                         self.container.removeClass( 'expanding' );
1360                                         self.container.addClass( 'expanded' );
1361                                         self.container.trigger( 'expanded' );
1362                                 };
1363                                 if ( args.completeCallback ) {
1364                                         prevComplete = complete;
1365                                         complete = function () {
1366                                                 prevComplete();
1367                                                 args.completeCallback();
1368                                         };
1369                                 }
1370
1371                                 if ( self.params.is_wide ) {
1372                                         $inside.fadeIn( args.duration, complete );
1373                                 } else {
1374                                         $inside.slideDown( args.duration, complete );
1375                                 }
1376
1377                                 self.container.trigger( 'expand' );
1378                                 self.container.addClass( 'expanding' );
1379                         } else {
1380
1381                                 complete = function() {
1382                                         self.container.removeClass( 'collapsing' );
1383                                         self.container.removeClass( 'expanded' );
1384                                         self.container.trigger( 'collapsed' );
1385                                 };
1386                                 if ( args.completeCallback ) {
1387                                         prevComplete = complete;
1388                                         complete = function () {
1389                                                 prevComplete();
1390                                                 args.completeCallback();
1391                                         };
1392                                 }
1393
1394                                 self.container.trigger( 'collapse' );
1395                                 self.container.addClass( 'collapsing' );
1396
1397                                 if ( self.params.is_wide ) {
1398                                         $inside.fadeOut( args.duration, complete );
1399                                 } else {
1400                                         $inside.slideUp( args.duration, function() {
1401                                                 $widget.css( { width:'', margin:'' } );
1402                                                 complete();
1403                                         } );
1404                                 }
1405                         }
1406                 },
1407
1408                 /**
1409                  * Get the position (index) of the widget in the containing sidebar
1410                  *
1411                  * @returns {Number}
1412                  */
1413                 getWidgetSidebarPosition: function() {
1414                         var sidebarWidgetIds, position;
1415
1416                         sidebarWidgetIds = this.getSidebarWidgetsControl().setting();
1417                         position = _.indexOf( sidebarWidgetIds, this.params.widget_id );
1418
1419                         if ( position === -1 ) {
1420                                 return;
1421                         }
1422
1423                         return position;
1424                 },
1425
1426                 /**
1427                  * Move widget up one in the sidebar
1428                  */
1429                 moveUp: function() {
1430                         this._moveWidgetByOne( -1 );
1431                 },
1432
1433                 /**
1434                  * Move widget up one in the sidebar
1435                  */
1436                 moveDown: function() {
1437                         this._moveWidgetByOne( 1 );
1438                 },
1439
1440                 /**
1441                  * @private
1442                  *
1443                  * @param {Number} offset 1|-1
1444                  */
1445                 _moveWidgetByOne: function( offset ) {
1446                         var i, sidebarWidgetsSetting, sidebarWidgetIds, adjacentWidgetId;
1447
1448                         i = this.getWidgetSidebarPosition();
1449
1450                         sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting;
1451                         sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // clone
1452                         adjacentWidgetId = sidebarWidgetIds[i + offset];
1453                         sidebarWidgetIds[i + offset] = this.params.widget_id;
1454                         sidebarWidgetIds[i] = adjacentWidgetId;
1455
1456                         sidebarWidgetsSetting( sidebarWidgetIds );
1457                 },
1458
1459                 /**
1460                  * Toggle visibility of the widget move area
1461                  *
1462                  * @param {Boolean} [showOrHide]
1463                  */
1464                 toggleWidgetMoveArea: function( showOrHide ) {
1465                         var self = this, $moveWidgetArea;
1466
1467                         $moveWidgetArea = this.container.find( '.move-widget-area' );
1468
1469                         if ( typeof showOrHide === 'undefined' ) {
1470                                 showOrHide = ! $moveWidgetArea.hasClass( 'active' );
1471                         }
1472
1473                         if ( showOrHide ) {
1474                                 // reset the selected sidebar
1475                                 $moveWidgetArea.find( '.selected' ).removeClass( 'selected' );
1476
1477                                 $moveWidgetArea.find( 'li' ).filter( function() {
1478                                         return $( this ).data( 'id' ) === self.params.sidebar_id;
1479                                 } ).addClass( 'selected' );
1480
1481                                 this.container.find( '.move-widget-btn' ).prop( 'disabled', true );
1482                         }
1483
1484                         $moveWidgetArea.toggleClass( 'active', showOrHide );
1485                 },
1486
1487                 /**
1488                  * Highlight the widget control and section
1489                  */
1490                 highlightSectionAndControl: function() {
1491                         var $target;
1492
1493                         if ( this.container.is( ':hidden' ) ) {
1494                                 $target = this.container.closest( '.control-section' );
1495                         } else {
1496                                 $target = this.container;
1497                         }
1498
1499                         $( '.highlighted' ).removeClass( 'highlighted' );
1500                         $target.addClass( 'highlighted' );
1501
1502                         setTimeout( function() {
1503                                 $target.removeClass( 'highlighted' );
1504                         }, 500 );
1505                 }
1506         } );
1507
1508         /**
1509          * wp.customize.Widgets.WidgetsPanel
1510          *
1511          * Customizer panel containing the widget area sections.
1512          *
1513          * @since 4.4.0
1514          */
1515         api.Widgets.WidgetsPanel = api.Panel.extend({
1516
1517                 /**
1518                  * Add and manage the display of the no-rendered-areas notice.
1519                  *
1520                  * @since 4.4.0
1521                  */
1522                 ready: function () {
1523                         var panel = this;
1524
1525                         api.Panel.prototype.ready.call( panel );
1526
1527                         panel.deferred.embedded.done(function() {
1528                                 var panelMetaContainer, noRenderedAreasNotice, shouldShowNotice;
1529                                 panelMetaContainer = panel.container.find( '.panel-meta' );
1530                                 noRenderedAreasNotice = $( '<div></div>', {
1531                                         'class': 'no-widget-areas-rendered-notice'
1532                                 });
1533                                 noRenderedAreasNotice.append( $( '<em></em>', {
1534                                         text: l10n.noAreasRendered
1535                                 } ) );
1536                                 panelMetaContainer.append( noRenderedAreasNotice );
1537
1538                                 shouldShowNotice = function() {
1539                                         return ( 0 === _.filter( panel.sections(), function( section ) {
1540                                                 return section.active();
1541                                         } ).length );
1542                                 };
1543
1544                                 /*
1545                                  * Set the initial visibility state for rendered notice.
1546                                  * Update the visibility of the notice whenever a reflow happens.
1547                                  */
1548                                 noRenderedAreasNotice.toggle( shouldShowNotice() );
1549                                 api.previewer.deferred.active.done( function () {
1550                                         noRenderedAreasNotice.toggle( shouldShowNotice() );
1551                                 });
1552                                 api.bind( 'pane-contents-reflowed', function() {
1553                                         var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0;
1554                                         if ( shouldShowNotice() ) {
1555                                                 noRenderedAreasNotice.slideDown( duration );
1556                                         } else {
1557                                                 noRenderedAreasNotice.slideUp( duration );
1558                                         }
1559                                 });
1560                         });
1561                 },
1562
1563                 /**
1564                  * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas).
1565                  *
1566                  * This ensures that the widgets panel appears even when there are no
1567                  * sidebars displayed on the URL currently being previewed.
1568                  *
1569                  * @since 4.4.0
1570                  *
1571                  * @returns {boolean}
1572                  */
1573                 isContextuallyActive: function() {
1574                         var panel = this;
1575                         return panel.active();
1576                 }
1577         });
1578
1579         /**
1580          * wp.customize.Widgets.SidebarSection
1581          *
1582          * Customizer section representing a widget area widget
1583          *
1584          * @since 4.1.0
1585          */
1586         api.Widgets.SidebarSection = api.Section.extend({
1587
1588                 /**
1589                  * Sync the section's active state back to the Backbone model's is_rendered attribute
1590                  *
1591                  * @since 4.1.0
1592                  */
1593                 ready: function () {
1594                         var section = this, registeredSidebar;
1595                         api.Section.prototype.ready.call( this );
1596                         registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId );
1597                         section.active.bind( function ( active ) {
1598                                 registeredSidebar.set( 'is_rendered', active );
1599                         });
1600                         registeredSidebar.set( 'is_rendered', section.active() );
1601                 }
1602         });
1603
1604         /**
1605          * wp.customize.Widgets.SidebarControl
1606          *
1607          * Customizer control for widgets.
1608          * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type
1609          *
1610          * @since 3.9.0
1611          *
1612          * @constructor
1613          * @augments wp.customize.Control
1614          */
1615         api.Widgets.SidebarControl = api.Control.extend({
1616
1617                 /**
1618                  * Set up the control
1619                  */
1620                 ready: function() {
1621                         this.$controlSection = this.container.closest( '.control-section' );
1622                         this.$sectionContent = this.container.closest( '.accordion-section-content' );
1623
1624                         this._setupModel();
1625                         this._setupSortable();
1626                         this._setupAddition();
1627                         this._applyCardinalOrderClassNames();
1628                 },
1629
1630                 /**
1631                  * Update ordering of widget control forms when the setting is updated
1632                  */
1633                 _setupModel: function() {
1634                         var self = this;
1635
1636                         this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
1637                                 var widgetFormControls, removedWidgetIds, priority;
1638
1639                                 removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
1640
1641                                 // Filter out any persistent widget IDs for widgets which have been deactivated
1642                                 newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) {
1643                                         var parsedWidgetId = parseWidgetId( newWidgetId );
1644
1645                                         return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } );
1646                                 } );
1647
1648                                 widgetFormControls = _( newWidgetIds ).map( function( widgetId ) {
1649                                         var widgetFormControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
1650
1651                                         if ( ! widgetFormControl ) {
1652                                                 widgetFormControl = self.addWidget( widgetId );
1653                                         }
1654
1655                                         return widgetFormControl;
1656                                 } );
1657
1658                                 // Sort widget controls to their new positions
1659                                 widgetFormControls.sort( function( a, b ) {
1660                                         var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
1661                                                 bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
1662                                         return aIndex - bIndex;
1663                                 });
1664
1665                                 priority = 0;
1666                                 _( widgetFormControls ).each( function ( control ) {
1667                                         control.priority( priority );
1668                                         control.section( self.section() );
1669                                         priority += 1;
1670                                 });
1671                                 self.priority( priority ); // Make sure sidebar control remains at end
1672
1673                                 // Re-sort widget form controls (including widgets form other sidebars newly moved here)
1674                                 self._applyCardinalOrderClassNames();
1675
1676                                 // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated
1677                                 _( widgetFormControls ).each( function( widgetFormControl ) {
1678                                         widgetFormControl.params.sidebar_id = self.params.sidebar_id;
1679                                 } );
1680
1681                                 // Cleanup after widget removal
1682                                 _( removedWidgetIds ).each( function( removedWidgetId ) {
1683
1684                                         // Using setTimeout so that when moving a widget to another sidebar, the other sidebars_widgets settings get a chance to update
1685                                         setTimeout( function() {
1686                                                 var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase,
1687                                                         widget, isPresentInAnotherSidebar = false;
1688
1689                                                 // Check if the widget is in another sidebar
1690                                                 api.each( function( otherSetting ) {
1691                                                         if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
1692                                                                 return;
1693                                                         }
1694
1695                                                         var otherSidebarWidgets = otherSetting(), i;
1696
1697                                                         i = _.indexOf( otherSidebarWidgets, removedWidgetId );
1698                                                         if ( -1 !== i ) {
1699                                                                 isPresentInAnotherSidebar = true;
1700                                                         }
1701                                                 } );
1702
1703                                                 // If the widget is present in another sidebar, abort!
1704                                                 if ( isPresentInAnotherSidebar ) {
1705                                                         return;
1706                                                 }
1707
1708                                                 removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId );
1709
1710                                                 // Detect if widget control was dragged to another sidebar
1711                                                 wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] );
1712
1713                                                 // Delete any widget form controls for removed widgets
1714                                                 if ( removedControl && ! wasDraggedToAnotherSidebar ) {
1715                                                         api.control.remove( removedControl.id );
1716                                                         removedControl.container.remove();
1717                                                 }
1718
1719                                                 // Move widget to inactive widgets sidebar (move it to trash) if has been previously saved
1720                                                 // This prevents the inactive widgets sidebar from overflowing with throwaway widgets
1721                                                 if ( api.Widgets.savedWidgetIds[removedWidgetId] ) {
1722                                                         inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
1723                                                         inactiveWidgets.push( removedWidgetId );
1724                                                         api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() );
1725                                                 }
1726
1727                                                 // Make old single widget available for adding again
1728                                                 removedIdBase = parseWidgetId( removedWidgetId ).id_base;
1729                                                 widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } );
1730                                                 if ( widget && ! widget.get( 'is_multi' ) ) {
1731                                                         widget.set( 'is_disabled', false );
1732                                                 }
1733                                         } );
1734
1735                                 } );
1736                         } );
1737                 },
1738
1739                 /**
1740                  * Allow widgets in sidebar to be re-ordered, and for the order to be previewed
1741                  */
1742                 _setupSortable: function() {
1743                         var self = this;
1744
1745                         this.isReordering = false;
1746
1747                         /**
1748                          * Update widget order setting when controls are re-ordered
1749                          */
1750                         this.$sectionContent.sortable( {
1751                                 items: '> .customize-control-widget_form',
1752                                 handle: '.widget-top',
1753                                 axis: 'y',
1754                                 tolerance: 'pointer',
1755                                 connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
1756                                 update: function() {
1757                                         var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds;
1758
1759                                         widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) {
1760                                                 return $( '#' + widgetContainerId ).find( ':input[name=widget-id]' ).val();
1761                                         } );
1762
1763                                         self.setting( widgetIds );
1764                                 }
1765                         } );
1766
1767                         /**
1768                          * Expand other Customizer sidebar section when dragging a control widget over it,
1769                          * allowing the control to be dropped into another section
1770                          */
1771                         this.$controlSection.find( '.accordion-section-title' ).droppable({
1772                                 accept: '.customize-control-widget_form',
1773                                 over: function() {
1774                                         var section = api.section( self.section.get() );
1775                                         section.expand({
1776                                                 allowMultiple: true, // Prevent the section being dragged from to be collapsed
1777                                                 completeCallback: function () {
1778                                                         // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed
1779                                                         api.section.each( function ( otherSection ) {
1780                                                                 if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
1781                                                                         otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
1782                                                                 }
1783                                                         } );
1784                                                 }
1785                                         });
1786                                 }
1787                         });
1788
1789                         /**
1790                          * Keyboard-accessible reordering
1791                          */
1792                         this.container.find( '.reorder-toggle' ).on( 'click', function() {
1793                                 self.toggleReordering( ! self.isReordering );
1794                         } );
1795                 },
1796
1797                 /**
1798                  * Set up UI for adding a new widget
1799                  */
1800                 _setupAddition: function() {
1801                         var self = this;
1802
1803                         this.container.find( '.add-new-widget' ).on( 'click', function() {
1804                                 var addNewWidgetBtn = $( this );
1805
1806                                 if ( self.$sectionContent.hasClass( 'reordering' ) ) {
1807                                         return;
1808                                 }
1809
1810                                 if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
1811                                         addNewWidgetBtn.attr( 'aria-expanded', 'true' );
1812                                         api.Widgets.availableWidgetsPanel.open( self );
1813                                 } else {
1814                                         addNewWidgetBtn.attr( 'aria-expanded', 'false' );
1815                                         api.Widgets.availableWidgetsPanel.close();
1816                                 }
1817                         } );
1818                 },
1819
1820                 /**
1821                  * Add classes to the widget_form controls to assist with styling
1822                  */
1823                 _applyCardinalOrderClassNames: function() {
1824                         var widgetControls = [];
1825                         _.each( this.setting(), function ( widgetId ) {
1826                                 var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
1827                                 if ( widgetControl ) {
1828                                         widgetControls.push( widgetControl );
1829                                 }
1830                         });
1831
1832                         if ( ! widgetControls.length ) {
1833                                 this.container.find( '.reorder-toggle' ).hide();
1834                                 return;
1835                         } else {
1836                                 this.container.find( '.reorder-toggle' ).show();
1837                         }
1838
1839                         $( widgetControls ).each( function () {
1840                                 $( this.container )
1841                                         .removeClass( 'first-widget' )
1842                                         .removeClass( 'last-widget' )
1843                                         .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
1844                         });
1845
1846                         _.first( widgetControls ).container
1847                                 .addClass( 'first-widget' )
1848                                 .find( '.move-widget-up' ).prop( 'tabIndex', -1 );
1849
1850                         _.last( widgetControls ).container
1851                                 .addClass( 'last-widget' )
1852                                 .find( '.move-widget-down' ).prop( 'tabIndex', -1 );
1853                 },
1854
1855
1856                 /***********************************************************************
1857                  * Begin public API methods
1858                  **********************************************************************/
1859
1860                 /**
1861                  * Enable/disable the reordering UI
1862                  *
1863                  * @param {Boolean} showOrHide to enable/disable reordering
1864                  *
1865                  * @todo We should have a reordering state instead and rename this to onChangeReordering
1866                  */
1867                 toggleReordering: function( showOrHide ) {
1868                         var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ),
1869                                 reorderBtn = this.container.find( '.reorder-toggle' ),
1870                                 widgetsTitle = this.$sectionContent.find( '.widget-title' );
1871
1872                         showOrHide = Boolean( showOrHide );
1873
1874                         if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
1875                                 return;
1876                         }
1877
1878                         this.isReordering = showOrHide;
1879                         this.$sectionContent.toggleClass( 'reordering', showOrHide );
1880
1881                         if ( showOrHide ) {
1882                                 _( this.getWidgetFormControls() ).each( function( formControl ) {
1883                                         formControl.collapse();
1884                                 } );
1885
1886                                 addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
1887                                 reorderBtn.attr( 'aria-label', l10n.reorderLabelOff );
1888                                 wp.a11y.speak( l10n.reorderModeOn );
1889                                 // Hide widget titles while reordering: title is already in the reorder controls.
1890                                 widgetsTitle.attr( 'aria-hidden', 'true' );
1891                         } else {
1892                                 addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' );
1893                                 reorderBtn.attr( 'aria-label', l10n.reorderLabelOn );
1894                                 wp.a11y.speak( l10n.reorderModeOff );
1895                                 widgetsTitle.attr( 'aria-hidden', 'false' );
1896                         }
1897                 },
1898
1899                 /**
1900                  * Get the widget_form Customize controls associated with the current sidebar.
1901                  *
1902                  * @since 3.9
1903                  * @return {wp.customize.controlConstructor.widget_form[]}
1904                  */
1905                 getWidgetFormControls: function() {
1906                         var formControls = [];
1907
1908                         _( this.setting() ).each( function( widgetId ) {
1909                                 var settingId = widgetIdToSettingId( widgetId ),
1910                                         formControl = api.control( settingId );
1911                                 if ( formControl ) {
1912                                         formControls.push( formControl );
1913                                 }
1914                         } );
1915
1916                         return formControls;
1917                 },
1918
1919                 /**
1920                  * @param {string} widgetId or an id_base for adding a previously non-existing widget
1921                  * @returns {object|false} widget_form control instance, or false on error
1922                  */
1923                 addWidget: function( widgetId ) {
1924                         var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
1925                                 parsedWidgetId = parseWidgetId( widgetId ),
1926                                 widgetNumber = parsedWidgetId.number,
1927                                 widgetIdBase = parsedWidgetId.id_base,
1928                                 widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ),
1929                                 settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting;
1930
1931                         if ( ! widget ) {
1932                                 return false;
1933                         }
1934
1935                         if ( widgetNumber && ! widget.get( 'is_multi' ) ) {
1936                                 return false;
1937                         }
1938
1939                         // Set up new multi widget
1940                         if ( widget.get( 'is_multi' ) && ! widgetNumber ) {
1941                                 widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
1942                                 widgetNumber = widget.get( 'multi_number' );
1943                         }
1944
1945                         controlHtml = $.trim( $( '#widget-tpl-' + widget.get( 'id' ) ).html() );
1946                         if ( widget.get( 'is_multi' ) ) {
1947                                 controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) {
1948                                         return m.replace( /__i__|%i%/g, widgetNumber );
1949                                 } );
1950                         } else {
1951                                 widget.set( 'is_disabled', true ); // Prevent single widget from being added again now
1952                         }
1953
1954                         $widget = $( controlHtml );
1955
1956                         controlContainer = $( '<li/>' )
1957                                 .addClass( 'customize-control' )
1958                                 .addClass( 'customize-control-' + controlType )
1959                                 .append( $widget );
1960
1961                         // Remove icon which is visible inside the panel
1962                         controlContainer.find( '> .widget-icon' ).remove();
1963
1964                         if ( widget.get( 'is_multi' ) ) {
1965                                 controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
1966                                 controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
1967                         }
1968
1969                         widgetId = controlContainer.find( '[name="widget-id"]' ).val();
1970
1971                         controlContainer.hide(); // to be slid-down below
1972
1973                         settingId = 'widget_' + widget.get( 'id_base' );
1974                         if ( widget.get( 'is_multi' ) ) {
1975                                 settingId += '[' + widgetNumber + ']';
1976                         }
1977                         controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
1978
1979                         // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
1980                         isExistingWidget = api.has( settingId );
1981                         if ( ! isExistingWidget ) {
1982                                 settingArgs = {
1983                                         transport: 'refresh',
1984                                         previewer: this.setting.previewer
1985                                 };
1986                                 setting = api.create( settingId, settingId, '', settingArgs );
1987                                 setting.set( {} ); // mark dirty, changing from '' to {}
1988                         }
1989
1990                         controlConstructor = api.controlConstructor[controlType];
1991                         widgetFormControl = new controlConstructor( settingId, {
1992                                 params: {
1993                                         settings: {
1994                                                 'default': settingId
1995                                         },
1996                                         content: controlContainer,
1997                                         sidebar_id: self.params.sidebar_id,
1998                                         widget_id: widgetId,
1999                                         widget_id_base: widget.get( 'id_base' ),
2000                                         type: controlType,
2001                                         is_new: ! isExistingWidget,
2002                                         width: widget.get( 'width' ),
2003                                         height: widget.get( 'height' ),
2004                                         is_wide: widget.get( 'is_wide' ),
2005                                         active: true
2006                                 },
2007                                 previewer: self.setting.previewer
2008                         } );
2009                         api.control.add( settingId, widgetFormControl );
2010
2011                         // Make sure widget is removed from the other sidebars
2012                         api.each( function( otherSetting ) {
2013                                 if ( otherSetting.id === self.setting.id ) {
2014                                         return;
2015                                 }
2016
2017                                 if ( 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) ) {
2018                                         return;
2019                                 }
2020
2021                                 var otherSidebarWidgets = otherSetting().slice(),
2022                                         i = _.indexOf( otherSidebarWidgets, widgetId );
2023
2024                                 if ( -1 !== i ) {
2025                                         otherSidebarWidgets.splice( i );
2026                                         otherSetting( otherSidebarWidgets );
2027                                 }
2028                         } );
2029
2030                         // Add widget to this sidebar
2031                         sidebarWidgets = this.setting().slice();
2032                         if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) {
2033                                 sidebarWidgets.push( widgetId );
2034                                 this.setting( sidebarWidgets );
2035                         }
2036
2037                         controlContainer.slideDown( function() {
2038                                 if ( isExistingWidget ) {
2039                                         widgetFormControl.updateWidget( {
2040                                                 instance: widgetFormControl.setting()
2041                                         } );
2042                                 }
2043                         } );
2044
2045                         return widgetFormControl;
2046                 }
2047         } );
2048
2049         // Register models for custom panel, section, and control types
2050         $.extend( api.panelConstructor, {
2051                 widgets: api.Widgets.WidgetsPanel
2052         });
2053         $.extend( api.sectionConstructor, {
2054                 sidebar: api.Widgets.SidebarSection
2055         });
2056         $.extend( api.controlConstructor, {
2057                 widget_form: api.Widgets.WidgetControl,
2058                 sidebar_widgets: api.Widgets.SidebarControl
2059         });
2060
2061         // Refresh the nonce if login sends updated nonces over.
2062         api.bind( 'nonce-refresh', function( nonces ) {
2063                 api.Widgets.data.nonce = nonces['update-widget'];
2064         });
2065
2066         /**
2067          * Init Customizer for widgets.
2068          */
2069         api.bind( 'ready', function() {
2070                 // Set up the widgets panel
2071                 api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({
2072                         collection: api.Widgets.availableWidgets
2073                 });
2074
2075                 // Highlight widget control
2076                 api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
2077
2078                 // Open and focus widget control
2079                 api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
2080         } );
2081
2082         /**
2083          * Highlight a widget control.
2084          *
2085          * @param {string} widgetId
2086          */
2087         api.Widgets.highlightWidgetFormControl = function( widgetId ) {
2088                 var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
2089
2090                 if ( control ) {
2091                         control.highlightSectionAndControl();
2092                 }
2093         },
2094
2095         /**
2096          * Focus a widget control.
2097          *
2098          * @param {string} widgetId
2099          */
2100         api.Widgets.focusWidgetFormControl = function( widgetId ) {
2101                 var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
2102
2103                 if ( control ) {
2104                         control.focus();
2105                 }
2106         },
2107
2108         /**
2109          * Given a widget control, find the sidebar widgets control that contains it.
2110          * @param {string} widgetId
2111          * @return {object|null}
2112          */
2113         api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) {
2114                 var foundControl = null;
2115
2116                 // @todo this can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl()
2117                 api.control.each( function( control ) {
2118                         if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) {
2119                                 foundControl = control;
2120                         }
2121                 } );
2122
2123                 return foundControl;
2124         };
2125
2126         /**
2127          * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it.
2128          *
2129          * @param {string} widgetId
2130          * @return {object|null}
2131          */
2132         api.Widgets.getWidgetFormControlForWidget = function( widgetId ) {
2133                 var foundControl = null;
2134
2135                 // @todo We can just use widgetIdToSettingId() here
2136                 api.control.each( function( control ) {
2137                         if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) {
2138                                 foundControl = control;
2139                         }
2140                 } );
2141
2142                 return foundControl;
2143         };
2144
2145         /**
2146          * @param {String} widgetId
2147          * @returns {Object}
2148          */
2149         function parseWidgetId( widgetId ) {
2150                 var matches, parsed = {
2151                         number: null,
2152                         id_base: null
2153                 };
2154
2155                 matches = widgetId.match( /^(.+)-(\d+)$/ );
2156                 if ( matches ) {
2157                         parsed.id_base = matches[1];
2158                         parsed.number = parseInt( matches[2], 10 );
2159                 } else {
2160                         // likely an old single widget
2161                         parsed.id_base = widgetId;
2162                 }
2163
2164                 return parsed;
2165         }
2166
2167         /**
2168          * @param {String} widgetId
2169          * @returns {String} settingId
2170          */
2171         function widgetIdToSettingId( widgetId ) {
2172                 var parsed = parseWidgetId( widgetId ), settingId;
2173
2174                 settingId = 'widget_' + parsed.id_base;
2175                 if ( parsed.number ) {
2176                         settingId += '[' + parsed.number + ']';
2177                 }
2178
2179                 return settingId;
2180         }
2181
2182 })( window.wp, jQuery );