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