cb4e6e20a6818d7e153b1709a64ec34d85050460
[autoinstalls/wordpress.git] / wp-admin / js / customize-controls.dev.js
1 (function( exports, $ ){
2         var api = wp.customize;
3
4         /*
5          * @param options
6          * - previewer - The Previewer instance to sync with.
7          * - transport - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
8          */
9         api.Setting = api.Value.extend({
10                 initialize: function( id, value, options ) {
11                         var element;
12
13                         api.Value.prototype.initialize.call( this, value, options );
14
15                         this.id = id;
16                         this.transport = this.transport || 'refresh';
17
18                         this.bind( this.preview );
19                 },
20                 preview: function() {
21                         switch ( this.transport ) {
22                                 case 'refresh':
23                                         return this.previewer.refresh();
24                                 case 'postMessage':
25                                         return this.previewer.send( 'setting', [ this.id, this() ] );
26                         }
27                 }
28         });
29
30         api.Control = api.Class.extend({
31                 initialize: function( id, options ) {
32                         var control = this,
33                                 nodes, radios, settings;
34
35                         this.params = {};
36                         $.extend( this, options || {} );
37
38                         this.id = id;
39                         this.selector = '#customize-control-' + id.replace( ']', '' ).replace( '[', '-' );
40                         this.container = $( this.selector );
41
42                         settings = $.map( this.params.settings, function( value ) {
43                                 return value;
44                         });
45
46                         api.apply( api, settings.concat( function() {
47                                 var key;
48
49                                 control.settings = {};
50                                 for ( key in control.params.settings ) {
51                                         control.settings[ key ] = api( control.params.settings[ key ] );
52                                 }
53
54                                 control.setting = control.settings['default'] || null;
55                                 control.ready();
56                         }) );
57
58                         control.elements = [];
59
60                         nodes  = this.container.find('[data-customize-setting-link]');
61                         radios = {};
62
63                         nodes.each( function() {
64                                 var node = $(this),
65                                         name;
66
67                                 if ( node.is(':radio') ) {
68                                         name = node.prop('name');
69                                         if ( radios[ name ] )
70                                                 return;
71
72                                         radios[ name ] = true;
73                                         node = nodes.filter( '[name="' + name + '"]' );
74                                 }
75
76                                 api( node.data('customizeSettingLink'), function( setting ) {
77                                         var element = new api.Element( node );
78                                         control.elements.push( element );
79                                         element.sync( setting );
80                                         element.set( setting() );
81                                 });
82                         });
83                 },
84
85                 ready: function() {},
86
87                 dropdownInit: function() {
88                         var control  = this,
89                                 statuses = this.container.find('.dropdown-status'),
90                                 params   = this.params,
91                                 update   = function( to ) {
92                                         if ( typeof     to === 'string' && params.statuses && params.statuses[ to ] )
93                                                 statuses.html( params.statuses[ to ] ).show();
94                                         else
95                                                 statuses.hide();
96                                 };
97
98                         // Support the .dropdown class to open/close complex elements
99                         this.container.on( 'click', '.dropdown', function( event ) {
100                                 event.preventDefault();
101                                 control.container.toggleClass('open');
102                         });
103
104                         this.setting.bind( update );
105                         update( this.setting() );
106                 }
107         });
108
109         api.ColorControl = api.Control.extend({
110                 ready: function() {
111                         var control = this,
112                                 rhex, spot, input, text, update;
113
114                         rhex   = /^#([A-Fa-f0-9]{3}){0,2}$/;
115                         spot   = this.container.find('.dropdown-content');
116                         input  = new api.Element( this.container.find('.color-picker-hex') );
117                         update = function( color ) {
118                                 spot.css( 'background', color );
119                                 control.farbtastic.setColor( color );
120                         };
121
122                         this.farbtastic = $.farbtastic( this.container.find('.farbtastic-placeholder'), control.setting.set );
123
124                         // Only pass through values that are valid hexes/empty.
125                         input.sync( this.setting ).validate = function( to ) {
126                                 return rhex.test( to ) ? to : null;
127                         };
128
129                         this.setting.bind( update );
130                         update( this.setting() );
131
132                         this.dropdownInit();
133                 }
134         });
135
136         api.UploadControl = api.Control.extend({
137                 ready: function() {
138                         var control = this;
139
140                         this.params.removed = this.params.removed || '';
141
142                         this.success = $.proxy( this.success, this );
143
144                         this.uploader = $.extend({
145                                 container: this.container,
146                                 browser:   this.container.find('.upload'),
147                                 dropzone:  this.container.find('.upload-dropzone'),
148                                 success:   this.success
149                         }, this.uploader || {} );
150
151                         if ( this.uploader.supported ) {
152                                 if ( control.params.context )
153                                         control.uploader.param( 'post_data[context]', this.params.context );
154
155                                 control.uploader.param( 'post_data[theme]', api.settings.theme.stylesheet );
156                         }
157
158                         this.uploader = new wp.Uploader( this.uploader );
159
160                         this.remover = this.container.find('.remove');
161                         this.remover.click( function( event ) {
162                                 control.setting.set( control.params.removed );
163                                 event.preventDefault();
164                         });
165
166                         this.removerVisibility = $.proxy( this.removerVisibility, this );
167                         this.setting.bind( this.removerVisibility );
168                         this.removerVisibility( this.setting.get() );
169                 },
170                 success: function( attachment ) {
171                         this.setting.set( attachment.url );
172                 },
173                 removerVisibility: function( to ) {
174                         this.remover.toggle( to != this.params.removed );
175                 }
176         });
177
178         api.ImageControl = api.UploadControl.extend({
179                 ready: function() {
180                         var control = this,
181                                 panels;
182
183                         this.uploader = {
184                                 init: function( up ) {
185                                         var fallback, button;
186
187                                         if ( this.supports.dragdrop )
188                                                 return;
189
190                                         // Maintain references while wrapping the fallback button.
191                                         fallback = control.container.find( '.upload-fallback' );
192                                         button   = fallback.children().detach();
193
194                                         this.browser.detach().empty().append( button );
195                                         fallback.append( this.browser ).show();
196                                 }
197                         };
198
199                         api.UploadControl.prototype.ready.call( this );
200
201                         this.thumbnail    = this.container.find('.preview-thumbnail img');
202                         this.thumbnailSrc = $.proxy( this.thumbnailSrc, this );
203                         this.setting.bind( this.thumbnailSrc );
204
205                         this.library = this.container.find('.library');
206
207                         // Generate tab objects
208                         this.tabs = {};
209                         panels    = this.library.find('.library-content');
210
211                         this.library.children('ul').children('li').each( function() {
212                                 var link  = $(this),
213                                         id    = link.data('customizeTab'),
214                                         panel = panels.filter('[data-customize-tab="' + id + '"]');
215
216                                 control.tabs[ id ] = {
217                                         both:  link.add( panel ),
218                                         link:  link,
219                                         panel: panel
220                                 };
221                         });
222
223                         // Select a tab
224                         this.selected = this.tabs[ panels.first().data('customizeTab') ];
225                         this.selected.both.addClass('library-selected');
226
227                         // Bind tab switch events
228                         this.library.children('ul').on( 'click', 'li', function( event ) {
229                                 var id  = $(this).data('customizeTab'),
230                                         tab = control.tabs[ id ];
231
232                                 event.preventDefault();
233
234                                 if ( tab.link.hasClass('library-selected') )
235                                         return;
236
237                                 control.selected.both.removeClass('library-selected');
238                                 control.selected = tab;
239                                 control.selected.both.addClass('library-selected');
240                         });
241
242                         // Bind events to switch image urls.
243                         this.library.on( 'click', 'a', function( event ) {
244                                 var value = $(this).data('customizeImageValue');
245
246                                 if ( value ) {
247                                         control.setting.set( value );
248                                         event.preventDefault();
249                                 }
250                         });
251
252                         if ( this.tabs.uploaded ) {
253                                 this.tabs.uploaded.target = this.library.find('.uploaded-target');
254                                 if ( ! this.tabs.uploaded.panel.find('.thumbnail').length )
255                                         this.tabs.uploaded.both.addClass('hidden');
256                         }
257
258                         this.dropdownInit();
259                 },
260                 success: function( attachment ) {
261                         api.UploadControl.prototype.success.call( this, attachment );
262
263                         // Add the uploaded image to the uploaded tab.
264                         if ( this.tabs.uploaded && this.tabs.uploaded.target.length ) {
265                                 this.tabs.uploaded.both.removeClass('hidden');
266
267                                 attachment.element = $( '<a href="#" class="thumbnail"></a>' )
268                                         .data( 'customizeImageValue', attachment.url )
269                                         .append( '<img src="' +  attachment.url+ '" />' )
270                                         .appendTo( this.tabs.uploaded.target );
271                         }
272                 },
273                 thumbnailSrc: function( to ) {
274                         if ( /^(https?:)?\/\//.test( to ) )
275                                 this.thumbnail.prop( 'src', to ).show();
276                         else
277                                 this.thumbnail.hide();
278                 }
279         });
280
281         // Change objects contained within the main customize object to Settings.
282         api.defaultConstructor = api.Setting;
283
284         // Create the collection of Control objects.
285         api.control = new api.Values({ defaultConstructor: api.Control });
286
287         api.PreviewFrame = api.Messenger.extend({
288                 sensitivity: 2000,
289
290                 initialize: function( params, options ) {
291                         var deferred = $.Deferred(),
292                                 self     = this;
293
294                         // This is the promise object.
295                         deferred.promise( this );
296
297                         this.previewer = params.previewer;
298
299                         $.extend( params, { channel: api.PreviewFrame.uuid() });
300
301                         api.Messenger.prototype.initialize.call( this, params, options );
302
303                         this.add( 'previewUrl', params.previewUrl );
304
305                         this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
306
307                         this.run( deferred );
308                 },
309
310                 run: function( deferred ) {
311                         var self   = this,
312                                 loaded = false,
313                                 ready  = false;
314
315                         if ( this._ready )
316                                 this.unbind( 'ready', this._ready );
317
318                         this._ready = function() {
319                                 ready = true;
320
321                                 if ( loaded )
322                                         deferred.resolveWith( self );
323                         };
324
325                         this.bind( 'ready', this._ready );
326
327                         this.request = $.ajax( this.previewUrl(), {
328                                 type: 'POST',
329                                 data: this.query,
330                                 xhrFields: {
331                                         withCredentials: true
332                                 }
333                         } );
334
335                         this.request.fail( function() {
336                                 deferred.rejectWith( self, [ 'request failure' ] );
337                         });
338
339                         this.request.done( function( response ) {
340                                 var location = self.request.getResponseHeader('Location'),
341                                         signature = 'WP_CUSTOMIZER_SIGNATURE',
342                                         index;
343
344                                 // Check if the location response header differs from the current URL.
345                                 // If so, the request was redirected; try loading the requested page.
346                                 if ( location && location != self.previewUrl() ) {
347                                         deferred.rejectWith( self, [ 'redirect', location ] );
348                                         return;
349                                 }
350
351                                 // Check if the user is not logged in.
352                                 if ( '0' === response ) {
353                                         self.login( deferred );
354                                         return;
355                                 }
356
357                                 // Check for cheaters.
358                                 if ( '-1' === response ) {
359                                         deferred.rejectWith( self, [ 'cheatin' ] );
360                                         return;
361                                 }
362
363                                 // Check for a signature in the request.
364                                 index = response.lastIndexOf( signature );
365                                 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
366                                         deferred.rejectWith( self, [ 'unsigned' ] );
367                                         return;
368                                 }
369
370                                 // Strip the signature from the request.
371                                 response = response.slice( 0, index ) + response.slice( index + signature.length );
372
373                                 // Create the iframe and inject the html content.
374                                 self.iframe = $('<iframe />').appendTo( self.previewer.container );
375
376                                 // Bind load event after the iframe has been added to the page;
377                                 // otherwise it will fire when injected into the DOM.
378                                 self.iframe.one( 'load', function() {
379                                         loaded = true;
380
381                                         if ( ready ) {
382                                                 deferred.resolveWith( self );
383                                         } else {
384                                                 setTimeout( function() {
385                                                         deferred.rejectWith( self, [ 'ready timeout' ] );
386                                                 }, self.sensitivity );
387                                         }
388                                 });
389
390                                 self.targetWindow( self.iframe[0].contentWindow );
391
392                                 self.targetWindow().document.open();
393                                 self.targetWindow().document.write( response );
394                                 self.targetWindow().document.close();
395                         });
396                 },
397
398                 login: function( deferred ) {
399                         var self = this,
400                                 reject;
401
402                         reject = function() {
403                                 deferred.rejectWith( self, [ 'logged out' ] );
404                         };
405
406                         if ( this.triedLogin )
407                                 return reject();
408
409                         // Check if we have an admin cookie.
410                         $.get( api.settings.url.ajax, {
411                                 action: 'logged-in'
412                         }).fail( reject ).done( function( response ) {
413                                 var iframe;
414
415                                 if ( '1' !== response )
416                                         reject();
417
418                                 iframe = $('<iframe src="' + self.previewUrl() + '" />').hide();
419                                 iframe.appendTo( self.previewer.container );
420                                 iframe.load( function() {
421                                         self.triedLogin = true;
422
423                                         iframe.remove();
424                                         self.run( deferred );
425                                 });
426                         });
427                 },
428
429                 destroy: function() {
430                         api.Messenger.prototype.destroy.call( this );
431                         this.request.abort();
432
433                         if ( this.iframe )
434                                 this.iframe.remove();
435
436                         delete this.request;
437                         delete this.iframe;
438                         delete this.targetWindow;
439                 }
440         });
441
442         (function(){
443                 var uuid = 0;
444                 api.PreviewFrame.uuid = function() {
445                         return 'preview-' + uuid++;
446                 };
447         }());
448
449         api.Previewer = api.Messenger.extend({
450                 refreshBuffer: 250,
451
452                 /**
453                  * Requires params:
454                  *  - container  - a selector or jQuery element
455                  *  - previewUrl - the URL of preview frame
456                  */
457                 initialize: function( params, options ) {
458                         var self = this,
459                                 rscheme = /^https?/,
460                                 url;
461
462                         $.extend( this, options || {} );
463
464                         /*
465                          * Wrap this.refresh to prevent it from hammering the servers:
466                          *
467                          * If refresh is called once and no other refresh requests are
468                          * loading, trigger the request immediately.
469                          *
470                          * If refresh is called while another refresh request is loading,
471                          * debounce the refresh requests:
472                          * 1. Stop the loading request (as it is instantly outdated).
473                          * 2. Trigger the new request once refresh hasn't been called for
474                          *    self.refreshBuffer milliseconds.
475                          */
476                         this.refresh = (function( self ) {
477                                 var refresh  = self.refresh,
478                                         callback = function() {
479                                                 timeout = null;
480                                                 refresh.call( self );
481                                         },
482                                         timeout;
483
484                                 return function() {
485                                         if ( typeof timeout !== 'number' ) {
486                                                 if ( self.loading ) {
487                                                         self.abort();
488                                                 } else {
489                                                         return callback();
490                                                 }
491                                         }
492
493                                         clearTimeout( timeout );
494                                         timeout = setTimeout( callback, self.refreshBuffer );
495                                 };
496                         })( this );
497
498                         this.container   = api.ensure( params.container );
499                         this.allowedUrls = params.allowedUrls;
500
501                         params.url = window.location.href;
502
503                         api.Messenger.prototype.initialize.call( this, params );
504
505                         this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
506                                 var match = to.match( rscheme );
507                                 return match ? match[0] : '';
508                         });
509
510                         // Limit the URL to internal, front-end links.
511                         //
512                         // If the frontend and the admin are served from the same domain, load the
513                         // preview over ssl if the customizer is being loaded over ssl. This avoids
514                         // insecure content warnings. This is not attempted if the admin and frontend
515                         // are on different domains to avoid the case where the frontend doesn't have
516                         // ssl certs.
517
518                         this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
519                                 var result;
520
521                                 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
522                                 // Strip hashes and query strings before testing.
523                                 if ( /\/wp-admin(\/|$)/.test( to.replace(/[#?].*$/, '') ) )
524                                         return null;
525
526                                 // Attempt to match the URL to the control frame's scheme
527                                 // and check if it's allowed. If not, try the original URL.
528                                 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
529                                         $.each( self.allowedUrls, function( i, allowed ) {
530                                                 if ( 0 === url.indexOf( allowed ) ) {
531                                                         result = url;
532                                                         return false;
533                                                 }
534                                         });
535                                         if ( result )
536                                                 return false;
537                                 });
538
539                                 // If we found a matching result, return it. If not, bail.
540                                 return result ? result : null;
541                         });
542
543                         // Refresh the preview when the URL is changed (but not yet).
544                         this.previewUrl.bind( this.refresh );
545
546                         this.scroll = 0;
547                         this.bind( 'scroll', function( distance ) {
548                                 this.scroll = distance;
549                         });
550
551                         // Update the URL when the iframe sends a URL message.
552                         this.bind( 'url', this.previewUrl );
553                 },
554
555                 query: function() {},
556
557                 abort: function() {
558                         if ( this.loading ) {
559                                 this.loading.destroy();
560                                 delete this.loading;
561                         }
562                 },
563
564                 refresh: function() {
565                         var self = this;
566
567                         this.abort();
568
569                         this.loading = new api.PreviewFrame({
570                                 url:        this.url(),
571                                 previewUrl: this.previewUrl(),
572                                 query:      this.query() || {},
573                                 previewer:  this
574                         });
575
576                         this.loading.done( function() {
577                                 // 'this' is the loading frame
578                                 this.bind( 'synced', function() {
579                                         if ( self.preview )
580                                                 self.preview.destroy();
581                                         self.preview = this;
582                                         delete self.loading;
583
584                                         self.targetWindow( this.targetWindow() );
585                                         self.channel( this.channel() );
586                                 });
587
588                                 this.send( 'sync', {
589                                         scroll:   self.scroll,
590                                         settings: api.get()
591                                 });
592                         });
593
594                         this.loading.fail( function( reason, location ) {
595                                 if ( 'redirect' === reason && location )
596                                         self.previewUrl( location );
597
598                                 if ( 'logged out' === reason ) {
599                                         if ( self.preview ) {
600                                                 self.preview.destroy();
601                                                 delete self.preview;
602                                         }
603
604                                         self.login().done( self.refresh );
605                                 }
606
607                                 if ( 'cheatin' === reason )
608                                         self.cheatin();
609                         });
610                 },
611
612                 login: function() {
613                         var previewer = this,
614                                 deferred, messenger, iframe;
615
616                         if ( this._login )
617                                 return this._login;
618
619                         deferred = $.Deferred();
620                         this._login = deferred.promise();
621
622                         messenger = new api.Messenger({
623                                 channel: 'login',
624                                 url:     api.settings.url.login
625                         });
626
627                         iframe = $('<iframe src="' + api.settings.url.login + '" />').appendTo( this.container );
628
629                         messenger.targetWindow( iframe[0].contentWindow );
630
631                         messenger.bind( 'login', function() {
632                                 iframe.remove();
633                                 messenger.destroy();
634                                 delete previewer._login;
635                                 deferred.resolve();
636                         });
637
638                         return this._login;
639                 },
640
641                 cheatin: function() {
642                         $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
643                 }
644         });
645
646         /* =====================================================================
647          * Ready.
648          * ===================================================================== */
649
650         api.controlConstructor = {
651                 color:  api.ColorControl,
652                 upload: api.UploadControl,
653                 image:  api.ImageControl
654         };
655
656         $( function() {
657                 api.settings = window._wpCustomizeSettings;
658                 api.l10n = window._wpCustomizeControlsL10n;
659
660                 // Check if we can run the customizer.
661                 if ( ! api.settings )
662                         return;
663
664                 // Redirect to the fallback preview if any incompatibilities are found.
665                 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
666                         return window.location = api.settings.url.fallback;
667
668                 var body = $( document.body ),
669                         overlay = body.children('.wp-full-overlay'),
670                         query, previewer, parent;
671
672                 // Prevent the form from saving when enter is pressed.
673                 $('#customize-controls').on( 'keydown', function( e ) {
674                         if ( $( e.target ).is('textarea') )
675                                 return;
676
677                         if ( 13 === e.which ) // Enter
678                                 e.preventDefault();
679                 });
680
681                 // Initialize Previewer
682                 previewer = new api.Previewer({
683                         container:   '#customize-preview',
684                         form:        '#customize-controls',
685                         previewUrl:  api.settings.url.preview,
686                         allowedUrls: api.settings.url.allowed
687                 }, {
688                         query: function() {
689                                 return {
690                                         wp_customize: 'on',
691                                         theme:        api.settings.theme.stylesheet,
692                                         customized:   JSON.stringify( api.get() )
693                                 };
694                         },
695
696                         nonce: $('#_wpnonce').val(),
697
698                         save: function() {
699                                 var self  = this,
700                                         query = $.extend( this.query(), {
701                                                 action: 'customize_save',
702                                                 nonce:  this.nonce
703                                         }),
704                                         request = $.post( api.settings.url.ajax, query );
705
706                                 api.trigger( 'save', request );
707
708                                 body.addClass('saving');
709
710                                 request.always( function() {
711                                         body.removeClass('saving');
712                                 });
713
714                                 request.done( function( response ) {
715                                         // Check if the user is logged out.
716                                         if ( '0' === response ) {
717                                                 self.preview.iframe.hide();
718                                                 self.login().done( function() {
719                                                         self.save();
720                                                         self.preview.iframe.show();
721                                                 });
722                                                 return;
723                                         }
724
725                                         // Check for cheaters.
726                                         if ( '-1' === response ) {
727                                                 self.cheatin();
728                                                 return;
729                                         }
730
731                                         api.trigger( 'saved' );
732                                 });
733                         }
734                 });
735
736                 $.each( api.settings.settings, function( id, data ) {
737                         api.create( id, id, data.value, {
738                                 transport: data.transport,
739                                 previewer: previewer
740                         } );
741                 });
742
743                 $.each( api.settings.controls, function( id, data ) {
744                         var constructor = api.controlConstructor[ data.type ] || api.Control,
745                                 control;
746
747                         control = api.control.add( id, new constructor( id, {
748                                 params: data,
749                                 previewer: previewer
750                         } ) );
751                 });
752
753                 // Check if preview url is valid and load the preview frame.
754                 if ( previewer.previewUrl() )
755                         previewer.refresh();
756                 else
757                         previewer.previewUrl( api.settings.url.home );
758
759                 // Save and activated states
760                 (function() {
761                         var state = new api.Values(),
762                                 saved = state.create('saved'),
763                                 activated = state.create('activated');
764
765                         state.bind( 'change', function() {
766                                 var save = $('#save'),
767                                         back = $('.back');
768
769                                 if ( ! activated() ) {
770                                         save.val( api.l10n.activate ).prop( 'disabled', false );
771                                         back.text( api.l10n.cancel );
772
773                                 } else if ( saved() ) {
774                                         save.val( api.l10n.saved ).prop( 'disabled', true );
775                                         back.text( api.l10n.close );
776
777                                 } else {
778                                         save.val( api.l10n.save ).prop( 'disabled', false );
779                                         back.text( api.l10n.cancel );
780                                 }
781                         });
782
783                         // Set default states.
784                         saved( true );
785                         activated( api.settings.theme.active );
786
787                         api.bind( 'change', function() {
788                                 state('saved').set( false );
789                         });
790
791                         api.bind( 'saved', function() {
792                                 state('saved').set( true );
793                                 state('activated').set( true );
794                         });
795
796                         activated.bind( function( to ) {
797                                 if ( to )
798                                         api.trigger( 'activated' );
799                         });
800
801                         // Expose states to the API.
802                         api.state = state;
803                 }());
804
805                 // Temporary accordion code.
806                 $('.customize-section-title').click( function( event ) {
807                         var clicked = $( this ).parents( '.customize-section' );
808
809                         if ( clicked.hasClass('cannot-expand') )
810                                 return;
811
812                         $( '.customize-section' ).not( clicked ).removeClass( 'open' );
813                         clicked.toggleClass( 'open' );
814                         event.preventDefault();
815                 });
816
817                 // Button bindings.
818                 $('#save').click( function( event ) {
819                         previewer.save();
820                         event.preventDefault();
821                 });
822
823                 $('.collapse-sidebar').click( function( event ) {
824                         overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
825                         event.preventDefault();
826                 });
827
828                 // Create a potential postMessage connection with the parent frame.
829                 parent = new api.Messenger({
830                         url: api.settings.url.parent,
831                         channel: 'loader'
832                 });
833
834                 // If we receive a 'back' event, we're inside an iframe.
835                 // Send any clicks to the 'Return' link to the parent page.
836                 parent.bind( 'back', function() {
837                         $('.back').on( 'click.back', function( event ) {
838                                 event.preventDefault();
839                                 parent.send( 'close' );
840                         });
841                 });
842
843                 // Pass events through to the parent.
844                 api.bind( 'saved', function() {
845                         parent.send( 'saved' );
846                 });
847
848                 // When activated, let the loader handle redirecting the page.
849                 // If no loader exists, redirect the page ourselves (if a url exists).
850                 api.bind( 'activated', function() {
851                         if ( parent.targetWindow() )
852                                 parent.send( 'activated', api.settings.url.activated );
853                         else if ( api.settings.url.activated )
854                                 window.location = api.settings.url.activated;
855                 });
856
857                 // Initialize the connection with the parent frame.
858                 parent.send( 'ready' );
859
860                 // Control visibility for default controls
861                 $.each({
862                         'background_image': {
863                                 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
864                                 callback: function( to ) { return !! to }
865                         },
866                         'show_on_front': {
867                                 controls: [ 'page_on_front', 'page_for_posts' ],
868                                 callback: function( to ) { return 'page' === to }
869                         },
870                         'header_textcolor': {
871                                 controls: [ 'header_textcolor' ],
872                                 callback: function( to ) { return 'blank' !== to }
873                         }
874                 }, function( settingId, o ) {
875                         api( settingId, function( setting ) {
876                                 $.each( o.controls, function( i, controlId ) {
877                                         api.control( controlId, function( control ) {
878                                                 var visibility = function( to ) {
879                                                         control.container.toggle( o.callback( to ) );
880                                                 };
881
882                                                 visibility( setting.get() );
883                                                 setting.bind( visibility );
884                                         });
885                                 });
886                         });
887                 });
888
889                 // Juggle the two controls that use header_textcolor
890                 api.control( 'display_header_text', function( control ) {
891                         var last = '';
892
893                         control.elements[0].unsync( api( 'header_textcolor' ) );
894
895                         control.element = new api.Element( control.container.find('input') );
896                         control.element.set( 'blank' !== control.setting() );
897
898                         control.element.bind( function( to ) {
899                                 if ( ! to )
900                                         last = api( 'header_textcolor' ).get();
901
902                                 control.setting.set( to ? last : 'blank' );
903                         });
904
905                         control.setting.bind( function( to ) {
906                                 control.element.set( 'blank' !== to );
907                         });
908                 });
909
910                 // Handle header image data
911                 api.control( 'header_image', function( control ) {
912                         control.setting.bind( function( to ) {
913                                 if ( to === control.params.removed )
914                                         control.settings.data.set( false );
915                         });
916
917                         control.library.on( 'click', 'a', function( event ) {
918                                 control.settings.data.set( $(this).data('customizeHeaderImageData') );
919                         });
920
921                         control.uploader.success = function( attachment ) {
922                                 var data;
923
924                                 api.ImageControl.prototype.success.call( control, attachment );
925
926                                 data = {
927                                         attachment_id: attachment.id,
928                                         url:           attachment.url,
929                                         thumbnail_url: attachment.url,
930                                         height:        attachment.meta.height,
931                                         width:         attachment.meta.width
932                                 };
933
934                                 attachment.element.data( 'customizeHeaderImageData', data );
935                                 control.settings.data.set( data );
936                         }
937                 });
938
939                 api.trigger( 'ready' );
940         });
941
942 })( wp, jQuery );