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