c37e43bd024ceef9a750716810ac4fcc05ed451b
[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.container = params.container;
298                         this.signature = params.signature;
299
300                         $.extend( params, { channel: api.PreviewFrame.uuid() });
301
302                         api.Messenger.prototype.initialize.call( this, params, options );
303
304                         this.add( 'previewUrl', params.previewUrl );
305
306                         this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
307
308                         this.run( deferred );
309                 },
310
311                 run: function( deferred ) {
312                         var self   = this,
313                                 loaded = false,
314                                 ready  = false;
315
316                         if ( this._ready )
317                                 this.unbind( 'ready', this._ready );
318
319                         this._ready = function() {
320                                 ready = true;
321
322                                 if ( loaded )
323                                         deferred.resolveWith( self );
324                         };
325
326                         this.bind( 'ready', this._ready );
327
328                         this.request = $.ajax( this.previewUrl(), {
329                                 type: 'POST',
330                                 data: this.query,
331                                 xhrFields: {
332                                         withCredentials: true
333                                 }
334                         } );
335
336                         this.request.fail( function() {
337                                 deferred.rejectWith( self, [ 'request failure' ] );
338                         });
339
340                         this.request.done( function( response ) {
341                                 var location = self.request.getResponseHeader('Location'),
342                                         signature = self.signature,
343                                         index;
344
345                                 // Check if the location response header differs from the current URL.
346                                 // If so, the request was redirected; try loading the requested page.
347                                 if ( location && location != self.previewUrl() ) {
348                                         deferred.rejectWith( self, [ 'redirect', location ] );
349                                         return;
350                                 }
351
352                                 // Check if the user is not logged in.
353                                 if ( '0' === response ) {
354                                         self.login( deferred );
355                                         return;
356                                 }
357
358                                 // Check for cheaters.
359                                 if ( '-1' === response ) {
360                                         deferred.rejectWith( self, [ 'cheatin' ] );
361                                         return;
362                                 }
363
364                                 // Check for a signature in the request.
365                                 index = response.lastIndexOf( signature );
366                                 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
367                                         deferred.rejectWith( self, [ 'unsigned' ] );
368                                         return;
369                                 }
370
371                                 // Strip the signature from the request.
372                                 response = response.slice( 0, index ) + response.slice( index + signature.length );
373
374                                 // Create the iframe and inject the html content.
375                                 self.iframe = $('<iframe />').appendTo( self.container );
376
377                                 // Bind load event after the iframe has been added to the page;
378                                 // otherwise it will fire when injected into the DOM.
379                                 self.iframe.one( 'load', function() {
380                                         loaded = true;
381
382                                         if ( ready ) {
383                                                 deferred.resolveWith( self );
384                                         } else {
385                                                 setTimeout( function() {
386                                                         deferred.rejectWith( self, [ 'ready timeout' ] );
387                                                 }, self.sensitivity );
388                                         }
389                                 });
390
391                                 self.targetWindow( self.iframe[0].contentWindow );
392
393                                 self.targetWindow().document.open();
394                                 self.targetWindow().document.write( response );
395                                 self.targetWindow().document.close();
396                         });
397                 },
398
399                 login: function( deferred ) {
400                         var self = this,
401                                 reject;
402
403                         reject = function() {
404                                 deferred.rejectWith( self, [ 'logged out' ] );
405                         };
406
407                         if ( this.triedLogin )
408                                 return reject();
409
410                         // Check if we have an admin cookie.
411                         $.get( api.settings.url.ajax, {
412                                 action: 'logged-in'
413                         }).fail( reject ).done( function( response ) {
414                                 var iframe;
415
416                                 if ( '1' !== response )
417                                         reject();
418
419                                 iframe = $('<iframe src="' + self.previewUrl() + '" />').hide();
420                                 iframe.appendTo( self.container );
421                                 iframe.load( function() {
422                                         self.triedLogin = true;
423
424                                         iframe.remove();
425                                         self.run( deferred );
426                                 });
427                         });
428                 },
429
430                 destroy: function() {
431                         api.Messenger.prototype.destroy.call( this );
432                         this.request.abort();
433
434                         if ( this.iframe )
435                                 this.iframe.remove();
436
437                         delete this.request;
438                         delete this.iframe;
439                         delete this.targetWindow;
440                 }
441         });
442
443         (function(){
444                 var uuid = 0;
445                 api.PreviewFrame.uuid = function() {
446                         return 'preview-' + uuid++;
447                 };
448         }());
449
450         api.Previewer = api.Messenger.extend({
451                 refreshBuffer: 250,
452
453                 /**
454                  * Requires params:
455                  *  - container  - a selector or jQuery element
456                  *  - previewUrl - the URL of preview frame
457                  */
458                 initialize: function( params, options ) {
459                         var self = this,
460                                 rscheme = /^https?/,
461                                 url;
462
463                         $.extend( this, options || {} );
464
465                         /*
466                          * Wrap this.refresh to prevent it from hammering the servers:
467                          *
468                          * If refresh is called once and no other refresh requests are
469                          * loading, trigger the request immediately.
470                          *
471                          * If refresh is called while another refresh request is loading,
472                          * debounce the refresh requests:
473                          * 1. Stop the loading request (as it is instantly outdated).
474                          * 2. Trigger the new request once refresh hasn't been called for
475                          *    self.refreshBuffer milliseconds.
476                          */
477                         this.refresh = (function( self ) {
478                                 var refresh  = self.refresh,
479                                         callback = function() {
480                                                 timeout = null;
481                                                 refresh.call( self );
482                                         },
483                                         timeout;
484
485                                 return function() {
486                                         if ( typeof timeout !== 'number' ) {
487                                                 if ( self.loading ) {
488                                                         self.abort();
489                                                 } else {
490                                                         return callback();
491                                                 }
492                                         }
493
494                                         clearTimeout( timeout );
495                                         timeout = setTimeout( callback, self.refreshBuffer );
496                                 };
497                         })( this );
498
499                         this.container   = api.ensure( params.container );
500                         this.allowedUrls = params.allowedUrls;
501                         this.signature   = params.signature;
502
503                         params.url = window.location.href;
504
505                         api.Messenger.prototype.initialize.call( this, params );
506
507                         this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
508                                 var match = to.match( rscheme );
509                                 return match ? match[0] : '';
510                         });
511
512                         // Limit the URL to internal, front-end links.
513                         //
514                         // If the frontend and the admin are served from the same domain, load the
515                         // preview over ssl if the customizer is being loaded over ssl. This avoids
516                         // insecure content warnings. This is not attempted if the admin and frontend
517                         // are on different domains to avoid the case where the frontend doesn't have
518                         // ssl certs.
519
520                         this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
521                                 var result;
522
523                                 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
524                                 // Strip hashes and query strings before testing.
525                                 if ( /\/wp-admin(\/|$)/.test( to.replace(/[#?].*$/, '') ) )
526                                         return null;
527
528                                 // Attempt to match the URL to the control frame's scheme
529                                 // and check if it's allowed. If not, try the original URL.
530                                 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
531                                         $.each( self.allowedUrls, function( i, allowed ) {
532                                                 if ( 0 === url.indexOf( allowed ) ) {
533                                                         result = url;
534                                                         return false;
535                                                 }
536                                         });
537                                         if ( result )
538                                                 return false;
539                                 });
540
541                                 // If we found a matching result, return it. If not, bail.
542                                 return result ? result : null;
543                         });
544
545                         // Refresh the preview when the URL is changed (but not yet).
546                         this.previewUrl.bind( this.refresh );
547
548                         this.scroll = 0;
549                         this.bind( 'scroll', function( distance ) {
550                                 this.scroll = distance;
551                         });
552
553                         // Update the URL when the iframe sends a URL message.
554                         this.bind( 'url', this.previewUrl );
555                 },
556
557                 query: function() {},
558
559                 abort: function() {
560                         if ( this.loading ) {
561                                 this.loading.destroy();
562                                 delete this.loading;
563                         }
564                 },
565
566                 refresh: function() {
567                         var self = this;
568
569                         this.abort();
570
571                         this.loading = new api.PreviewFrame({
572                                 url:        this.url(),
573                                 previewUrl: this.previewUrl(),
574                                 query:      this.query() || {},
575                                 container:  this.container,
576                                 signature:  this.signature
577                         });
578
579                         this.loading.done( function() {
580                                 // 'this' is the loading frame
581                                 this.bind( 'synced', function() {
582                                         if ( self.preview )
583                                                 self.preview.destroy();
584                                         self.preview = this;
585                                         delete self.loading;
586
587                                         self.targetWindow( this.targetWindow() );
588                                         self.channel( this.channel() );
589
590                                         self.send( 'active' );
591                                 });
592
593                                 this.send( 'sync', {
594                                         scroll:   self.scroll,
595                                         settings: api.get()
596                                 });
597                         });
598
599                         this.loading.fail( function( reason, location ) {
600                                 if ( 'redirect' === reason && location )
601                                         self.previewUrl( location );
602
603                                 if ( 'logged out' === reason ) {
604                                         if ( self.preview ) {
605                                                 self.preview.destroy();
606                                                 delete self.preview;
607                                         }
608
609                                         self.login().done( self.refresh );
610                                 }
611
612                                 if ( 'cheatin' === reason )
613                                         self.cheatin();
614                         });
615                 },
616
617                 login: function() {
618                         var previewer = this,
619                                 deferred, messenger, iframe;
620
621                         if ( this._login )
622                                 return this._login;
623
624                         deferred = $.Deferred();
625                         this._login = deferred.promise();
626
627                         messenger = new api.Messenger({
628                                 channel: 'login',
629                                 url:     api.settings.url.login
630                         });
631
632                         iframe = $('<iframe src="' + api.settings.url.login + '" />').appendTo( this.container );
633
634                         messenger.targetWindow( iframe[0].contentWindow );
635
636                         messenger.bind( 'login', function() {
637                                 iframe.remove();
638                                 messenger.destroy();
639                                 delete previewer._login;
640                                 deferred.resolve();
641                         });
642
643                         return this._login;
644                 },
645
646                 cheatin: function() {
647                         $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
648                 }
649         });
650
651         /* =====================================================================
652          * Ready.
653          * ===================================================================== */
654
655         api.controlConstructor = {
656                 color:  api.ColorControl,
657                 upload: api.UploadControl,
658                 image:  api.ImageControl
659         };
660
661         $( function() {
662                 api.settings = window._wpCustomizeSettings;
663                 api.l10n = window._wpCustomizeControlsL10n;
664
665                 // Check if we can run the customizer.
666                 if ( ! api.settings )
667                         return;
668
669                 // Redirect to the fallback preview if any incompatibilities are found.
670                 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
671                         return window.location = api.settings.url.fallback;
672
673                 var body = $( document.body ),
674                         overlay = body.children('.wp-full-overlay'),
675                         query, previewer, parent;
676
677                 // Prevent the form from saving when enter is pressed.
678                 $('#customize-controls').on( 'keydown', function( e ) {
679                         if ( $( e.target ).is('textarea') )
680                                 return;
681
682                         if ( 13 === e.which ) // Enter
683                                 e.preventDefault();
684                 });
685
686                 // Initialize Previewer
687                 previewer = new api.Previewer({
688                         container:   '#customize-preview',
689                         form:        '#customize-controls',
690                         previewUrl:  api.settings.url.preview,
691                         allowedUrls: api.settings.url.allowed,
692                         signature:   'WP_CUSTOMIZER_SIGNATURE'
693                 }, {
694
695                         nonce: api.settings.nonce,
696
697                         query: function() {
698                                 return {
699                                         wp_customize: 'on',
700                                         theme:        api.settings.theme.stylesheet,
701                                         customized:   JSON.stringify( api.get() ),
702                                         nonce:        this.nonce.preview
703                                 };
704                         },
705
706                         save: function() {
707                                 var self  = this,
708                                         query = $.extend( this.query(), {
709                                                 action: 'customize_save',
710                                                 nonce:  this.nonce.save
711                                         }),
712                                         request = $.post( api.settings.url.ajax, query );
713
714                                 api.trigger( 'save', request );
715
716                                 body.addClass('saving');
717
718                                 request.always( function() {
719                                         body.removeClass('saving');
720                                 });
721
722                                 request.done( function( response ) {
723                                         // Check if the user is logged out.
724                                         if ( '0' === response ) {
725                                                 self.preview.iframe.hide();
726                                                 self.login().done( function() {
727                                                         self.save();
728                                                         self.preview.iframe.show();
729                                                 });
730                                                 return;
731                                         }
732
733                                         // Check for cheaters.
734                                         if ( '-1' === response ) {
735                                                 self.cheatin();
736                                                 return;
737                                         }
738
739                                         api.trigger( 'saved' );
740                                 });
741                         }
742                 });
743
744                 // Refresh the nonces if the preview sends updated nonces over.
745                 previewer.bind( 'nonce', function( nonce ) {
746                         $.extend( this.nonce, nonce );
747                 });
748
749                 $.each( api.settings.settings, function( id, data ) {
750                         api.create( id, id, data.value, {
751                                 transport: data.transport,
752                                 previewer: previewer
753                         } );
754                 });
755
756                 $.each( api.settings.controls, function( id, data ) {
757                         var constructor = api.controlConstructor[ data.type ] || api.Control,
758                                 control;
759
760                         control = api.control.add( id, new constructor( id, {
761                                 params: data,
762                                 previewer: previewer
763                         } ) );
764                 });
765
766                 // Check if preview url is valid and load the preview frame.
767                 if ( previewer.previewUrl() )
768                         previewer.refresh();
769                 else
770                         previewer.previewUrl( api.settings.url.home );
771
772                 // Save and activated states
773                 (function() {
774                         var state = new api.Values(),
775                                 saved = state.create('saved'),
776                                 activated = state.create('activated');
777
778                         state.bind( 'change', function() {
779                                 var save = $('#save'),
780                                         back = $('.back');
781
782                                 if ( ! activated() ) {
783                                         save.val( api.l10n.activate ).prop( 'disabled', false );
784                                         back.text( api.l10n.cancel );
785
786                                 } else if ( saved() ) {
787                                         save.val( api.l10n.saved ).prop( 'disabled', true );
788                                         back.text( api.l10n.close );
789
790                                 } else {
791                                         save.val( api.l10n.save ).prop( 'disabled', false );
792                                         back.text( api.l10n.cancel );
793                                 }
794                         });
795
796                         // Set default states.
797                         saved( true );
798                         activated( api.settings.theme.active );
799
800                         api.bind( 'change', function() {
801                                 state('saved').set( false );
802                         });
803
804                         api.bind( 'saved', function() {
805                                 state('saved').set( true );
806                                 state('activated').set( true );
807                         });
808
809                         activated.bind( function( to ) {
810                                 if ( to )
811                                         api.trigger( 'activated' );
812                         });
813
814                         // Expose states to the API.
815                         api.state = state;
816                 }());
817
818                 // Temporary accordion code.
819                 $('.customize-section-title').click( function( event ) {
820                         var clicked = $( this ).parents( '.customize-section' );
821
822                         if ( clicked.hasClass('cannot-expand') )
823                                 return;
824
825                         $( '.customize-section' ).not( clicked ).removeClass( 'open' );
826                         clicked.toggleClass( 'open' );
827                         event.preventDefault();
828                 });
829
830                 // Button bindings.
831                 $('#save').click( function( event ) {
832                         previewer.save();
833                         event.preventDefault();
834                 });
835
836                 $('.collapse-sidebar').click( function( event ) {
837                         overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
838                         event.preventDefault();
839                 });
840
841                 // Create a potential postMessage connection with the parent frame.
842                 parent = new api.Messenger({
843                         url: api.settings.url.parent,
844                         channel: 'loader'
845                 });
846
847                 // If we receive a 'back' event, we're inside an iframe.
848                 // Send any clicks to the 'Return' link to the parent page.
849                 parent.bind( 'back', function() {
850                         $('.back').on( 'click.back', function( event ) {
851                                 event.preventDefault();
852                                 parent.send( 'close' );
853                         });
854                 });
855
856                 // Pass events through to the parent.
857                 api.bind( 'saved', function() {
858                         parent.send( 'saved' );
859                 });
860
861                 // When activated, let the loader handle redirecting the page.
862                 // If no loader exists, redirect the page ourselves (if a url exists).
863                 api.bind( 'activated', function() {
864                         if ( parent.targetWindow() )
865                                 parent.send( 'activated', api.settings.url.activated );
866                         else if ( api.settings.url.activated )
867                                 window.location = api.settings.url.activated;
868                 });
869
870                 // Initialize the connection with the parent frame.
871                 parent.send( 'ready' );
872
873                 // Control visibility for default controls
874                 $.each({
875                         'background_image': {
876                                 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
877                                 callback: function( to ) { return !! to }
878                         },
879                         'show_on_front': {
880                                 controls: [ 'page_on_front', 'page_for_posts' ],
881                                 callback: function( to ) { return 'page' === to }
882                         },
883                         'header_textcolor': {
884                                 controls: [ 'header_textcolor' ],
885                                 callback: function( to ) { return 'blank' !== to }
886                         }
887                 }, function( settingId, o ) {
888                         api( settingId, function( setting ) {
889                                 $.each( o.controls, function( i, controlId ) {
890                                         api.control( controlId, function( control ) {
891                                                 var visibility = function( to ) {
892                                                         control.container.toggle( o.callback( to ) );
893                                                 };
894
895                                                 visibility( setting.get() );
896                                                 setting.bind( visibility );
897                                         });
898                                 });
899                         });
900                 });
901
902                 // Juggle the two controls that use header_textcolor
903                 api.control( 'display_header_text', function( control ) {
904                         var last = '';
905
906                         control.elements[0].unsync( api( 'header_textcolor' ) );
907
908                         control.element = new api.Element( control.container.find('input') );
909                         control.element.set( 'blank' !== control.setting() );
910
911                         control.element.bind( function( to ) {
912                                 if ( ! to )
913                                         last = api( 'header_textcolor' ).get();
914
915                                 control.setting.set( to ? last : 'blank' );
916                         });
917
918                         control.setting.bind( function( to ) {
919                                 control.element.set( 'blank' !== to );
920                         });
921                 });
922
923                 // Handle header image data
924                 api.control( 'header_image', function( control ) {
925                         control.setting.bind( function( to ) {
926                                 if ( to === control.params.removed )
927                                         control.settings.data.set( false );
928                         });
929
930                         control.library.on( 'click', 'a', function( event ) {
931                                 control.settings.data.set( $(this).data('customizeHeaderImageData') );
932                         });
933
934                         control.uploader.success = function( attachment ) {
935                                 var data;
936
937                                 api.ImageControl.prototype.success.call( control, attachment );
938
939                                 data = {
940                                         attachment_id: attachment.id,
941                                         url:           attachment.url,
942                                         thumbnail_url: attachment.url,
943                                         height:        attachment.meta.height,
944                                         width:         attachment.meta.width
945                                 };
946
947                                 attachment.element.data( 'customizeHeaderImageData', data );
948                                 control.settings.data.set( data );
949                         }
950                 });
951
952                 api.trigger( 'ready' );
953         });
954
955 })( wp, jQuery );