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