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