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