]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-admin/js/customize-controls.js
WordPress 4.5.2
[autoinstalls/wordpress.git] / wp-admin / js / customize-controls.js
1 /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
2 (function( exports, $ ){
3         var Container, focus, api = wp.customize;
4
5         /**
6          * A Customizer Setting.
7          *
8          * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
9          * draft changes to in the Customizer.
10          *
11          * @see PHP class WP_Customize_Setting.
12          *
13          * @class
14          * @augments wp.customize.Value
15          * @augments wp.customize.Class
16          *
17          * @param {object} id                The Setting ID.
18          * @param {object} value             The initial value of the setting.
19          * @param {object} options.previewer The Previewer instance to sync with.
20          * @param {object} options.transport The transport to use for previewing. Supports 'refresh' and 'postMessage'.
21          * @param {object} options.dirty
22          */
23         api.Setting = api.Value.extend({
24                 initialize: function( id, value, options ) {
25                         api.Value.prototype.initialize.call( this, value, options );
26
27                         this.id = id;
28                         this.transport = this.transport || 'refresh';
29                         this._dirty = options.dirty || false;
30
31                         // Whenever the setting's value changes, refresh the preview.
32                         this.bind( this.preview );
33                 },
34
35                 /**
36                  * Refresh the preview, respective of the setting's refresh policy.
37                  */
38                 preview: function() {
39                         switch ( this.transport ) {
40                                 case 'refresh':
41                                         return this.previewer.refresh();
42                                 case 'postMessage':
43                                         return this.previewer.send( 'setting', [ this.id, this() ] );
44                         }
45                 }
46         });
47
48         /**
49          * Utility function namespace
50          */
51         api.utils = {};
52
53         /**
54          * Watch all changes to Value properties, and bubble changes to parent Values instance
55          *
56          * @since 4.1.0
57          *
58          * @param {wp.customize.Class} instance
59          * @param {Array}              properties  The names of the Value instances to watch.
60          */
61         api.utils.bubbleChildValueChanges = function ( instance, properties ) {
62                 $.each( properties, function ( i, key ) {
63                         instance[ key ].bind( function ( to, from ) {
64                                 if ( instance.parent && to !== from ) {
65                                         instance.parent.trigger( 'change', instance );
66                                 }
67                         } );
68                 } );
69         };
70
71         /**
72          * Expand a panel, section, or control and focus on the first focusable element.
73          *
74          * @since 4.1.0
75          *
76          * @param {Object}   [params]
77          * @param {Function} [params.completeCallback]
78          */
79         focus = function ( params ) {
80                 var construct, completeCallback, focus, focusElement;
81                 construct = this;
82                 params = params || {};
83                 focus = function () {
84                         var focusContainer;
85                         if ( construct.extended( api.Panel ) && construct.expanded && construct.expanded() ) {
86                                 focusContainer = construct.container.find( 'ul.control-panel-content' );
87                         } else if ( construct.extended( api.Section ) && construct.expanded && construct.expanded() ) {
88                                 focusContainer = construct.container.find( 'ul.accordion-section-content' );
89                         } else {
90                                 focusContainer = construct.container;
91                         }
92
93                         focusElement = focusContainer.find( '.control-focus:first' );
94                         if ( 0 === focusElement.length ) {
95                                 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
96                                 focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
97                         }
98                         focusElement.focus();
99                 };
100                 if ( params.completeCallback ) {
101                         completeCallback = params.completeCallback;
102                         params.completeCallback = function () {
103                                 focus();
104                                 completeCallback();
105                         };
106                 } else {
107                         params.completeCallback = focus;
108                 }
109                 if ( construct.expand ) {
110                         construct.expand( params );
111                 } else {
112                         params.completeCallback();
113                 }
114         };
115
116         /**
117          * Stable sort for Panels, Sections, and Controls.
118          *
119          * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
120          *
121          * @since 4.1.0
122          *
123          * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
124          * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
125          * @returns {Number}
126          */
127         api.utils.prioritySort = function ( a, b ) {
128                 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
129                         return a.params.instanceNumber - b.params.instanceNumber;
130                 } else {
131                         return a.priority() - b.priority();
132                 }
133         };
134
135         /**
136          * Return whether the supplied Event object is for a keydown event but not the Enter key.
137          *
138          * @since 4.1.0
139          *
140          * @param {jQuery.Event} event
141          * @returns {boolean}
142          */
143         api.utils.isKeydownButNotEnterEvent = function ( event ) {
144                 return ( 'keydown' === event.type && 13 !== event.which );
145         };
146
147         /**
148          * Return whether the two lists of elements are the same and are in the same order.
149          *
150          * @since 4.1.0
151          *
152          * @param {Array|jQuery} listA
153          * @param {Array|jQuery} listB
154          * @returns {boolean}
155          */
156         api.utils.areElementListsEqual = function ( listA, listB ) {
157                 var equal = (
158                         listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
159                         -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
160                                 _.zip( listA, listB ), // pair up each element between the two lists
161                                 function ( pair ) {
162                                         return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
163                                 }
164                         ), false ) // check for presence of false in map's return value
165                 );
166                 return equal;
167         };
168
169         /**
170          * Base class for Panel and Section.
171          *
172          * @since 4.1.0
173          *
174          * @class
175          * @augments wp.customize.Class
176          */
177         Container = api.Class.extend({
178                 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
179                 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
180                 containerType: 'container',
181                 defaults: {
182                         title: '',
183                         description: '',
184                         priority: 100,
185                         type: 'default',
186                         content: null,
187                         active: true,
188                         instanceNumber: null
189                 },
190
191                 /**
192                  * @since 4.1.0
193                  *
194                  * @param {string}         id - The ID for the container.
195                  * @param {object}         options - Object containing one property: params.
196                  * @param {object}         options.params - Object containing the following properties.
197                  * @param {string}         options.params.title - Title shown when panel is collapsed and expanded.
198                  * @param {string=}        [options.params.description] - Description shown at the top of the panel.
199                  * @param {number=100}     [options.params.priority] - The sort priority for the panel.
200                  * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
201                  * @param {string=}        [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
202                  * @param {boolean=true}   [options.params.active] - Whether the panel is active or not.
203                  */
204                 initialize: function ( id, options ) {
205                         var container = this;
206                         container.id = id;
207                         options = options || {};
208
209                         options.params = _.defaults(
210                                 options.params || {},
211                                 container.defaults
212                         );
213
214                         $.extend( container, options );
215                         container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
216                         container.container = $( container.params.content );
217                         if ( 0 === container.container.length ) {
218                                 container.container = $( container.getContainer() );
219                         }
220
221                         container.deferred = {
222                                 embedded: new $.Deferred()
223                         };
224                         container.priority = new api.Value();
225                         container.active = new api.Value();
226                         container.activeArgumentsQueue = [];
227                         container.expanded = new api.Value();
228                         container.expandedArgumentsQueue = [];
229
230                         container.active.bind( function ( active ) {
231                                 var args = container.activeArgumentsQueue.shift();
232                                 args = $.extend( {}, container.defaultActiveArguments, args );
233                                 active = ( active && container.isContextuallyActive() );
234                                 container.onChangeActive( active, args );
235                         });
236                         container.expanded.bind( function ( expanded ) {
237                                 var args = container.expandedArgumentsQueue.shift();
238                                 args = $.extend( {}, container.defaultExpandedArguments, args );
239                                 container.onChangeExpanded( expanded, args );
240                         });
241
242                         container.deferred.embedded.done( function () {
243                                 container.attachEvents();
244                         });
245
246                         api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
247
248                         container.priority.set( container.params.priority );
249                         container.active.set( container.params.active );
250                         container.expanded.set( false );
251                 },
252
253                 /**
254                  * @since 4.1.0
255                  *
256                  * @abstract
257                  */
258                 ready: function() {},
259
260                 /**
261                  * Get the child models associated with this parent, sorting them by their priority Value.
262                  *
263                  * @since 4.1.0
264                  *
265                  * @param {String} parentType
266                  * @param {String} childType
267                  * @returns {Array}
268                  */
269                 _children: function ( parentType, childType ) {
270                         var parent = this,
271                                 children = [];
272                         api[ childType ].each( function ( child ) {
273                                 if ( child[ parentType ].get() === parent.id ) {
274                                         children.push( child );
275                                 }
276                         } );
277                         children.sort( api.utils.prioritySort );
278                         return children;
279                 },
280
281                 /**
282                  * To override by subclass, to return whether the container has active children.
283                  *
284                  * @since 4.1.0
285                  *
286                  * @abstract
287                  */
288                 isContextuallyActive: function () {
289                         throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
290                 },
291
292                 /**
293                  * Active state change handler.
294                  *
295                  * Shows the container if it is active, hides it if not.
296                  *
297                  * To override by subclass, update the container's UI to reflect the provided active state.
298                  *
299                  * @since 4.1.0
300                  *
301                  * @param {Boolean} active
302                  * @param {Object}  args
303                  * @param {Object}  args.duration
304                  * @param {Object}  args.completeCallback
305                  */
306                 onChangeActive: function( active, args ) {
307                         var duration, construct = this, expandedOtherPanel;
308                         if ( args.unchanged ) {
309                                 if ( args.completeCallback ) {
310                                         args.completeCallback();
311                                 }
312                                 return;
313                         }
314
315                         duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
316
317                         if ( construct.extended( api.Panel ) ) {
318                                 // If this is a panel is not currently expanded but another panel is expanded, do not animate.
319                                 api.panel.each(function ( panel ) {
320                                         if ( panel !== construct && panel.expanded() ) {
321                                                 expandedOtherPanel = panel;
322                                                 duration = 0;
323                                         }
324                                 });
325
326                                 // Collapse any expanded sections inside of this panel first before deactivating.
327                                 if ( ! active ) {
328                                         _.each( construct.sections(), function( section ) {
329                                                 section.collapse( { duration: 0 } );
330                                         } );
331                                 }
332                         }
333
334                         if ( ! $.contains( document, construct.container[0] ) ) {
335                                 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
336                                 construct.container.toggle( active );
337                                 if ( args.completeCallback ) {
338                                         args.completeCallback();
339                                 }
340                         } else if ( active ) {
341                                 construct.container.stop( true, true ).slideDown( duration, args.completeCallback );
342                         } else {
343                                 if ( construct.expanded() ) {
344                                         construct.collapse({
345                                                 duration: duration,
346                                                 completeCallback: function() {
347                                                         construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
348                                                 }
349                                         });
350                                 } else {
351                                         construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
352                                 }
353                         }
354
355                         // Recalculate the margin-top immediately, not waiting for debounced reflow, to prevent momentary (100ms) vertical jiggle.
356                         if ( expandedOtherPanel ) {
357                                 expandedOtherPanel._recalculateTopMargin();
358                         }
359                 },
360
361                 /**
362                  * @since 4.1.0
363                  *
364                  * @params {Boolean} active
365                  * @param {Object}   [params]
366                  * @returns {Boolean} false if state already applied
367                  */
368                 _toggleActive: function ( active, params ) {
369                         var self = this;
370                         params = params || {};
371                         if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
372                                 params.unchanged = true;
373                                 self.onChangeActive( self.active.get(), params );
374                                 return false;
375                         } else {
376                                 params.unchanged = false;
377                                 this.activeArgumentsQueue.push( params );
378                                 this.active.set( active );
379                                 return true;
380                         }
381                 },
382
383                 /**
384                  * @param {Object} [params]
385                  * @returns {Boolean} false if already active
386                  */
387                 activate: function ( params ) {
388                         return this._toggleActive( true, params );
389                 },
390
391                 /**
392                  * @param {Object} [params]
393                  * @returns {Boolean} false if already inactive
394                  */
395                 deactivate: function ( params ) {
396                         return this._toggleActive( false, params );
397                 },
398
399                 /**
400                  * To override by subclass, update the container's UI to reflect the provided active state.
401                  * @abstract
402                  */
403                 onChangeExpanded: function () {
404                         throw new Error( 'Must override with subclass.' );
405                 },
406
407                 /**
408                  * Handle the toggle logic for expand/collapse.
409                  *
410                  * @param {Boolean}  expanded - The new state to apply.
411                  * @param {Object}   [params] - Object containing options for expand/collapse.
412                  * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
413                  * @returns {Boolean} false if state already applied or active state is false
414                  */
415                 _toggleExpanded: function( expanded, params ) {
416                         var instance = this, previousCompleteCallback;
417                         params = params || {};
418                         previousCompleteCallback = params.completeCallback;
419
420                         // Short-circuit expand() if the instance is not active.
421                         if ( expanded && ! instance.active() ) {
422                                 return false;
423                         }
424
425                         params.completeCallback = function() {
426                                 if ( previousCompleteCallback ) {
427                                         previousCompleteCallback.apply( instance, arguments );
428                                 }
429                                 if ( expanded ) {
430                                         instance.container.trigger( 'expanded' );
431                                 } else {
432                                         instance.container.trigger( 'collapsed' );
433                                 }
434                         };
435                         if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
436                                 params.unchanged = true;
437                                 instance.onChangeExpanded( instance.expanded.get(), params );
438                                 return false;
439                         } else {
440                                 params.unchanged = false;
441                                 instance.expandedArgumentsQueue.push( params );
442                                 instance.expanded.set( expanded );
443                                 return true;
444                         }
445                 },
446
447                 /**
448                  * @param {Object} [params]
449                  * @returns {Boolean} false if already expanded or if inactive.
450                  */
451                 expand: function ( params ) {
452                         return this._toggleExpanded( true, params );
453                 },
454
455                 /**
456                  * @param {Object} [params]
457                  * @returns {Boolean} false if already collapsed.
458                  */
459                 collapse: function ( params ) {
460                         return this._toggleExpanded( false, params );
461                 },
462
463                 /**
464                  * Bring the container into view and then expand this and bring it into view
465                  * @param {Object} [params]
466                  */
467                 focus: focus,
468
469                 /**
470                  * Return the container html, generated from its JS template, if it exists.
471                  *
472                  * @since 4.3.0
473                  */
474                 getContainer: function () {
475                         var template,
476                                 container = this;
477
478                         if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
479                                 template = wp.template( container.templateSelector );
480                         } else {
481                                 template = wp.template( 'customize-' + container.containerType + '-default' );
482                         }
483                         if ( template && container.container ) {
484                                 return $.trim( template( container.params ) );
485                         }
486
487                         return '<li></li>';
488                 }
489         });
490
491         /**
492          * @since 4.1.0
493          *
494          * @class
495          * @augments wp.customize.Class
496          */
497         api.Section = Container.extend({
498                 containerType: 'section',
499                 defaults: {
500                         title: '',
501                         description: '',
502                         priority: 100,
503                         type: 'default',
504                         content: null,
505                         active: true,
506                         instanceNumber: null,
507                         panel: null,
508                         customizeAction: ''
509                 },
510
511                 /**
512                  * @since 4.1.0
513                  *
514                  * @param {string}         id - The ID for the section.
515                  * @param {object}         options - Object containing one property: params.
516                  * @param {object}         options.params - Object containing the following properties.
517                  * @param {string}         options.params.title - Title shown when section is collapsed and expanded.
518                  * @param {string=}        [options.params.description] - Description shown at the top of the section.
519                  * @param {number=100}     [options.params.priority] - The sort priority for the section.
520                  * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
521                  * @param {string=}        [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
522                  * @param {boolean=true}   [options.params.active] - Whether the section is active or not.
523                  * @param {string}         options.params.panel - The ID for the panel this section is associated with.
524                  * @param {string=}        [options.params.customizeAction] - Additional context information shown before the section title when expanded.
525                  */
526                 initialize: function ( id, options ) {
527                         var section = this;
528                         Container.prototype.initialize.call( section, id, options );
529
530                         section.id = id;
531                         section.panel = new api.Value();
532                         section.panel.bind( function ( id ) {
533                                 $( section.container ).toggleClass( 'control-subsection', !! id );
534                         });
535                         section.panel.set( section.params.panel || '' );
536                         api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
537
538                         section.embed();
539                         section.deferred.embedded.done( function () {
540                                 section.ready();
541                         });
542                 },
543
544                 /**
545                  * Embed the container in the DOM when any parent panel is ready.
546                  *
547                  * @since 4.1.0
548                  */
549                 embed: function () {
550                         var section = this, inject;
551
552                         // Watch for changes to the panel state
553                         inject = function ( panelId ) {
554                                 var parentContainer;
555                                 if ( panelId ) {
556                                         // The panel has been supplied, so wait until the panel object is registered
557                                         api.panel( panelId, function ( panel ) {
558                                                 // The panel has been registered, wait for it to become ready/initialized
559                                                 panel.deferred.embedded.done( function () {
560                                                         parentContainer = panel.container.find( 'ul:first' );
561                                                         if ( ! section.container.parent().is( parentContainer ) ) {
562                                                                 parentContainer.append( section.container );
563                                                         }
564                                                         section.deferred.embedded.resolve();
565                                                 });
566                                         } );
567                                 } else {
568                                         // There is no panel, so embed the section in the root of the customizer
569                                         parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
570                                         if ( ! section.container.parent().is( parentContainer ) ) {
571                                                 parentContainer.append( section.container );
572                                         }
573                                         section.deferred.embedded.resolve();
574                                 }
575                         };
576                         section.panel.bind( inject );
577                         inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
578
579                         section.deferred.embedded.done(function() {
580                                 // Fix the top margin after reflow.
581                                 api.bind( 'pane-contents-reflowed', _.debounce( function() {
582                                         section._recalculateTopMargin();
583                                 }, 100 ) );
584                         });
585                 },
586
587                 /**
588                  * Add behaviors for the accordion section.
589                  *
590                  * @since 4.1.0
591                  */
592                 attachEvents: function () {
593                         var section = this;
594
595                         // Expand/Collapse accordion sections on click.
596                         section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
597                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
598                                         return;
599                                 }
600                                 event.preventDefault(); // Keep this AFTER the key filter above
601
602                                 if ( section.expanded() ) {
603                                         section.collapse();
604                                 } else {
605                                         section.expand();
606                                 }
607                         });
608                 },
609
610                 /**
611                  * Return whether this section has any active controls.
612                  *
613                  * @since 4.1.0
614                  *
615                  * @returns {Boolean}
616                  */
617                 isContextuallyActive: function () {
618                         var section = this,
619                                 controls = section.controls(),
620                                 activeCount = 0;
621                         _( controls ).each( function ( control ) {
622                                 if ( control.active() ) {
623                                         activeCount += 1;
624                                 }
625                         } );
626                         return ( activeCount !== 0 );
627                 },
628
629                 /**
630                  * Get the controls that are associated with this section, sorted by their priority Value.
631                  *
632                  * @since 4.1.0
633                  *
634                  * @returns {Array}
635                  */
636                 controls: function () {
637                         return this._children( 'section', 'control' );
638                 },
639
640                 /**
641                  * Update UI to reflect expanded state.
642                  *
643                  * @since 4.1.0
644                  *
645                  * @param {Boolean} expanded
646                  * @param {Object}  args
647                  */
648                 onChangeExpanded: function ( expanded, args ) {
649                         var section = this,
650                                 container = section.container.closest( '.wp-full-overlay-sidebar-content' ),
651                                 content = section.container.find( '.accordion-section-content' ),
652                                 overlay = section.container.closest( '.wp-full-overlay' ),
653                                 backBtn = section.container.find( '.customize-section-back' ),
654                                 sectionTitle = section.container.find( '.accordion-section-title' ).first(),
655                                 headerActionsHeight = $( '#customize-header-actions' ).height(),
656                                 resizeContentHeight, expand, position, scroll;
657
658                         if ( expanded && ! section.container.hasClass( 'open' ) ) {
659
660                                 if ( args.unchanged ) {
661                                         expand = args.completeCallback;
662                                 } else {
663                                         container.scrollTop( 0 );
664                                         resizeContentHeight = function() {
665                                                 var matchMedia, offset;
666                                                 matchMedia = window.matchMedia || window.msMatchMedia;
667                                                 offset = 90; // 45px for customize header actions + 45px for footer actions.
668
669                                                 // No footer on small screens.
670                                                 if ( matchMedia && matchMedia( '(max-width: 640px)' ).matches ) {
671                                                         offset = 45;
672                                                 }
673                                                 content.css( 'height', ( window.innerHeight - offset ) );
674                                         };
675                                         expand = function() {
676                                                 section.container.addClass( 'open' );
677                                                 overlay.addClass( 'section-open' );
678                                                 position = content.offset().top;
679                                                 scroll = container.scrollTop();
680                                                 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
681                                                 resizeContentHeight();
682                                                 sectionTitle.attr( 'tabindex', '-1' );
683                                                 backBtn.attr( 'tabindex', '0' );
684                                                 backBtn.focus();
685                                                 if ( args.completeCallback ) {
686                                                         args.completeCallback();
687                                                 }
688
689                                                 // Fix the height after browser resize.
690                                                 $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
691
692                                                 setTimeout( _.bind( section._recalculateTopMargin, section ), 0 );
693                                         };
694                                 }
695
696                                 if ( ! args.allowMultiple ) {
697                                         api.section.each( function ( otherSection ) {
698                                                 if ( otherSection !== section ) {
699                                                         otherSection.collapse( { duration: args.duration } );
700                                                 }
701                                         });
702                                 }
703
704                                 if ( section.panel() ) {
705                                         api.panel( section.panel() ).expand({
706                                                 duration: args.duration,
707                                                 completeCallback: expand
708                                         });
709                                 } else {
710                                         api.panel.each( function( panel ) {
711                                                 panel.collapse();
712                                         });
713                                         expand();
714                                 }
715
716                         } else if ( ! expanded && section.container.hasClass( 'open' ) ) {
717                                 section.container.removeClass( 'open' );
718                                 overlay.removeClass( 'section-open' );
719                                 content.css( 'margin-top', '' );
720                                 container.scrollTop( 0 );
721                                 backBtn.attr( 'tabindex', '-1' );
722                                 sectionTitle.attr( 'tabindex', '0' );
723                                 sectionTitle.focus();
724                                 if ( args.completeCallback ) {
725                                         args.completeCallback();
726                                 }
727                                 $( window ).off( 'resize.customizer-section' );
728                         } else {
729                                 if ( args.completeCallback ) {
730                                         args.completeCallback();
731                                 }
732                         }
733                 },
734
735                 /**
736                  * Recalculate the top margin.
737                  *
738                  * @since 4.4.0
739                  * @private
740                  */
741                 _recalculateTopMargin: function() {
742                         var section = this, content, offset, headerActionsHeight;
743                         content = section.container.find( '.accordion-section-content' );
744                         if ( 0 === content.length ) {
745                                 return;
746                         }
747                         headerActionsHeight = $( '#customize-header-actions' ).height();
748                         offset = ( content.offset().top - headerActionsHeight );
749                         if ( 0 < offset ) {
750                                 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
751                         }
752                 }
753         });
754
755         /**
756          * wp.customize.ThemesSection
757          *
758          * Custom section for themes that functions similarly to a backwards panel,
759          * and also handles the theme-details view rendering and navigation.
760          *
761          * @constructor
762          * @augments wp.customize.Section
763          * @augments wp.customize.Container
764          */
765         api.ThemesSection = api.Section.extend({
766                 currentTheme: '',
767                 overlay: '',
768                 template: '',
769                 screenshotQueue: null,
770                 $window: $( window ),
771
772                 /**
773                  * @since 4.2.0
774                  */
775                 initialize: function () {
776                         this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
777                         return api.Section.prototype.initialize.apply( this, arguments );
778                 },
779
780                 /**
781                  * @since 4.2.0
782                  */
783                 ready: function () {
784                         var section = this;
785                         section.overlay = section.container.find( '.theme-overlay' );
786                         section.template = wp.template( 'customize-themes-details-view' );
787
788                         // Bind global keyboard events.
789                         $( 'body' ).on( 'keyup', function( event ) {
790                                 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
791                                         return;
792                                 }
793
794                                 // Pressing the right arrow key fires a theme:next event
795                                 if ( 39 === event.keyCode ) {
796                                         section.nextTheme();
797                                 }
798
799                                 // Pressing the left arrow key fires a theme:previous event
800                                 if ( 37 === event.keyCode ) {
801                                         section.previousTheme();
802                                 }
803
804                                 // Pressing the escape key fires a theme:collapse event
805                                 if ( 27 === event.keyCode ) {
806                                         section.closeDetails();
807                                 }
808                         });
809
810                         _.bindAll( this, 'renderScreenshots' );
811                 },
812
813                 /**
814                  * Override Section.isContextuallyActive method.
815                  *
816                  * Ignore the active states' of the contained theme controls, and just
817                  * use the section's own active state instead. This ensures empty search
818                  * results for themes to cause the section to become inactive.
819                  *
820                  * @since 4.2.0
821                  *
822                  * @returns {Boolean}
823                  */
824                 isContextuallyActive: function () {
825                         return this.active();
826                 },
827
828                 /**
829                  * @since 4.2.0
830                  */
831                 attachEvents: function () {
832                         var section = this;
833
834                         // Expand/Collapse section/panel.
835                         section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
836                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
837                                         return;
838                                 }
839                                 event.preventDefault(); // Keep this AFTER the key filter above
840
841                                 if ( section.expanded() ) {
842                                         section.collapse();
843                                 } else {
844                                         section.expand();
845                                 }
846                         });
847
848                         // Theme navigation in details view.
849                         section.container.on( 'click keydown', '.left', function( event ) {
850                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
851                                         return;
852                                 }
853
854                                 event.preventDefault(); // Keep this AFTER the key filter above
855
856                                 section.previousTheme();
857                         });
858
859                         section.container.on( 'click keydown', '.right', function( event ) {
860                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
861                                         return;
862                                 }
863
864                                 event.preventDefault(); // Keep this AFTER the key filter above
865
866                                 section.nextTheme();
867                         });
868
869                         section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
870                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
871                                         return;
872                                 }
873
874                                 event.preventDefault(); // Keep this AFTER the key filter above
875
876                                 section.closeDetails();
877                         });
878
879                         var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
880                         section.container.on( 'input', '#themes-filter', function( event ) {
881                                 var count,
882                                         term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
883                                         controls = section.controls();
884
885                                 _.each( controls, function( control ) {
886                                         control.filter( term );
887                                 });
888
889                                 renderScreenshots();
890
891                                 // Update theme count.
892                                 count = section.container.find( 'li.customize-control:visible' ).length;
893                                 section.container.find( '.theme-count' ).text( count );
894                         });
895
896                         // Pre-load the first 3 theme screenshots.
897                         api.bind( 'ready', function () {
898                                 _.each( section.controls().slice( 0, 3 ), function ( control ) {
899                                         var img, src = control.params.theme.screenshot[0];
900                                         if ( src ) {
901                                                 img = new Image();
902                                                 img.src = src;
903                                         }
904                                 });
905                         });
906                 },
907
908                 /**
909                  * Update UI to reflect expanded state
910                  *
911                  * @since 4.2.0
912                  *
913                  * @param {Boolean}  expanded
914                  * @param {Object}   args
915                  * @param {Boolean}  args.unchanged
916                  * @param {Callback} args.completeCallback
917                  */
918                 onChangeExpanded: function ( expanded, args ) {
919
920                         // Immediately call the complete callback if there were no changes
921                         if ( args.unchanged ) {
922                                 if ( args.completeCallback ) {
923                                         args.completeCallback();
924                                 }
925                                 return;
926                         }
927
928                         // Note: there is a second argument 'args' passed
929                         var position, scroll,
930                                 panel = this,
931                                 section = panel.container.closest( '.accordion-section' ),
932                                 overlay = section.closest( '.wp-full-overlay' ),
933                                 container = section.closest( '.wp-full-overlay-sidebar-content' ),
934                                 siblings = container.find( '.open' ),
935                                 customizeBtn = section.find( '.customize-theme' ),
936                                 changeBtn = section.find( '.change-theme' ),
937                                 content = section.find( '.control-panel-content' );
938
939                         if ( expanded ) {
940
941                                 // Collapse any sibling sections/panels
942                                 api.section.each( function ( otherSection ) {
943                                         if ( otherSection !== panel ) {
944                                                 otherSection.collapse( { duration: args.duration } );
945                                         }
946                                 });
947                                 api.panel.each( function ( otherPanel ) {
948                                         otherPanel.collapse( { duration: 0 } );
949                                 });
950
951                                 content.show( 0, function() {
952                                         position = content.offset().top;
953                                         scroll = container.scrollTop();
954                                         content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
955                                         section.addClass( 'current-panel' );
956                                         overlay.addClass( 'in-themes-panel' );
957                                         container.scrollTop( 0 );
958                                         _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
959                                         panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
960                                         if ( args.completeCallback ) {
961                                                 args.completeCallback();
962                                         }
963                                 } );
964                                 customizeBtn.focus();
965                         } else {
966                                 siblings.removeClass( 'open' );
967                                 section.removeClass( 'current-panel' );
968                                 overlay.removeClass( 'in-themes-panel' );
969                                 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
970                                 content.delay( 180 ).hide( 0, function() {
971                                         content.css( 'margin-top', 'inherit' ); // Reset
972                                         if ( args.completeCallback ) {
973                                                 args.completeCallback();
974                                         }
975                                 } );
976                                 customizeBtn.attr( 'tabindex', '0' );
977                                 changeBtn.focus();
978                                 container.scrollTop( 0 );
979                         }
980                 },
981
982                 /**
983                  * Recalculate the top margin.
984                  *
985                  * @since 4.4.0
986                  * @private
987                  */
988                 _recalculateTopMargin: function() {
989                         api.Panel.prototype._recalculateTopMargin.call( this );
990                 },
991
992                 /**
993                  * Render control's screenshot if the control comes into view.
994                  *
995                  * @since 4.2.0
996                  */
997                 renderScreenshots: function( ) {
998                         var section = this;
999
1000                         // Fill queue initially.
1001                         if ( section.screenshotQueue === null ) {
1002                                 section.screenshotQueue = section.controls();
1003                         }
1004
1005                         // Are all screenshots rendered?
1006                         if ( ! section.screenshotQueue.length ) {
1007                                 return;
1008                         }
1009
1010                         section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
1011                                 var $imageWrapper = control.container.find( '.theme-screenshot' ),
1012                                         $image = $imageWrapper.find( 'img' );
1013
1014                                 if ( ! $image.length ) {
1015                                         return false;
1016                                 }
1017
1018                                 if ( $image.is( ':hidden' ) ) {
1019                                         return true;
1020                                 }
1021
1022                                 // Based on unveil.js.
1023                                 var wt = section.$window.scrollTop(),
1024                                         wb = wt + section.$window.height(),
1025                                         et = $image.offset().top,
1026                                         ih = $imageWrapper.height(),
1027                                         eb = et + ih,
1028                                         threshold = ih * 3,
1029                                         inView = eb >= wt - threshold && et <= wb + threshold;
1030
1031                                 if ( inView ) {
1032                                         control.container.trigger( 'render-screenshot' );
1033                                 }
1034
1035                                 // If the image is in view return false so it's cleared from the queue.
1036                                 return ! inView;
1037                         } );
1038                 },
1039
1040                 /**
1041                  * Advance the modal to the next theme.
1042                  *
1043                  * @since 4.2.0
1044                  */
1045                 nextTheme: function () {
1046                         var section = this;
1047                         if ( section.getNextTheme() ) {
1048                                 section.showDetails( section.getNextTheme(), function() {
1049                                         section.overlay.find( '.right' ).focus();
1050                                 } );
1051                         }
1052                 },
1053
1054                 /**
1055                  * Get the next theme model.
1056                  *
1057                  * @since 4.2.0
1058                  */
1059                 getNextTheme: function () {
1060                         var control, next;
1061                         control = api.control( 'theme_' + this.currentTheme );
1062                         next = control.container.next( 'li.customize-control-theme' );
1063                         if ( ! next.length ) {
1064                                 return false;
1065                         }
1066                         next = next[0].id.replace( 'customize-control-', '' );
1067                         control = api.control( next );
1068
1069                         return control.params.theme;
1070                 },
1071
1072                 /**
1073                  * Advance the modal to the previous theme.
1074                  *
1075                  * @since 4.2.0
1076                  */
1077                 previousTheme: function () {
1078                         var section = this;
1079                         if ( section.getPreviousTheme() ) {
1080                                 section.showDetails( section.getPreviousTheme(), function() {
1081                                         section.overlay.find( '.left' ).focus();
1082                                 } );
1083                         }
1084                 },
1085
1086                 /**
1087                  * Get the previous theme model.
1088                  *
1089                  * @since 4.2.0
1090                  */
1091                 getPreviousTheme: function () {
1092                         var control, previous;
1093                         control = api.control( 'theme_' + this.currentTheme );
1094                         previous = control.container.prev( 'li.customize-control-theme' );
1095                         if ( ! previous.length ) {
1096                                 return false;
1097                         }
1098                         previous = previous[0].id.replace( 'customize-control-', '' );
1099                         control = api.control( previous );
1100
1101                         return control.params.theme;
1102                 },
1103
1104                 /**
1105                  * Disable buttons when we're viewing the first or last theme.
1106                  *
1107                  * @since 4.2.0
1108                  */
1109                 updateLimits: function () {
1110                         if ( ! this.getNextTheme() ) {
1111                                 this.overlay.find( '.right' ).addClass( 'disabled' );
1112                         }
1113                         if ( ! this.getPreviousTheme() ) {
1114                                 this.overlay.find( '.left' ).addClass( 'disabled' );
1115                         }
1116                 },
1117
1118                 /**
1119                  * Render & show the theme details for a given theme model.
1120                  *
1121                  * @since 4.2.0
1122                  *
1123                  * @param {Object}   theme
1124                  */
1125                 showDetails: function ( theme, callback ) {
1126                         var section = this;
1127                         callback = callback || function(){};
1128                         section.currentTheme = theme.id;
1129                         section.overlay.html( section.template( theme ) )
1130                                 .fadeIn( 'fast' )
1131                                 .focus();
1132                         $( 'body' ).addClass( 'modal-open' );
1133                         section.containFocus( section.overlay );
1134                         section.updateLimits();
1135                         callback();
1136                 },
1137
1138                 /**
1139                  * Close the theme details modal.
1140                  *
1141                  * @since 4.2.0
1142                  */
1143                 closeDetails: function () {
1144                         $( 'body' ).removeClass( 'modal-open' );
1145                         this.overlay.fadeOut( 'fast' );
1146                         api.control( 'theme_' + this.currentTheme ).focus();
1147                 },
1148
1149                 /**
1150                  * Keep tab focus within the theme details modal.
1151                  *
1152                  * @since 4.2.0
1153                  */
1154                 containFocus: function( el ) {
1155                         var tabbables;
1156
1157                         el.on( 'keydown', function( event ) {
1158
1159                                 // Return if it's not the tab key
1160                                 // When navigating with prev/next focus is already handled
1161                                 if ( 9 !== event.keyCode ) {
1162                                         return;
1163                                 }
1164
1165                                 // uses jQuery UI to get the tabbable elements
1166                                 tabbables = $( ':tabbable', el );
1167
1168                                 // Keep focus within the overlay
1169                                 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
1170                                         tabbables.first().focus();
1171                                         return false;
1172                                 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
1173                                         tabbables.last().focus();
1174                                         return false;
1175                                 }
1176                         });
1177                 }
1178         });
1179
1180         /**
1181          * @since 4.1.0
1182          *
1183          * @class
1184          * @augments wp.customize.Class
1185          */
1186         api.Panel = Container.extend({
1187                 containerType: 'panel',
1188
1189                 /**
1190                  * @since 4.1.0
1191                  *
1192                  * @param {string}         id - The ID for the panel.
1193                  * @param {object}         options - Object containing one property: params.
1194                  * @param {object}         options.params - Object containing the following properties.
1195                  * @param {string}         options.params.title - Title shown when panel is collapsed and expanded.
1196                  * @param {string=}        [options.params.description] - Description shown at the top of the panel.
1197                  * @param {number=100}     [options.params.priority] - The sort priority for the panel.
1198                  * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
1199                  * @param {string=}        [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
1200                  * @param {boolean=true}   [options.params.active] - Whether the panel is active or not.
1201                  */
1202                 initialize: function ( id, options ) {
1203                         var panel = this;
1204                         Container.prototype.initialize.call( panel, id, options );
1205                         panel.embed();
1206                         panel.deferred.embedded.done( function () {
1207                                 panel.ready();
1208                         });
1209                 },
1210
1211                 /**
1212                  * Embed the container in the DOM when any parent panel is ready.
1213                  *
1214                  * @since 4.1.0
1215                  */
1216                 embed: function () {
1217                         var panel = this,
1218                                 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
1219
1220                         if ( ! panel.container.parent().is( parentContainer ) ) {
1221                                 parentContainer.append( panel.container );
1222                                 panel.renderContent();
1223                         }
1224
1225                         api.bind( 'pane-contents-reflowed', _.debounce( function() {
1226                                 panel._recalculateTopMargin();
1227                         }, 100 ) );
1228
1229                         panel.deferred.embedded.resolve();
1230                 },
1231
1232                 /**
1233                  * @since 4.1.0
1234                  */
1235                 attachEvents: function () {
1236                         var meta, panel = this;
1237
1238                         // Expand/Collapse accordion sections on click.
1239                         panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
1240                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1241                                         return;
1242                                 }
1243                                 event.preventDefault(); // Keep this AFTER the key filter above
1244
1245                                 if ( ! panel.expanded() ) {
1246                                         panel.expand();
1247                                 }
1248                         });
1249
1250                         // Close panel.
1251                         panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
1252                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1253                                         return;
1254                                 }
1255                                 event.preventDefault(); // Keep this AFTER the key filter above
1256
1257                                 if ( panel.expanded() ) {
1258                                         panel.collapse();
1259                                 }
1260                         });
1261
1262                         meta = panel.container.find( '.panel-meta:first' );
1263
1264                         meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
1265                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1266                                         return;
1267                                 }
1268                                 event.preventDefault(); // Keep this AFTER the key filter above
1269
1270                                 meta = panel.container.find( '.panel-meta' );
1271                                 if ( meta.hasClass( 'cannot-expand' ) ) {
1272                                         return;
1273                                 }
1274
1275                                 var content = meta.find( '.customize-panel-description:first' );
1276                                 if ( meta.hasClass( 'open' ) ) {
1277                                         meta.toggleClass( 'open' );
1278                                         content.slideUp( panel.defaultExpandedArguments.duration );
1279                                         $( this ).attr( 'aria-expanded', false );
1280                                 } else {
1281                                         content.slideDown( panel.defaultExpandedArguments.duration );
1282                                         meta.toggleClass( 'open' );
1283                                         $( this ).attr( 'aria-expanded', true );
1284                                 }
1285                         });
1286
1287                 },
1288
1289                 /**
1290                  * Get the sections that are associated with this panel, sorted by their priority Value.
1291                  *
1292                  * @since 4.1.0
1293                  *
1294                  * @returns {Array}
1295                  */
1296                 sections: function () {
1297                         return this._children( 'panel', 'section' );
1298                 },
1299
1300                 /**
1301                  * Return whether this panel has any active sections.
1302                  *
1303                  * @since 4.1.0
1304                  *
1305                  * @returns {boolean}
1306                  */
1307                 isContextuallyActive: function () {
1308                         var panel = this,
1309                                 sections = panel.sections(),
1310                                 activeCount = 0;
1311                         _( sections ).each( function ( section ) {
1312                                 if ( section.active() && section.isContextuallyActive() ) {
1313                                         activeCount += 1;
1314                                 }
1315                         } );
1316                         return ( activeCount !== 0 );
1317                 },
1318
1319                 /**
1320                  * Update UI to reflect expanded state
1321                  *
1322                  * @since 4.1.0
1323                  *
1324                  * @param {Boolean}  expanded
1325                  * @param {Object}   args
1326                  * @param {Boolean}  args.unchanged
1327                  * @param {Function} args.completeCallback
1328                  */
1329                 onChangeExpanded: function ( expanded, args ) {
1330
1331                         // Immediately call the complete callback if there were no changes
1332                         if ( args.unchanged ) {
1333                                 if ( args.completeCallback ) {
1334                                         args.completeCallback();
1335                                 }
1336                                 return;
1337                         }
1338
1339                         // Note: there is a second argument 'args' passed
1340                         var position, scroll,
1341                                 panel = this,
1342                                 accordionSection = panel.container.closest( '.accordion-section' ),
1343                                 overlay = accordionSection.closest( '.wp-full-overlay' ),
1344                                 container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
1345                                 siblings = container.find( '.open' ),
1346                                 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
1347                                 backBtn = accordionSection.find( '.customize-panel-back' ),
1348                                 panelTitle = accordionSection.find( '.accordion-section-title' ).first(),
1349                                 content = accordionSection.find( '.control-panel-content' ),
1350                                 headerActionsHeight = $( '#customize-header-actions' ).height();
1351
1352                         if ( expanded ) {
1353
1354                                 // Collapse any sibling sections/panels
1355                                 api.section.each( function ( section ) {
1356                                         if ( panel.id !== section.panel() ) {
1357                                                 section.collapse( { duration: 0 } );
1358                                         }
1359                                 });
1360                                 api.panel.each( function ( otherPanel ) {
1361                                         if ( panel !== otherPanel ) {
1362                                                 otherPanel.collapse( { duration: 0 } );
1363                                         }
1364                                 });
1365
1366                                 content.show( 0, function() {
1367                                         content.parent().show();
1368                                         position = content.offset().top;
1369                                         scroll = container.scrollTop();
1370                                         content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
1371                                         accordionSection.addClass( 'current-panel' );
1372                                         overlay.addClass( 'in-sub-panel' );
1373                                         container.scrollTop( 0 );
1374                                         if ( args.completeCallback ) {
1375                                                 args.completeCallback();
1376                                         }
1377                                 } );
1378                                 topPanel.attr( 'tabindex', '-1' );
1379                                 backBtn.attr( 'tabindex', '0' );
1380                                 backBtn.focus();
1381                                 panel._recalculateTopMargin();
1382                         } else {
1383                                 siblings.removeClass( 'open' );
1384                                 accordionSection.removeClass( 'current-panel' );
1385                                 overlay.removeClass( 'in-sub-panel' );
1386                                 content.delay( 180 ).hide( 0, function() {
1387                                         content.css( 'margin-top', 'inherit' ); // Reset
1388                                         if ( args.completeCallback ) {
1389                                                 args.completeCallback();
1390                                         }
1391                                 } );
1392                                 topPanel.attr( 'tabindex', '0' );
1393                                 backBtn.attr( 'tabindex', '-1' );
1394                                 panelTitle.focus();
1395                                 container.scrollTop( 0 );
1396                         }
1397                 },
1398
1399                 /**
1400                  * Recalculate the top margin.
1401                  *
1402                  * @since 4.4.0
1403                  * @private
1404                  */
1405                 _recalculateTopMargin: function() {
1406                         var panel = this, headerActionsHeight, content, accordionSection;
1407                         headerActionsHeight = $( '#customize-header-actions' ).height();
1408                         accordionSection = panel.container.closest( '.accordion-section' );
1409                         content = accordionSection.find( '.control-panel-content' );
1410                         content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
1411                 },
1412
1413                 /**
1414                  * Render the panel from its JS template, if it exists.
1415                  *
1416                  * The panel's container must already exist in the DOM.
1417                  *
1418                  * @since 4.3.0
1419                  */
1420                 renderContent: function () {
1421                         var template,
1422                                 panel = this;
1423
1424                         // Add the content to the container.
1425                         if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
1426                                 template = wp.template( panel.templateSelector + '-content' );
1427                         } else {
1428                                 template = wp.template( 'customize-panel-default-content' );
1429                         }
1430                         if ( template && panel.container ) {
1431                                 panel.container.find( '.accordion-sub-container' ).html( template( panel.params ) );
1432                         }
1433                 }
1434         });
1435
1436         /**
1437          * A Customizer Control.
1438          *
1439          * A control provides a UI element that allows a user to modify a Customizer Setting.
1440          *
1441          * @see PHP class WP_Customize_Control.
1442          *
1443          * @class
1444          * @augments wp.customize.Class
1445          *
1446          * @param {string} id                              Unique identifier for the control instance.
1447          * @param {object} options                         Options hash for the control instance.
1448          * @param {object} options.params
1449          * @param {object} options.params.type             Type of control (e.g. text, radio, dropdown-pages, etc.)
1450          * @param {string} options.params.content          The HTML content for the control.
1451          * @param {string} options.params.priority         Order of priority to show the control within the section.
1452          * @param {string} options.params.active
1453          * @param {string} options.params.section          The ID of the section the control belongs to.
1454          * @param {string} options.params.settings.default The ID of the setting the control relates to.
1455          * @param {string} options.params.settings.data
1456          * @param {string} options.params.label
1457          * @param {string} options.params.description
1458          * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
1459          */
1460         api.Control = api.Class.extend({
1461                 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
1462
1463                 initialize: function( id, options ) {
1464                         var control = this,
1465                                 nodes, radios, settings;
1466
1467                         control.params = {};
1468                         $.extend( control, options || {} );
1469                         control.id = id;
1470                         control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
1471                         control.templateSelector = 'customize-control-' + control.params.type + '-content';
1472                         control.container = control.params.content ? $( control.params.content ) : $( control.selector );
1473
1474                         control.deferred = {
1475                                 embedded: new $.Deferred()
1476                         };
1477                         control.section = new api.Value();
1478                         control.priority = new api.Value();
1479                         control.active = new api.Value();
1480                         control.activeArgumentsQueue = [];
1481
1482                         control.elements = [];
1483
1484                         nodes  = control.container.find('[data-customize-setting-link]');
1485                         radios = {};
1486
1487                         nodes.each( function() {
1488                                 var node = $( this ),
1489                                         name;
1490
1491                                 if ( node.is( ':radio' ) ) {
1492                                         name = node.prop( 'name' );
1493                                         if ( radios[ name ] ) {
1494                                                 return;
1495                                         }
1496
1497                                         radios[ name ] = true;
1498                                         node = nodes.filter( '[name="' + name + '"]' );
1499                                 }
1500
1501                                 api( node.data( 'customizeSettingLink' ), function( setting ) {
1502                                         var element = new api.Element( node );
1503                                         control.elements.push( element );
1504                                         element.sync( setting );
1505                                         element.set( setting() );
1506                                 });
1507                         });
1508
1509                         control.active.bind( function ( active ) {
1510                                 var args = control.activeArgumentsQueue.shift();
1511                                 args = $.extend( {}, control.defaultActiveArguments, args );
1512                                 control.onChangeActive( active, args );
1513                         } );
1514
1515                         control.section.set( control.params.section );
1516                         control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
1517                         control.active.set( control.params.active );
1518
1519                         api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
1520
1521                         /*
1522                          * After all settings related to the control are available,
1523                          * make them available on the control and embed the control into the page.
1524                          */
1525                         settings = $.map( control.params.settings, function( value ) {
1526                                 return value;
1527                         });
1528
1529                         if ( 0 === settings.length ) {
1530                                 control.setting = null;
1531                                 control.settings = {};
1532                                 control.embed();
1533                         } else {
1534                                 api.apply( api, settings.concat( function() {
1535                                         var key;
1536
1537                                         control.settings = {};
1538                                         for ( key in control.params.settings ) {
1539                                                 control.settings[ key ] = api( control.params.settings[ key ] );
1540                                         }
1541
1542                                         control.setting = control.settings['default'] || null;
1543
1544                                         control.embed();
1545                                 }) );
1546                         }
1547
1548                         // After the control is embedded on the page, invoke the "ready" method.
1549                         control.deferred.embedded.done( function () {
1550                                 control.ready();
1551                         });
1552                 },
1553
1554                 /**
1555                  * Embed the control into the page.
1556                  */
1557                 embed: function () {
1558                         var control = this,
1559                                 inject;
1560
1561                         // Watch for changes to the section state
1562                         inject = function ( sectionId ) {
1563                                 var parentContainer;
1564                                 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the front end.
1565                                         return;
1566                                 }
1567                                 // Wait for the section to be registered
1568                                 api.section( sectionId, function ( section ) {
1569                                         // Wait for the section to be ready/initialized
1570                                         section.deferred.embedded.done( function () {
1571                                                 parentContainer = section.container.find( 'ul:first' );
1572                                                 if ( ! control.container.parent().is( parentContainer ) ) {
1573                                                         parentContainer.append( control.container );
1574                                                         control.renderContent();
1575                                                 }
1576                                                 control.deferred.embedded.resolve();
1577                                         });
1578                                 });
1579                         };
1580                         control.section.bind( inject );
1581                         inject( control.section.get() );
1582                 },
1583
1584                 /**
1585                  * Triggered when the control's markup has been injected into the DOM.
1586                  *
1587                  * @abstract
1588                  */
1589                 ready: function() {},
1590
1591                 /**
1592                  * Normal controls do not expand, so just expand its parent
1593                  *
1594                  * @param {Object} [params]
1595                  */
1596                 expand: function ( params ) {
1597                         api.section( this.section() ).expand( params );
1598                 },
1599
1600                 /**
1601                  * Bring the containing section and panel into view and then
1602                  * this control into view, focusing on the first input.
1603                  */
1604                 focus: focus,
1605
1606                 /**
1607                  * Update UI in response to a change in the control's active state.
1608                  * This does not change the active state, it merely handles the behavior
1609                  * for when it does change.
1610                  *
1611                  * @since 4.1.0
1612                  *
1613                  * @param {Boolean}  active
1614                  * @param {Object}   args
1615                  * @param {Number}   args.duration
1616                  * @param {Callback} args.completeCallback
1617                  */
1618                 onChangeActive: function ( active, args ) {
1619                         if ( args.unchanged ) {
1620                                 if ( args.completeCallback ) {
1621                                         args.completeCallback();
1622                                 }
1623                                 return;
1624                         }
1625
1626                         if ( ! $.contains( document, this.container[0] ) ) {
1627                                 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
1628                                 this.container.toggle( active );
1629                                 if ( args.completeCallback ) {
1630                                         args.completeCallback();
1631                                 }
1632                         } else if ( active ) {
1633                                 this.container.slideDown( args.duration, args.completeCallback );
1634                         } else {
1635                                 this.container.slideUp( args.duration, args.completeCallback );
1636                         }
1637                 },
1638
1639                 /**
1640                  * @deprecated 4.1.0 Use this.onChangeActive() instead.
1641                  */
1642                 toggle: function ( active ) {
1643                         return this.onChangeActive( active, this.defaultActiveArguments );
1644                 },
1645
1646                 /**
1647                  * Shorthand way to enable the active state.
1648                  *
1649                  * @since 4.1.0
1650                  *
1651                  * @param {Object} [params]
1652                  * @returns {Boolean} false if already active
1653                  */
1654                 activate: Container.prototype.activate,
1655
1656                 /**
1657                  * Shorthand way to disable the active state.
1658                  *
1659                  * @since 4.1.0
1660                  *
1661                  * @param {Object} [params]
1662                  * @returns {Boolean} false if already inactive
1663                  */
1664                 deactivate: Container.prototype.deactivate,
1665
1666                 /**
1667                  * Re-use _toggleActive from Container class.
1668                  *
1669                  * @access private
1670                  */
1671                 _toggleActive: Container.prototype._toggleActive,
1672
1673                 dropdownInit: function() {
1674                         var control      = this,
1675                                 statuses     = this.container.find('.dropdown-status'),
1676                                 params       = this.params,
1677                                 toggleFreeze = false,
1678                                 update       = function( to ) {
1679                                         if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
1680                                                 statuses.html( params.statuses[ to ] ).show();
1681                                         else
1682                                                 statuses.hide();
1683                                 };
1684
1685                         // Support the .dropdown class to open/close complex elements
1686                         this.container.on( 'click keydown', '.dropdown', function( event ) {
1687                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1688                                         return;
1689                                 }
1690
1691                                 event.preventDefault();
1692
1693                                 if (!toggleFreeze)
1694                                         control.container.toggleClass('open');
1695
1696                                 if ( control.container.hasClass('open') )
1697                                         control.container.parent().parent().find('li.library-selected').focus();
1698
1699                                 // Don't want to fire focus and click at same time
1700                                 toggleFreeze = true;
1701                                 setTimeout(function () {
1702                                         toggleFreeze = false;
1703                                 }, 400);
1704                         });
1705
1706                         this.setting.bind( update );
1707                         update( this.setting() );
1708                 },
1709
1710                 /**
1711                  * Render the control from its JS template, if it exists.
1712                  *
1713                  * The control's container must already exist in the DOM.
1714                  *
1715                  * @since 4.1.0
1716                  */
1717                 renderContent: function () {
1718                         var template,
1719                                 control = this;
1720
1721                         // Replace the container element's content with the control.
1722                         if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
1723                                 template = wp.template( control.templateSelector );
1724                                 if ( template && control.container ) {
1725                                         control.container.html( template( control.params ) );
1726                                 }
1727                         }
1728                 }
1729         });
1730
1731         /**
1732          * A colorpicker control.
1733          *
1734          * @class
1735          * @augments wp.customize.Control
1736          * @augments wp.customize.Class
1737          */
1738         api.ColorControl = api.Control.extend({
1739                 ready: function() {
1740                         var control = this,
1741                                 picker = this.container.find('.color-picker-hex');
1742
1743                         picker.val( control.setting() ).wpColorPicker({
1744                                 change: function() {
1745                                         control.setting.set( picker.wpColorPicker('color') );
1746                                 },
1747                                 clear: function() {
1748                                         control.setting.set( '' );
1749                                 }
1750                         });
1751
1752                         this.setting.bind( function ( value ) {
1753                                 picker.val( value );
1754                                 picker.wpColorPicker( 'color', value );
1755                         });
1756                 }
1757         });
1758
1759         /**
1760          * A control that implements the media modal.
1761          *
1762          * @class
1763          * @augments wp.customize.Control
1764          * @augments wp.customize.Class
1765          */
1766         api.MediaControl = api.Control.extend({
1767
1768                 /**
1769                  * When the control's DOM structure is ready,
1770                  * set up internal event bindings.
1771                  */
1772                 ready: function() {
1773                         var control = this;
1774                         // Shortcut so that we don't have to use _.bind every time we add a callback.
1775                         _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
1776
1777                         // Bind events, with delegation to facilitate re-rendering.
1778                         control.container.on( 'click keydown', '.upload-button', control.openFrame );
1779                         control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
1780                         control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1781                         control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1782                         control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
1783                         control.container.on( 'click keydown', '.remove-button', control.removeFile );
1784                         control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
1785
1786                         // Resize the player controls when it becomes visible (ie when section is expanded)
1787                         api.section( control.section() ).container
1788                                 .on( 'expanded', function() {
1789                                         if ( control.player ) {
1790                                                 control.player.setControlsSize();
1791                                         }
1792                                 })
1793                                 .on( 'collapsed', function() {
1794                                         control.pausePlayer();
1795                                 });
1796
1797                         control.setting.bind( function( value ) {
1798
1799                                 // Send attachment information to the preview for possible use in `postMessage` transport.
1800                                 wp.media.attachment( value ).fetch().done( function() {
1801                                         wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
1802                                 } );
1803
1804                                 // Re-render whenever the control's setting changes.
1805                                 control.renderContent();
1806                         } );
1807                 },
1808
1809                 pausePlayer: function () {
1810                         this.player && this.player.pause();
1811                 },
1812
1813                 cleanupPlayer: function () {
1814                         this.player && wp.media.mixin.removePlayer( this.player );
1815                 },
1816
1817                 /**
1818                  * Open the media modal.
1819                  */
1820                 openFrame: function( event ) {
1821                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1822                                 return;
1823                         }
1824
1825                         event.preventDefault();
1826
1827                         if ( ! this.frame ) {
1828                                 this.initFrame();
1829                         }
1830
1831                         this.frame.open();
1832                 },
1833
1834                 /**
1835                  * Create a media modal select frame, and store it so the instance can be reused when needed.
1836                  */
1837                 initFrame: function() {
1838                         this.frame = wp.media({
1839                                 button: {
1840                                         text: this.params.button_labels.frame_button
1841                                 },
1842                                 states: [
1843                                         new wp.media.controller.Library({
1844                                                 title:     this.params.button_labels.frame_title,
1845                                                 library:   wp.media.query({ type: this.params.mime_type }),
1846                                                 multiple:  false,
1847                                                 date:      false
1848                                         })
1849                                 ]
1850                         });
1851
1852                         // When a file is selected, run a callback.
1853                         this.frame.on( 'select', this.select );
1854                 },
1855
1856                 /**
1857                  * Callback handler for when an attachment is selected in the media modal.
1858                  * Gets the selected image information, and sets it within the control.
1859                  */
1860                 select: function() {
1861                         // Get the attachment from the modal frame.
1862                         var node,
1863                                 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1864                                 mejsSettings = window._wpmejsSettings || {};
1865
1866                         this.params.attachment = attachment;
1867
1868                         // Set the Customizer setting; the callback takes care of rendering.
1869                         this.setting( attachment.id );
1870                         node = this.container.find( 'audio, video' ).get(0);
1871
1872                         // Initialize audio/video previews.
1873                         if ( node ) {
1874                                 this.player = new MediaElementPlayer( node, mejsSettings );
1875                         } else {
1876                                 this.cleanupPlayer();
1877                         }
1878                 },
1879
1880                 /**
1881                  * Reset the setting to the default value.
1882                  */
1883                 restoreDefault: function( event ) {
1884                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1885                                 return;
1886                         }
1887                         event.preventDefault();
1888
1889                         this.params.attachment = this.params.defaultAttachment;
1890                         this.setting( this.params.defaultAttachment.url );
1891                 },
1892
1893                 /**
1894                  * Called when the "Remove" link is clicked. Empties the setting.
1895                  *
1896                  * @param {object} event jQuery Event object
1897                  */
1898                 removeFile: function( event ) {
1899                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1900                                 return;
1901                         }
1902                         event.preventDefault();
1903
1904                         this.params.attachment = {};
1905                         this.setting( '' );
1906                         this.renderContent(); // Not bound to setting change when emptying.
1907                 }
1908         });
1909
1910         /**
1911          * An upload control, which utilizes the media modal.
1912          *
1913          * @class
1914          * @augments wp.customize.MediaControl
1915          * @augments wp.customize.Control
1916          * @augments wp.customize.Class
1917          */
1918         api.UploadControl = api.MediaControl.extend({
1919
1920                 /**
1921                  * Callback handler for when an attachment is selected in the media modal.
1922                  * Gets the selected image information, and sets it within the control.
1923                  */
1924                 select: function() {
1925                         // Get the attachment from the modal frame.
1926                         var node,
1927                                 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1928                                 mejsSettings = window._wpmejsSettings || {};
1929
1930                         this.params.attachment = attachment;
1931
1932                         // Set the Customizer setting; the callback takes care of rendering.
1933                         this.setting( attachment.url );
1934                         node = this.container.find( 'audio, video' ).get(0);
1935
1936                         // Initialize audio/video previews.
1937                         if ( node ) {
1938                                 this.player = new MediaElementPlayer( node, mejsSettings );
1939                         } else {
1940                                 this.cleanupPlayer();
1941                         }
1942                 },
1943
1944                 // @deprecated
1945                 success: function() {},
1946
1947                 // @deprecated
1948                 removerVisibility: function() {}
1949         });
1950
1951         /**
1952          * A control for uploading images.
1953          *
1954          * This control no longer needs to do anything more
1955          * than what the upload control does in JS.
1956          *
1957          * @class
1958          * @augments wp.customize.UploadControl
1959          * @augments wp.customize.MediaControl
1960          * @augments wp.customize.Control
1961          * @augments wp.customize.Class
1962          */
1963         api.ImageControl = api.UploadControl.extend({
1964                 // @deprecated
1965                 thumbnailSrc: function() {}
1966         });
1967
1968         /**
1969          * A control for uploading background images.
1970          *
1971          * @class
1972          * @augments wp.customize.UploadControl
1973          * @augments wp.customize.MediaControl
1974          * @augments wp.customize.Control
1975          * @augments wp.customize.Class
1976          */
1977         api.BackgroundControl = api.UploadControl.extend({
1978
1979                 /**
1980                  * When the control's DOM structure is ready,
1981                  * set up internal event bindings.
1982                  */
1983                 ready: function() {
1984                         api.UploadControl.prototype.ready.apply( this, arguments );
1985                 },
1986
1987                 /**
1988                  * Callback handler for when an attachment is selected in the media modal.
1989                  * Does an additional AJAX request for setting the background context.
1990                  */
1991                 select: function() {
1992                         api.UploadControl.prototype.select.apply( this, arguments );
1993
1994                         wp.ajax.post( 'custom-background-add', {
1995                                 nonce: _wpCustomizeBackground.nonces.add,
1996                                 wp_customize: 'on',
1997                                 theme: api.settings.theme.stylesheet,
1998                                 attachment_id: this.params.attachment.id
1999                         } );
2000                 }
2001         });
2002
2003         /**
2004          * A control for selecting and cropping an image.
2005          *
2006          * @class
2007          * @augments wp.customize.MediaControl
2008          * @augments wp.customize.Control
2009          * @augments wp.customize.Class
2010          */
2011         api.CroppedImageControl = api.MediaControl.extend({
2012
2013                 /**
2014                  * Open the media modal to the library state.
2015                  */
2016                 openFrame: function( event ) {
2017                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2018                                 return;
2019                         }
2020
2021                         this.initFrame();
2022                         this.frame.setState( 'library' ).open();
2023                 },
2024
2025                 /**
2026                  * Create a media modal select frame, and store it so the instance can be reused when needed.
2027                  */
2028                 initFrame: function() {
2029                         var l10n = _wpMediaViewsL10n;
2030
2031                         this.frame = wp.media({
2032                                 button: {
2033                                         text: l10n.select,
2034                                         close: false
2035                                 },
2036                                 states: [
2037                                         new wp.media.controller.Library({
2038                                                 title: this.params.button_labels.frame_title,
2039                                                 library: wp.media.query({ type: 'image' }),
2040                                                 multiple: false,
2041                                                 date: false,
2042                                                 priority: 20,
2043                                                 suggestedWidth: this.params.width,
2044                                                 suggestedHeight: this.params.height
2045                                         }),
2046                                         new wp.media.controller.CustomizeImageCropper({
2047                                                 imgSelectOptions: this.calculateImageSelectOptions,
2048                                                 control: this
2049                                         })
2050                                 ]
2051                         });
2052
2053                         this.frame.on( 'select', this.onSelect, this );
2054                         this.frame.on( 'cropped', this.onCropped, this );
2055                         this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2056                 },
2057
2058                 /**
2059                  * After an image is selected in the media modal, switch to the cropper
2060                  * state if the image isn't the right size.
2061                  */
2062                 onSelect: function() {
2063                         var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2064
2065                         if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2066                                 this.setImageFromAttachment( attachment );
2067                                 this.frame.close();
2068                         } else {
2069                                 this.frame.setState( 'cropper' );
2070                         }
2071                 },
2072
2073                 /**
2074                  * After the image has been cropped, apply the cropped image data to the setting.
2075                  *
2076                  * @param {object} croppedImage Cropped attachment data.
2077                  */
2078                 onCropped: function( croppedImage ) {
2079                         this.setImageFromAttachment( croppedImage );
2080                 },
2081
2082                 /**
2083                  * Returns a set of options, computed from the attached image data and
2084                  * control-specific data, to be fed to the imgAreaSelect plugin in
2085                  * wp.media.view.Cropper.
2086                  *
2087                  * @param {wp.media.model.Attachment} attachment
2088                  * @param {wp.media.controller.Cropper} controller
2089                  * @returns {Object} Options
2090                  */
2091                 calculateImageSelectOptions: function( attachment, controller ) {
2092                         var control    = controller.get( 'control' ),
2093                                 flexWidth  = !! parseInt( control.params.flex_width, 10 ),
2094                                 flexHeight = !! parseInt( control.params.flex_height, 10 ),
2095                                 realWidth  = attachment.get( 'width' ),
2096                                 realHeight = attachment.get( 'height' ),
2097                                 xInit = parseInt( control.params.width, 10 ),
2098                                 yInit = parseInt( control.params.height, 10 ),
2099                                 ratio = xInit / yInit,
2100                                 xImg  = xInit,
2101                                 yImg  = yInit,
2102                                 x1, y1, imgSelectOptions;
2103
2104                         controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
2105
2106                         if ( realWidth / realHeight > ratio ) {
2107                                 yInit = realHeight;
2108                                 xInit = yInit * ratio;
2109                         } else {
2110                                 xInit = realWidth;
2111                                 yInit = xInit / ratio;
2112                         }
2113
2114                         x1 = ( realWidth - xInit ) / 2;
2115                         y1 = ( realHeight - yInit ) / 2;
2116
2117                         imgSelectOptions = {
2118                                 handles: true,
2119                                 keys: true,
2120                                 instance: true,
2121                                 persistent: true,
2122                                 imageWidth: realWidth,
2123                                 imageHeight: realHeight,
2124                                 minWidth: xImg > xInit ? xInit : xImg,
2125                                 minHeight: yImg > yInit ? yInit : yImg,
2126                                 x1: x1,
2127                                 y1: y1,
2128                                 x2: xInit + x1,
2129                                 y2: yInit + y1
2130                         };
2131
2132                         if ( flexHeight === false && flexWidth === false ) {
2133                                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2134                         }
2135
2136                         if ( true === flexHeight ) {
2137                                 delete imgSelectOptions.minHeight;
2138                                 imgSelectOptions.maxWidth = realWidth;
2139                         }
2140
2141                         if ( true === flexWidth ) {
2142                                 delete imgSelectOptions.minWidth;
2143                                 imgSelectOptions.maxHeight = realHeight;
2144                         }
2145
2146                         return imgSelectOptions;
2147                 },
2148
2149                 /**
2150                  * Return whether the image must be cropped, based on required dimensions.
2151                  *
2152                  * @param {bool} flexW
2153                  * @param {bool} flexH
2154                  * @param {int}  dstW
2155                  * @param {int}  dstH
2156                  * @param {int}  imgW
2157                  * @param {int}  imgH
2158                  * @return {bool}
2159                  */
2160                 mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
2161                         if ( true === flexW && true === flexH ) {
2162                                 return false;
2163                         }
2164
2165                         if ( true === flexW && dstH === imgH ) {
2166                                 return false;
2167                         }
2168
2169                         if ( true === flexH && dstW === imgW ) {
2170                                 return false;
2171                         }
2172
2173                         if ( dstW === imgW && dstH === imgH ) {
2174                                 return false;
2175                         }
2176
2177                         if ( imgW <= dstW ) {
2178                                 return false;
2179                         }
2180
2181                         return true;
2182                 },
2183
2184                 /**
2185                  * If cropping was skipped, apply the image data directly to the setting.
2186                  */
2187                 onSkippedCrop: function() {
2188                         var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2189                         this.setImageFromAttachment( attachment );
2190                 },
2191
2192                 /**
2193                  * Updates the setting and re-renders the control UI.
2194                  *
2195                  * @param {object} attachment
2196                  */
2197                 setImageFromAttachment: function( attachment ) {
2198                         this.params.attachment = attachment;
2199
2200                         // Set the Customizer setting; the callback takes care of rendering.
2201                         this.setting( attachment.id );
2202                 }
2203         });
2204
2205         /**
2206          * A control for selecting and cropping Site Icons.
2207          *
2208          * @class
2209          * @augments wp.customize.CroppedImageControl
2210          * @augments wp.customize.MediaControl
2211          * @augments wp.customize.Control
2212          * @augments wp.customize.Class
2213          */
2214         api.SiteIconControl = api.CroppedImageControl.extend({
2215
2216                 /**
2217                  * Create a media modal select frame, and store it so the instance can be reused when needed.
2218                  */
2219                 initFrame: function() {
2220                         var l10n = _wpMediaViewsL10n;
2221
2222                         this.frame = wp.media({
2223                                 button: {
2224                                         text: l10n.select,
2225                                         close: false
2226                                 },
2227                                 states: [
2228                                         new wp.media.controller.Library({
2229                                                 title: this.params.button_labels.frame_title,
2230                                                 library: wp.media.query({ type: 'image' }),
2231                                                 multiple: false,
2232                                                 date: false,
2233                                                 priority: 20,
2234                                                 suggestedWidth: this.params.width,
2235                                                 suggestedHeight: this.params.height
2236                                         }),
2237                                         new wp.media.controller.SiteIconCropper({
2238                                                 imgSelectOptions: this.calculateImageSelectOptions,
2239                                                 control: this
2240                                         })
2241                                 ]
2242                         });
2243
2244                         this.frame.on( 'select', this.onSelect, this );
2245                         this.frame.on( 'cropped', this.onCropped, this );
2246                         this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2247                 },
2248
2249                 /**
2250                  * After an image is selected in the media modal, switch to the cropper
2251                  * state if the image isn't the right size.
2252                  */
2253                 onSelect: function() {
2254                         var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2255                                 controller = this;
2256
2257                         if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2258                                 wp.ajax.post( 'crop-image', {
2259                                         nonce: attachment.nonces.edit,
2260                                         id: attachment.id,
2261                                         context: 'site-icon',
2262                                         cropDetails: {
2263                                                 x1: 0,
2264                                                 y1: 0,
2265                                                 width: this.params.width,
2266                                                 height: this.params.height,
2267                                                 dst_width: this.params.width,
2268                                                 dst_height: this.params.height
2269                                         }
2270                                 } ).done( function( croppedImage ) {
2271                                         controller.setImageFromAttachment( croppedImage );
2272                                         controller.frame.close();
2273                                 } ).fail( function() {
2274                                         controller.trigger('content:error:crop');
2275                                 } );
2276                         } else {
2277                                 this.frame.setState( 'cropper' );
2278                         }
2279                 },
2280
2281                 /**
2282                  * Updates the setting and re-renders the control UI.
2283                  *
2284                  * @param {object} attachment
2285                  */
2286                 setImageFromAttachment: function( attachment ) {
2287                         var sizes = [ 'site_icon-32', 'thumbnail', 'full' ],
2288                                 icon;
2289
2290                         _.each( sizes, function( size ) {
2291                                 if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
2292                                         icon = attachment.sizes[ size ];
2293                                 }
2294                         } );
2295
2296                         this.params.attachment = attachment;
2297
2298                         // Set the Customizer setting; the callback takes care of rendering.
2299                         this.setting( attachment.id );
2300
2301                         // Update the icon in-browser.
2302                         $( 'link[sizes="32x32"]' ).attr( 'href', icon.url );
2303                 },
2304
2305                 /**
2306                  * Called when the "Remove" link is clicked. Empties the setting.
2307                  *
2308                  * @param {object} event jQuery Event object
2309                  */
2310                 removeFile: function( event ) {
2311                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2312                                 return;
2313                         }
2314                         event.preventDefault();
2315
2316                         this.params.attachment = {};
2317                         this.setting( '' );
2318                         this.renderContent(); // Not bound to setting change when emptying.
2319                         $( 'link[rel="icon"]' ).attr( 'href', '' );
2320                 }
2321         });
2322
2323         /**
2324          * @class
2325          * @augments wp.customize.Control
2326          * @augments wp.customize.Class
2327          */
2328         api.HeaderControl = api.Control.extend({
2329                 ready: function() {
2330                         this.btnRemove = $('#customize-control-header_image .actions .remove');
2331                         this.btnNew    = $('#customize-control-header_image .actions .new');
2332
2333                         _.bindAll(this, 'openMedia', 'removeImage');
2334
2335                         this.btnNew.on( 'click', this.openMedia );
2336                         this.btnRemove.on( 'click', this.removeImage );
2337
2338                         api.HeaderTool.currentHeader = this.getInitialHeaderImage();
2339
2340                         new api.HeaderTool.CurrentView({
2341                                 model: api.HeaderTool.currentHeader,
2342                                 el: '#customize-control-header_image .current .container'
2343                         });
2344
2345                         new api.HeaderTool.ChoiceListView({
2346                                 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
2347                                 el: '#customize-control-header_image .choices .uploaded .list'
2348                         });
2349
2350                         new api.HeaderTool.ChoiceListView({
2351                                 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
2352                                 el: '#customize-control-header_image .choices .default .list'
2353                         });
2354
2355                         api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
2356                                 api.HeaderTool.UploadsList,
2357                                 api.HeaderTool.DefaultsList
2358                         ]);
2359
2360                         // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
2361                         wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
2362                         wp.media.controller.Cropper.prototype.defaults.doCropArgs.theme = api.settings.theme.stylesheet;
2363                 },
2364
2365                 /**
2366                  * Returns a new instance of api.HeaderTool.ImageModel based on the currently
2367                  * saved header image (if any).
2368                  *
2369                  * @since 4.2.0
2370                  *
2371                  * @returns {Object} Options
2372                  */
2373                 getInitialHeaderImage: function() {
2374                         if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
2375                                 return new api.HeaderTool.ImageModel();
2376                         }
2377
2378                         // Get the matching uploaded image object.
2379                         var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
2380                                 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
2381                         } );
2382                         // Fall back to raw current header image.
2383                         if ( ! currentHeaderObject ) {
2384                                 currentHeaderObject = {
2385                                         url: api.get().header_image,
2386                                         thumbnail_url: api.get().header_image,
2387                                         attachment_id: api.get().header_image_data.attachment_id
2388                                 };
2389                         }
2390
2391                         return new api.HeaderTool.ImageModel({
2392                                 header: currentHeaderObject,
2393                                 choice: currentHeaderObject.url.split( '/' ).pop()
2394                         });
2395                 },
2396
2397                 /**
2398                  * Returns a set of options, computed from the attached image data and
2399                  * theme-specific data, to be fed to the imgAreaSelect plugin in
2400                  * wp.media.view.Cropper.
2401                  *
2402                  * @param {wp.media.model.Attachment} attachment
2403                  * @param {wp.media.controller.Cropper} controller
2404                  * @returns {Object} Options
2405                  */
2406                 calculateImageSelectOptions: function(attachment, controller) {
2407                         var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
2408                                 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
2409                                 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
2410                                 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
2411                                 ratio, xImg, yImg, realHeight, realWidth,
2412                                 imgSelectOptions;
2413
2414                         realWidth = attachment.get('width');
2415                         realHeight = attachment.get('height');
2416
2417                         this.headerImage = new api.HeaderTool.ImageModel();
2418                         this.headerImage.set({
2419                                 themeWidth: xInit,
2420                                 themeHeight: yInit,
2421                                 themeFlexWidth: flexWidth,
2422                                 themeFlexHeight: flexHeight,
2423                                 imageWidth: realWidth,
2424                                 imageHeight: realHeight
2425                         });
2426
2427                         controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
2428
2429                         ratio = xInit / yInit;
2430                         xImg = realWidth;
2431                         yImg = realHeight;
2432
2433                         if ( xImg / yImg > ratio ) {
2434                                 yInit = yImg;
2435                                 xInit = yInit * ratio;
2436                         } else {
2437                                 xInit = xImg;
2438                                 yInit = xInit / ratio;
2439                         }
2440
2441                         imgSelectOptions = {
2442                                 handles: true,
2443                                 keys: true,
2444                                 instance: true,
2445                                 persistent: true,
2446                                 imageWidth: realWidth,
2447                                 imageHeight: realHeight,
2448                                 x1: 0,
2449                                 y1: 0,
2450                                 x2: xInit,
2451                                 y2: yInit
2452                         };
2453
2454                         if (flexHeight === false && flexWidth === false) {
2455                                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2456                         }
2457                         if (flexHeight === false ) {
2458                                 imgSelectOptions.maxHeight = yInit;
2459                         }
2460                         if (flexWidth === false ) {
2461                                 imgSelectOptions.maxWidth = xInit;
2462                         }
2463
2464                         return imgSelectOptions;
2465                 },
2466
2467                 /**
2468                  * Sets up and opens the Media Manager in order to select an image.
2469                  * Depending on both the size of the image and the properties of the
2470                  * current theme, a cropping step after selection may be required or
2471                  * skippable.
2472                  *
2473                  * @param {event} event
2474                  */
2475                 openMedia: function(event) {
2476                         var l10n = _wpMediaViewsL10n;
2477
2478                         event.preventDefault();
2479
2480                         this.frame = wp.media({
2481                                 button: {
2482                                         text: l10n.selectAndCrop,
2483                                         close: false
2484                                 },
2485                                 states: [
2486                                         new wp.media.controller.Library({
2487                                                 title:     l10n.chooseImage,
2488                                                 library:   wp.media.query({ type: 'image' }),
2489                                                 multiple:  false,
2490                                                 date:      false,
2491                                                 priority:  20,
2492                                                 suggestedWidth: _wpCustomizeHeader.data.width,
2493                                                 suggestedHeight: _wpCustomizeHeader.data.height
2494                                         }),
2495                                         new wp.media.controller.Cropper({
2496                                                 imgSelectOptions: this.calculateImageSelectOptions
2497                                         })
2498                                 ]
2499                         });
2500
2501                         this.frame.on('select', this.onSelect, this);
2502                         this.frame.on('cropped', this.onCropped, this);
2503                         this.frame.on('skippedcrop', this.onSkippedCrop, this);
2504
2505                         this.frame.open();
2506                 },
2507
2508                 /**
2509                  * After an image is selected in the media modal,
2510                  * switch to the cropper state.
2511                  */
2512                 onSelect: function() {
2513                         this.frame.setState('cropper');
2514                 },
2515
2516                 /**
2517                  * After the image has been cropped, apply the cropped image data to the setting.
2518                  *
2519                  * @param {object} croppedImage Cropped attachment data.
2520                  */
2521                 onCropped: function(croppedImage) {
2522                         var url = croppedImage.url,
2523                                 attachmentId = croppedImage.attachment_id,
2524                                 w = croppedImage.width,
2525                                 h = croppedImage.height;
2526                         this.setImageFromURL(url, attachmentId, w, h);
2527                 },
2528
2529                 /**
2530                  * If cropping was skipped, apply the image data directly to the setting.
2531                  *
2532                  * @param {object} selection
2533                  */
2534                 onSkippedCrop: function(selection) {
2535                         var url = selection.get('url'),
2536                                 w = selection.get('width'),
2537                                 h = selection.get('height');
2538                         this.setImageFromURL(url, selection.id, w, h);
2539                 },
2540
2541                 /**
2542                  * Creates a new wp.customize.HeaderTool.ImageModel from provided
2543                  * header image data and inserts it into the user-uploaded headers
2544                  * collection.
2545                  *
2546                  * @param {String} url
2547                  * @param {Number} attachmentId
2548                  * @param {Number} width
2549                  * @param {Number} height
2550                  */
2551                 setImageFromURL: function(url, attachmentId, width, height) {
2552                         var choice, data = {};
2553
2554                         data.url = url;
2555                         data.thumbnail_url = url;
2556                         data.timestamp = _.now();
2557
2558                         if (attachmentId) {
2559                                 data.attachment_id = attachmentId;
2560                         }
2561
2562                         if (width) {
2563                                 data.width = width;
2564                         }
2565
2566                         if (height) {
2567                                 data.height = height;
2568                         }
2569
2570                         choice = new api.HeaderTool.ImageModel({
2571                                 header: data,
2572                                 choice: url.split('/').pop()
2573                         });
2574                         api.HeaderTool.UploadsList.add(choice);
2575                         api.HeaderTool.currentHeader.set(choice.toJSON());
2576                         choice.save();
2577                         choice.importImage();
2578                 },
2579
2580                 /**
2581                  * Triggers the necessary events to deselect an image which was set as
2582                  * the currently selected one.
2583                  */
2584                 removeImage: function() {
2585                         api.HeaderTool.currentHeader.trigger('hide');
2586                         api.HeaderTool.CombinedList.trigger('control:removeImage');
2587                 }
2588
2589         });
2590
2591         /**
2592          * wp.customize.ThemeControl
2593          *
2594          * @constructor
2595          * @augments wp.customize.Control
2596          * @augments wp.customize.Class
2597          */
2598         api.ThemeControl = api.Control.extend({
2599
2600                 touchDrag: false,
2601                 isRendered: false,
2602
2603                 /**
2604                  * Defer rendering the theme control until the section is displayed.
2605                  *
2606                  * @since 4.2.0
2607                  */
2608                 renderContent: function () {
2609                         var control = this,
2610                                 renderContentArgs = arguments;
2611
2612                         api.section( control.section(), function( section ) {
2613                                 if ( section.expanded() ) {
2614                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
2615                                         control.isRendered = true;
2616                                 } else {
2617                                         section.expanded.bind( function( expanded ) {
2618                                                 if ( expanded && ! control.isRendered ) {
2619                                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
2620                                                         control.isRendered = true;
2621                                                 }
2622                                         } );
2623                                 }
2624                         } );
2625                 },
2626
2627                 /**
2628                  * @since 4.2.0
2629                  */
2630                 ready: function() {
2631                         var control = this;
2632
2633                         control.container.on( 'touchmove', '.theme', function() {
2634                                 control.touchDrag = true;
2635                         });
2636
2637                         // Bind details view trigger.
2638                         control.container.on( 'click keydown touchend', '.theme', function( event ) {
2639                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2640                                         return;
2641                                 }
2642
2643                                 // Bail if the user scrolled on a touch device.
2644                                 if ( control.touchDrag === true ) {
2645                                         return control.touchDrag = false;
2646                                 }
2647
2648                                 // Prevent the modal from showing when the user clicks the action button.
2649                                 if ( $( event.target ).is( '.theme-actions .button' ) ) {
2650                                         return;
2651                                 }
2652
2653                                 var previewUrl = $( this ).data( 'previewUrl' );
2654
2655                                 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
2656
2657                                 window.parent.location = previewUrl;
2658                         });
2659
2660                         control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
2661                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2662                                         return;
2663                                 }
2664
2665                                 event.preventDefault(); // Keep this AFTER the key filter above
2666
2667                                 api.section( control.section() ).showDetails( control.params.theme );
2668                         });
2669
2670                         control.container.on( 'render-screenshot', function() {
2671                                 var $screenshot = $( this ).find( 'img' ),
2672                                         source = $screenshot.data( 'src' );
2673
2674                                 if ( source ) {
2675                                         $screenshot.attr( 'src', source );
2676                                 }
2677                         });
2678                 },
2679
2680                 /**
2681                  * Show or hide the theme based on the presence of the term in the title, description, and author.
2682                  *
2683                  * @since 4.2.0
2684                  */
2685                 filter: function( term ) {
2686                         var control = this,
2687                                 haystack = control.params.theme.name + ' ' +
2688                                         control.params.theme.description + ' ' +
2689                                         control.params.theme.tags + ' ' +
2690                                         control.params.theme.author;
2691                         haystack = haystack.toLowerCase().replace( '-', ' ' );
2692                         if ( -1 !== haystack.search( term ) ) {
2693                                 control.activate();
2694                         } else {
2695                                 control.deactivate();
2696                         }
2697                 }
2698         });
2699
2700         // Change objects contained within the main customize object to Settings.
2701         api.defaultConstructor = api.Setting;
2702
2703         // Create the collections for Controls, Sections and Panels.
2704         api.control = new api.Values({ defaultConstructor: api.Control });
2705         api.section = new api.Values({ defaultConstructor: api.Section });
2706         api.panel = new api.Values({ defaultConstructor: api.Panel });
2707
2708         /**
2709          * An object that fetches a preview in the background of the document, which
2710          * allows for seamless replacement of an existing preview.
2711          *
2712          * @class
2713          * @augments wp.customize.Messenger
2714          * @augments wp.customize.Class
2715          * @mixes wp.customize.Events
2716          */
2717         api.PreviewFrame = api.Messenger.extend({
2718                 sensitivity: 2000,
2719
2720                 /**
2721                  * Initialize the PreviewFrame.
2722                  *
2723                  * @param {object} params.container
2724                  * @param {object} params.signature
2725                  * @param {object} params.previewUrl
2726                  * @param {object} params.query
2727                  * @param {object} options
2728                  */
2729                 initialize: function( params, options ) {
2730                         var deferred = $.Deferred();
2731
2732                         /*
2733                          * Make the instance of the PreviewFrame the promise object
2734                          * so other objects can easily interact with it.
2735                          */
2736                         deferred.promise( this );
2737
2738                         this.container = params.container;
2739                         this.signature = params.signature;
2740
2741                         $.extend( params, { channel: api.PreviewFrame.uuid() });
2742
2743                         api.Messenger.prototype.initialize.call( this, params, options );
2744
2745                         this.add( 'previewUrl', params.previewUrl );
2746
2747                         this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
2748
2749                         this.run( deferred );
2750                 },
2751
2752                 /**
2753                  * Run the preview request.
2754                  *
2755                  * @param {object} deferred jQuery Deferred object to be resolved with
2756                  *                          the request.
2757                  */
2758                 run: function( deferred ) {
2759                         var self   = this,
2760                                 loaded = false,
2761                                 ready  = false;
2762
2763                         if ( this._ready ) {
2764                                 this.unbind( 'ready', this._ready );
2765                         }
2766
2767                         this._ready = function() {
2768                                 ready = true;
2769
2770                                 if ( loaded ) {
2771                                         deferred.resolveWith( self );
2772                                 }
2773                         };
2774
2775                         this.bind( 'ready', this._ready );
2776
2777                         this.bind( 'ready', function ( data ) {
2778
2779                                 this.container.addClass( 'iframe-ready' );
2780
2781                                 if ( ! data ) {
2782                                         return;
2783                                 }
2784
2785                                 /*
2786                                  * Walk over all panels, sections, and controls and set their
2787                                  * respective active states to true if the preview explicitly
2788                                  * indicates as such.
2789                                  */
2790                                 var constructs = {
2791                                         panel: data.activePanels,
2792                                         section: data.activeSections,
2793                                         control: data.activeControls
2794                                 };
2795                                 _( constructs ).each( function ( activeConstructs, type ) {
2796                                         api[ type ].each( function ( construct, id ) {
2797                                                 var active = !! ( activeConstructs && activeConstructs[ id ] );
2798                                                 if ( active ) {
2799                                                         construct.activate();
2800                                                 } else {
2801                                                         construct.deactivate();
2802                                                 }
2803                                         } );
2804                                 } );
2805                         } );
2806
2807                         this.request = $.ajax( this.previewUrl(), {
2808                                 type: 'POST',
2809                                 data: this.query,
2810                                 xhrFields: {
2811                                         withCredentials: true
2812                                 }
2813                         } );
2814
2815                         this.request.fail( function() {
2816                                 deferred.rejectWith( self, [ 'request failure' ] );
2817                         });
2818
2819                         this.request.done( function( response ) {
2820                                 var location = self.request.getResponseHeader('Location'),
2821                                         signature = self.signature,
2822                                         index;
2823
2824                                 // Check if the location response header differs from the current URL.
2825                                 // If so, the request was redirected; try loading the requested page.
2826                                 if ( location && location !== self.previewUrl() ) {
2827                                         deferred.rejectWith( self, [ 'redirect', location ] );
2828                                         return;
2829                                 }
2830
2831                                 // Check if the user is not logged in.
2832                                 if ( '0' === response ) {
2833                                         self.login( deferred );
2834                                         return;
2835                                 }
2836
2837                                 // Check for cheaters.
2838                                 if ( '-1' === response ) {
2839                                         deferred.rejectWith( self, [ 'cheatin' ] );
2840                                         return;
2841                                 }
2842
2843                                 // Check for a signature in the request.
2844                                 index = response.lastIndexOf( signature );
2845                                 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
2846                                         deferred.rejectWith( self, [ 'unsigned' ] );
2847                                         return;
2848                                 }
2849
2850                                 // Strip the signature from the request.
2851                                 response = response.slice( 0, index ) + response.slice( index + signature.length );
2852
2853                                 // Create the iframe and inject the html content.
2854                                 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
2855
2856                                 // Bind load event after the iframe has been added to the page;
2857                                 // otherwise it will fire when injected into the DOM.
2858                                 self.iframe.one( 'load', function() {
2859                                         loaded = true;
2860
2861                                         if ( ready ) {
2862                                                 deferred.resolveWith( self );
2863                                         } else {
2864                                                 setTimeout( function() {
2865                                                         deferred.rejectWith( self, [ 'ready timeout' ] );
2866                                                 }, self.sensitivity );
2867                                         }
2868                                 });
2869
2870                                 self.targetWindow( self.iframe[0].contentWindow );
2871
2872                                 self.targetWindow().document.open();
2873                                 self.targetWindow().document.write( response );
2874                                 self.targetWindow().document.close();
2875                         });
2876                 },
2877
2878                 login: function( deferred ) {
2879                         var self = this,
2880                                 reject;
2881
2882                         reject = function() {
2883                                 deferred.rejectWith( self, [ 'logged out' ] );
2884                         };
2885
2886                         if ( this.triedLogin ) {
2887                                 return reject();
2888                         }
2889
2890                         // Check if we have an admin cookie.
2891                         $.get( api.settings.url.ajax, {
2892                                 action: 'logged-in'
2893                         }).fail( reject ).done( function( response ) {
2894                                 var iframe;
2895
2896                                 if ( '1' !== response ) {
2897                                         reject();
2898                                 }
2899
2900                                 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
2901                                 iframe.appendTo( self.container );
2902                                 iframe.on( 'load', function() {
2903                                         self.triedLogin = true;
2904
2905                                         iframe.remove();
2906                                         self.run( deferred );
2907                                 });
2908                         });
2909                 },
2910
2911                 destroy: function() {
2912                         api.Messenger.prototype.destroy.call( this );
2913                         this.request.abort();
2914
2915                         if ( this.iframe )
2916                                 this.iframe.remove();
2917
2918                         delete this.request;
2919                         delete this.iframe;
2920                         delete this.targetWindow;
2921                 }
2922         });
2923
2924         (function(){
2925                 var uuid = 0;
2926                 /**
2927                  * Create a universally unique identifier.
2928                  *
2929                  * @return {int}
2930                  */
2931                 api.PreviewFrame.uuid = function() {
2932                         return 'preview-' + uuid++;
2933                 };
2934         }());
2935
2936         /**
2937          * Set the document title of the customizer.
2938          *
2939          * @since 4.1.0
2940          *
2941          * @param {string} documentTitle
2942          */
2943         api.setDocumentTitle = function ( documentTitle ) {
2944                 var tmpl, title;
2945                 tmpl = api.settings.documentTitleTmpl;
2946                 title = tmpl.replace( '%s', documentTitle );
2947                 document.title = title;
2948                 api.trigger( 'title', title );
2949         };
2950
2951         /**
2952          * @class
2953          * @augments wp.customize.Messenger
2954          * @augments wp.customize.Class
2955          * @mixes wp.customize.Events
2956          */
2957         api.Previewer = api.Messenger.extend({
2958                 refreshBuffer: 250,
2959
2960                 /**
2961                  * @param {array}  params.allowedUrls
2962                  * @param {string} params.container   A selector or jQuery element for the preview
2963                  *                                    frame to be placed.
2964                  * @param {string} params.form
2965                  * @param {string} params.previewUrl  The URL to preview.
2966                  * @param {string} params.signature
2967                  * @param {object} options
2968                  */
2969                 initialize: function( params, options ) {
2970                         var self = this,
2971                                 rscheme = /^https?/;
2972
2973                         $.extend( this, options || {} );
2974                         this.deferred = {
2975                                 active: $.Deferred()
2976                         };
2977
2978                         /*
2979                          * Wrap this.refresh to prevent it from hammering the servers:
2980                          *
2981                          * If refresh is called once and no other refresh requests are
2982                          * loading, trigger the request immediately.
2983                          *
2984                          * If refresh is called while another refresh request is loading,
2985                          * debounce the refresh requests:
2986                          * 1. Stop the loading request (as it is instantly outdated).
2987                          * 2. Trigger the new request once refresh hasn't been called for
2988                          *    self.refreshBuffer milliseconds.
2989                          */
2990                         this.refresh = (function( self ) {
2991                                 var refresh  = self.refresh,
2992                                         callback = function() {
2993                                                 timeout = null;
2994                                                 refresh.call( self );
2995                                         },
2996                                         timeout;
2997
2998                                 return function() {
2999                                         if ( typeof timeout !== 'number' ) {
3000                                                 if ( self.loading ) {
3001                                                         self.abort();
3002                                                 } else {
3003                                                         return callback();
3004                                                 }
3005                                         }
3006
3007                                         clearTimeout( timeout );
3008                                         timeout = setTimeout( callback, self.refreshBuffer );
3009                                 };
3010                         })( this );
3011
3012                         this.container   = api.ensure( params.container );
3013                         this.allowedUrls = params.allowedUrls;
3014                         this.signature   = params.signature;
3015
3016                         params.url = window.location.href;
3017
3018                         api.Messenger.prototype.initialize.call( this, params );
3019
3020                         this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
3021                                 var match = to.match( rscheme );
3022                                 return match ? match[0] : '';
3023                         });
3024
3025                         // Limit the URL to internal, front-end links.
3026                         //
3027                         // If the front end and the admin are served from the same domain, load the
3028                         // preview over ssl if the Customizer is being loaded over ssl. This avoids
3029                         // insecure content warnings. This is not attempted if the admin and front end
3030                         // are on different domains to avoid the case where the front end doesn't have
3031                         // ssl certs.
3032
3033                         this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
3034                                 var result;
3035
3036                                 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
3037                                 // Strip hashes and query strings before testing.
3038                                 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
3039                                         return null;
3040
3041                                 // Attempt to match the URL to the control frame's scheme
3042                                 // and check if it's allowed. If not, try the original URL.
3043                                 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
3044                                         $.each( self.allowedUrls, function( i, allowed ) {
3045                                                 var path;
3046
3047                                                 allowed = allowed.replace( /\/+$/, '' );
3048                                                 path = url.replace( allowed, '' );
3049
3050                                                 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
3051                                                         result = url;
3052                                                         return false;
3053                                                 }
3054                                         });
3055                                         if ( result )
3056                                                 return false;
3057                                 });
3058
3059                                 // If we found a matching result, return it. If not, bail.
3060                                 return result ? result : null;
3061                         });
3062
3063                         // Refresh the preview when the URL is changed (but not yet).
3064                         this.previewUrl.bind( this.refresh );
3065
3066                         this.scroll = 0;
3067                         this.bind( 'scroll', function( distance ) {
3068                                 this.scroll = distance;
3069                         });
3070
3071                         // Update the URL when the iframe sends a URL message.
3072                         this.bind( 'url', this.previewUrl );
3073
3074                         // Update the document title when the preview changes.
3075                         this.bind( 'documentTitle', function ( title ) {
3076                                 api.setDocumentTitle( title );
3077                         } );
3078                 },
3079
3080                 /**
3081                  * Query string data sent with each preview request.
3082                  *
3083                  * @abstract
3084                  */
3085                 query: function() {},
3086
3087                 abort: function() {
3088                         if ( this.loading ) {
3089                                 this.loading.destroy();
3090                                 delete this.loading;
3091                         }
3092                 },
3093
3094                 /**
3095                  * Refresh the preview.
3096                  */
3097                 refresh: function() {
3098                         var self = this;
3099
3100                         // Display loading indicator
3101                         this.send( 'loading-initiated' );
3102
3103                         this.abort();
3104
3105                         this.loading = new api.PreviewFrame({
3106                                 url:        this.url(),
3107                                 previewUrl: this.previewUrl(),
3108                                 query:      this.query() || {},
3109                                 container:  this.container,
3110                                 signature:  this.signature
3111                         });
3112
3113                         this.loading.done( function() {
3114                                 // 'this' is the loading frame
3115                                 this.bind( 'synced', function() {
3116                                         if ( self.preview )
3117                                                 self.preview.destroy();
3118                                         self.preview = this;
3119                                         delete self.loading;
3120
3121                                         self.targetWindow( this.targetWindow() );
3122                                         self.channel( this.channel() );
3123
3124                                         self.deferred.active.resolve();
3125                                         self.send( 'active' );
3126                                 });
3127
3128                                 this.send( 'sync', {
3129                                         scroll:   self.scroll,
3130                                         settings: api.get()
3131                                 });
3132                         });
3133
3134                         this.loading.fail( function( reason, location ) {
3135                                 self.send( 'loading-failed' );
3136                                 if ( 'redirect' === reason && location ) {
3137                                         self.previewUrl( location );
3138                                 }
3139
3140                                 if ( 'logged out' === reason ) {
3141                                         if ( self.preview ) {
3142                                                 self.preview.destroy();
3143                                                 delete self.preview;
3144                                         }
3145
3146                                         self.login().done( self.refresh );
3147                                 }
3148
3149                                 if ( 'cheatin' === reason ) {
3150                                         self.cheatin();
3151                                 }
3152                         });
3153                 },
3154
3155                 login: function() {
3156                         var previewer = this,
3157                                 deferred, messenger, iframe;
3158
3159                         if ( this._login )
3160                                 return this._login;
3161
3162                         deferred = $.Deferred();
3163                         this._login = deferred.promise();
3164
3165                         messenger = new api.Messenger({
3166                                 channel: 'login',
3167                                 url:     api.settings.url.login
3168                         });
3169
3170                         iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
3171
3172                         messenger.targetWindow( iframe[0].contentWindow );
3173
3174                         messenger.bind( 'login', function () {
3175                                 var refreshNonces = previewer.refreshNonces();
3176
3177                                 refreshNonces.always( function() {
3178                                         iframe.remove();
3179                                         messenger.destroy();
3180                                         delete previewer._login;
3181                                 });
3182
3183                                 refreshNonces.done( function() {
3184                                         deferred.resolve();
3185                                 });
3186
3187                                 refreshNonces.fail( function() {
3188                                         previewer.cheatin();
3189                                         deferred.reject();
3190                                 });
3191                         });
3192
3193                         return this._login;
3194                 },
3195
3196                 cheatin: function() {
3197                         $( document.body ).empty().addClass( 'cheatin' ).append(
3198                                 '<h1>' + api.l10n.cheatin + '</h1>' +
3199                                 '<p>' + api.l10n.notAllowed + '</p>'
3200                         );
3201                 },
3202
3203                 refreshNonces: function() {
3204                         var request, deferred = $.Deferred();
3205
3206                         deferred.promise();
3207
3208                         request = wp.ajax.post( 'customize_refresh_nonces', {
3209                                 wp_customize: 'on',
3210                                 theme: api.settings.theme.stylesheet
3211                         });
3212
3213                         request.done( function( response ) {
3214                                 api.trigger( 'nonce-refresh', response );
3215                                 deferred.resolve();
3216                         });
3217
3218                         request.fail( function() {
3219                                 deferred.reject();
3220                         });
3221
3222                         return deferred;
3223                 }
3224         });
3225
3226         api.controlConstructor = {
3227                 color:         api.ColorControl,
3228                 media:         api.MediaControl,
3229                 upload:        api.UploadControl,
3230                 image:         api.ImageControl,
3231                 cropped_image: api.CroppedImageControl,
3232                 site_icon:     api.SiteIconControl,
3233                 header:        api.HeaderControl,
3234                 background:    api.BackgroundControl,
3235                 theme:         api.ThemeControl
3236         };
3237         api.panelConstructor = {};
3238         api.sectionConstructor = {
3239                 themes: api.ThemesSection
3240         };
3241
3242         $( function() {
3243                 api.settings = window._wpCustomizeSettings;
3244                 api.l10n = window._wpCustomizeControlsL10n;
3245
3246                 // Check if we can run the Customizer.
3247                 if ( ! api.settings ) {
3248                         return;
3249                 }
3250
3251                 // Bail if any incompatibilities are found.
3252                 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
3253                         return;
3254                 }
3255
3256                 var parent, topFocus,
3257                         body = $( document.body ),
3258                         overlay = body.children( '.wp-full-overlay' ),
3259                         title = $( '#customize-info .panel-title.site-title' ),
3260                         closeBtn = $( '.customize-controls-close' ),
3261                         saveBtn = $( '#save' ),
3262                         footerActions = $( '#customize-footer-actions' );
3263
3264                 // Prevent the form from saving when enter is pressed on an input or select element.
3265                 $('#customize-controls').on( 'keydown', function( e ) {
3266                         var isEnter = ( 13 === e.which ),
3267                                 $el = $( e.target );
3268
3269                         if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
3270                                 e.preventDefault();
3271                         }
3272                 });
3273
3274                 // Expand/Collapse the main customizer customize info.
3275                 $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
3276                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3277                                 return;
3278                         }
3279                         event.preventDefault(); // Keep this AFTER the key filter above
3280
3281                         var section = $( this ).closest( '.accordion-section' ),
3282                                 content = section.find( '.customize-panel-description:first' );
3283
3284                         if ( section.hasClass( 'cannot-expand' ) ) {
3285                                 return;
3286                         }
3287
3288                         if ( section.hasClass( 'open' ) ) {
3289                                 section.toggleClass( 'open' );
3290                                 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
3291                                 $( this ).attr( 'aria-expanded', false );
3292                         } else {
3293                                 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
3294                                 section.toggleClass( 'open' );
3295                                 $( this ).attr( 'aria-expanded', true );
3296                         }
3297                 });
3298
3299                 // Initialize Previewer
3300                 api.previewer = new api.Previewer({
3301                         container:   '#customize-preview',
3302                         form:        '#customize-controls',
3303                         previewUrl:  api.settings.url.preview,
3304                         allowedUrls: api.settings.url.allowed,
3305                         signature:   'WP_CUSTOMIZER_SIGNATURE'
3306                 }, {
3307
3308                         nonce: api.settings.nonce,
3309
3310                         /**
3311                          * Build the query to send along with the Preview request.
3312                          *
3313                          * @return {object}
3314                          */
3315                         query: function() {
3316                                 var dirtyCustomized = {};
3317                                 api.each( function ( value, key ) {
3318                                         if ( value._dirty ) {
3319                                                 dirtyCustomized[ key ] = value();
3320                                         }
3321                                 } );
3322
3323                                 return {
3324                                         wp_customize: 'on',
3325                                         theme:      api.settings.theme.stylesheet,
3326                                         customized: JSON.stringify( dirtyCustomized ),
3327                                         nonce:      this.nonce.preview
3328                                 };
3329                         },
3330
3331                         save: function() {
3332                                 var self = this,
3333                                         processing = api.state( 'processing' ),
3334                                         submitWhenDoneProcessing,
3335                                         submit;
3336
3337                                 body.addClass( 'saving' );
3338
3339                                 submit = function () {
3340                                         var request, query;
3341                                         query = $.extend( self.query(), {
3342                                                 nonce:  self.nonce.save
3343                                         } );
3344                                         request = wp.ajax.post( 'customize_save', query );
3345
3346                                         api.trigger( 'save', request );
3347
3348                                         request.always( function () {
3349                                                 body.removeClass( 'saving' );
3350                                         } );
3351
3352                                         request.fail( function ( response ) {
3353                                                 if ( '0' === response ) {
3354                                                         response = 'not_logged_in';
3355                                                 } else if ( '-1' === response ) {
3356                                                         // Back-compat in case any other check_ajax_referer() call is dying
3357                                                         response = 'invalid_nonce';
3358                                                 }
3359
3360                                                 if ( 'invalid_nonce' === response ) {
3361                                                         self.cheatin();
3362                                                 } else if ( 'not_logged_in' === response ) {
3363                                                         self.preview.iframe.hide();
3364                                                         self.login().done( function() {
3365                                                                 self.save();
3366                                                                 self.preview.iframe.show();
3367                                                         } );
3368                                                 }
3369                                                 api.trigger( 'error', response );
3370                                         } );
3371
3372                                         request.done( function( response ) {
3373                                                 // Clear setting dirty states
3374                                                 api.each( function ( value ) {
3375                                                         value._dirty = false;
3376                                                 } );
3377
3378                                                 api.previewer.send( 'saved', response );
3379
3380                                                 api.trigger( 'saved', response );
3381                                         } );
3382                                 };
3383
3384                                 if ( 0 === processing() ) {
3385                                         submit();
3386                                 } else {
3387                                         submitWhenDoneProcessing = function () {
3388                                                 if ( 0 === processing() ) {
3389                                                         api.state.unbind( 'change', submitWhenDoneProcessing );
3390                                                         submit();
3391                                                 }
3392                                         };
3393                                         api.state.bind( 'change', submitWhenDoneProcessing );
3394                                 }
3395
3396                         }
3397                 });
3398
3399                 // Refresh the nonces if the preview sends updated nonces over.
3400                 api.previewer.bind( 'nonce', function( nonce ) {
3401                         $.extend( this.nonce, nonce );
3402                 });
3403
3404                 // Refresh the nonces if login sends updated nonces over.
3405                 api.bind( 'nonce-refresh', function( nonce ) {
3406                         $.extend( api.settings.nonce, nonce );
3407                         $.extend( api.previewer.nonce, nonce );
3408                         api.previewer.send( 'nonce-refresh', nonce );
3409                 });
3410
3411                 // Create Settings
3412                 $.each( api.settings.settings, function( id, data ) {
3413                         api.create( id, id, data.value, {
3414                                 transport: data.transport,
3415                                 previewer: api.previewer,
3416                                 dirty: !! data.dirty
3417                         } );
3418                 });
3419
3420                 // Create Panels
3421                 $.each( api.settings.panels, function ( id, data ) {
3422                         var constructor = api.panelConstructor[ data.type ] || api.Panel,
3423                                 panel;
3424
3425                         panel = new constructor( id, {
3426                                 params: data
3427                         } );
3428                         api.panel.add( id, panel );
3429                 });
3430
3431                 // Create Sections
3432                 $.each( api.settings.sections, function ( id, data ) {
3433                         var constructor = api.sectionConstructor[ data.type ] || api.Section,
3434                                 section;
3435
3436                         section = new constructor( id, {
3437                                 params: data
3438                         } );
3439                         api.section.add( id, section );
3440                 });
3441
3442                 // Create Controls
3443                 $.each( api.settings.controls, function( id, data ) {
3444                         var constructor = api.controlConstructor[ data.type ] || api.Control,
3445                                 control;
3446
3447                         control = new constructor( id, {
3448                                 params: data,
3449                                 previewer: api.previewer
3450                         } );
3451                         api.control.add( id, control );
3452                 });
3453
3454                 // Focus the autofocused element
3455                 _.each( [ 'panel', 'section', 'control' ], function( type ) {
3456                         var id = api.settings.autofocus[ type ];
3457                         if ( ! id ) {
3458                                 return;
3459                         }
3460
3461                         /*
3462                          * Defer focus until:
3463                          * 1. The panel, section, or control exists (especially for dynamically-created ones).
3464                          * 2. The instance is embedded in the document (and so is focusable).
3465                          * 3. The preview has finished loading so that the active states have been set.
3466                          */
3467                         api[ type ]( id, function( instance ) {
3468                                 instance.deferred.embedded.done( function() {
3469                                         api.previewer.deferred.active.done( function() {
3470                                                 instance.focus();
3471                                         });
3472                                 });
3473                         });
3474                 });
3475
3476                 /**
3477                  * Sort panels, sections, controls by priorities. Hide empty sections and panels.
3478                  *
3479                  * @since 4.1.0
3480                  */
3481                 api.reflowPaneContents = _.bind( function () {
3482
3483                         var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3484
3485                         if ( document.activeElement ) {
3486                                 activeElement = $( document.activeElement );
3487                         }
3488
3489                         // Sort the sections within each panel
3490                         api.panel.each( function ( panel ) {
3491                                 var sections = panel.sections(),
3492                                         sectionContainers = _.pluck( sections, 'container' );
3493                                 rootNodes.push( panel );
3494                                 appendContainer = panel.container.find( 'ul:first' );
3495                                 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
3496                                         _( sections ).each( function ( section ) {
3497                                                 appendContainer.append( section.container );
3498                                         } );
3499                                         wasReflowed = true;
3500                                 }
3501                         } );
3502
3503                         // Sort the controls within each section
3504                         api.section.each( function ( section ) {
3505                                 var controls = section.controls(),
3506                                         controlContainers = _.pluck( controls, 'container' );
3507                                 if ( ! section.panel() ) {
3508                                         rootNodes.push( section );
3509                                 }
3510                                 appendContainer = section.container.find( 'ul:first' );
3511                                 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
3512                                         _( controls ).each( function ( control ) {
3513                                                 appendContainer.append( control.container );
3514                                         } );
3515                                         wasReflowed = true;
3516                                 }
3517                         } );
3518
3519                         // Sort the root panels and sections
3520                         rootNodes.sort( api.utils.prioritySort );
3521                         rootContainers = _.pluck( rootNodes, 'container' );
3522                         appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
3523                         if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
3524                                 _( rootNodes ).each( function ( rootNode ) {
3525                                         appendContainer.append( rootNode.container );
3526                                 } );
3527                                 wasReflowed = true;
3528                         }
3529
3530                         // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
3531                         api.panel.each( function ( panel ) {
3532                                 var value = panel.active();
3533                                 panel.active.callbacks.fireWith( panel.active, [ value, value ] );
3534                         } );
3535                         api.section.each( function ( section ) {
3536                                 var value = section.active();
3537                                 section.active.callbacks.fireWith( section.active, [ value, value ] );
3538                         } );
3539
3540                         // Restore focus if there was a reflow and there was an active (focused) element
3541                         if ( wasReflowed && activeElement ) {
3542                                 activeElement.focus();
3543                         }
3544                         api.trigger( 'pane-contents-reflowed' );
3545                 }, api );
3546                 api.bind( 'ready', api.reflowPaneContents );
3547                 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
3548                 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
3549                         values.bind( 'add', api.reflowPaneContents );
3550                         values.bind( 'change', api.reflowPaneContents );
3551                         values.bind( 'remove', api.reflowPaneContents );
3552                 } );
3553
3554                 // Check if preview url is valid and load the preview frame.
3555                 if ( api.previewer.previewUrl() ) {
3556                         api.previewer.refresh();
3557                 } else {
3558                         api.previewer.previewUrl( api.settings.url.home );
3559                 }
3560
3561                 // Save and activated states
3562                 (function() {
3563                         var state = new api.Values(),
3564                                 saved = state.create( 'saved' ),
3565                                 activated = state.create( 'activated' ),
3566                                 processing = state.create( 'processing' );
3567
3568                         state.bind( 'change', function() {
3569                                 if ( ! activated() ) {
3570                                         saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
3571                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3572
3573                                 } else if ( saved() ) {
3574                                         saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
3575                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
3576
3577                                 } else {
3578                                         saveBtn.val( api.l10n.save ).prop( 'disabled', false );
3579                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3580                                 }
3581                         });
3582
3583                         // Set default states.
3584                         saved( true );
3585                         activated( api.settings.theme.active );
3586                         processing( 0 );
3587
3588                         api.bind( 'change', function() {
3589                                 state('saved').set( false );
3590                         });
3591
3592                         api.bind( 'saved', function() {
3593                                 state('saved').set( true );
3594                                 state('activated').set( true );
3595                         });
3596
3597                         activated.bind( function( to ) {
3598                                 if ( to )
3599                                         api.trigger( 'activated' );
3600                         });
3601
3602                         // Expose states to the API.
3603                         api.state = state;
3604                 }());
3605
3606                 // Button bindings.
3607                 saveBtn.click( function( event ) {
3608                         api.previewer.save();
3609                         event.preventDefault();
3610                 }).keydown( function( event ) {
3611                         if ( 9 === event.which ) // tab
3612                                 return;
3613                         if ( 13 === event.which ) // enter
3614                                 api.previewer.save();
3615                         event.preventDefault();
3616                 });
3617
3618                 closeBtn.keydown( function( event ) {
3619                         if ( 9 === event.which ) // tab
3620                                 return;
3621                         if ( 13 === event.which ) // enter
3622                                 this.click();
3623                         event.preventDefault();
3624                 });
3625
3626                 $( '.collapse-sidebar' ).on( 'click', function() {
3627                         if ( 'true' === $( this ).attr( 'aria-expanded' ) ) {
3628                                 $( this ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
3629                         } else {
3630                                 $( this ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
3631                         }
3632
3633                         overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
3634                 });
3635
3636                 $( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) {
3637                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3638                                 return;
3639                         }
3640
3641                         overlay.toggleClass( 'preview-only' );
3642                         event.preventDefault();
3643                 });
3644
3645                 // Previewed device bindings.
3646                 api.previewedDevice = new api.Value();
3647
3648                 // Set the default device.
3649                 api.bind( 'ready', function() {
3650                         _.find( api.settings.previewableDevices, function( value, key ) {
3651                                 if ( true === value['default'] ) {
3652                                         api.previewedDevice.set( key );
3653                                         return true;
3654                                 }
3655                         } );
3656                 } );
3657
3658                 // Set the toggled device.
3659                 footerActions.find( '.devices button' ).on( 'click', function( event ) {
3660                         api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
3661                 });
3662
3663                 // Bind device changes.
3664                 api.previewedDevice.bind( function( newDevice ) {
3665                         var overlay = $( '.wp-full-overlay' ),
3666                                 devices = '';
3667
3668                         footerActions.find( '.devices button' )
3669                                 .removeClass( 'active' )
3670                                 .attr( 'aria-pressed', false );
3671
3672                         footerActions.find( '.devices .preview-' + newDevice )
3673                                 .addClass( 'active' )
3674                                 .attr( 'aria-pressed', true );
3675
3676                         $.each( api.settings.previewableDevices, function( device ) {
3677                                 devices += ' preview-' + device;
3678                         } );
3679
3680                         overlay
3681                                 .removeClass( devices )
3682                                 .addClass( 'preview-' + newDevice );
3683                 } );
3684
3685                 // Bind site title display to the corresponding field.
3686                 if ( title.length ) {
3687                         api( 'blogname', function( setting ) {
3688                                 var updateTitle = function() {
3689                                         title.text( $.trim( setting() ) || api.l10n.untitledBlogName );
3690                                 };
3691                                 setting.bind( updateTitle );
3692                                 updateTitle();
3693                         } );
3694                 }
3695
3696                 /*
3697                  * Create a postMessage connection with a parent frame,
3698                  * in case the Customizer frame was opened with the Customize loader.
3699                  *
3700                  * @see wp.customize.Loader
3701                  */
3702                 parent = new api.Messenger({
3703                         url: api.settings.url.parent,
3704                         channel: 'loader'
3705                 });
3706
3707                 /*
3708                  * If we receive a 'back' event, we're inside an iframe.
3709                  * Send any clicks to the 'Return' link to the parent page.
3710                  */
3711                 parent.bind( 'back', function() {
3712                         closeBtn.on( 'click.customize-controls-close', function( event ) {
3713                                 event.preventDefault();
3714                                 parent.send( 'close' );
3715                         });
3716                 });
3717
3718                 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
3719                 $( window ).on( 'beforeunload', function () {
3720                         if ( ! api.state( 'saved' )() ) {
3721                                 setTimeout( function() {
3722                                         overlay.removeClass( 'customize-loading' );
3723                                 }, 1 );
3724                                 return api.l10n.saveAlert;
3725                         }
3726                 } );
3727
3728                 // Pass events through to the parent.
3729                 $.each( [ 'saved', 'change' ], function ( i, event ) {
3730                         api.bind( event, function() {
3731                                 parent.send( event );
3732                         });
3733                 } );
3734
3735                 /*
3736                  * When activated, let the loader handle redirecting the page.
3737                  * If no loader exists, redirect the page ourselves (if a url exists).
3738                  */
3739                 api.bind( 'activated', function() {
3740                         if ( parent.targetWindow() )
3741                                 parent.send( 'activated', api.settings.url.activated );
3742                         else if ( api.settings.url.activated )
3743                                 window.location = api.settings.url.activated;
3744                 });
3745
3746                 // Pass titles to the parent
3747                 api.bind( 'title', function( newTitle ) {
3748                         parent.send( 'title', newTitle );
3749                 });
3750
3751                 // Initialize the connection with the parent frame.
3752                 parent.send( 'ready' );
3753
3754                 // Control visibility for default controls
3755                 $.each({
3756                         'background_image': {
3757                                 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
3758                                 callback: function( to ) { return !! to; }
3759                         },
3760                         'show_on_front': {
3761                                 controls: [ 'page_on_front', 'page_for_posts' ],
3762                                 callback: function( to ) { return 'page' === to; }
3763                         },
3764                         'header_textcolor': {
3765                                 controls: [ 'header_textcolor' ],
3766                                 callback: function( to ) { return 'blank' !== to; }
3767                         }
3768                 }, function( settingId, o ) {
3769                         api( settingId, function( setting ) {
3770                                 $.each( o.controls, function( i, controlId ) {
3771                                         api.control( controlId, function( control ) {
3772                                                 var visibility = function( to ) {
3773                                                         control.container.toggle( o.callback( to ) );
3774                                                 };
3775
3776                                                 visibility( setting.get() );
3777                                                 setting.bind( visibility );
3778                                         });
3779                                 });
3780                         });
3781                 });
3782
3783                 // Juggle the two controls that use header_textcolor
3784                 api.control( 'display_header_text', function( control ) {
3785                         var last = '';
3786
3787                         control.elements[0].unsync( api( 'header_textcolor' ) );
3788
3789                         control.element = new api.Element( control.container.find('input') );
3790                         control.element.set( 'blank' !== control.setting() );
3791
3792                         control.element.bind( function( to ) {
3793                                 if ( ! to )
3794                                         last = api( 'header_textcolor' ).get();
3795
3796                                 control.setting.set( to ? last : 'blank' );
3797                         });
3798
3799                         control.setting.bind( function( to ) {
3800                                 control.element.set( 'blank' !== to );
3801                         });
3802                 });
3803
3804                 // Change previewed URL to the homepage when changing the page_on_front.
3805                 api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
3806                         var updatePreviewUrl = function() {
3807                                 if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
3808                                         api.previewer.previewUrl.set( api.settings.url.home );
3809                                 }
3810                         };
3811                         showOnFront.bind( updatePreviewUrl );
3812                         pageOnFront.bind( updatePreviewUrl );
3813                 });
3814
3815                 // Change the previewed URL to the selected page when changing the page_for_posts.
3816                 api( 'page_for_posts', function( setting ) {
3817                         setting.bind(function( pageId ) {
3818                                 pageId = parseInt( pageId, 10 );
3819                                 if ( pageId > 0 ) {
3820                                         api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
3821                                 }
3822                         });
3823                 });
3824
3825                 // Focus on the control that is associated with the given setting.
3826                 api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
3827                         var matchedControl;
3828                         api.control.each( function( control ) {
3829                                 var settingIds = _.pluck( control.settings, 'id' );
3830                                 if ( -1 !== _.indexOf( settingIds, settingId ) ) {
3831                                         matchedControl = control;
3832                                 }
3833                         } );
3834
3835                         if ( matchedControl ) {
3836                                 matchedControl.focus();
3837                         }
3838                 } );
3839
3840                 // Refresh the preview when it requests.
3841                 api.previewer.bind( 'refresh', function() {
3842                         api.previewer.refresh();
3843                 });
3844
3845                 api.trigger( 'ready' );
3846
3847                 // Make sure left column gets focus
3848                 topFocus = closeBtn;
3849                 topFocus.focus();
3850                 setTimeout(function () {
3851                         topFocus.focus();
3852                 }, 200);
3853
3854         });
3855
3856 })( wp, jQuery );