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