880e5839a897a2c1ea4fe8c999de32f212e4960b
[autoinstalls/wordpress.git] / wp-admin / js / customize-controls.js
1 /* globals _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n */
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
21                         this.bind( this.preview );
22                 },
23                 preview: function() {
24                         switch ( this.transport ) {
25                                 case 'refresh':
26                                         return this.previewer.refresh();
27                                 case 'postMessage':
28                                         return this.previewer.send( 'setting', [ this.id, this() ] );
29                         }
30                 }
31         });
32
33         /**
34          * Utility function namespace
35          */
36         api.utils = {};
37
38         /**
39          * Watch all changes to Value properties, and bubble changes to parent Values instance
40          *
41          * @since 4.1.0
42          *
43          * @param {wp.customize.Class} instance
44          * @param {Array}              properties  The names of the Value instances to watch.
45          */
46         api.utils.bubbleChildValueChanges = function ( instance, properties ) {
47                 $.each( properties, function ( i, key ) {
48                         instance[ key ].bind( function ( to, from ) {
49                                 if ( instance.parent && to !== from ) {
50                                         instance.parent.trigger( 'change', instance );
51                                 }
52                         } );
53                 } );
54         };
55
56         /**
57          * Expand a panel, section, or control and focus on the first focusable element.
58          *
59          * @since 4.1.0
60          *
61          * @param {Object}   [params]
62          * @param {Callback} [params.completeCallback]
63          */
64         focus = function ( params ) {
65                 var construct, completeCallback, focus;
66                 construct = this;
67                 params = params || {};
68                 focus = function () {
69                         construct.container.find( ':focusable:first' ).focus();
70                         construct.container[0].scrollIntoView( true );
71                 };
72                 if ( params.completeCallback ) {
73                         completeCallback = params.completeCallback;
74                         params.completeCallback = function () {
75                                 focus();
76                                 completeCallback();
77                         };
78                 } else {
79                         params.completeCallback = focus;
80                 }
81                 if ( construct.expand ) {
82                         construct.expand( params );
83                 } else {
84                         params.completeCallback();
85                 }
86         };
87
88         /**
89          * Stable sort for Panels, Sections, and Controls.
90          *
91          * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
92          *
93          * @since 4.1.0
94          *
95          * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
96          * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
97          * @returns {Number}
98          */
99         api.utils.prioritySort = function ( a, b ) {
100                 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
101                         return a.params.instanceNumber - b.params.instanceNumber;
102                 } else {
103                         return a.priority() - b.priority();
104                 }
105         };
106
107         /**
108          * Return whether the supplied Event object is for a keydown event but not the Enter key.
109          *
110          * @since 4.1.0
111          *
112          * @param {jQuery.Event} event
113          * @returns {boolean}
114          */
115         api.utils.isKeydownButNotEnterEvent = function ( event ) {
116                 return ( 'keydown' === event.type && 13 !== event.which );
117         };
118
119         /**
120          * Return whether the two lists of elements are the same and are in the same order.
121          *
122          * @since 4.1.0
123          *
124          * @param {Array|jQuery} listA
125          * @param {Array|jQuery} listB
126          * @returns {boolean}
127          */
128         api.utils.areElementListsEqual = function ( listA, listB ) {
129                 var equal = (
130                         listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
131                         -1 === _.map( // are there any false values in the list returned by map?
132                                 _.zip( listA, listB ), // pair up each element between the two lists
133                                 function ( pair ) {
134                                         return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
135                                 }
136                         ).indexOf( false ) // check for presence of false in map's return value
137                 );
138                 return equal;
139         };
140
141         /**
142          * Base class for Panel and Section.
143          *
144          * @since 4.1.0
145          *
146          * @class
147          * @augments wp.customize.Class
148          */
149         Container = api.Class.extend({
150                 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
151                 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
152
153                 /**
154                  * @since 4.1.0
155                  *
156                  * @param {String} id
157                  * @param {Object} options
158                  */
159                 initialize: function ( id, options ) {
160                         var container = this;
161                         container.id = id;
162                         container.params = {};
163                         $.extend( container, options || {} );
164                         container.container = $( container.params.content );
165
166                         container.deferred = {
167                                 embedded: new $.Deferred()
168                         };
169                         container.priority = new api.Value();
170                         container.active = new api.Value();
171                         container.activeArgumentsQueue = [];
172                         container.expanded = new api.Value();
173                         container.expandedArgumentsQueue = [];
174
175                         container.active.bind( function ( active ) {
176                                 var args = container.activeArgumentsQueue.shift();
177                                 args = $.extend( {}, container.defaultActiveArguments, args );
178                                 active = ( active && container.isContextuallyActive() );
179                                 container.onChangeActive( active, args );
180                         });
181                         container.expanded.bind( function ( expanded ) {
182                                 var args = container.expandedArgumentsQueue.shift();
183                                 args = $.extend( {}, container.defaultExpandedArguments, args );
184                                 container.onChangeExpanded( expanded, args );
185                         });
186
187                         container.attachEvents();
188
189                         api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
190
191                         container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority );
192                         container.active.set( container.params.active );
193                         container.expanded.set( false );
194                 },
195
196                 /**
197                  * @since 4.1.0
198                  *
199                  * @abstract
200                  */
201                 ready: function() {},
202
203                 /**
204                  * Get the child models associated with this parent, sorting them by their priority Value.
205                  *
206                  * @since 4.1.0
207                  *
208                  * @param {String} parentType
209                  * @param {String} childType
210                  * @returns {Array}
211                  */
212                 _children: function ( parentType, childType ) {
213                         var parent = this,
214                                 children = [];
215                         api[ childType ].each( function ( child ) {
216                                 if ( child[ parentType ].get() === parent.id ) {
217                                         children.push( child );
218                                 }
219                         } );
220                         children.sort( api.utils.prioritySort );
221                         return children;
222                 },
223
224                 /**
225                  * To override by subclass, to return whether the container has active children.
226                  *
227                  * @since 4.1.0
228                  *
229                  * @abstract
230                  */
231                 isContextuallyActive: function () {
232                         throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
233                 },
234
235                 /**
236                  * Handle changes to the active state.
237                  *
238                  * This does not change the active state, it merely handles the behavior
239                  * for when it does change.
240                  *
241                  * To override by subclass, update the container's UI to reflect the provided active state.
242                  *
243                  * @since 4.1.0
244                  *
245                  * @param {Boolean} active
246                  * @param {Object}  args
247                  * @param {Object}  args.duration
248                  * @param {Object}  args.completeCallback
249                  */
250                 onChangeActive: function ( active, args ) {
251                         var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
252                         if ( ! $.contains( document, this.container ) ) {
253                                 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
254                                 this.container.toggle( active );
255                                 if ( args.completeCallback ) {
256                                         args.completeCallback();
257                                 }
258                         } else if ( active ) {
259                                 this.container.stop( true, true ).slideDown( duration, args.completeCallback );
260                         } else {
261                                 this.container.stop( true, true ).slideUp( duration, args.completeCallback );
262                         }
263                 },
264
265                 /**
266                  * @since 4.1.0
267                  *
268                  * @params {Boolean} active
269                  * @param {Object}   [params]
270                  * @returns {Boolean} false if state already applied
271                  */
272                 _toggleActive: function ( active, params ) {
273                         var self = this;
274                         params = params || {};
275                         if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
276                                 params.unchanged = true;
277                                 self.onChangeActive( self.active.get(), params );
278                                 return false;
279                         } else {
280                                 params.unchanged = false;
281                                 this.activeArgumentsQueue.push( params );
282                                 this.active.set( active );
283                                 return true;
284                         }
285                 },
286
287                 /**
288                  * @param {Object} [params]
289                  * @returns {Boolean} false if already active
290                  */
291                 activate: function ( params ) {
292                         return this._toggleActive( true, params );
293                 },
294
295                 /**
296                  * @param {Object} [params]
297                  * @returns {Boolean} false if already inactive
298                  */
299                 deactivate: function ( params ) {
300                         return this._toggleActive( false, params );
301                 },
302
303                 /**
304                  * To override by subclass, update the container's UI to reflect the provided active state.
305                  * @abstract
306                  */
307                 onChangeExpanded: function () {
308                         throw new Error( 'Must override with subclass.' );
309                 },
310
311                 /**
312                  * @param {Boolean} expanded
313                  * @param {Object} [params]
314                  * @returns {Boolean} false if state already applied
315                  */
316                 _toggleExpanded: function ( expanded, params ) {
317                         var self = this;
318                         params = params || {};
319                         if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
320                                 params.unchanged = true;
321                                 self.onChangeExpanded( self.expanded.get(), params );
322                                 return false;
323                         } else {
324                                 params.unchanged = false;
325                                 this.expandedArgumentsQueue.push( params );
326                                 this.expanded.set( expanded );
327                                 return true;
328                         }
329                 },
330
331                 /**
332                  * @param {Object} [params]
333                  * @returns {Boolean} false if already expanded
334                  */
335                 expand: function ( params ) {
336                         return this._toggleExpanded( true, params );
337                 },
338
339                 /**
340                  * @param {Object} [params]
341                  * @returns {Boolean} false if already collapsed
342                  */
343                 collapse: function ( params ) {
344                         return this._toggleExpanded( false, params );
345                 },
346
347                 /**
348                  * Bring the container into view and then expand this and bring it into view
349                  * @param {Object} [params]
350                  */
351                 focus: focus
352         });
353
354         /**
355          * @since 4.1.0
356          *
357          * @class
358          * @augments wp.customize.Class
359          */
360         api.Section = Container.extend({
361
362                 /**
363                  * @since 4.1.0
364                  *
365                  * @param {String} id
366                  * @param {Array}  options
367                  */
368                 initialize: function ( id, options ) {
369                         var section = this;
370                         Container.prototype.initialize.call( section, id, options );
371
372                         section.id = id;
373                         section.panel = new api.Value();
374                         section.panel.bind( function ( id ) {
375                                 $( section.container ).toggleClass( 'control-subsection', !! id );
376                         });
377                         section.panel.set( section.params.panel || '' );
378                         api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
379
380                         section.embed();
381                         section.deferred.embedded.done( function () {
382                                 section.ready();
383                         });
384                 },
385
386                 /**
387                  * Embed the container in the DOM when any parent panel is ready.
388                  *
389                  * @since 4.1.0
390                  */
391                 embed: function () {
392                         var section = this, inject;
393
394                         // Watch for changes to the panel state
395                         inject = function ( panelId ) {
396                                 var parentContainer;
397                                 if ( panelId ) {
398                                         // The panel has been supplied, so wait until the panel object is registered
399                                         api.panel( panelId, function ( panel ) {
400                                                 // The panel has been registered, wait for it to become ready/initialized
401                                                 panel.deferred.embedded.done( function () {
402                                                         parentContainer = panel.container.find( 'ul:first' );
403                                                         if ( ! section.container.parent().is( parentContainer ) ) {
404                                                                 parentContainer.append( section.container );
405                                                         }
406                                                         section.deferred.embedded.resolve();
407                                                 });
408                                         } );
409                                 } else {
410                                         // There is no panel, so embed the section in the root of the customizer
411                                         parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
412                                         if ( ! section.container.parent().is( parentContainer ) ) {
413                                                 parentContainer.append( section.container );
414                                         }
415                                         section.deferred.embedded.resolve();
416                                 }
417                         };
418                         section.panel.bind( inject );
419                         inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
420                 },
421
422                 /**
423                  * Add behaviors for the accordion section.
424                  *
425                  * @since 4.1.0
426                  */
427                 attachEvents: function () {
428                         var section = this;
429
430                         // Expand/Collapse accordion sections on click.
431                         section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
432                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
433                                         return;
434                                 }
435                                 event.preventDefault(); // Keep this AFTER the key filter above
436
437                                 if ( section.expanded() ) {
438                                         section.collapse();
439                                 } else {
440                                         section.expand();
441                                 }
442                         });
443                 },
444
445                 /**
446                  * Return whether this section has any active controls.
447                  *
448                  * @since 4.1.0
449                  *
450                  * @returns {Boolean}
451                  */
452                 isContextuallyActive: function () {
453                         var section = this,
454                                 controls = section.controls(),
455                                 activeCount = 0;
456                         _( controls ).each( function ( control ) {
457                                 if ( control.active() ) {
458                                         activeCount += 1;
459                                 }
460                         } );
461                         return ( activeCount !== 0 );
462                 },
463
464                 /**
465                  * Get the controls that are associated with this section, sorted by their priority Value.
466                  *
467                  * @since 4.1.0
468                  *
469                  * @returns {Array}
470                  */
471                 controls: function () {
472                         return this._children( 'section', 'control' );
473                 },
474
475                 /**
476                  * Update UI to reflect expanded state.
477                  *
478                  * @since 4.1.0
479                  *
480                  * @param {Boolean} expanded
481                  * @param {Object}  args
482                  */
483                 onChangeExpanded: function ( expanded, args ) {
484                         var section = this,
485                                 content = section.container.find( '.accordion-section-content' ),
486                                 expand;
487
488                         if ( expanded ) {
489
490                                 if ( args.unchanged ) {
491                                         expand = args.completeCallback;
492                                 } else {
493                                         expand = function () {
494                                                 content.stop().slideDown( args.duration, args.completeCallback );
495                                                 section.container.addClass( 'open' );
496                                         };
497                                 }
498
499                                 if ( ! args.allowMultiple ) {
500                                         api.section.each( function ( otherSection ) {
501                                                 if ( otherSection !== section ) {
502                                                         otherSection.collapse( { duration: args.duration } );
503                                                 }
504                                         });
505                                 }
506
507                                 if ( section.panel() ) {
508                                         api.panel( section.panel() ).expand({
509                                                 duration: args.duration,
510                                                 completeCallback: expand
511                                         });
512                                 } else {
513                                         expand();
514                                 }
515
516                         } else {
517                                 section.container.removeClass( 'open' );
518                                 content.slideUp( args.duration, args.completeCallback );
519                         }
520                 }
521         });
522
523         /**
524          * @since 4.1.0
525          *
526          * @class
527          * @augments wp.customize.Class
528          */
529         api.Panel = Container.extend({
530                 /**
531                  * @since 4.1.0
532                  *
533                  * @param  {String} id
534                  * @param  {Object} options
535                  */
536                 initialize: function ( id, options ) {
537                         var panel = this;
538                         Container.prototype.initialize.call( panel, id, options );
539                         panel.embed();
540                         panel.deferred.embedded.done( function () {
541                                 panel.ready();
542                         });
543                 },
544
545                 /**
546                  * Embed the container in the DOM when any parent panel is ready.
547                  *
548                  * @since 4.1.0
549                  */
550                 embed: function () {
551                         var panel = this,
552                                 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
553
554                         if ( ! panel.container.parent().is( parentContainer ) ) {
555                                 parentContainer.append( panel.container );
556                         }
557                         panel.deferred.embedded.resolve();
558                 },
559
560                 /**
561                  * @since 4.1.0
562                  */
563                 attachEvents: function () {
564                         var meta, panel = this;
565
566                         // Expand/Collapse accordion sections on click.
567                         panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
568                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
569                                         return;
570                                 }
571                                 event.preventDefault(); // Keep this AFTER the key filter above
572
573                                 if ( ! panel.expanded() ) {
574                                         panel.expand();
575                                 }
576                         });
577
578                         meta = panel.container.find( '.panel-meta:first' );
579
580                         meta.find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
581                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
582                                         return;
583                                 }
584                                 event.preventDefault(); // Keep this AFTER the key filter above
585
586                                 if ( meta.hasClass( 'cannot-expand' ) ) {
587                                         return;
588                                 }
589
590                                 var content = meta.find( '.accordion-section-content:first' );
591                                 if ( meta.hasClass( 'open' ) ) {
592                                         meta.toggleClass( 'open' );
593                                         content.slideUp( panel.defaultExpandedArguments.duration );
594                                 } else {
595                                         content.slideDown( panel.defaultExpandedArguments.duration );
596                                         meta.toggleClass( 'open' );
597                                 }
598                         });
599
600                 },
601
602                 /**
603                  * Get the sections that are associated with this panel, sorted by their priority Value.
604                  *
605                  * @since 4.1.0
606                  *
607                  * @returns {Array}
608                  */
609                 sections: function () {
610                         return this._children( 'panel', 'section' );
611                 },
612
613                 /**
614                  * Return whether this panel has any active sections.
615                  *
616                  * @since 4.1.0
617                  *
618                  * @returns {boolean}
619                  */
620                 isContextuallyActive: function () {
621                         var panel = this,
622                                 sections = panel.sections(),
623                                 activeCount = 0;
624                         _( sections ).each( function ( section ) {
625                                 if ( section.active() && section.isContextuallyActive() ) {
626                                         activeCount += 1;
627                                 }
628                         } );
629                         return ( activeCount !== 0 );
630                 },
631
632                 /**
633                  * Update UI to reflect expanded state
634                  *
635                  * @since 4.1.0
636                  *
637                  * @param {Boolean}  expanded
638                  * @param {Object}   args
639                  * @param {Boolean}  args.unchanged
640                  * @param {Callback} args.completeCallback
641                  */
642                 onChangeExpanded: function ( expanded, args ) {
643
644                         // Immediately call the complete callback if there were no changes
645                         if ( args.unchanged ) {
646                                 if ( args.completeCallback ) {
647                                         args.completeCallback();
648                                 }
649                                 return;
650                         }
651
652                         // Note: there is a second argument 'args' passed
653                         var position, scroll,
654                                 panel = this,
655                                 section = panel.container.closest( '.accordion-section' ),
656                                 overlay = section.closest( '.wp-full-overlay' ),
657                                 container = section.closest( '.accordion-container' ),
658                                 siblings = container.find( '.open' ),
659                                 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
660                                 backBtn = overlay.find( '.control-panel-back' ),
661                                 panelTitle = section.find( '.accordion-section-title' ).first(),
662                                 content = section.find( '.control-panel-content' );
663
664                         if ( expanded ) {
665
666                                 // Collapse any sibling sections/panels
667                                 api.section.each( function ( section ) {
668                                         if ( ! section.panel() ) {
669                                                 section.collapse( { duration: 0 } );
670                                         }
671                                 });
672                                 api.panel.each( function ( otherPanel ) {
673                                         if ( panel !== otherPanel ) {
674                                                 otherPanel.collapse( { duration: 0 } );
675                                         }
676                                 });
677
678                                 content.show( 0, function() {
679                                         position = content.offset().top;
680                                         scroll = container.scrollTop();
681                                         content.css( 'margin-top', ( 45 - position - scroll ) );
682                                         section.addClass( 'current-panel' );
683                                         overlay.addClass( 'in-sub-panel' );
684                                         container.scrollTop( 0 );
685                                         if ( args.completeCallback ) {
686                                                 args.completeCallback();
687                                         }
688                                 } );
689                                 topPanel.attr( 'tabindex', '-1' );
690                                 backBtn.attr( 'tabindex', '0' );
691                                 backBtn.focus();
692                         } else {
693                                 siblings.removeClass( 'open' );
694                                 section.removeClass( 'current-panel' );
695                                 overlay.removeClass( 'in-sub-panel' );
696                                 content.delay( 180 ).hide( 0, function() {
697                                         content.css( 'margin-top', 'inherit' ); // Reset
698                                         if ( args.completeCallback ) {
699                                                 args.completeCallback();
700                                         }
701                                 } );
702                                 topPanel.attr( 'tabindex', '0' );
703                                 backBtn.attr( 'tabindex', '-1' );
704                                 panelTitle.focus();
705                                 container.scrollTop( 0 );
706                         }
707                 }
708         });
709
710         /**
711          * A Customizer Control.
712          *
713          * A control provides a UI element that allows a user to modify a Customizer Setting.
714          *
715          * @see PHP class WP_Customize_Control.
716          *
717          * @class
718          * @augments wp.customize.Class
719          *
720          * @param {string} id                            Unique identifier for the control instance.
721          * @param {object} options                       Options hash for the control instance.
722          * @param {object} options.params
723          * @param {object} options.params.type           Type of control (e.g. text, radio, dropdown-pages, etc.)
724          * @param {string} options.params.content        The HTML content for the control.
725          * @param {string} options.params.priority       Order of priority to show the control within the section.
726          * @param {string} options.params.active
727          * @param {string} options.params.section
728          * @param {string} options.params.label
729          * @param {string} options.params.description
730          * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
731          */
732         api.Control = api.Class.extend({
733                 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
734
735                 initialize: function( id, options ) {
736                         var control = this,
737                                 nodes, radios, settings;
738
739                         control.params = {};
740                         $.extend( control, options || {} );
741                         control.id = id;
742                         control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
743                         control.templateSelector = 'customize-control-' + control.params.type + '-content';
744                         control.container = control.params.content ? $( control.params.content ) : $( control.selector );
745
746                         control.deferred = {
747                                 embedded: new $.Deferred()
748                         };
749                         control.section = new api.Value();
750                         control.priority = new api.Value();
751                         control.active = new api.Value();
752                         control.activeArgumentsQueue = [];
753
754                         control.elements = [];
755
756                         nodes  = control.container.find('[data-customize-setting-link]');
757                         radios = {};
758
759                         nodes.each( function() {
760                                 var node = $( this ),
761                                         name;
762
763                                 if ( node.is( ':radio' ) ) {
764                                         name = node.prop( 'name' );
765                                         if ( radios[ name ] ) {
766                                                 return;
767                                         }
768
769                                         radios[ name ] = true;
770                                         node = nodes.filter( '[name="' + name + '"]' );
771                                 }
772
773                                 api( node.data( 'customizeSettingLink' ), function( setting ) {
774                                         var element = new api.Element( node );
775                                         control.elements.push( element );
776                                         element.sync( setting );
777                                         element.set( setting() );
778                                 });
779                         });
780
781                         control.active.bind( function ( active ) {
782                                 var args = control.activeArgumentsQueue.shift();
783                                 args = $.extend( {}, control.defaultActiveArguments, args );
784                                 control.onChangeActive( active, args );
785                         } );
786
787                         control.section.set( control.params.section );
788                         control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
789                         control.active.set( control.params.active );
790
791                         api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
792
793                         // Associate this control with its settings when they are created
794                         settings = $.map( control.params.settings, function( value ) {
795                                 return value;
796                         });
797                         api.apply( api, settings.concat( function () {
798                                 var key;
799
800                                 control.settings = {};
801                                 for ( key in control.params.settings ) {
802                                         control.settings[ key ] = api( control.params.settings[ key ] );
803                                 }
804
805                                 control.setting = control.settings['default'] || null;
806
807                                 control.embed();
808                         }) );
809
810                         control.deferred.embedded.done( function () {
811                                 control.ready();
812                         });
813                 },
814
815                 /**
816                  * Embed the control into the page.
817                  */
818                 embed: function () {
819                         var control = this,
820                                 inject;
821
822                         // Watch for changes to the section state
823                         inject = function ( sectionId ) {
824                                 var parentContainer;
825                                 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the frontend
826                                         return;
827                                 }
828                                 // Wait for the section to be registered
829                                 api.section( sectionId, function ( section ) {
830                                         // Wait for the section to be ready/initialized
831                                         section.deferred.embedded.done( function () {
832                                                 parentContainer = section.container.find( 'ul:first' );
833                                                 if ( ! control.container.parent().is( parentContainer ) ) {
834                                                         parentContainer.append( control.container );
835                                                         control.renderContent();
836                                                 }
837                                                 control.deferred.embedded.resolve();
838                                         });
839                                 });
840                         };
841                         control.section.bind( inject );
842                         inject( control.section.get() );
843                 },
844
845                 /**
846                  * Triggered when the control's markup has been injected into the DOM.
847                  *
848                  * @abstract
849                  */
850                 ready: function() {},
851
852                 /**
853                  * Normal controls do not expand, so just expand its parent
854                  *
855                  * @param {Object} [params]
856                  */
857                 expand: function ( params ) {
858                         api.section( this.section() ).expand( params );
859                 },
860
861                 /**
862                  * Bring the containing section and panel into view and then
863                  * this control into view, focusing on the first input.
864                  */
865                 focus: focus,
866
867                 /**
868                  * Update UI in response to a change in the control's active state.
869                  * This does not change the active state, it merely handles the behavior
870                  * for when it does change.
871                  *
872                  * @since 4.1.0
873                  *
874                  * @param {Boolean}  active
875                  * @param {Object}   args
876                  * @param {Number}   args.duration
877                  * @param {Callback} args.completeCallback
878                  */
879                 onChangeActive: function ( active, args ) {
880                         if ( ! $.contains( document, this.container ) ) {
881                                 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
882                                 this.container.toggle( active );
883                                 if ( args.completeCallback ) {
884                                         args.completeCallback();
885                                 }
886                         } else if ( active ) {
887                                 this.container.slideDown( args.duration, args.completeCallback );
888                         } else {
889                                 this.container.slideUp( args.duration, args.completeCallback );
890                         }
891                 },
892
893                 /**
894                  * @deprecated 4.1.0 Use this.onChangeActive() instead.
895                  */
896                 toggle: function ( active ) {
897                         return this.onChangeActive( active, this.defaultActiveArguments );
898                 },
899
900                 /**
901                  * Shorthand way to enable the active state.
902                  *
903                  * @since 4.1.0
904                  *
905                  * @param {Object} [params]
906                  * @returns {Boolean} false if already active
907                  */
908                 activate: Container.prototype.activate,
909
910                 /**
911                  * Shorthand way to disable the active state.
912                  *
913                  * @since 4.1.0
914                  *
915                  * @param {Object} [params]
916                  * @returns {Boolean} false if already inactive
917                  */
918                 deactivate: Container.prototype.deactivate,
919
920                 /**
921                  * Re-use _toggleActive from Container class.
922                  *
923                  * @access private
924                  */
925                 _toggleActive: Container.prototype._toggleActive,
926
927                 dropdownInit: function() {
928                         var control      = this,
929                                 statuses     = this.container.find('.dropdown-status'),
930                                 params       = this.params,
931                                 toggleFreeze = false,
932                                 update       = function( to ) {
933                                         if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
934                                                 statuses.html( params.statuses[ to ] ).show();
935                                         else
936                                                 statuses.hide();
937                                 };
938
939                         // Support the .dropdown class to open/close complex elements
940                         this.container.on( 'click keydown', '.dropdown', function( event ) {
941                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
942                                         return;
943                                 }
944
945                                 event.preventDefault();
946
947                                 if (!toggleFreeze)
948                                         control.container.toggleClass('open');
949
950                                 if ( control.container.hasClass('open') )
951                                         control.container.parent().parent().find('li.library-selected').focus();
952
953                                 // Don't want to fire focus and click at same time
954                                 toggleFreeze = true;
955                                 setTimeout(function () {
956                                         toggleFreeze = false;
957                                 }, 400);
958                         });
959
960                         this.setting.bind( update );
961                         update( this.setting() );
962                 },
963
964                 /**
965                  * Render the control from its JS template, if it exists.
966                  *
967                  * The control's container must already exist in the DOM.
968                  *
969                  * @since 4.1.0
970                  */
971                 renderContent: function () {
972                         var template,
973                                 control = this;
974
975                         // Replace the container element's content with the control.
976                         if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
977                                 template = wp.template( control.templateSelector );
978                                 if ( template && control.container ) {
979                                         control.container.html( template( control.params ) );
980                                 }
981                         }
982                 }
983         });
984
985         /**
986          * A colorpicker control.
987          *
988          * @class
989          * @augments wp.customize.Control
990          * @augments wp.customize.Class
991          */
992         api.ColorControl = api.Control.extend({
993                 ready: function() {
994                         var control = this,
995                                 picker = this.container.find('.color-picker-hex');
996
997                         picker.val( control.setting() ).wpColorPicker({
998                                 change: function() {
999                                         control.setting.set( picker.wpColorPicker('color') );
1000                                 },
1001                                 clear: function() {
1002                                         control.setting.set( false );
1003                                 }
1004                         });
1005
1006                         this.setting.bind( function ( value ) {
1007                                 picker.val( value );
1008                                 picker.wpColorPicker( 'color', value );
1009                         });
1010                 }
1011         });
1012
1013         /**
1014          * An upload control, which utilizes the media modal.
1015          *
1016          * @class
1017          * @augments wp.customize.Control
1018          * @augments wp.customize.Class
1019          */
1020         api.UploadControl = api.Control.extend({
1021
1022                 /**
1023                  * When the control's DOM structure is ready,
1024                  * set up internal event bindings.
1025                  */
1026                 ready: function() {
1027                         var control = this;
1028                         // Shortcut so that we don't have to use _.bind every time we add a callback.
1029                         _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select' );
1030
1031                         // Bind events, with delegation to facilitate re-rendering.
1032                         control.container.on( 'click keydown', '.upload-button', control.openFrame );
1033                         control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1034                         control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1035                         control.container.on( 'click keydown', '.remove-button', control.removeFile );
1036
1037                         // Re-render whenever the control's setting changes.
1038                         control.setting.bind( function () { control.renderContent(); } );
1039                 },
1040
1041                 /**
1042                  * Open the media modal.
1043                  */
1044                 openFrame: function( event ) {
1045                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1046                                 return;
1047                         }
1048
1049                         event.preventDefault();
1050
1051                         if ( ! this.frame ) {
1052                                 this.initFrame();
1053                         }
1054
1055                         this.frame.open();
1056                 },
1057
1058                 /**
1059                  * Create a media modal select frame, and store it so the instance can be reused when needed.
1060                  */
1061                 initFrame: function() {
1062                         this.frame = wp.media({
1063                                 button: {
1064                                         text: this.params.button_labels.frame_button
1065                                 },
1066                                 states: [
1067                                         new wp.media.controller.Library({
1068                                                 title:     this.params.button_labels.frame_title,
1069                                                 library:   wp.media.query({ type: this.params.mime_type }),
1070                                                 multiple:  false,
1071                                                 date:      false
1072                                         })
1073                                 ]
1074                         });
1075
1076                         // When a file is selected, run a callback.
1077                         this.frame.on( 'select', this.select );
1078                 },
1079
1080                 /**
1081                  * Callback handler for when an attachment is selected in the media modal.
1082                  * Gets the selected image information, and sets it within the control.
1083                  */
1084                 select: function() {
1085                         // Get the attachment from the modal frame.
1086                         var attachment = this.frame.state().get( 'selection' ).first().toJSON();
1087
1088                         this.params.attachment = attachment;
1089
1090                         // Set the Customizer setting; the callback takes care of rendering.
1091                         this.setting( attachment.url );
1092                 },
1093
1094                 /**
1095                  * Reset the setting to the default value.
1096                  */
1097                 restoreDefault: function( event ) {
1098                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1099                                 return;
1100                         }
1101                         event.preventDefault();
1102
1103                         this.params.attachment = this.params.defaultAttachment;
1104                         this.setting( this.params.defaultAttachment.url );
1105                 },
1106
1107                 /**
1108                  * Called when the "Remove" link is clicked. Empties the setting.
1109                  *
1110                  * @param {object} event jQuery Event object
1111                  */
1112                 removeFile: function( event ) {
1113                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1114                                 return;
1115                         }
1116                         event.preventDefault();
1117
1118                         this.params.attachment = {};
1119                         this.setting( '' );
1120                         this.renderContent(); // Not bound to setting change when emptying.
1121                 },
1122
1123                 // @deprecated
1124                 success: function() {},
1125
1126                 // @deprecated
1127                 removerVisibility: function() {}
1128         });
1129
1130         /**
1131          * A control for uploading images.
1132          *
1133          * This control no longer needs to do anything more
1134          * than what the upload control does in JS.
1135          *
1136          * @class
1137          * @augments wp.customize.UploadControl
1138          * @augments wp.customize.Control
1139          * @augments wp.customize.Class
1140          */
1141         api.ImageControl = api.UploadControl.extend({
1142                 // @deprecated
1143                 thumbnailSrc: function() {}
1144         });
1145
1146         /**
1147          * A control for uploading background images.
1148          *
1149          * @class
1150          * @augments wp.customize.UploadControl
1151          * @augments wp.customize.Control
1152          * @augments wp.customize.Class
1153          */
1154         api.BackgroundControl = api.UploadControl.extend({
1155
1156                 /**
1157                  * When the control's DOM structure is ready,
1158                  * set up internal event bindings.
1159                  */
1160                 ready: function() {
1161                         api.UploadControl.prototype.ready.apply( this, arguments );
1162                 },
1163
1164                 /**
1165                  * Callback handler for when an attachment is selected in the media modal.
1166                  * Does an additional AJAX request for setting the background context.
1167                  */
1168                 select: function() {
1169                         api.UploadControl.prototype.select.apply( this, arguments );
1170
1171                         wp.ajax.post( 'custom-background-add', {
1172                                 nonce: _wpCustomizeBackground.nonces.add,
1173                                 wp_customize: 'on',
1174                                 theme: api.settings.theme.stylesheet,
1175                                 attachment_id: this.params.attachment.id
1176                         } );
1177                 }
1178         });
1179
1180         /**
1181          * @class
1182          * @augments wp.customize.Control
1183          * @augments wp.customize.Class
1184          */
1185         api.HeaderControl = api.Control.extend({
1186                 ready: function() {
1187                         this.btnRemove        = $('#customize-control-header_image .actions .remove');
1188                         this.btnNew           = $('#customize-control-header_image .actions .new');
1189
1190                         _.bindAll(this, 'openMedia', 'removeImage');
1191
1192                         this.btnNew.on( 'click', this.openMedia );
1193                         this.btnRemove.on( 'click', this.removeImage );
1194
1195                         api.HeaderTool.currentHeader = new api.HeaderTool.ImageModel();
1196
1197                         new api.HeaderTool.CurrentView({
1198                                 model: api.HeaderTool.currentHeader,
1199                                 el: '#customize-control-header_image .current .container'
1200                         });
1201
1202                         new api.HeaderTool.ChoiceListView({
1203                                 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
1204                                 el: '#customize-control-header_image .choices .uploaded .list'
1205                         });
1206
1207                         new api.HeaderTool.ChoiceListView({
1208                                 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
1209                                 el: '#customize-control-header_image .choices .default .list'
1210                         });
1211
1212                         api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
1213                                 api.HeaderTool.UploadsList,
1214                                 api.HeaderTool.DefaultsList
1215                         ]);
1216                 },
1217
1218                 /**
1219                  * Returns a set of options, computed from the attached image data and
1220                  * theme-specific data, to be fed to the imgAreaSelect plugin in
1221                  * wp.media.view.Cropper.
1222                  *
1223                  * @param {wp.media.model.Attachment} attachment
1224                  * @param {wp.media.controller.Cropper} controller
1225                  * @returns {Object} Options
1226                  */
1227                 calculateImageSelectOptions: function(attachment, controller) {
1228                         var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
1229                                 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
1230                                 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
1231                                 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
1232                                 ratio, xImg, yImg, realHeight, realWidth,
1233                                 imgSelectOptions;
1234
1235                         realWidth = attachment.get('width');
1236                         realHeight = attachment.get('height');
1237
1238                         this.headerImage = new api.HeaderTool.ImageModel();
1239                         this.headerImage.set({
1240                                 themeWidth: xInit,
1241                                 themeHeight: yInit,
1242                                 themeFlexWidth: flexWidth,
1243                                 themeFlexHeight: flexHeight,
1244                                 imageWidth: realWidth,
1245                                 imageHeight: realHeight
1246                         });
1247
1248                         controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
1249
1250                         ratio = xInit / yInit;
1251                         xImg = realWidth;
1252                         yImg = realHeight;
1253
1254                         if ( xImg / yImg > ratio ) {
1255                                 yInit = yImg;
1256                                 xInit = yInit * ratio;
1257                         } else {
1258                                 xInit = xImg;
1259                                 yInit = xInit / ratio;
1260                         }
1261
1262                         imgSelectOptions = {
1263                                 handles: true,
1264                                 keys: true,
1265                                 instance: true,
1266                                 persistent: true,
1267                                 imageWidth: realWidth,
1268                                 imageHeight: realHeight,
1269                                 x1: 0,
1270                                 y1: 0,
1271                                 x2: xInit,
1272                                 y2: yInit
1273                         };
1274
1275                         if (flexHeight === false && flexWidth === false) {
1276                                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
1277                         }
1278                         if (flexHeight === false ) {
1279                                 imgSelectOptions.maxHeight = yInit;
1280                         }
1281                         if (flexWidth === false ) {
1282                                 imgSelectOptions.maxWidth = xInit;
1283                         }
1284
1285                         return imgSelectOptions;
1286                 },
1287
1288                 /**
1289                  * Sets up and opens the Media Manager in order to select an image.
1290                  * Depending on both the size of the image and the properties of the
1291                  * current theme, a cropping step after selection may be required or
1292                  * skippable.
1293                  *
1294                  * @param {event} event
1295                  */
1296                 openMedia: function(event) {
1297                         var l10n = _wpMediaViewsL10n;
1298
1299                         event.preventDefault();
1300
1301                         this.frame = wp.media({
1302                                 button: {
1303                                         text: l10n.selectAndCrop,
1304                                         close: false
1305                                 },
1306                                 states: [
1307                                         new wp.media.controller.Library({
1308                                                 title:     l10n.chooseImage,
1309                                                 library:   wp.media.query({ type: 'image' }),
1310                                                 multiple:  false,
1311                                                 date:      false,
1312                                                 priority:  20,
1313                                                 suggestedWidth: _wpCustomizeHeader.data.width,
1314                                                 suggestedHeight: _wpCustomizeHeader.data.height
1315                                         }),
1316                                         new wp.media.controller.Cropper({
1317                                                 imgSelectOptions: this.calculateImageSelectOptions
1318                                         })
1319                                 ]
1320                         });
1321
1322                         this.frame.on('select', this.onSelect, this);
1323                         this.frame.on('cropped', this.onCropped, this);
1324                         this.frame.on('skippedcrop', this.onSkippedCrop, this);
1325
1326                         this.frame.open();
1327                 },
1328
1329                 /**
1330                  * After an image is selected in the media modal,
1331                  * switch to the cropper state.
1332                  */
1333                 onSelect: function() {
1334                         this.frame.setState('cropper');
1335                 },
1336
1337                 /**
1338                  * After the image has been cropped, apply the cropped image data to the setting.
1339                  *
1340                  * @param {object} croppedImage Cropped attachment data.
1341                  */
1342                 onCropped: function(croppedImage) {
1343                         var url = croppedImage.post_content,
1344                                 attachmentId = croppedImage.attachment_id,
1345                                 w = croppedImage.width,
1346                                 h = croppedImage.height;
1347                         this.setImageFromURL(url, attachmentId, w, h);
1348                 },
1349
1350                 /**
1351                  * If cropping was skipped, apply the image data directly to the setting.
1352                  *
1353                  * @param {object} selection
1354                  */
1355                 onSkippedCrop: function(selection) {
1356                         var url = selection.get('url'),
1357                                 w = selection.get('width'),
1358                                 h = selection.get('height');
1359                         this.setImageFromURL(url, selection.id, w, h);
1360                 },
1361
1362                 /**
1363                  * Creates a new wp.customize.HeaderTool.ImageModel from provided
1364                  * header image data and inserts it into the user-uploaded headers
1365                  * collection.
1366                  *
1367                  * @param {String} url
1368                  * @param {Number} attachmentId
1369                  * @param {Number} width
1370                  * @param {Number} height
1371                  */
1372                 setImageFromURL: function(url, attachmentId, width, height) {
1373                         var choice, data = {};
1374
1375                         data.url = url;
1376                         data.thumbnail_url = url;
1377                         data.timestamp = _.now();
1378
1379                         if (attachmentId) {
1380                                 data.attachment_id = attachmentId;
1381                         }
1382
1383                         if (width) {
1384                                 data.width = width;
1385                         }
1386
1387                         if (height) {
1388                                 data.height = height;
1389                         }
1390
1391                         choice = new api.HeaderTool.ImageModel({
1392                                 header: data,
1393                                 choice: url.split('/').pop()
1394                         });
1395                         api.HeaderTool.UploadsList.add(choice);
1396                         api.HeaderTool.currentHeader.set(choice.toJSON());
1397                         choice.save();
1398                         choice.importImage();
1399                 },
1400
1401                 /**
1402                  * Triggers the necessary events to deselect an image which was set as
1403                  * the currently selected one.
1404                  */
1405                 removeImage: function() {
1406                         api.HeaderTool.currentHeader.trigger('hide');
1407                         api.HeaderTool.CombinedList.trigger('control:removeImage');
1408                 }
1409
1410         });
1411
1412         // Change objects contained within the main customize object to Settings.
1413         api.defaultConstructor = api.Setting;
1414
1415         // Create the collections for Controls, Sections and Panels.
1416         api.control = new api.Values({ defaultConstructor: api.Control });
1417         api.section = new api.Values({ defaultConstructor: api.Section });
1418         api.panel = new api.Values({ defaultConstructor: api.Panel });
1419
1420         /**
1421          * @class
1422          * @augments wp.customize.Messenger
1423          * @augments wp.customize.Class
1424          * @mixes wp.customize.Events
1425          */
1426         api.PreviewFrame = api.Messenger.extend({
1427                 sensitivity: 2000,
1428
1429                 initialize: function( params, options ) {
1430                         var deferred = $.Deferred();
1431
1432                         // This is the promise object.
1433                         deferred.promise( this );
1434
1435                         this.container = params.container;
1436                         this.signature = params.signature;
1437
1438                         $.extend( params, { channel: api.PreviewFrame.uuid() });
1439
1440                         api.Messenger.prototype.initialize.call( this, params, options );
1441
1442                         this.add( 'previewUrl', params.previewUrl );
1443
1444                         this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
1445
1446                         this.run( deferred );
1447                 },
1448
1449                 run: function( deferred ) {
1450                         var self   = this,
1451                                 loaded = false,
1452                                 ready  = false;
1453
1454                         if ( this._ready ) {
1455                                 this.unbind( 'ready', this._ready );
1456                         }
1457
1458                         this._ready = function() {
1459                                 ready = true;
1460
1461                                 if ( loaded ) {
1462                                         deferred.resolveWith( self );
1463                                 }
1464                         };
1465
1466                         this.bind( 'ready', this._ready );
1467
1468                         this.bind( 'ready', function ( data ) {
1469                                 if ( ! data ) {
1470                                         return;
1471                                 }
1472
1473                                 /*
1474                                  * Walk over all panels, sections, and controls and set their
1475                                  * respective active states to true if the preview explicitly
1476                                  * indicates as such.
1477                                  */
1478                                 var constructs = {
1479                                         panel: data.activePanels,
1480                                         section: data.activeSections,
1481                                         control: data.activeControls
1482                                 };
1483                                 _( constructs ).each( function ( activeConstructs, type ) {
1484                                         api[ type ].each( function ( construct, id ) {
1485                                                 var active = !! ( activeConstructs && activeConstructs[ id ] );
1486                                                 construct.active( active );
1487                                         } );
1488                                 } );
1489                         } );
1490
1491                         this.request = $.ajax( this.previewUrl(), {
1492                                 type: 'POST',
1493                                 data: this.query,
1494                                 xhrFields: {
1495                                         withCredentials: true
1496                                 }
1497                         } );
1498
1499                         this.request.fail( function() {
1500                                 deferred.rejectWith( self, [ 'request failure' ] );
1501                         });
1502
1503                         this.request.done( function( response ) {
1504                                 var location = self.request.getResponseHeader('Location'),
1505                                         signature = self.signature,
1506                                         index;
1507
1508                                 // Check if the location response header differs from the current URL.
1509                                 // If so, the request was redirected; try loading the requested page.
1510                                 if ( location && location !== self.previewUrl() ) {
1511                                         deferred.rejectWith( self, [ 'redirect', location ] );
1512                                         return;
1513                                 }
1514
1515                                 // Check if the user is not logged in.
1516                                 if ( '0' === response ) {
1517                                         self.login( deferred );
1518                                         return;
1519                                 }
1520
1521                                 // Check for cheaters.
1522                                 if ( '-1' === response ) {
1523                                         deferred.rejectWith( self, [ 'cheatin' ] );
1524                                         return;
1525                                 }
1526
1527                                 // Check for a signature in the request.
1528                                 index = response.lastIndexOf( signature );
1529                                 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
1530                                         deferred.rejectWith( self, [ 'unsigned' ] );
1531                                         return;
1532                                 }
1533
1534                                 // Strip the signature from the request.
1535                                 response = response.slice( 0, index ) + response.slice( index + signature.length );
1536
1537                                 // Create the iframe and inject the html content.
1538                                 self.iframe = $('<iframe />').appendTo( self.container );
1539
1540                                 // Bind load event after the iframe has been added to the page;
1541                                 // otherwise it will fire when injected into the DOM.
1542                                 self.iframe.one( 'load', function() {
1543                                         loaded = true;
1544
1545                                         if ( ready ) {
1546                                                 deferred.resolveWith( self );
1547                                         } else {
1548                                                 setTimeout( function() {
1549                                                         deferred.rejectWith( self, [ 'ready timeout' ] );
1550                                                 }, self.sensitivity );
1551                                         }
1552                                 });
1553
1554                                 self.targetWindow( self.iframe[0].contentWindow );
1555
1556                                 self.targetWindow().document.open();
1557                                 self.targetWindow().document.write( response );
1558                                 self.targetWindow().document.close();
1559                         });
1560                 },
1561
1562                 login: function( deferred ) {
1563                         var self = this,
1564                                 reject;
1565
1566                         reject = function() {
1567                                 deferred.rejectWith( self, [ 'logged out' ] );
1568                         };
1569
1570                         if ( this.triedLogin )
1571                                 return reject();
1572
1573                         // Check if we have an admin cookie.
1574                         $.get( api.settings.url.ajax, {
1575                                 action: 'logged-in'
1576                         }).fail( reject ).done( function( response ) {
1577                                 var iframe;
1578
1579                                 if ( '1' !== response )
1580                                         reject();
1581
1582                                 iframe = $('<iframe src="' + self.previewUrl() + '" />').hide();
1583                                 iframe.appendTo( self.container );
1584                                 iframe.load( function() {
1585                                         self.triedLogin = true;
1586
1587                                         iframe.remove();
1588                                         self.run( deferred );
1589                                 });
1590                         });
1591                 },
1592
1593                 destroy: function() {
1594                         api.Messenger.prototype.destroy.call( this );
1595                         this.request.abort();
1596
1597                         if ( this.iframe )
1598                                 this.iframe.remove();
1599
1600                         delete this.request;
1601                         delete this.iframe;
1602                         delete this.targetWindow;
1603                 }
1604         });
1605
1606         (function(){
1607                 var uuid = 0;
1608                 /**
1609                  * Create a universally unique identifier.
1610                  *
1611                  * @return {int}
1612                  */
1613                 api.PreviewFrame.uuid = function() {
1614                         return 'preview-' + uuid++;
1615                 };
1616         }());
1617
1618         /**
1619          * Set the document title of the customizer.
1620          *
1621          * @since 4.1.0
1622          *
1623          * @param {string} documentTitle
1624          */
1625         api.setDocumentTitle = function ( documentTitle ) {
1626                 var tmpl, title;
1627                 tmpl = api.settings.documentTitleTmpl;
1628                 title = tmpl.replace( '%s', documentTitle );
1629                 document.title = title;
1630                 if ( window !== window.parent ) {
1631                         window.parent.document.title = document.title;
1632                 }
1633         };
1634
1635         /**
1636          * @class
1637          * @augments wp.customize.Messenger
1638          * @augments wp.customize.Class
1639          * @mixes wp.customize.Events
1640          */
1641         api.Previewer = api.Messenger.extend({
1642                 refreshBuffer: 250,
1643
1644                 /**
1645                  * Requires params:
1646                  *  - container  - a selector or jQuery element
1647                  *  - previewUrl - the URL of preview frame
1648                  */
1649                 initialize: function( params, options ) {
1650                         var self = this,
1651                                 rscheme = /^https?/;
1652
1653                         $.extend( this, options || {} );
1654                         this.deferred = {
1655                                 active: $.Deferred()
1656                         };
1657
1658                         /*
1659                          * Wrap this.refresh to prevent it from hammering the servers:
1660                          *
1661                          * If refresh is called once and no other refresh requests are
1662                          * loading, trigger the request immediately.
1663                          *
1664                          * If refresh is called while another refresh request is loading,
1665                          * debounce the refresh requests:
1666                          * 1. Stop the loading request (as it is instantly outdated).
1667                          * 2. Trigger the new request once refresh hasn't been called for
1668                          *    self.refreshBuffer milliseconds.
1669                          */
1670                         this.refresh = (function( self ) {
1671                                 var refresh  = self.refresh,
1672                                         callback = function() {
1673                                                 timeout = null;
1674                                                 refresh.call( self );
1675                                         },
1676                                         timeout;
1677
1678                                 return function() {
1679                                         if ( typeof timeout !== 'number' ) {
1680                                                 if ( self.loading ) {
1681                                                         self.abort();
1682                                                 } else {
1683                                                         return callback();
1684                                                 }
1685                                         }
1686
1687                                         clearTimeout( timeout );
1688                                         timeout = setTimeout( callback, self.refreshBuffer );
1689                                 };
1690                         })( this );
1691
1692                         this.container   = api.ensure( params.container );
1693                         this.allowedUrls = params.allowedUrls;
1694                         this.signature   = params.signature;
1695
1696                         params.url = window.location.href;
1697
1698                         api.Messenger.prototype.initialize.call( this, params );
1699
1700                         this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
1701                                 var match = to.match( rscheme );
1702                                 return match ? match[0] : '';
1703                         });
1704
1705                         // Limit the URL to internal, front-end links.
1706                         //
1707                         // If the frontend and the admin are served from the same domain, load the
1708                         // preview over ssl if the Customizer is being loaded over ssl. This avoids
1709                         // insecure content warnings. This is not attempted if the admin and frontend
1710                         // are on different domains to avoid the case where the frontend doesn't have
1711                         // ssl certs.
1712
1713                         this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
1714                                 var result;
1715
1716                                 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
1717                                 // Strip hashes and query strings before testing.
1718                                 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
1719                                         return null;
1720
1721                                 // Attempt to match the URL to the control frame's scheme
1722                                 // and check if it's allowed. If not, try the original URL.
1723                                 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
1724                                         $.each( self.allowedUrls, function( i, allowed ) {
1725                                                 var path;
1726
1727                                                 allowed = allowed.replace( /\/+$/, '' );
1728                                                 path = url.replace( allowed, '' );
1729
1730                                                 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
1731                                                         result = url;
1732                                                         return false;
1733                                                 }
1734                                         });
1735                                         if ( result )
1736                                                 return false;
1737                                 });
1738
1739                                 // If we found a matching result, return it. If not, bail.
1740                                 return result ? result : null;
1741                         });
1742
1743                         // Refresh the preview when the URL is changed (but not yet).
1744                         this.previewUrl.bind( this.refresh );
1745
1746                         this.scroll = 0;
1747                         this.bind( 'scroll', function( distance ) {
1748                                 this.scroll = distance;
1749                         });
1750
1751                         // Update the URL when the iframe sends a URL message.
1752                         this.bind( 'url', this.previewUrl );
1753
1754                         // Update the document title when the preview changes.
1755                         this.bind( 'documentTitle', function ( title ) {
1756                                 api.setDocumentTitle( title );
1757                         } );
1758                 },
1759
1760                 query: function() {},
1761
1762                 abort: function() {
1763                         if ( this.loading ) {
1764                                 this.loading.destroy();
1765                                 delete this.loading;
1766                         }
1767                 },
1768
1769                 refresh: function() {
1770                         var self = this;
1771
1772                         this.abort();
1773
1774                         this.loading = new api.PreviewFrame({
1775                                 url:        this.url(),
1776                                 previewUrl: this.previewUrl(),
1777                                 query:      this.query() || {},
1778                                 container:  this.container,
1779                                 signature:  this.signature
1780                         });
1781
1782                         this.loading.done( function() {
1783                                 // 'this' is the loading frame
1784                                 this.bind( 'synced', function() {
1785                                         if ( self.preview )
1786                                                 self.preview.destroy();
1787                                         self.preview = this;
1788                                         delete self.loading;
1789
1790                                         self.targetWindow( this.targetWindow() );
1791                                         self.channel( this.channel() );
1792
1793                                         self.deferred.active.resolve();
1794                                         self.send( 'active' );
1795                                 });
1796
1797                                 this.send( 'sync', {
1798                                         scroll:   self.scroll,
1799                                         settings: api.get()
1800                                 });
1801                         });
1802
1803                         this.loading.fail( function( reason, location ) {
1804                                 if ( 'redirect' === reason && location )
1805                                         self.previewUrl( location );
1806
1807                                 if ( 'logged out' === reason ) {
1808                                         if ( self.preview ) {
1809                                                 self.preview.destroy();
1810                                                 delete self.preview;
1811                                         }
1812
1813                                         self.login().done( self.refresh );
1814                                 }
1815
1816                                 if ( 'cheatin' === reason )
1817                                         self.cheatin();
1818                         });
1819                 },
1820
1821                 login: function() {
1822                         var previewer = this,
1823                                 deferred, messenger, iframe;
1824
1825                         if ( this._login )
1826                                 return this._login;
1827
1828                         deferred = $.Deferred();
1829                         this._login = deferred.promise();
1830
1831                         messenger = new api.Messenger({
1832                                 channel: 'login',
1833                                 url:     api.settings.url.login
1834                         });
1835
1836                         iframe = $('<iframe src="' + api.settings.url.login + '" />').appendTo( this.container );
1837
1838                         messenger.targetWindow( iframe[0].contentWindow );
1839
1840                         messenger.bind( 'login', function() {
1841                                 iframe.remove();
1842                                 messenger.destroy();
1843                                 delete previewer._login;
1844                                 deferred.resolve();
1845                         });
1846
1847                         return this._login;
1848                 },
1849
1850                 cheatin: function() {
1851                         $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
1852                 }
1853         });
1854
1855         api.controlConstructor = {
1856                 color:  api.ColorControl,
1857                 upload: api.UploadControl,
1858                 image:  api.ImageControl,
1859                 header: api.HeaderControl,
1860                 background: api.BackgroundControl
1861         };
1862         api.panelConstructor = {};
1863         api.sectionConstructor = {};
1864
1865         $( function() {
1866                 api.settings = window._wpCustomizeSettings;
1867                 api.l10n = window._wpCustomizeControlsL10n;
1868
1869                 // Check if we can run the Customizer.
1870                 if ( ! api.settings ) {
1871                         return;
1872                 }
1873
1874                 // Redirect to the fallback preview if any incompatibilities are found.
1875                 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
1876                         return window.location = api.settings.url.fallback;
1877
1878                 var parent, topFocus,
1879                         body = $( document.body ),
1880                         overlay = body.children( '.wp-full-overlay' ),
1881                         title = $( '#customize-info .theme-name.site-title' ),
1882                         closeBtn = $( '.customize-controls-close' ),
1883                         saveBtn = $( '#save' );
1884
1885                 // Prevent the form from saving when enter is pressed on an input or select element.
1886                 $('#customize-controls').on( 'keydown', function( e ) {
1887                         var isEnter = ( 13 === e.which ),
1888                                 $el = $( e.target );
1889
1890                         if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
1891                                 e.preventDefault();
1892                         }
1893                 });
1894
1895                 // Expand/Collapse the main customizer customize info.
1896                 $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
1897                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1898                                 return;
1899                         }
1900                         event.preventDefault(); // Keep this AFTER the key filter above
1901
1902                         var section = $( this ).parent(),
1903                                 content = section.find( '.accordion-section-content:first' );
1904
1905                         if ( section.hasClass( 'cannot-expand' ) ) {
1906                                 return;
1907                         }
1908
1909                         if ( section.hasClass( 'open' ) ) {
1910                                 section.toggleClass( 'open' );
1911                                 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
1912                         } else {
1913                                 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
1914                                 section.toggleClass( 'open' );
1915                         }
1916                 });
1917
1918                 // Initialize Previewer
1919                 api.previewer = new api.Previewer({
1920                         container:   '#customize-preview',
1921                         form:        '#customize-controls',
1922                         previewUrl:  api.settings.url.preview,
1923                         allowedUrls: api.settings.url.allowed,
1924                         signature:   'WP_CUSTOMIZER_SIGNATURE'
1925                 }, {
1926
1927                         nonce: api.settings.nonce,
1928
1929                         query: function() {
1930                                 var dirtyCustomized = {};
1931                                 api.each( function ( value, key ) {
1932                                         if ( value._dirty ) {
1933                                                 dirtyCustomized[ key ] = value();
1934                                         }
1935                                 } );
1936
1937                                 return {
1938                                         wp_customize: 'on',
1939                                         theme:      api.settings.theme.stylesheet,
1940                                         customized: JSON.stringify( dirtyCustomized ),
1941                                         nonce:      this.nonce.preview
1942                                 };
1943                         },
1944
1945                         save: function() {
1946                                 var self  = this,
1947                                         query = $.extend( this.query(), {
1948                                                 action: 'customize_save',
1949                                                 nonce:  this.nonce.save
1950                                         } ),
1951                                         processing = api.state( 'processing' ),
1952                                         submitWhenDoneProcessing,
1953                                         submit;
1954
1955                                 body.addClass( 'saving' );
1956
1957                                 submit = function () {
1958                                         var request = $.post( api.settings.url.ajax, query );
1959
1960                                         api.trigger( 'save', request );
1961
1962                                         request.always( function () {
1963                                                 body.removeClass( 'saving' );
1964                                         } );
1965
1966                                         request.done( function( response ) {
1967                                                 // Check if the user is logged out.
1968                                                 if ( '0' === response ) {
1969                                                         self.preview.iframe.hide();
1970                                                         self.login().done( function() {
1971                                                                 self.save();
1972                                                                 self.preview.iframe.show();
1973                                                         } );
1974                                                         return;
1975                                                 }
1976
1977                                                 // Check for cheaters.
1978                                                 if ( '-1' === response ) {
1979                                                         self.cheatin();
1980                                                         return;
1981                                                 }
1982
1983                                                 // Clear setting dirty states
1984                                                 api.each( function ( value ) {
1985                                                         value._dirty = false;
1986                                                 } );
1987                                                 api.trigger( 'saved' );
1988                                         } );
1989                                 };
1990
1991                                 if ( 0 === processing() ) {
1992                                         submit();
1993                                 } else {
1994                                         submitWhenDoneProcessing = function () {
1995                                                 if ( 0 === processing() ) {
1996                                                         api.state.unbind( 'change', submitWhenDoneProcessing );
1997                                                         submit();
1998                                                 }
1999                                         };
2000                                         api.state.bind( 'change', submitWhenDoneProcessing );
2001                                 }
2002
2003                         }
2004                 });
2005
2006                 // Refresh the nonces if the preview sends updated nonces over.
2007                 api.previewer.bind( 'nonce', function( nonce ) {
2008                         $.extend( this.nonce, nonce );
2009                 });
2010
2011                 // Create Settings
2012                 $.each( api.settings.settings, function( id, data ) {
2013                         api.create( id, id, data.value, {
2014                                 transport: data.transport,
2015                                 previewer: api.previewer
2016                         } );
2017                 });
2018
2019                 // Create Panels
2020                 $.each( api.settings.panels, function ( id, data ) {
2021                         var constructor = api.panelConstructor[ data.type ] || api.Panel,
2022                                 panel;
2023
2024                         panel = new constructor( id, {
2025                                 params: data
2026                         } );
2027                         api.panel.add( id, panel );
2028                 });
2029
2030                 // Create Sections
2031                 $.each( api.settings.sections, function ( id, data ) {
2032                         var constructor = api.sectionConstructor[ data.type ] || api.Section,
2033                                 section;
2034
2035                         section = new constructor( id, {
2036                                 params: data
2037                         } );
2038                         api.section.add( id, section );
2039                 });
2040
2041                 // Create Controls
2042                 $.each( api.settings.controls, function( id, data ) {
2043                         var constructor = api.controlConstructor[ data.type ] || api.Control,
2044                                 control;
2045
2046                         control = new constructor( id, {
2047                                 params: data,
2048                                 previewer: api.previewer
2049                         } );
2050                         api.control.add( id, control );
2051                 });
2052
2053                 // Focus the autofocused element
2054                 _.each( [ 'panel', 'section', 'control' ], function ( type ) {
2055                         var instance, id = api.settings.autofocus[ type ];
2056                         if ( id && api[ type ]( id ) ) {
2057                                 instance = api[ type ]( id );
2058                                 // Wait until the element is embedded in the DOM
2059                                 instance.deferred.embedded.done( function () {
2060                                         // Wait until the preview has activated and so active panels, sections, controls have been set
2061                                         api.previewer.deferred.active.done( function () {
2062                                                 instance.focus();
2063                                         });
2064                                 });
2065                         }
2066                 });
2067
2068                 /**
2069                  * Sort panels, sections, controls by priorities. Hide empty sections and panels.
2070                  *
2071                  * @since 4.1.0
2072                  */
2073                 api.reflowPaneContents = _.bind( function () {
2074
2075                         var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
2076
2077                         if ( document.activeElement ) {
2078                                 activeElement = $( document.activeElement );
2079                         }
2080
2081                         // Sort the sections within each panel
2082                         api.panel.each( function ( panel ) {
2083                                 var sections = panel.sections(),
2084                                         sectionContainers = _.pluck( sections, 'container' );
2085                                 rootNodes.push( panel );
2086                                 appendContainer = panel.container.find( 'ul:first' );
2087                                 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
2088                                         _( sections ).each( function ( section ) {
2089                                                 appendContainer.append( section.container );
2090                                         } );
2091                                         wasReflowed = true;
2092                                 }
2093                         } );
2094
2095                         // Sort the controls within each section
2096                         api.section.each( function ( section ) {
2097                                 var controls = section.controls(),
2098                                         controlContainers = _.pluck( controls, 'container' );
2099                                 if ( ! section.panel() ) {
2100                                         rootNodes.push( section );
2101                                 }
2102                                 appendContainer = section.container.find( 'ul:first' );
2103                                 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
2104                                         _( controls ).each( function ( control ) {
2105                                                 appendContainer.append( control.container );
2106                                         } );
2107                                         wasReflowed = true;
2108                                 }
2109                         } );
2110
2111                         // Sort the root panels and sections
2112                         rootNodes.sort( api.utils.prioritySort );
2113                         rootContainers = _.pluck( rootNodes, 'container' );
2114                         appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
2115                         if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
2116                                 _( rootNodes ).each( function ( rootNode ) {
2117                                         appendContainer.append( rootNode.container );
2118                                 } );
2119                                 wasReflowed = true;
2120                         }
2121
2122                         // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
2123                         api.panel.each( function ( panel ) {
2124                                 var value = panel.active();
2125                                 panel.active.callbacks.fireWith( panel.active, [ value, value ] );
2126                         } );
2127                         api.section.each( function ( section ) {
2128                                 var value = section.active();
2129                                 section.active.callbacks.fireWith( section.active, [ value, value ] );
2130                         } );
2131
2132                         // Restore focus if there was a reflow and there was an active (focused) element
2133                         if ( wasReflowed && activeElement ) {
2134                                 activeElement.focus();
2135                         }
2136                 }, api );
2137                 api.bind( 'ready', api.reflowPaneContents );
2138                 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
2139                 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
2140                         values.bind( 'add', api.reflowPaneContents );
2141                         values.bind( 'change', api.reflowPaneContents );
2142                         values.bind( 'remove', api.reflowPaneContents );
2143                 } );
2144
2145                 // Check if preview url is valid and load the preview frame.
2146                 if ( api.previewer.previewUrl() ) {
2147                         api.previewer.refresh();
2148                 } else {
2149                         api.previewer.previewUrl( api.settings.url.home );
2150                 }
2151
2152                 // Save and activated states
2153                 (function() {
2154                         var state = new api.Values(),
2155                                 saved = state.create( 'saved' ),
2156                                 activated = state.create( 'activated' ),
2157                                 processing = state.create( 'processing' );
2158
2159                         state.bind( 'change', function() {
2160                                 if ( ! activated() ) {
2161                                         saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
2162                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
2163
2164                                 } else if ( saved() ) {
2165                                         saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
2166                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
2167
2168                                 } else {
2169                                         saveBtn.val( api.l10n.save ).prop( 'disabled', false );
2170                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
2171                                 }
2172                         });
2173
2174                         // Set default states.
2175                         saved( true );
2176                         activated( api.settings.theme.active );
2177                         processing( 0 );
2178
2179                         api.bind( 'change', function() {
2180                                 state('saved').set( false );
2181                         });
2182
2183                         api.bind( 'saved', function() {
2184                                 state('saved').set( true );
2185                                 state('activated').set( true );
2186                         });
2187
2188                         activated.bind( function( to ) {
2189                                 if ( to )
2190                                         api.trigger( 'activated' );
2191                         });
2192
2193                         // Expose states to the API.
2194                         api.state = state;
2195                 }());
2196
2197                 // Button bindings.
2198                 saveBtn.click( function( event ) {
2199                         api.previewer.save();
2200                         event.preventDefault();
2201                 }).keydown( function( event ) {
2202                         if ( 9 === event.which ) // tab
2203                                 return;
2204                         if ( 13 === event.which ) // enter
2205                                 api.previewer.save();
2206                         event.preventDefault();
2207                 });
2208
2209                 // Go back to the top-level Customizer accordion.
2210                 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( event ) {
2211                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2212                                 return;
2213                         }
2214
2215                         event.preventDefault(); // Keep this AFTER the key filter above
2216                         api.panel.each( function ( panel ) {
2217                                 panel.collapse();
2218                         });
2219                 });
2220
2221                 closeBtn.keydown( function( event ) {
2222                         if ( 9 === event.which ) // tab
2223                                 return;
2224                         if ( 13 === event.which ) // enter
2225                                 this.click();
2226                         event.preventDefault();
2227                 });
2228
2229                 $('.collapse-sidebar').on( 'click keydown', function( event ) {
2230                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2231                                 return;
2232                         }
2233
2234                         overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
2235                         event.preventDefault();
2236                 });
2237
2238                 // Bind site title display to the corresponding field.
2239                 if ( title.length ) {
2240                         $( '#customize-control-blogname input' ).on( 'input', function() {
2241                                 title.text( this.value );
2242                         } );
2243                 }
2244
2245                 // Create a potential postMessage connection with the parent frame.
2246                 parent = new api.Messenger({
2247                         url: api.settings.url.parent,
2248                         channel: 'loader'
2249                 });
2250
2251                 // If we receive a 'back' event, we're inside an iframe.
2252                 // Send any clicks to the 'Return' link to the parent page.
2253                 parent.bind( 'back', function() {
2254                         closeBtn.on( 'click.customize-controls-close', function( event ) {
2255                                 event.preventDefault();
2256                                 parent.send( 'close' );
2257                         });
2258                 });
2259
2260                 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
2261                 $( window ).on( 'beforeunload', function () {
2262                         if ( ! api.state( 'saved' )() ) {
2263                                 return api.l10n.saveAlert;
2264                         }
2265                 } );
2266
2267                 // Pass events through to the parent.
2268                 $.each( [ 'saved', 'change' ], function ( i, event ) {
2269                         api.bind( event, function() {
2270                                 parent.send( event );
2271                         });
2272                 } );
2273
2274                 // When activated, let the loader handle redirecting the page.
2275                 // If no loader exists, redirect the page ourselves (if a url exists).
2276                 api.bind( 'activated', function() {
2277                         if ( parent.targetWindow() )
2278                                 parent.send( 'activated', api.settings.url.activated );
2279                         else if ( api.settings.url.activated )
2280                                 window.location = api.settings.url.activated;
2281                 });
2282
2283                 // Initialize the connection with the parent frame.
2284                 parent.send( 'ready' );
2285
2286                 // Control visibility for default controls
2287                 $.each({
2288                         'background_image': {
2289                                 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
2290                                 callback: function( to ) { return !! to; }
2291                         },
2292                         'show_on_front': {
2293                                 controls: [ 'page_on_front', 'page_for_posts' ],
2294                                 callback: function( to ) { return 'page' === to; }
2295                         },
2296                         'header_textcolor': {
2297                                 controls: [ 'header_textcolor' ],
2298                                 callback: function( to ) { return 'blank' !== to; }
2299                         }
2300                 }, function( settingId, o ) {
2301                         api( settingId, function( setting ) {
2302                                 $.each( o.controls, function( i, controlId ) {
2303                                         api.control( controlId, function( control ) {
2304                                                 var visibility = function( to ) {
2305                                                         control.container.toggle( o.callback( to ) );
2306                                                 };
2307
2308                                                 visibility( setting.get() );
2309                                                 setting.bind( visibility );
2310                                         });
2311                                 });
2312                         });
2313                 });
2314
2315                 // Juggle the two controls that use header_textcolor
2316                 api.control( 'display_header_text', function( control ) {
2317                         var last = '';
2318
2319                         control.elements[0].unsync( api( 'header_textcolor' ) );
2320
2321                         control.element = new api.Element( control.container.find('input') );
2322                         control.element.set( 'blank' !== control.setting() );
2323
2324                         control.element.bind( function( to ) {
2325                                 if ( ! to )
2326                                         last = api( 'header_textcolor' ).get();
2327
2328                                 control.setting.set( to ? last : 'blank' );
2329                         });
2330
2331                         control.setting.bind( function( to ) {
2332                                 control.element.set( 'blank' !== to );
2333                         });
2334                 });
2335
2336                 api.trigger( 'ready' );
2337
2338                 // Make sure left column gets focus
2339                 topFocus = closeBtn;
2340                 topFocus.focus();
2341                 setTimeout(function () {
2342                         topFocus.focus();
2343                 }, 200);
2344
2345         });
2346
2347 })( wp, jQuery );