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