~canonical-sysadmins/wordpress/4.7.4

« back to all changes in this revision

Viewing changes to wp-admin/js/customize-widgets.js

  • Committer: Jacek Nykis
  • Date: 2015-01-05 16:17:05 UTC
  • Revision ID: jacek.nykis@canonical.com-20150105161705-w544l1h5mcg7u4w9
Initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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 );