]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-admin/js/customize-controls.js
WordPress 4.0.1
[autoinstalls/wordpress.git] / wp-admin / js / customize-controls.js
1 /* globals _wpCustomizeHeader, _wpMediaViewsL10n */
2 (function( exports, $ ){
3         var api = wp.customize;
4
5         /**
6          * @constructor
7          * @augments wp.customize.Value
8          * @augments wp.customize.Class
9          *
10          * @param options
11          * - previewer - The Previewer instance to sync with.
12          * - transport - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
13          */
14         api.Setting = api.Value.extend({
15                 initialize: function( id, value, options ) {
16                         api.Value.prototype.initialize.call( this, value, options );
17
18                         this.id = id;
19                         this.transport = this.transport || 'refresh';
20
21                         this.bind( this.preview );
22                 },
23                 preview: function() {
24                         switch ( this.transport ) {
25                                 case 'refresh':
26                                         return this.previewer.refresh();
27                                 case 'postMessage':
28                                         return this.previewer.send( 'setting', [ this.id, this() ] );
29                         }
30                 }
31         });
32
33         /**
34          * @constructor
35          * @augments wp.customize.Class
36          */
37         api.Control = api.Class.extend({
38                 initialize: function( id, options ) {
39                         var control = this,
40                                 nodes, radios, settings;
41
42                         this.params = {};
43                         $.extend( this, options || {} );
44
45                         this.id = id;
46                         this.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
47                         this.container = $( this.selector );
48                         this.active = new api.Value( this.params.active );
49
50                         settings = $.map( this.params.settings, function( value ) {
51                                 return value;
52                         });
53
54                         api.apply( api, settings.concat( function() {
55                                 var key;
56
57                                 control.settings = {};
58                                 for ( key in control.params.settings ) {
59                                         control.settings[ key ] = api( control.params.settings[ key ] );
60                                 }
61
62                                 control.setting = control.settings['default'] || null;
63                                 control.ready();
64                         }) );
65
66                         control.elements = [];
67
68                         nodes  = this.container.find('[data-customize-setting-link]');
69                         radios = {};
70
71                         nodes.each( function() {
72                                 var node = $(this),
73                                         name;
74
75                                 if ( node.is(':radio') ) {
76                                         name = node.prop('name');
77                                         if ( radios[ name ] )
78                                                 return;
79
80                                         radios[ name ] = true;
81                                         node = nodes.filter( '[name="' + name + '"]' );
82                                 }
83
84                                 api( node.data('customizeSettingLink'), function( setting ) {
85                                         var element = new api.Element( node );
86                                         control.elements.push( element );
87                                         element.sync( setting );
88                                         element.set( setting() );
89                                 });
90                         });
91
92                         control.active.bind( function ( active ) {
93                                 control.toggle( active );
94                         } );
95                         control.toggle( control.active() );
96                 },
97
98                 /**
99                  * @abstract
100                  */
101                 ready: function() {},
102
103                 /**
104                  * Callback for change to the control's active state.
105                  *
106                  * Override function for custom behavior for the control being active/inactive.
107                  *
108                  * @param {Boolean} active
109                  */
110                 toggle: function ( active ) {
111                         if ( active ) {
112                                 this.container.slideDown();
113                         } else {
114                                 this.container.slideUp();
115                         }
116                 },
117
118                 dropdownInit: function() {
119                         var control      = this,
120                                 statuses     = this.container.find('.dropdown-status'),
121                                 params       = this.params,
122                                 toggleFreeze = false,
123                                 update       = function( to ) {
124                                         if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
125                                                 statuses.html( params.statuses[ to ] ).show();
126                                         else
127                                                 statuses.hide();
128                                 };
129
130                         // Support the .dropdown class to open/close complex elements
131                         this.container.on( 'click keydown', '.dropdown', function( event ) {
132                                 if ( event.type === 'keydown' &&  13 !== event.which ) // enter
133                                         return;
134
135                                 event.preventDefault();
136
137                                 if (!toggleFreeze)
138                                         control.container.toggleClass('open');
139
140                                 if ( control.container.hasClass('open') )
141                                         control.container.parent().parent().find('li.library-selected').focus();
142
143                                 // Don't want to fire focus and click at same time
144                                 toggleFreeze = true;
145                                 setTimeout(function () {
146                                         toggleFreeze = false;
147                                 }, 400);
148                         });
149
150                         this.setting.bind( update );
151                         update( this.setting() );
152                 }
153         });
154
155         /**
156          * @constructor
157          * @augments wp.customize.Control
158          * @augments wp.customize.Class
159          */
160         api.ColorControl = api.Control.extend({
161                 ready: function() {
162                         var control = this,
163                                 picker = this.container.find('.color-picker-hex');
164
165                         picker.val( control.setting() ).wpColorPicker({
166                                 change: function() {
167                                         control.setting.set( picker.wpColorPicker('color') );
168                                 },
169                                 clear: function() {
170                                         control.setting.set( false );
171                                 }
172                         });
173                 }
174         });
175
176         /**
177          * @constructor
178          * @augments wp.customize.Control
179          * @augments wp.customize.Class
180          */
181         api.UploadControl = api.Control.extend({
182                 ready: function() {
183                         var control = this;
184
185                         this.params.removed = this.params.removed || '';
186
187                         this.success = $.proxy( this.success, this );
188
189                         this.uploader = $.extend({
190                                 container: this.container,
191                                 browser:   this.container.find('.upload'),
192                                 dropzone:  this.container.find('.upload-dropzone'),
193                                 success:   this.success,
194                                 plupload:  {},
195                                 params:    {}
196                         }, this.uploader || {} );
197
198                         if ( control.params.extensions ) {
199                                 control.uploader.plupload.filters = [{
200                                         title:      api.l10n.allowedFiles,
201                                         extensions: control.params.extensions
202                                 }];
203                         }
204
205                         if ( control.params.context )
206                                 control.uploader.params['post_data[context]'] = this.params.context;
207
208                         if ( api.settings.theme.stylesheet )
209                                 control.uploader.params['post_data[theme]'] = api.settings.theme.stylesheet;
210
211                         this.uploader = new wp.Uploader( this.uploader );
212
213                         this.remover = this.container.find('.remove');
214                         this.remover.on( 'click keydown', function( event ) {
215                                 if ( event.type === 'keydown' &&  13 !== event.which ) // enter
216                                         return;
217
218                                 control.setting.set( control.params.removed );
219                                 event.preventDefault();
220                         });
221
222                         this.removerVisibility = $.proxy( this.removerVisibility, this );
223                         this.setting.bind( this.removerVisibility );
224                         this.removerVisibility( this.setting.get() );
225                 },
226                 success: function( attachment ) {
227                         this.setting.set( attachment.get('url') );
228                 },
229                 removerVisibility: function( to ) {
230                         this.remover.toggle( to != this.params.removed );
231                 }
232         });
233
234         /**
235          * @constructor
236          * @augments wp.customize.UploadControl
237          * @augments wp.customize.Control
238          * @augments wp.customize.Class
239          */
240         api.ImageControl = api.UploadControl.extend({
241                 ready: function() {
242                         var control = this,
243                                 panels;
244
245                         this.uploader = {
246                                 init: function() {
247                                         var fallback, button;
248
249                                         if ( this.supports.dragdrop )
250                                                 return;
251
252                                         // Maintain references while wrapping the fallback button.
253                                         fallback = control.container.find( '.upload-fallback' );
254                                         button   = fallback.children().detach();
255
256                                         this.browser.detach().empty().append( button );
257                                         fallback.append( this.browser ).show();
258                                 }
259                         };
260
261                         api.UploadControl.prototype.ready.call( this );
262
263                         this.thumbnail    = this.container.find('.preview-thumbnail img');
264                         this.thumbnailSrc = $.proxy( this.thumbnailSrc, this );
265                         this.setting.bind( this.thumbnailSrc );
266
267                         this.library = this.container.find('.library');
268
269                         // Generate tab objects
270                         this.tabs = {};
271                         panels    = this.library.find('.library-content');
272
273                         this.library.children('ul').children('li').each( function() {
274                                 var link  = $(this),
275                                         id    = link.data('customizeTab'),
276                                         panel = panels.filter('[data-customize-tab="' + id + '"]');
277
278                                 control.tabs[ id ] = {
279                                         both:  link.add( panel ),
280                                         link:  link,
281                                         panel: panel
282                                 };
283                         });
284
285                         // Bind tab switch events
286                         this.library.children('ul').on( 'click keydown', 'li', function( event ) {
287                                 if ( event.type === 'keydown' &&  13 !== event.which ) // enter
288                                         return;
289
290                                 var id  = $(this).data('customizeTab'),
291                                         tab = control.tabs[ id ];
292
293                                 event.preventDefault();
294
295                                 if ( tab.link.hasClass('library-selected') )
296                                         return;
297
298                                 control.selected.both.removeClass('library-selected');
299                                 control.selected = tab;
300                                 control.selected.both.addClass('library-selected');
301                         });
302
303                         // Bind events to switch image urls.
304                         this.library.on( 'click keydown', 'a', function( event ) {
305                                 if ( event.type === 'keydown' && 13 !== event.which ) // enter
306                                         return;
307
308                                 var value = $(this).data('customizeImageValue');
309
310                                 if ( value ) {
311                                         control.setting.set( value );
312                                         event.preventDefault();
313                                 }
314                         });
315
316                         if ( this.tabs.uploaded ) {
317                                 this.tabs.uploaded.target = this.library.find('.uploaded-target');
318                                 if ( ! this.tabs.uploaded.panel.find('.thumbnail').length )
319                                         this.tabs.uploaded.both.addClass('hidden');
320                         }
321
322                         // Select a tab
323                         panels.each( function() {
324                                 var tab = control.tabs[ $(this).data('customizeTab') ];
325
326                                 // Select the first visible tab.
327                                 if ( ! tab.link.hasClass('hidden') ) {
328                                         control.selected = tab;
329                                         tab.both.addClass('library-selected');
330                                         return false;
331                                 }
332                         });
333
334                         this.dropdownInit();
335                 },
336                 success: function( attachment ) {
337                         api.UploadControl.prototype.success.call( this, attachment );
338
339                         // Add the uploaded image to the uploaded tab.
340                         if ( this.tabs.uploaded && this.tabs.uploaded.target.length ) {
341                                 this.tabs.uploaded.both.removeClass('hidden');
342
343                                 // @todo: Do NOT store this on the attachment model. That is bad.
344                                 attachment.element = $( '<a href="#" class="thumbnail"></a>' )
345                                         .data( 'customizeImageValue', attachment.get('url') )
346                                         .append( '<img src="' +  attachment.get('url')+ '" />' )
347                                         .appendTo( this.tabs.uploaded.target );
348                         }
349                 },
350                 thumbnailSrc: function( to ) {
351                         if ( /^(https?:)?\/\//.test( to ) )
352                                 this.thumbnail.prop( 'src', to ).show();
353                         else
354                                 this.thumbnail.hide();
355                 }
356         });
357
358         /**
359          * @constructor
360          * @augments wp.customize.Control
361          * @augments wp.customize.Class
362          */
363         api.HeaderControl = api.Control.extend({
364                 ready: function() {
365                         this.btnRemove        = $('#customize-control-header_image .actions .remove');
366                         this.btnNew           = $('#customize-control-header_image .actions .new');
367
368                         _.bindAll(this, 'openMedia', 'removeImage');
369
370                         this.btnNew.on( 'click', this.openMedia );
371                         this.btnRemove.on( 'click', this.removeImage );
372
373                         api.HeaderTool.currentHeader = new api.HeaderTool.ImageModel();
374
375                         new api.HeaderTool.CurrentView({
376                                 model: api.HeaderTool.currentHeader,
377                                 el: '.current .container'
378                         });
379
380                         new api.HeaderTool.ChoiceListView({
381                                 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
382                                 el: '.choices .uploaded .list'
383                         });
384
385                         new api.HeaderTool.ChoiceListView({
386                                 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
387                                 el: '.choices .default .list'
388                         });
389
390                         api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
391                                 api.HeaderTool.UploadsList,
392                                 api.HeaderTool.DefaultsList
393                         ]);
394                 },
395
396                 /**
397                  * Returns a set of options, computed from the attached image data and
398                  * theme-specific data, to be fed to the imgAreaSelect plugin in
399                  * wp.media.view.Cropper.
400                  *
401                  * @param {wp.media.model.Attachment} attachment
402                  * @param {wp.media.controller.Cropper} controller
403                  * @returns {Object} Options
404                  */
405                 calculateImageSelectOptions: function(attachment, controller) {
406                         var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
407                                 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
408                                 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
409                                 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
410                                 ratio, xImg, yImg, realHeight, realWidth,
411                                 imgSelectOptions;
412
413                         realWidth = attachment.get('width');
414                         realHeight = attachment.get('height');
415
416                         this.headerImage = new api.HeaderTool.ImageModel();
417                         this.headerImage.set({
418                                 themeWidth: xInit,
419                                 themeHeight: yInit,
420                                 themeFlexWidth: flexWidth,
421                                 themeFlexHeight: flexHeight,
422                                 imageWidth: realWidth,
423                                 imageHeight: realHeight
424                         });
425
426                         controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
427
428                         ratio = xInit / yInit;
429                         xImg = realWidth;
430                         yImg = realHeight;
431
432                         if ( xImg / yImg > ratio ) {
433                                 yInit = yImg;
434                                 xInit = yInit * ratio;
435                         } else {
436                                 xInit = xImg;
437                                 yInit = xInit / ratio;
438                         }
439
440                         imgSelectOptions = {
441                                 handles: true,
442                                 keys: true,
443                                 instance: true,
444                                 persistent: true,
445                                 imageWidth: realWidth,
446                                 imageHeight: realHeight,
447                                 x1: 0,
448                                 y1: 0,
449                                 x2: xInit,
450                                 y2: yInit
451                         };
452
453                         if (flexHeight === false && flexWidth === false) {
454                                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
455                         }
456                         if (flexHeight === false ) {
457                                 imgSelectOptions.maxHeight = yInit;
458                         }
459                         if (flexWidth === false ) {
460                                 imgSelectOptions.maxWidth = xInit;
461                         }
462
463                         return imgSelectOptions;
464                 },
465
466                 /**
467                  * Sets up and opens the Media Manager in order to select an image.
468                  * Depending on both the size of the image and the properties of the
469                  * current theme, a cropping step after selection may be required or
470                  * skippable.
471                  *
472                  * @param {event} event
473                  */
474                 openMedia: function(event) {
475                         var l10n = _wpMediaViewsL10n;
476
477                         event.preventDefault();
478
479                         this.frame = wp.media({
480                                 button: {
481                                         text: l10n.selectAndCrop,
482                                         close: false
483                                 },
484                                 states: [
485                                         new wp.media.controller.Library({
486                                                 title:     l10n.chooseImage,
487                                                 library:   wp.media.query({ type: 'image' }),
488                                                 multiple:  false,
489                                                 priority:  20,
490                                                 suggestedWidth: _wpCustomizeHeader.data.width,
491                                                 suggestedHeight: _wpCustomizeHeader.data.height
492                                         }),
493                                         new wp.media.controller.Cropper({
494                                                 imgSelectOptions: this.calculateImageSelectOptions
495                                         })
496                                 ]
497                         });
498
499                         this.frame.on('select', this.onSelect, this);
500                         this.frame.on('cropped', this.onCropped, this);
501                         this.frame.on('skippedcrop', this.onSkippedCrop, this);
502
503                         this.frame.open();
504                 },
505
506                 onSelect: function() {
507                         this.frame.setState('cropper');
508                 },
509                 onCropped: function(croppedImage) {
510                         var url = croppedImage.post_content,
511                                 attachmentId = croppedImage.attachment_id,
512                                 w = croppedImage.width,
513                                 h = croppedImage.height;
514                         this.setImageFromURL(url, attachmentId, w, h);
515                 },
516                 onSkippedCrop: function(selection) {
517                         var url = selection.get('url'),
518                                 w = selection.get('width'),
519                                 h = selection.get('height');
520                         this.setImageFromURL(url, selection.id, w, h);
521                 },
522
523                 /**
524                  * Creates a new wp.customize.HeaderTool.ImageModel from provided
525                  * header image data and inserts it into the user-uploaded headers
526                  * collection.
527                  *
528                  * @param {String} url
529                  * @param {Number} attachmentId
530                  * @param {Number} width
531                  * @param {Number} height
532                  */
533                 setImageFromURL: function(url, attachmentId, width, height) {
534                         var choice, data = {};
535
536                         data.url = url;
537                         data.thumbnail_url = url;
538                         data.timestamp = _.now();
539
540                         if (attachmentId) {
541                                 data.attachment_id = attachmentId;
542                         }
543
544                         if (width) {
545                                 data.width = width;
546                         }
547
548                         if (height) {
549                                 data.height = height;
550                         }
551
552                         choice = new api.HeaderTool.ImageModel({
553                                 header: data,
554                                 choice: url.split('/').pop()
555                         });
556                         api.HeaderTool.UploadsList.add(choice);
557                         api.HeaderTool.currentHeader.set(choice.toJSON());
558                         choice.save();
559                         choice.importImage();
560                 },
561
562                 /**
563                  * Triggers the necessary events to deselect an image which was set as
564                  * the currently selected one.
565                  */
566                 removeImage: function() {
567                         api.HeaderTool.currentHeader.trigger('hide');
568                         api.HeaderTool.CombinedList.trigger('control:removeImage');
569                 }
570
571         });
572
573         // Change objects contained within the main customize object to Settings.
574         api.defaultConstructor = api.Setting;
575
576         // Create the collection of Control objects.
577         api.control = new api.Values({ defaultConstructor: api.Control });
578
579         /**
580          * @constructor
581          * @augments wp.customize.Messenger
582          * @augments wp.customize.Class
583          * @mixes wp.customize.Events
584          */
585         api.PreviewFrame = api.Messenger.extend({
586                 sensitivity: 2000,
587
588                 initialize: function( params, options ) {
589                         var deferred = $.Deferred();
590
591                         // This is the promise object.
592                         deferred.promise( this );
593
594                         this.container = params.container;
595                         this.signature = params.signature;
596
597                         $.extend( params, { channel: api.PreviewFrame.uuid() });
598
599                         api.Messenger.prototype.initialize.call( this, params, options );
600
601                         this.add( 'previewUrl', params.previewUrl );
602
603                         this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
604
605                         this.run( deferred );
606                 },
607
608                 run: function( deferred ) {
609                         var self   = this,
610                                 loaded = false,
611                                 ready  = false;
612
613                         if ( this._ready )
614                                 this.unbind( 'ready', this._ready );
615
616                         this._ready = function() {
617                                 ready = true;
618
619                                 if ( loaded )
620                                         deferred.resolveWith( self );
621                         };
622
623                         this.bind( 'ready', this._ready );
624
625                         this.bind( 'ready', function ( data ) {
626                                 if ( ! data || ! data.activeControls ) {
627                                         return;
628                                 }
629
630                                 $.each( data.activeControls, function ( id, active ) {
631                                         var control = api.control( id );
632                                         if ( control ) {
633                                                 control.active( active );
634                                         }
635                                 } );
636                         } );
637
638                         this.request = $.ajax( this.previewUrl(), {
639                                 type: 'POST',
640                                 data: this.query,
641                                 xhrFields: {
642                                         withCredentials: true
643                                 }
644                         } );
645
646                         this.request.fail( function() {
647                                 deferred.rejectWith( self, [ 'request failure' ] );
648                         });
649
650                         this.request.done( function( response ) {
651                                 var location = self.request.getResponseHeader('Location'),
652                                         signature = self.signature,
653                                         index;
654
655                                 // Check if the location response header differs from the current URL.
656                                 // If so, the request was redirected; try loading the requested page.
657                                 if ( location && location != self.previewUrl() ) {
658                                         deferred.rejectWith( self, [ 'redirect', location ] );
659                                         return;
660                                 }
661
662                                 // Check if the user is not logged in.
663                                 if ( '0' === response ) {
664                                         self.login( deferred );
665                                         return;
666                                 }
667
668                                 // Check for cheaters.
669                                 if ( '-1' === response ) {
670                                         deferred.rejectWith( self, [ 'cheatin' ] );
671                                         return;
672                                 }
673
674                                 // Check for a signature in the request.
675                                 index = response.lastIndexOf( signature );
676                                 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
677                                         deferred.rejectWith( self, [ 'unsigned' ] );
678                                         return;
679                                 }
680
681                                 // Strip the signature from the request.
682                                 response = response.slice( 0, index ) + response.slice( index + signature.length );
683
684                                 // Create the iframe and inject the html content.
685                                 self.iframe = $('<iframe />').appendTo( self.container );
686
687                                 // Bind load event after the iframe has been added to the page;
688                                 // otherwise it will fire when injected into the DOM.
689                                 self.iframe.one( 'load', function() {
690                                         loaded = true;
691
692                                         if ( ready ) {
693                                                 deferred.resolveWith( self );
694                                         } else {
695                                                 setTimeout( function() {
696                                                         deferred.rejectWith( self, [ 'ready timeout' ] );
697                                                 }, self.sensitivity );
698                                         }
699                                 });
700
701                                 self.targetWindow( self.iframe[0].contentWindow );
702
703                                 self.targetWindow().document.open();
704                                 self.targetWindow().document.write( response );
705                                 self.targetWindow().document.close();
706                         });
707                 },
708
709                 login: function( deferred ) {
710                         var self = this,
711                                 reject;
712
713                         reject = function() {
714                                 deferred.rejectWith( self, [ 'logged out' ] );
715                         };
716
717                         if ( this.triedLogin )
718                                 return reject();
719
720                         // Check if we have an admin cookie.
721                         $.get( api.settings.url.ajax, {
722                                 action: 'logged-in'
723                         }).fail( reject ).done( function( response ) {
724                                 var iframe;
725
726                                 if ( '1' !== response )
727                                         reject();
728
729                                 iframe = $('<iframe src="' + self.previewUrl() + '" />').hide();
730                                 iframe.appendTo( self.container );
731                                 iframe.load( function() {
732                                         self.triedLogin = true;
733
734                                         iframe.remove();
735                                         self.run( deferred );
736                                 });
737                         });
738                 },
739
740                 destroy: function() {
741                         api.Messenger.prototype.destroy.call( this );
742                         this.request.abort();
743
744                         if ( this.iframe )
745                                 this.iframe.remove();
746
747                         delete this.request;
748                         delete this.iframe;
749                         delete this.targetWindow;
750                 }
751         });
752
753         (function(){
754                 var uuid = 0;
755                 /**
756                  * Create a universally unique identifier.
757                  *
758                  * @return {int}
759                  */
760                 api.PreviewFrame.uuid = function() {
761                         return 'preview-' + uuid++;
762                 };
763         }());
764
765         /**
766          * @constructor
767          * @augments wp.customize.Messenger
768          * @augments wp.customize.Class
769          * @mixes wp.customize.Events
770          */
771         api.Previewer = api.Messenger.extend({
772                 refreshBuffer: 250,
773
774                 /**
775                  * Requires params:
776                  *  - container  - a selector or jQuery element
777                  *  - previewUrl - the URL of preview frame
778                  */
779                 initialize: function( params, options ) {
780                         var self = this,
781                                 rscheme = /^https?/;
782
783                         $.extend( this, options || {} );
784
785                         /*
786                          * Wrap this.refresh to prevent it from hammering the servers:
787                          *
788                          * If refresh is called once and no other refresh requests are
789                          * loading, trigger the request immediately.
790                          *
791                          * If refresh is called while another refresh request is loading,
792                          * debounce the refresh requests:
793                          * 1. Stop the loading request (as it is instantly outdated).
794                          * 2. Trigger the new request once refresh hasn't been called for
795                          *    self.refreshBuffer milliseconds.
796                          */
797                         this.refresh = (function( self ) {
798                                 var refresh  = self.refresh,
799                                         callback = function() {
800                                                 timeout = null;
801                                                 refresh.call( self );
802                                         },
803                                         timeout;
804
805                                 return function() {
806                                         if ( typeof timeout !== 'number' ) {
807                                                 if ( self.loading ) {
808                                                         self.abort();
809                                                 } else {
810                                                         return callback();
811                                                 }
812                                         }
813
814                                         clearTimeout( timeout );
815                                         timeout = setTimeout( callback, self.refreshBuffer );
816                                 };
817                         })( this );
818
819                         this.container   = api.ensure( params.container );
820                         this.allowedUrls = params.allowedUrls;
821                         this.signature   = params.signature;
822
823                         params.url = window.location.href;
824
825                         api.Messenger.prototype.initialize.call( this, params );
826
827                         this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
828                                 var match = to.match( rscheme );
829                                 return match ? match[0] : '';
830                         });
831
832                         // Limit the URL to internal, front-end links.
833                         //
834                         // If the frontend and the admin are served from the same domain, load the
835                         // preview over ssl if the customizer is being loaded over ssl. This avoids
836                         // insecure content warnings. This is not attempted if the admin and frontend
837                         // are on different domains to avoid the case where the frontend doesn't have
838                         // ssl certs.
839
840                         this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
841                                 var result;
842
843                                 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
844                                 // Strip hashes and query strings before testing.
845                                 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
846                                         return null;
847
848                                 // Attempt to match the URL to the control frame's scheme
849                                 // and check if it's allowed. If not, try the original URL.
850                                 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
851                                         $.each( self.allowedUrls, function( i, allowed ) {
852                                                 var path;
853
854                                                 allowed = allowed.replace( /\/+$/, '' );
855                                                 path = url.replace( allowed, '' );
856
857                                                 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
858                                                         result = url;
859                                                         return false;
860                                                 }
861                                         });
862                                         if ( result )
863                                                 return false;
864                                 });
865
866                                 // If we found a matching result, return it. If not, bail.
867                                 return result ? result : null;
868                         });
869
870                         // Refresh the preview when the URL is changed (but not yet).
871                         this.previewUrl.bind( this.refresh );
872
873                         this.scroll = 0;
874                         this.bind( 'scroll', function( distance ) {
875                                 this.scroll = distance;
876                         });
877
878                         // Update the URL when the iframe sends a URL message.
879                         this.bind( 'url', this.previewUrl );
880                 },
881
882                 query: function() {},
883
884                 abort: function() {
885                         if ( this.loading ) {
886                                 this.loading.destroy();
887                                 delete this.loading;
888                         }
889                 },
890
891                 refresh: function() {
892                         var self = this;
893
894                         this.abort();
895
896                         this.loading = new api.PreviewFrame({
897                                 url:        this.url(),
898                                 previewUrl: this.previewUrl(),
899                                 query:      this.query() || {},
900                                 container:  this.container,
901                                 signature:  this.signature
902                         });
903
904                         this.loading.done( function() {
905                                 // 'this' is the loading frame
906                                 this.bind( 'synced', function() {
907                                         if ( self.preview )
908                                                 self.preview.destroy();
909                                         self.preview = this;
910                                         delete self.loading;
911
912                                         self.targetWindow( this.targetWindow() );
913                                         self.channel( this.channel() );
914
915                                         self.send( 'active' );
916                                 });
917
918                                 this.send( 'sync', {
919                                         scroll:   self.scroll,
920                                         settings: api.get()
921                                 });
922                         });
923
924                         this.loading.fail( function( reason, location ) {
925                                 if ( 'redirect' === reason && location )
926                                         self.previewUrl( location );
927
928                                 if ( 'logged out' === reason ) {
929                                         if ( self.preview ) {
930                                                 self.preview.destroy();
931                                                 delete self.preview;
932                                         }
933
934                                         self.login().done( self.refresh );
935                                 }
936
937                                 if ( 'cheatin' === reason )
938                                         self.cheatin();
939                         });
940                 },
941
942                 login: function() {
943                         var previewer = this,
944                                 deferred, messenger, iframe;
945
946                         if ( this._login )
947                                 return this._login;
948
949                         deferred = $.Deferred();
950                         this._login = deferred.promise();
951
952                         messenger = new api.Messenger({
953                                 channel: 'login',
954                                 url:     api.settings.url.login
955                         });
956
957                         iframe = $('<iframe src="' + api.settings.url.login + '" />').appendTo( this.container );
958
959                         messenger.targetWindow( iframe[0].contentWindow );
960
961                         messenger.bind( 'login', function() {
962                                 iframe.remove();
963                                 messenger.destroy();
964                                 delete previewer._login;
965                                 deferred.resolve();
966                         });
967
968                         return this._login;
969                 },
970
971                 cheatin: function() {
972                         $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
973                 }
974         });
975
976         api.controlConstructor = {
977                 color:  api.ColorControl,
978                 upload: api.UploadControl,
979                 image:  api.ImageControl,
980                 header: api.HeaderControl
981         };
982
983         $( function() {
984                 api.settings = window._wpCustomizeSettings;
985                 api.l10n = window._wpCustomizeControlsL10n;
986
987                 // Check if we can run the customizer.
988                 if ( ! api.settings )
989                         return;
990
991                 // Redirect to the fallback preview if any incompatibilities are found.
992                 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
993                         return window.location = api.settings.url.fallback;
994
995                 var parent, topFocus,
996                         body = $( document.body ),
997                         overlay = body.children( '.wp-full-overlay' ),
998                         title = $( '#customize-info .theme-name.site-title' ),
999                         closeBtn = $( '.customize-controls-close' ),
1000                         saveBtn = $( '#save' );
1001
1002                 // Prevent the form from saving when enter is pressed on an input or select element.
1003                 $('#customize-controls').on( 'keydown', function( e ) {
1004                         var isEnter = ( 13 === e.which ),
1005                                 $el = $( e.target );
1006
1007                         if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
1008                                 e.preventDefault();
1009                         }
1010                 });
1011
1012                 // Initialize Previewer
1013                 api.previewer = new api.Previewer({
1014                         container:   '#customize-preview',
1015                         form:        '#customize-controls',
1016                         previewUrl:  api.settings.url.preview,
1017                         allowedUrls: api.settings.url.allowed,
1018                         signature:   'WP_CUSTOMIZER_SIGNATURE'
1019                 }, {
1020
1021                         nonce: api.settings.nonce,
1022
1023                         query: function() {
1024                                 return {
1025                                         wp_customize: 'on',
1026                                         theme:      api.settings.theme.stylesheet,
1027                                         customized: JSON.stringify( api.get() ),
1028                                         nonce:      this.nonce.preview
1029                                 };
1030                         },
1031
1032                         save: function() {
1033                                 var self  = this,
1034                                         query = $.extend( this.query(), {
1035                                                 action: 'customize_save',
1036                                                 nonce:  this.nonce.save
1037                                         } ),
1038                                         processing = api.state( 'processing' ),
1039                                         submitWhenDoneProcessing,
1040                                         submit;
1041
1042                                 body.addClass( 'saving' );
1043
1044                                 submit = function () {
1045                                         var request = $.post( api.settings.url.ajax, query );
1046
1047                                         api.trigger( 'save', request );
1048
1049                                         request.always( function () {
1050                                                 body.removeClass( 'saving' );
1051                                         } );
1052
1053                                         request.done( function( response ) {
1054                                                 // Check if the user is logged out.
1055                                                 if ( '0' === response ) {
1056                                                         self.preview.iframe.hide();
1057                                                         self.login().done( function() {
1058                                                                 self.save();
1059                                                                 self.preview.iframe.show();
1060                                                         } );
1061                                                         return;
1062                                                 }
1063
1064                                                 // Check for cheaters.
1065                                                 if ( '-1' === response ) {
1066                                                         self.cheatin();
1067                                                         return;
1068                                                 }
1069
1070                                                 api.trigger( 'saved' );
1071                                         } );
1072                                 };
1073
1074                                 if ( 0 === processing() ) {
1075                                         submit();
1076                                 } else {
1077                                         submitWhenDoneProcessing = function () {
1078                                                 if ( 0 === processing() ) {
1079                                                         api.state.unbind( 'change', submitWhenDoneProcessing );
1080                                                         submit();
1081                                                 }
1082                                         };
1083                                         api.state.bind( 'change', submitWhenDoneProcessing );
1084                                 }
1085
1086                         }
1087                 });
1088
1089                 // Refresh the nonces if the preview sends updated nonces over.
1090                 api.previewer.bind( 'nonce', function( nonce ) {
1091                         $.extend( this.nonce, nonce );
1092                 });
1093
1094                 $.each( api.settings.settings, function( id, data ) {
1095                         api.create( id, id, data.value, {
1096                                 transport: data.transport,
1097                                 previewer: api.previewer
1098                         } );
1099                 });
1100
1101                 $.each( api.settings.controls, function( id, data ) {
1102                         var constructor = api.controlConstructor[ data.type ] || api.Control,
1103                                 control;
1104
1105                         control = api.control.add( id, new constructor( id, {
1106                                 params: data,
1107                                 previewer: api.previewer
1108                         } ) );
1109                 });
1110
1111                 // Check if preview url is valid and load the preview frame.
1112                 if ( api.previewer.previewUrl() ) {
1113                         api.previewer.refresh();
1114                 } else {
1115                         api.previewer.previewUrl( api.settings.url.home );
1116                 }
1117
1118                 // Save and activated states
1119                 (function() {
1120                         var state = new api.Values(),
1121                                 saved = state.create( 'saved' ),
1122                                 activated = state.create( 'activated' ),
1123                                 processing = state.create( 'processing' );
1124
1125                         state.bind( 'change', function() {
1126                                 if ( ! activated() ) {
1127                                         saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
1128                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
1129
1130                                 } else if ( saved() ) {
1131                                         saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
1132                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
1133
1134                                 } else {
1135                                         saveBtn.val( api.l10n.save ).prop( 'disabled', false );
1136                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
1137                                 }
1138                         });
1139
1140                         // Set default states.
1141                         saved( true );
1142                         activated( api.settings.theme.active );
1143                         processing( 0 );
1144
1145                         api.bind( 'change', function() {
1146                                 state('saved').set( false );
1147                         });
1148
1149                         api.bind( 'saved', function() {
1150                                 state('saved').set( true );
1151                                 state('activated').set( true );
1152                         });
1153
1154                         activated.bind( function( to ) {
1155                                 if ( to )
1156                                         api.trigger( 'activated' );
1157                         });
1158
1159                         // Expose states to the API.
1160                         api.state = state;
1161                 }());
1162
1163                 // Button bindings.
1164                 saveBtn.click( function( event ) {
1165                         api.previewer.save();
1166                         event.preventDefault();
1167                 }).keydown( function( event ) {
1168                         if ( 9 === event.which ) // tab
1169                                 return;
1170                         if ( 13 === event.which ) // enter
1171                                 api.previewer.save();
1172                         event.preventDefault();
1173                 });
1174
1175                 closeBtn.keydown( function( event ) {
1176                         if ( 9 === event.which ) // tab
1177                                 return;
1178                         if ( 13 === event.which ) // enter
1179                                 this.click();
1180                         event.preventDefault();
1181                 });
1182
1183                 $('.upload-dropzone a.upload').keydown( function( event ) {
1184                         if ( 13 === event.which ) // enter
1185                                 this.click();
1186                 });
1187
1188                 $('.collapse-sidebar').on( 'click keydown', function( event ) {
1189                         if ( event.type === 'keydown' &&  13 !== event.which ) // enter
1190                                 return;
1191
1192                         overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
1193                         event.preventDefault();
1194                 });
1195
1196                 // Bind site title display to the corresponding field.
1197                 if ( title.length ) {
1198                         $( '#customize-control-blogname input' ).on( 'input', function() {
1199                                 title.text(  this.value );
1200                         } );
1201                 }
1202
1203                 // Create a potential postMessage connection with the parent frame.
1204                 parent = new api.Messenger({
1205                         url: api.settings.url.parent,
1206                         channel: 'loader'
1207                 });
1208
1209                 // If we receive a 'back' event, we're inside an iframe.
1210                 // Send any clicks to the 'Return' link to the parent page.
1211                 parent.bind( 'back', function() {
1212                         closeBtn.on( 'click.customize-controls-close', function( event ) {
1213                                 event.preventDefault();
1214                                 parent.send( 'close' );
1215                         });
1216                 });
1217
1218                 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
1219                 $( window ).on( 'beforeunload', function () {
1220                         if ( ! api.state( 'saved' )() ) {
1221                                 return api.l10n.saveAlert;
1222                         }
1223                 } );
1224
1225                 // Pass events through to the parent.
1226                 $.each( [ 'saved', 'change' ], function ( i, event ) {
1227                         api.bind( event, function() {
1228                                 parent.send( event );
1229                         });
1230                 } );
1231
1232                 // When activated, let the loader handle redirecting the page.
1233                 // If no loader exists, redirect the page ourselves (if a url exists).
1234                 api.bind( 'activated', function() {
1235                         if ( parent.targetWindow() )
1236                                 parent.send( 'activated', api.settings.url.activated );
1237                         else if ( api.settings.url.activated )
1238                                 window.location = api.settings.url.activated;
1239                 });
1240
1241                 // Initialize the connection with the parent frame.
1242                 parent.send( 'ready' );
1243
1244                 // Control visibility for default controls
1245                 $.each({
1246                         'background_image': {
1247                                 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
1248                                 callback: function( to ) { return !! to; }
1249                         },
1250                         'show_on_front': {
1251                                 controls: [ 'page_on_front', 'page_for_posts' ],
1252                                 callback: function( to ) { return 'page' === to; }
1253                         },
1254                         'header_textcolor': {
1255                                 controls: [ 'header_textcolor' ],
1256                                 callback: function( to ) { return 'blank' !== to; }
1257                         }
1258                 }, function( settingId, o ) {
1259                         api( settingId, function( setting ) {
1260                                 $.each( o.controls, function( i, controlId ) {
1261                                         api.control( controlId, function( control ) {
1262                                                 var visibility = function( to ) {
1263                                                         control.container.toggle( o.callback( to ) );
1264                                                 };
1265
1266                                                 visibility( setting.get() );
1267                                                 setting.bind( visibility );
1268                                         });
1269                                 });
1270                         });
1271                 });
1272
1273                 // Juggle the two controls that use header_textcolor
1274                 api.control( 'display_header_text', function( control ) {
1275                         var last = '';
1276
1277                         control.elements[0].unsync( api( 'header_textcolor' ) );
1278
1279                         control.element = new api.Element( control.container.find('input') );
1280                         control.element.set( 'blank' !== control.setting() );
1281
1282                         control.element.bind( function( to ) {
1283                                 if ( ! to )
1284                                         last = api( 'header_textcolor' ).get();
1285
1286                                 control.setting.set( to ? last : 'blank' );
1287                         });
1288
1289                         control.setting.bind( function( to ) {
1290                                 control.element.set( 'blank' !== to );
1291                         });
1292                 });
1293
1294                 api.trigger( 'ready' );
1295
1296                 // Make sure left column gets focus
1297                 topFocus = closeBtn;
1298                 topFocus.focus();
1299                 setTimeout(function () {
1300                         topFocus.focus();
1301                 }, 200);
1302
1303         });
1304
1305 })( wp, jQuery );