1 /* globals _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n */
2 (function( exports, $ ){
3 var Container, focus, api = wp.customize;
7 * @augments wp.customize.Value
8 * @augments wp.customize.Class
11 * - previewer - The Previewer instance to sync with.
12 * - transport - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
14 api.Setting = api.Value.extend({
15 initialize: function( id, value, options ) {
16 api.Value.prototype.initialize.call( this, value, options );
19 this.transport = this.transport || 'refresh';
21 this.bind( this.preview );
24 switch ( this.transport ) {
26 return this.previewer.refresh();
28 return this.previewer.send( 'setting', [ this.id, this() ] );
34 * Utility function namespace
39 * Watch all changes to Value properties, and bubble changes to parent Values instance
43 * @param {wp.customize.Class} instance
44 * @param {Array} properties The names of the Value instances to watch.
46 api.utils.bubbleChildValueChanges = function ( instance, properties ) {
47 $.each( properties, function ( i, key ) {
48 instance[ key ].bind( function ( to, from ) {
49 if ( instance.parent && to !== from ) {
50 instance.parent.trigger( 'change', instance );
57 * Expand a panel, section, or control and focus on the first focusable element.
61 * @param {Object} [params]
62 * @param {Callback} [params.completeCallback]
64 focus = function ( params ) {
65 var construct, completeCallback, focus;
67 params = params || {};
69 construct.container.find( ':focusable:first' ).focus();
70 construct.container[0].scrollIntoView( true );
72 if ( params.completeCallback ) {
73 completeCallback = params.completeCallback;
74 params.completeCallback = function () {
79 params.completeCallback = focus;
81 if ( construct.expand ) {
82 construct.expand( params );
84 params.completeCallback();
89 * Stable sort for Panels, Sections, and Controls.
91 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
95 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
96 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
99 api.utils.prioritySort = function ( a, b ) {
100 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
101 return a.params.instanceNumber - b.params.instanceNumber;
103 return a.priority() - b.priority();
108 * Return whether the supplied Event object is for a keydown event but not the Enter key.
112 * @param {jQuery.Event} event
115 api.utils.isKeydownButNotEnterEvent = function ( event ) {
116 return ( 'keydown' === event.type && 13 !== event.which );
120 * Return whether the two lists of elements are the same and are in the same order.
124 * @param {Array|jQuery} listA
125 * @param {Array|jQuery} listB
128 api.utils.areElementListsEqual = function ( listA, listB ) {
130 listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
131 -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
132 _.zip( listA, listB ), // pair up each element between the two lists
134 return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
136 ), false ) // check for presence of false in map's return value
142 * Base class for Panel and Section.
147 * @augments wp.customize.Class
149 Container = api.Class.extend({
150 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
151 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
157 * @param {Object} options
159 initialize: function ( id, options ) {
160 var container = this;
162 container.params = {};
163 $.extend( container, options || {} );
164 container.container = $( container.params.content );
166 container.deferred = {
167 embedded: new $.Deferred()
169 container.priority = new api.Value();
170 container.active = new api.Value();
171 container.activeArgumentsQueue = [];
172 container.expanded = new api.Value();
173 container.expandedArgumentsQueue = [];
175 container.active.bind( function ( active ) {
176 var args = container.activeArgumentsQueue.shift();
177 args = $.extend( {}, container.defaultActiveArguments, args );
178 active = ( active && container.isContextuallyActive() );
179 container.onChangeActive( active, args );
181 container.expanded.bind( function ( expanded ) {
182 var args = container.expandedArgumentsQueue.shift();
183 args = $.extend( {}, container.defaultExpandedArguments, args );
184 container.onChangeExpanded( expanded, args );
187 container.attachEvents();
189 api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
191 container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority );
192 container.active.set( container.params.active );
193 container.expanded.set( false );
201 ready: function() {},
204 * Get the child models associated with this parent, sorting them by their priority Value.
208 * @param {String} parentType
209 * @param {String} childType
212 _children: function ( parentType, childType ) {
215 api[ childType ].each( function ( child ) {
216 if ( child[ parentType ].get() === parent.id ) {
217 children.push( child );
220 children.sort( api.utils.prioritySort );
225 * To override by subclass, to return whether the container has active children.
231 isContextuallyActive: function () {
232 throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
236 * Handle changes to the active state.
238 * This does not change the active state, it merely handles the behavior
239 * for when it does change.
241 * To override by subclass, update the container's UI to reflect the provided active state.
245 * @param {Boolean} active
246 * @param {Object} args
247 * @param {Object} args.duration
248 * @param {Object} args.completeCallback
250 onChangeActive: function ( active, args ) {
251 var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
252 if ( ! $.contains( document, this.container ) ) {
253 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
254 this.container.toggle( active );
255 if ( args.completeCallback ) {
256 args.completeCallback();
258 } else if ( active ) {
259 this.container.stop( true, true ).slideDown( duration, args.completeCallback );
261 this.container.stop( true, true ).slideUp( duration, args.completeCallback );
268 * @params {Boolean} active
269 * @param {Object} [params]
270 * @returns {Boolean} false if state already applied
272 _toggleActive: function ( active, params ) {
274 params = params || {};
275 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
276 params.unchanged = true;
277 self.onChangeActive( self.active.get(), params );
280 params.unchanged = false;
281 this.activeArgumentsQueue.push( params );
282 this.active.set( active );
288 * @param {Object} [params]
289 * @returns {Boolean} false if already active
291 activate: function ( params ) {
292 return this._toggleActive( true, params );
296 * @param {Object} [params]
297 * @returns {Boolean} false if already inactive
299 deactivate: function ( params ) {
300 return this._toggleActive( false, params );
304 * To override by subclass, update the container's UI to reflect the provided active state.
307 onChangeExpanded: function () {
308 throw new Error( 'Must override with subclass.' );
312 * @param {Boolean} expanded
313 * @param {Object} [params]
314 * @returns {Boolean} false if state already applied
316 _toggleExpanded: function ( expanded, params ) {
318 params = params || {};
319 if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
320 params.unchanged = true;
321 self.onChangeExpanded( self.expanded.get(), params );
324 params.unchanged = false;
325 this.expandedArgumentsQueue.push( params );
326 this.expanded.set( expanded );
332 * @param {Object} [params]
333 * @returns {Boolean} false if already expanded
335 expand: function ( params ) {
336 return this._toggleExpanded( true, params );
340 * @param {Object} [params]
341 * @returns {Boolean} false if already collapsed
343 collapse: function ( params ) {
344 return this._toggleExpanded( false, params );
348 * Bring the container into view and then expand this and bring it into view
349 * @param {Object} [params]
358 * @augments wp.customize.Class
360 api.Section = Container.extend({
366 * @param {Array} options
368 initialize: function ( id, options ) {
370 Container.prototype.initialize.call( section, id, options );
373 section.panel = new api.Value();
374 section.panel.bind( function ( id ) {
375 $( section.container ).toggleClass( 'control-subsection', !! id );
377 section.panel.set( section.params.panel || '' );
378 api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
381 section.deferred.embedded.done( function () {
387 * Embed the container in the DOM when any parent panel is ready.
392 var section = this, inject;
394 // Watch for changes to the panel state
395 inject = function ( panelId ) {
398 // The panel has been supplied, so wait until the panel object is registered
399 api.panel( panelId, function ( panel ) {
400 // The panel has been registered, wait for it to become ready/initialized
401 panel.deferred.embedded.done( function () {
402 parentContainer = panel.container.find( 'ul:first' );
403 if ( ! section.container.parent().is( parentContainer ) ) {
404 parentContainer.append( section.container );
406 section.deferred.embedded.resolve();
410 // There is no panel, so embed the section in the root of the customizer
411 parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
412 if ( ! section.container.parent().is( parentContainer ) ) {
413 parentContainer.append( section.container );
415 section.deferred.embedded.resolve();
418 section.panel.bind( inject );
419 inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
423 * Add behaviors for the accordion section.
427 attachEvents: function () {
430 // Expand/Collapse accordion sections on click.
431 section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
432 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
435 event.preventDefault(); // Keep this AFTER the key filter above
437 if ( section.expanded() ) {
446 * Return whether this section has any active controls.
452 isContextuallyActive: function () {
454 controls = section.controls(),
456 _( controls ).each( function ( control ) {
457 if ( control.active() ) {
461 return ( activeCount !== 0 );
465 * Get the controls that are associated with this section, sorted by their priority Value.
471 controls: function () {
472 return this._children( 'section', 'control' );
476 * Update UI to reflect expanded state.
480 * @param {Boolean} expanded
481 * @param {Object} args
483 onChangeExpanded: function ( expanded, args ) {
485 content = section.container.find( '.accordion-section-content' ),
490 if ( args.unchanged ) {
491 expand = args.completeCallback;
493 expand = function () {
494 content.stop().slideDown( args.duration, args.completeCallback );
495 section.container.addClass( 'open' );
499 if ( ! args.allowMultiple ) {
500 api.section.each( function ( otherSection ) {
501 if ( otherSection !== section ) {
502 otherSection.collapse( { duration: args.duration } );
507 if ( section.panel() ) {
508 api.panel( section.panel() ).expand({
509 duration: args.duration,
510 completeCallback: expand
517 section.container.removeClass( 'open' );
518 content.slideUp( args.duration, args.completeCallback );
527 * @augments wp.customize.Class
529 api.Panel = Container.extend({
534 * @param {Object} options
536 initialize: function ( id, options ) {
538 Container.prototype.initialize.call( panel, id, options );
540 panel.deferred.embedded.done( function () {
546 * Embed the container in the DOM when any parent panel is ready.
552 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
554 if ( ! panel.container.parent().is( parentContainer ) ) {
555 parentContainer.append( panel.container );
557 panel.deferred.embedded.resolve();
563 attachEvents: function () {
564 var meta, panel = this;
566 // Expand/Collapse accordion sections on click.
567 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
568 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
571 event.preventDefault(); // Keep this AFTER the key filter above
573 if ( ! panel.expanded() ) {
578 meta = panel.container.find( '.panel-meta:first' );
580 meta.find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
581 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
584 event.preventDefault(); // Keep this AFTER the key filter above
586 if ( meta.hasClass( 'cannot-expand' ) ) {
590 var content = meta.find( '.accordion-section-content:first' );
591 if ( meta.hasClass( 'open' ) ) {
592 meta.toggleClass( 'open' );
593 content.slideUp( panel.defaultExpandedArguments.duration );
595 content.slideDown( panel.defaultExpandedArguments.duration );
596 meta.toggleClass( 'open' );
603 * Get the sections that are associated with this panel, sorted by their priority Value.
609 sections: function () {
610 return this._children( 'panel', 'section' );
614 * Return whether this panel has any active sections.
620 isContextuallyActive: function () {
622 sections = panel.sections(),
624 _( sections ).each( function ( section ) {
625 if ( section.active() && section.isContextuallyActive() ) {
629 return ( activeCount !== 0 );
633 * Update UI to reflect expanded state
637 * @param {Boolean} expanded
638 * @param {Object} args
639 * @param {Boolean} args.unchanged
640 * @param {Callback} args.completeCallback
642 onChangeExpanded: function ( expanded, args ) {
644 // Immediately call the complete callback if there were no changes
645 if ( args.unchanged ) {
646 if ( args.completeCallback ) {
647 args.completeCallback();
652 // Note: there is a second argument 'args' passed
653 var position, scroll,
655 section = panel.container.closest( '.accordion-section' ),
656 overlay = section.closest( '.wp-full-overlay' ),
657 container = section.closest( '.accordion-container' ),
658 siblings = container.find( '.open' ),
659 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
660 backBtn = overlay.find( '.control-panel-back' ),
661 panelTitle = section.find( '.accordion-section-title' ).first(),
662 content = section.find( '.control-panel-content' );
666 // Collapse any sibling sections/panels
667 api.section.each( function ( section ) {
668 if ( ! section.panel() ) {
669 section.collapse( { duration: 0 } );
672 api.panel.each( function ( otherPanel ) {
673 if ( panel !== otherPanel ) {
674 otherPanel.collapse( { duration: 0 } );
678 content.show( 0, function() {
679 position = content.offset().top;
680 scroll = container.scrollTop();
681 content.css( 'margin-top', ( 45 - position - scroll ) );
682 section.addClass( 'current-panel' );
683 overlay.addClass( 'in-sub-panel' );
684 container.scrollTop( 0 );
685 if ( args.completeCallback ) {
686 args.completeCallback();
689 topPanel.attr( 'tabindex', '-1' );
690 backBtn.attr( 'tabindex', '0' );
693 siblings.removeClass( 'open' );
694 section.removeClass( 'current-panel' );
695 overlay.removeClass( 'in-sub-panel' );
696 content.delay( 180 ).hide( 0, function() {
697 content.css( 'margin-top', 'inherit' ); // Reset
698 if ( args.completeCallback ) {
699 args.completeCallback();
702 topPanel.attr( 'tabindex', '0' );
703 backBtn.attr( 'tabindex', '-1' );
705 container.scrollTop( 0 );
711 * A Customizer Control.
713 * A control provides a UI element that allows a user to modify a Customizer Setting.
715 * @see PHP class WP_Customize_Control.
718 * @augments wp.customize.Class
720 * @param {string} id Unique identifier for the control instance.
721 * @param {object} options Options hash for the control instance.
722 * @param {object} options.params
723 * @param {object} options.params.type Type of control (e.g. text, radio, dropdown-pages, etc.)
724 * @param {string} options.params.content The HTML content for the control.
725 * @param {string} options.params.priority Order of priority to show the control within the section.
726 * @param {string} options.params.active
727 * @param {string} options.params.section
728 * @param {string} options.params.label
729 * @param {string} options.params.description
730 * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
732 api.Control = api.Class.extend({
733 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
735 initialize: function( id, options ) {
737 nodes, radios, settings;
740 $.extend( control, options || {} );
742 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
743 control.templateSelector = 'customize-control-' + control.params.type + '-content';
744 control.container = control.params.content ? $( control.params.content ) : $( control.selector );
747 embedded: new $.Deferred()
749 control.section = new api.Value();
750 control.priority = new api.Value();
751 control.active = new api.Value();
752 control.activeArgumentsQueue = [];
754 control.elements = [];
756 nodes = control.container.find('[data-customize-setting-link]');
759 nodes.each( function() {
760 var node = $( this ),
763 if ( node.is( ':radio' ) ) {
764 name = node.prop( 'name' );
765 if ( radios[ name ] ) {
769 radios[ name ] = true;
770 node = nodes.filter( '[name="' + name + '"]' );
773 api( node.data( 'customizeSettingLink' ), function( setting ) {
774 var element = new api.Element( node );
775 control.elements.push( element );
776 element.sync( setting );
777 element.set( setting() );
781 control.active.bind( function ( active ) {
782 var args = control.activeArgumentsQueue.shift();
783 args = $.extend( {}, control.defaultActiveArguments, args );
784 control.onChangeActive( active, args );
787 control.section.set( control.params.section );
788 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
789 control.active.set( control.params.active );
791 api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
793 // Associate this control with its settings when they are created
794 settings = $.map( control.params.settings, function( value ) {
797 api.apply( api, settings.concat( function () {
800 control.settings = {};
801 for ( key in control.params.settings ) {
802 control.settings[ key ] = api( control.params.settings[ key ] );
805 control.setting = control.settings['default'] || null;
810 control.deferred.embedded.done( function () {
816 * Embed the control into the page.
822 // Watch for changes to the section state
823 inject = function ( sectionId ) {
825 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the frontend
828 // Wait for the section to be registered
829 api.section( sectionId, function ( section ) {
830 // Wait for the section to be ready/initialized
831 section.deferred.embedded.done( function () {
832 parentContainer = section.container.find( 'ul:first' );
833 if ( ! control.container.parent().is( parentContainer ) ) {
834 parentContainer.append( control.container );
835 control.renderContent();
837 control.deferred.embedded.resolve();
841 control.section.bind( inject );
842 inject( control.section.get() );
846 * Triggered when the control's markup has been injected into the DOM.
850 ready: function() {},
853 * Normal controls do not expand, so just expand its parent
855 * @param {Object} [params]
857 expand: function ( params ) {
858 api.section( this.section() ).expand( params );
862 * Bring the containing section and panel into view and then
863 * this control into view, focusing on the first input.
868 * Update UI in response to a change in the control's active state.
869 * This does not change the active state, it merely handles the behavior
870 * for when it does change.
874 * @param {Boolean} active
875 * @param {Object} args
876 * @param {Number} args.duration
877 * @param {Callback} args.completeCallback
879 onChangeActive: function ( active, args ) {
880 if ( ! $.contains( document, this.container ) ) {
881 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
882 this.container.toggle( active );
883 if ( args.completeCallback ) {
884 args.completeCallback();
886 } else if ( active ) {
887 this.container.slideDown( args.duration, args.completeCallback );
889 this.container.slideUp( args.duration, args.completeCallback );
894 * @deprecated 4.1.0 Use this.onChangeActive() instead.
896 toggle: function ( active ) {
897 return this.onChangeActive( active, this.defaultActiveArguments );
901 * Shorthand way to enable the active state.
905 * @param {Object} [params]
906 * @returns {Boolean} false if already active
908 activate: Container.prototype.activate,
911 * Shorthand way to disable the active state.
915 * @param {Object} [params]
916 * @returns {Boolean} false if already inactive
918 deactivate: Container.prototype.deactivate,
921 * Re-use _toggleActive from Container class.
925 _toggleActive: Container.prototype._toggleActive,
927 dropdownInit: function() {
929 statuses = this.container.find('.dropdown-status'),
930 params = this.params,
931 toggleFreeze = false,
932 update = function( to ) {
933 if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
934 statuses.html( params.statuses[ to ] ).show();
939 // Support the .dropdown class to open/close complex elements
940 this.container.on( 'click keydown', '.dropdown', function( event ) {
941 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
945 event.preventDefault();
948 control.container.toggleClass('open');
950 if ( control.container.hasClass('open') )
951 control.container.parent().parent().find('li.library-selected').focus();
953 // Don't want to fire focus and click at same time
955 setTimeout(function () {
956 toggleFreeze = false;
960 this.setting.bind( update );
961 update( this.setting() );
965 * Render the control from its JS template, if it exists.
967 * The control's container must already exist in the DOM.
971 renderContent: function () {
975 // Replace the container element's content with the control.
976 if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
977 template = wp.template( control.templateSelector );
978 if ( template && control.container ) {
979 control.container.html( template( control.params ) );
986 * A colorpicker control.
989 * @augments wp.customize.Control
990 * @augments wp.customize.Class
992 api.ColorControl = api.Control.extend({
995 picker = this.container.find('.color-picker-hex');
997 picker.val( control.setting() ).wpColorPicker({
999 control.setting.set( picker.wpColorPicker('color') );
1002 control.setting.set( false );
1006 this.setting.bind( function ( value ) {
1007 picker.val( value );
1008 picker.wpColorPicker( 'color', value );
1014 * An upload control, which utilizes the media modal.
1017 * @augments wp.customize.Control
1018 * @augments wp.customize.Class
1020 api.UploadControl = api.Control.extend({
1023 * When the control's DOM structure is ready,
1024 * set up internal event bindings.
1028 // Shortcut so that we don't have to use _.bind every time we add a callback.
1029 _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select' );
1031 // Bind events, with delegation to facilitate re-rendering.
1032 control.container.on( 'click keydown', '.upload-button', control.openFrame );
1033 control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1034 control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1035 control.container.on( 'click keydown', '.remove-button', control.removeFile );
1037 // Re-render whenever the control's setting changes.
1038 control.setting.bind( function () { control.renderContent(); } );
1042 * Open the media modal.
1044 openFrame: function( event ) {
1045 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1049 event.preventDefault();
1051 if ( ! this.frame ) {
1059 * Create a media modal select frame, and store it so the instance can be reused when needed.
1061 initFrame: function() {
1062 this.frame = wp.media({
1064 text: this.params.button_labels.frame_button
1067 new wp.media.controller.Library({
1068 title: this.params.button_labels.frame_title,
1069 library: wp.media.query({ type: this.params.mime_type }),
1076 // When a file is selected, run a callback.
1077 this.frame.on( 'select', this.select );
1081 * Callback handler for when an attachment is selected in the media modal.
1082 * Gets the selected image information, and sets it within the control.
1084 select: function() {
1085 // Get the attachment from the modal frame.
1086 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
1088 this.params.attachment = attachment;
1090 // Set the Customizer setting; the callback takes care of rendering.
1091 this.setting( attachment.url );
1095 * Reset the setting to the default value.
1097 restoreDefault: function( event ) {
1098 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1101 event.preventDefault();
1103 this.params.attachment = this.params.defaultAttachment;
1104 this.setting( this.params.defaultAttachment.url );
1108 * Called when the "Remove" link is clicked. Empties the setting.
1110 * @param {object} event jQuery Event object
1112 removeFile: function( event ) {
1113 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1116 event.preventDefault();
1118 this.params.attachment = {};
1120 this.renderContent(); // Not bound to setting change when emptying.
1124 success: function() {},
1127 removerVisibility: function() {}
1131 * A control for uploading images.
1133 * This control no longer needs to do anything more
1134 * than what the upload control does in JS.
1137 * @augments wp.customize.UploadControl
1138 * @augments wp.customize.Control
1139 * @augments wp.customize.Class
1141 api.ImageControl = api.UploadControl.extend({
1143 thumbnailSrc: function() {}
1147 * A control for uploading background images.
1150 * @augments wp.customize.UploadControl
1151 * @augments wp.customize.Control
1152 * @augments wp.customize.Class
1154 api.BackgroundControl = api.UploadControl.extend({
1157 * When the control's DOM structure is ready,
1158 * set up internal event bindings.
1161 api.UploadControl.prototype.ready.apply( this, arguments );
1165 * Callback handler for when an attachment is selected in the media modal.
1166 * Does an additional AJAX request for setting the background context.
1168 select: function() {
1169 api.UploadControl.prototype.select.apply( this, arguments );
1171 wp.ajax.post( 'custom-background-add', {
1172 nonce: _wpCustomizeBackground.nonces.add,
1174 theme: api.settings.theme.stylesheet,
1175 attachment_id: this.params.attachment.id
1182 * @augments wp.customize.Control
1183 * @augments wp.customize.Class
1185 api.HeaderControl = api.Control.extend({
1187 this.btnRemove = $('#customize-control-header_image .actions .remove');
1188 this.btnNew = $('#customize-control-header_image .actions .new');
1190 _.bindAll(this, 'openMedia', 'removeImage');
1192 this.btnNew.on( 'click', this.openMedia );
1193 this.btnRemove.on( 'click', this.removeImage );
1195 api.HeaderTool.currentHeader = new api.HeaderTool.ImageModel();
1197 new api.HeaderTool.CurrentView({
1198 model: api.HeaderTool.currentHeader,
1199 el: '#customize-control-header_image .current .container'
1202 new api.HeaderTool.ChoiceListView({
1203 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
1204 el: '#customize-control-header_image .choices .uploaded .list'
1207 new api.HeaderTool.ChoiceListView({
1208 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
1209 el: '#customize-control-header_image .choices .default .list'
1212 api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
1213 api.HeaderTool.UploadsList,
1214 api.HeaderTool.DefaultsList
1219 * Returns a set of options, computed from the attached image data and
1220 * theme-specific data, to be fed to the imgAreaSelect plugin in
1221 * wp.media.view.Cropper.
1223 * @param {wp.media.model.Attachment} attachment
1224 * @param {wp.media.controller.Cropper} controller
1225 * @returns {Object} Options
1227 calculateImageSelectOptions: function(attachment, controller) {
1228 var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
1229 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
1230 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
1231 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
1232 ratio, xImg, yImg, realHeight, realWidth,
1235 realWidth = attachment.get('width');
1236 realHeight = attachment.get('height');
1238 this.headerImage = new api.HeaderTool.ImageModel();
1239 this.headerImage.set({
1242 themeFlexWidth: flexWidth,
1243 themeFlexHeight: flexHeight,
1244 imageWidth: realWidth,
1245 imageHeight: realHeight
1248 controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
1250 ratio = xInit / yInit;
1254 if ( xImg / yImg > ratio ) {
1256 xInit = yInit * ratio;
1259 yInit = xInit / ratio;
1262 imgSelectOptions = {
1267 imageWidth: realWidth,
1268 imageHeight: realHeight,
1275 if (flexHeight === false && flexWidth === false) {
1276 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
1278 if (flexHeight === false ) {
1279 imgSelectOptions.maxHeight = yInit;
1281 if (flexWidth === false ) {
1282 imgSelectOptions.maxWidth = xInit;
1285 return imgSelectOptions;
1289 * Sets up and opens the Media Manager in order to select an image.
1290 * Depending on both the size of the image and the properties of the
1291 * current theme, a cropping step after selection may be required or
1294 * @param {event} event
1296 openMedia: function(event) {
1297 var l10n = _wpMediaViewsL10n;
1299 event.preventDefault();
1301 this.frame = wp.media({
1303 text: l10n.selectAndCrop,
1307 new wp.media.controller.Library({
1308 title: l10n.chooseImage,
1309 library: wp.media.query({ type: 'image' }),
1313 suggestedWidth: _wpCustomizeHeader.data.width,
1314 suggestedHeight: _wpCustomizeHeader.data.height
1316 new wp.media.controller.Cropper({
1317 imgSelectOptions: this.calculateImageSelectOptions
1322 this.frame.on('select', this.onSelect, this);
1323 this.frame.on('cropped', this.onCropped, this);
1324 this.frame.on('skippedcrop', this.onSkippedCrop, this);
1330 * After an image is selected in the media modal,
1331 * switch to the cropper state.
1333 onSelect: function() {
1334 this.frame.setState('cropper');
1338 * After the image has been cropped, apply the cropped image data to the setting.
1340 * @param {object} croppedImage Cropped attachment data.
1342 onCropped: function(croppedImage) {
1343 var url = croppedImage.post_content,
1344 attachmentId = croppedImage.attachment_id,
1345 w = croppedImage.width,
1346 h = croppedImage.height;
1347 this.setImageFromURL(url, attachmentId, w, h);
1351 * If cropping was skipped, apply the image data directly to the setting.
1353 * @param {object} selection
1355 onSkippedCrop: function(selection) {
1356 var url = selection.get('url'),
1357 w = selection.get('width'),
1358 h = selection.get('height');
1359 this.setImageFromURL(url, selection.id, w, h);
1363 * Creates a new wp.customize.HeaderTool.ImageModel from provided
1364 * header image data and inserts it into the user-uploaded headers
1367 * @param {String} url
1368 * @param {Number} attachmentId
1369 * @param {Number} width
1370 * @param {Number} height
1372 setImageFromURL: function(url, attachmentId, width, height) {
1373 var choice, data = {};
1376 data.thumbnail_url = url;
1377 data.timestamp = _.now();
1380 data.attachment_id = attachmentId;
1388 data.height = height;
1391 choice = new api.HeaderTool.ImageModel({
1393 choice: url.split('/').pop()
1395 api.HeaderTool.UploadsList.add(choice);
1396 api.HeaderTool.currentHeader.set(choice.toJSON());
1398 choice.importImage();
1402 * Triggers the necessary events to deselect an image which was set as
1403 * the currently selected one.
1405 removeImage: function() {
1406 api.HeaderTool.currentHeader.trigger('hide');
1407 api.HeaderTool.CombinedList.trigger('control:removeImage');
1412 // Change objects contained within the main customize object to Settings.
1413 api.defaultConstructor = api.Setting;
1415 // Create the collections for Controls, Sections and Panels.
1416 api.control = new api.Values({ defaultConstructor: api.Control });
1417 api.section = new api.Values({ defaultConstructor: api.Section });
1418 api.panel = new api.Values({ defaultConstructor: api.Panel });
1422 * @augments wp.customize.Messenger
1423 * @augments wp.customize.Class
1424 * @mixes wp.customize.Events
1426 api.PreviewFrame = api.Messenger.extend({
1429 initialize: function( params, options ) {
1430 var deferred = $.Deferred();
1432 // This is the promise object.
1433 deferred.promise( this );
1435 this.container = params.container;
1436 this.signature = params.signature;
1438 $.extend( params, { channel: api.PreviewFrame.uuid() });
1440 api.Messenger.prototype.initialize.call( this, params, options );
1442 this.add( 'previewUrl', params.previewUrl );
1444 this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
1446 this.run( deferred );
1449 run: function( deferred ) {
1454 if ( this._ready ) {
1455 this.unbind( 'ready', this._ready );
1458 this._ready = function() {
1462 deferred.resolveWith( self );
1466 this.bind( 'ready', this._ready );
1468 this.bind( 'ready', function ( data ) {
1474 * Walk over all panels, sections, and controls and set their
1475 * respective active states to true if the preview explicitly
1476 * indicates as such.
1479 panel: data.activePanels,
1480 section: data.activeSections,
1481 control: data.activeControls
1483 _( constructs ).each( function ( activeConstructs, type ) {
1484 api[ type ].each( function ( construct, id ) {
1485 var active = !! ( activeConstructs && activeConstructs[ id ] );
1486 construct.active( active );
1491 this.request = $.ajax( this.previewUrl(), {
1495 withCredentials: true
1499 this.request.fail( function() {
1500 deferred.rejectWith( self, [ 'request failure' ] );
1503 this.request.done( function( response ) {
1504 var location = self.request.getResponseHeader('Location'),
1505 signature = self.signature,
1508 // Check if the location response header differs from the current URL.
1509 // If so, the request was redirected; try loading the requested page.
1510 if ( location && location !== self.previewUrl() ) {
1511 deferred.rejectWith( self, [ 'redirect', location ] );
1515 // Check if the user is not logged in.
1516 if ( '0' === response ) {
1517 self.login( deferred );
1521 // Check for cheaters.
1522 if ( '-1' === response ) {
1523 deferred.rejectWith( self, [ 'cheatin' ] );
1527 // Check for a signature in the request.
1528 index = response.lastIndexOf( signature );
1529 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
1530 deferred.rejectWith( self, [ 'unsigned' ] );
1534 // Strip the signature from the request.
1535 response = response.slice( 0, index ) + response.slice( index + signature.length );
1537 // Create the iframe and inject the html content.
1538 self.iframe = $('<iframe />').appendTo( self.container );
1540 // Bind load event after the iframe has been added to the page;
1541 // otherwise it will fire when injected into the DOM.
1542 self.iframe.one( 'load', function() {
1546 deferred.resolveWith( self );
1548 setTimeout( function() {
1549 deferred.rejectWith( self, [ 'ready timeout' ] );
1550 }, self.sensitivity );
1554 self.targetWindow( self.iframe[0].contentWindow );
1556 self.targetWindow().document.open();
1557 self.targetWindow().document.write( response );
1558 self.targetWindow().document.close();
1562 login: function( deferred ) {
1566 reject = function() {
1567 deferred.rejectWith( self, [ 'logged out' ] );
1570 if ( this.triedLogin )
1573 // Check if we have an admin cookie.
1574 $.get( api.settings.url.ajax, {
1576 }).fail( reject ).done( function( response ) {
1579 if ( '1' !== response )
1582 iframe = $('<iframe src="' + self.previewUrl() + '" />').hide();
1583 iframe.appendTo( self.container );
1584 iframe.load( function() {
1585 self.triedLogin = true;
1588 self.run( deferred );
1593 destroy: function() {
1594 api.Messenger.prototype.destroy.call( this );
1595 this.request.abort();
1598 this.iframe.remove();
1600 delete this.request;
1602 delete this.targetWindow;
1609 * Create a universally unique identifier.
1613 api.PreviewFrame.uuid = function() {
1614 return 'preview-' + uuid++;
1619 * Set the document title of the customizer.
1623 * @param {string} documentTitle
1625 api.setDocumentTitle = function ( documentTitle ) {
1627 tmpl = api.settings.documentTitleTmpl;
1628 title = tmpl.replace( '%s', documentTitle );
1629 document.title = title;
1630 if ( window !== window.parent ) {
1631 window.parent.document.title = document.title;
1637 * @augments wp.customize.Messenger
1638 * @augments wp.customize.Class
1639 * @mixes wp.customize.Events
1641 api.Previewer = api.Messenger.extend({
1646 * - container - a selector or jQuery element
1647 * - previewUrl - the URL of preview frame
1649 initialize: function( params, options ) {
1651 rscheme = /^https?/;
1653 $.extend( this, options || {} );
1655 active: $.Deferred()
1659 * Wrap this.refresh to prevent it from hammering the servers:
1661 * If refresh is called once and no other refresh requests are
1662 * loading, trigger the request immediately.
1664 * If refresh is called while another refresh request is loading,
1665 * debounce the refresh requests:
1666 * 1. Stop the loading request (as it is instantly outdated).
1667 * 2. Trigger the new request once refresh hasn't been called for
1668 * self.refreshBuffer milliseconds.
1670 this.refresh = (function( self ) {
1671 var refresh = self.refresh,
1672 callback = function() {
1674 refresh.call( self );
1679 if ( typeof timeout !== 'number' ) {
1680 if ( self.loading ) {
1687 clearTimeout( timeout );
1688 timeout = setTimeout( callback, self.refreshBuffer );
1692 this.container = api.ensure( params.container );
1693 this.allowedUrls = params.allowedUrls;
1694 this.signature = params.signature;
1696 params.url = window.location.href;
1698 api.Messenger.prototype.initialize.call( this, params );
1700 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
1701 var match = to.match( rscheme );
1702 return match ? match[0] : '';
1705 // Limit the URL to internal, front-end links.
1707 // If the frontend and the admin are served from the same domain, load the
1708 // preview over ssl if the Customizer is being loaded over ssl. This avoids
1709 // insecure content warnings. This is not attempted if the admin and frontend
1710 // are on different domains to avoid the case where the frontend doesn't have
1713 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
1716 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
1717 // Strip hashes and query strings before testing.
1718 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
1721 // Attempt to match the URL to the control frame's scheme
1722 // and check if it's allowed. If not, try the original URL.
1723 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
1724 $.each( self.allowedUrls, function( i, allowed ) {
1727 allowed = allowed.replace( /\/+$/, '' );
1728 path = url.replace( allowed, '' );
1730 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
1739 // If we found a matching result, return it. If not, bail.
1740 return result ? result : null;
1743 // Refresh the preview when the URL is changed (but not yet).
1744 this.previewUrl.bind( this.refresh );
1747 this.bind( 'scroll', function( distance ) {
1748 this.scroll = distance;
1751 // Update the URL when the iframe sends a URL message.
1752 this.bind( 'url', this.previewUrl );
1754 // Update the document title when the preview changes.
1755 this.bind( 'documentTitle', function ( title ) {
1756 api.setDocumentTitle( title );
1760 query: function() {},
1763 if ( this.loading ) {
1764 this.loading.destroy();
1765 delete this.loading;
1769 refresh: function() {
1774 this.loading = new api.PreviewFrame({
1776 previewUrl: this.previewUrl(),
1777 query: this.query() || {},
1778 container: this.container,
1779 signature: this.signature
1782 this.loading.done( function() {
1783 // 'this' is the loading frame
1784 this.bind( 'synced', function() {
1786 self.preview.destroy();
1787 self.preview = this;
1788 delete self.loading;
1790 self.targetWindow( this.targetWindow() );
1791 self.channel( this.channel() );
1793 self.deferred.active.resolve();
1794 self.send( 'active' );
1797 this.send( 'sync', {
1798 scroll: self.scroll,
1803 this.loading.fail( function( reason, location ) {
1804 if ( 'redirect' === reason && location )
1805 self.previewUrl( location );
1807 if ( 'logged out' === reason ) {
1808 if ( self.preview ) {
1809 self.preview.destroy();
1810 delete self.preview;
1813 self.login().done( self.refresh );
1816 if ( 'cheatin' === reason )
1822 var previewer = this,
1823 deferred, messenger, iframe;
1828 deferred = $.Deferred();
1829 this._login = deferred.promise();
1831 messenger = new api.Messenger({
1833 url: api.settings.url.login
1836 iframe = $('<iframe src="' + api.settings.url.login + '" />').appendTo( this.container );
1838 messenger.targetWindow( iframe[0].contentWindow );
1840 messenger.bind( 'login', function() {
1842 messenger.destroy();
1843 delete previewer._login;
1850 cheatin: function() {
1851 $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
1855 api.controlConstructor = {
1856 color: api.ColorControl,
1857 upload: api.UploadControl,
1858 image: api.ImageControl,
1859 header: api.HeaderControl,
1860 background: api.BackgroundControl
1862 api.panelConstructor = {};
1863 api.sectionConstructor = {};
1866 api.settings = window._wpCustomizeSettings;
1867 api.l10n = window._wpCustomizeControlsL10n;
1869 // Check if we can run the Customizer.
1870 if ( ! api.settings ) {
1874 // Redirect to the fallback preview if any incompatibilities are found.
1875 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
1876 return window.location = api.settings.url.fallback;
1878 var parent, topFocus,
1879 body = $( document.body ),
1880 overlay = body.children( '.wp-full-overlay' ),
1881 title = $( '#customize-info .theme-name.site-title' ),
1882 closeBtn = $( '.customize-controls-close' ),
1883 saveBtn = $( '#save' );
1885 // Prevent the form from saving when enter is pressed on an input or select element.
1886 $('#customize-controls').on( 'keydown', function( e ) {
1887 var isEnter = ( 13 === e.which ),
1888 $el = $( e.target );
1890 if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
1895 // Expand/Collapse the main customizer customize info.
1896 $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
1897 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1900 event.preventDefault(); // Keep this AFTER the key filter above
1902 var section = $( this ).parent(),
1903 content = section.find( '.accordion-section-content:first' );
1905 if ( section.hasClass( 'cannot-expand' ) ) {
1909 if ( section.hasClass( 'open' ) ) {
1910 section.toggleClass( 'open' );
1911 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
1913 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
1914 section.toggleClass( 'open' );
1918 // Initialize Previewer
1919 api.previewer = new api.Previewer({
1920 container: '#customize-preview',
1921 form: '#customize-controls',
1922 previewUrl: api.settings.url.preview,
1923 allowedUrls: api.settings.url.allowed,
1924 signature: 'WP_CUSTOMIZER_SIGNATURE'
1927 nonce: api.settings.nonce,
1930 var dirtyCustomized = {};
1931 api.each( function ( value, key ) {
1932 if ( value._dirty ) {
1933 dirtyCustomized[ key ] = value();
1939 theme: api.settings.theme.stylesheet,
1940 customized: JSON.stringify( dirtyCustomized ),
1941 nonce: this.nonce.preview
1947 query = $.extend( this.query(), {
1948 action: 'customize_save',
1949 nonce: this.nonce.save
1951 processing = api.state( 'processing' ),
1952 submitWhenDoneProcessing,
1955 body.addClass( 'saving' );
1957 submit = function () {
1958 var request = $.post( api.settings.url.ajax, query );
1960 api.trigger( 'save', request );
1962 request.always( function () {
1963 body.removeClass( 'saving' );
1966 request.done( function( response ) {
1967 // Check if the user is logged out.
1968 if ( '0' === response ) {
1969 self.preview.iframe.hide();
1970 self.login().done( function() {
1972 self.preview.iframe.show();
1977 // Check for cheaters.
1978 if ( '-1' === response ) {
1983 // Clear setting dirty states
1984 api.each( function ( value ) {
1985 value._dirty = false;
1987 api.trigger( 'saved' );
1991 if ( 0 === processing() ) {
1994 submitWhenDoneProcessing = function () {
1995 if ( 0 === processing() ) {
1996 api.state.unbind( 'change', submitWhenDoneProcessing );
2000 api.state.bind( 'change', submitWhenDoneProcessing );
2006 // Refresh the nonces if the preview sends updated nonces over.
2007 api.previewer.bind( 'nonce', function( nonce ) {
2008 $.extend( this.nonce, nonce );
2012 $.each( api.settings.settings, function( id, data ) {
2013 api.create( id, id, data.value, {
2014 transport: data.transport,
2015 previewer: api.previewer
2020 $.each( api.settings.panels, function ( id, data ) {
2021 var constructor = api.panelConstructor[ data.type ] || api.Panel,
2024 panel = new constructor( id, {
2027 api.panel.add( id, panel );
2031 $.each( api.settings.sections, function ( id, data ) {
2032 var constructor = api.sectionConstructor[ data.type ] || api.Section,
2035 section = new constructor( id, {
2038 api.section.add( id, section );
2042 $.each( api.settings.controls, function( id, data ) {
2043 var constructor = api.controlConstructor[ data.type ] || api.Control,
2046 control = new constructor( id, {
2048 previewer: api.previewer
2050 api.control.add( id, control );
2053 // Focus the autofocused element
2054 _.each( [ 'panel', 'section', 'control' ], function ( type ) {
2055 var instance, id = api.settings.autofocus[ type ];
2056 if ( id && api[ type ]( id ) ) {
2057 instance = api[ type ]( id );
2058 // Wait until the element is embedded in the DOM
2059 instance.deferred.embedded.done( function () {
2060 // Wait until the preview has activated and so active panels, sections, controls have been set
2061 api.previewer.deferred.active.done( function () {
2069 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
2073 api.reflowPaneContents = _.bind( function () {
2075 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
2077 if ( document.activeElement ) {
2078 activeElement = $( document.activeElement );
2081 // Sort the sections within each panel
2082 api.panel.each( function ( panel ) {
2083 var sections = panel.sections(),
2084 sectionContainers = _.pluck( sections, 'container' );
2085 rootNodes.push( panel );
2086 appendContainer = panel.container.find( 'ul:first' );
2087 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
2088 _( sections ).each( function ( section ) {
2089 appendContainer.append( section.container );
2095 // Sort the controls within each section
2096 api.section.each( function ( section ) {
2097 var controls = section.controls(),
2098 controlContainers = _.pluck( controls, 'container' );
2099 if ( ! section.panel() ) {
2100 rootNodes.push( section );
2102 appendContainer = section.container.find( 'ul:first' );
2103 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
2104 _( controls ).each( function ( control ) {
2105 appendContainer.append( control.container );
2111 // Sort the root panels and sections
2112 rootNodes.sort( api.utils.prioritySort );
2113 rootContainers = _.pluck( rootNodes, 'container' );
2114 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
2115 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
2116 _( rootNodes ).each( function ( rootNode ) {
2117 appendContainer.append( rootNode.container );
2122 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
2123 api.panel.each( function ( panel ) {
2124 var value = panel.active();
2125 panel.active.callbacks.fireWith( panel.active, [ value, value ] );
2127 api.section.each( function ( section ) {
2128 var value = section.active();
2129 section.active.callbacks.fireWith( section.active, [ value, value ] );
2132 // Restore focus if there was a reflow and there was an active (focused) element
2133 if ( wasReflowed && activeElement ) {
2134 activeElement.focus();
2137 api.bind( 'ready', api.reflowPaneContents );
2138 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
2139 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
2140 values.bind( 'add', api.reflowPaneContents );
2141 values.bind( 'change', api.reflowPaneContents );
2142 values.bind( 'remove', api.reflowPaneContents );
2145 // Check if preview url is valid and load the preview frame.
2146 if ( api.previewer.previewUrl() ) {
2147 api.previewer.refresh();
2149 api.previewer.previewUrl( api.settings.url.home );
2152 // Save and activated states
2154 var state = new api.Values(),
2155 saved = state.create( 'saved' ),
2156 activated = state.create( 'activated' ),
2157 processing = state.create( 'processing' );
2159 state.bind( 'change', function() {
2160 if ( ! activated() ) {
2161 saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
2162 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
2164 } else if ( saved() ) {
2165 saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
2166 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
2169 saveBtn.val( api.l10n.save ).prop( 'disabled', false );
2170 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
2174 // Set default states.
2176 activated( api.settings.theme.active );
2179 api.bind( 'change', function() {
2180 state('saved').set( false );
2183 api.bind( 'saved', function() {
2184 state('saved').set( true );
2185 state('activated').set( true );
2188 activated.bind( function( to ) {
2190 api.trigger( 'activated' );
2193 // Expose states to the API.
2198 saveBtn.click( function( event ) {
2199 api.previewer.save();
2200 event.preventDefault();
2201 }).keydown( function( event ) {
2202 if ( 9 === event.which ) // tab
2204 if ( 13 === event.which ) // enter
2205 api.previewer.save();
2206 event.preventDefault();
2209 // Go back to the top-level Customizer accordion.
2210 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( event ) {
2211 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2215 event.preventDefault(); // Keep this AFTER the key filter above
2216 api.panel.each( function ( panel ) {
2221 closeBtn.keydown( function( event ) {
2222 if ( 9 === event.which ) // tab
2224 if ( 13 === event.which ) // enter
2226 event.preventDefault();
2229 $('.collapse-sidebar').on( 'click keydown', function( event ) {
2230 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2234 overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
2235 event.preventDefault();
2238 // Bind site title display to the corresponding field.
2239 if ( title.length ) {
2240 $( '#customize-control-blogname input' ).on( 'input', function() {
2241 title.text( this.value );
2245 // Create a potential postMessage connection with the parent frame.
2246 parent = new api.Messenger({
2247 url: api.settings.url.parent,
2251 // If we receive a 'back' event, we're inside an iframe.
2252 // Send any clicks to the 'Return' link to the parent page.
2253 parent.bind( 'back', function() {
2254 closeBtn.on( 'click.customize-controls-close', function( event ) {
2255 event.preventDefault();
2256 parent.send( 'close' );
2260 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
2261 $( window ).on( 'beforeunload', function () {
2262 if ( ! api.state( 'saved' )() ) {
2263 return api.l10n.saveAlert;
2267 // Pass events through to the parent.
2268 $.each( [ 'saved', 'change' ], function ( i, event ) {
2269 api.bind( event, function() {
2270 parent.send( event );
2274 // When activated, let the loader handle redirecting the page.
2275 // If no loader exists, redirect the page ourselves (if a url exists).
2276 api.bind( 'activated', function() {
2277 if ( parent.targetWindow() )
2278 parent.send( 'activated', api.settings.url.activated );
2279 else if ( api.settings.url.activated )
2280 window.location = api.settings.url.activated;
2283 // Initialize the connection with the parent frame.
2284 parent.send( 'ready' );
2286 // Control visibility for default controls
2288 'background_image': {
2289 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
2290 callback: function( to ) { return !! to; }
2293 controls: [ 'page_on_front', 'page_for_posts' ],
2294 callback: function( to ) { return 'page' === to; }
2296 'header_textcolor': {
2297 controls: [ 'header_textcolor' ],
2298 callback: function( to ) { return 'blank' !== to; }
2300 }, function( settingId, o ) {
2301 api( settingId, function( setting ) {
2302 $.each( o.controls, function( i, controlId ) {
2303 api.control( controlId, function( control ) {
2304 var visibility = function( to ) {
2305 control.container.toggle( o.callback( to ) );
2308 visibility( setting.get() );
2309 setting.bind( visibility );
2315 // Juggle the two controls that use header_textcolor
2316 api.control( 'display_header_text', function( control ) {
2319 control.elements[0].unsync( api( 'header_textcolor' ) );
2321 control.element = new api.Element( control.container.find('input') );
2322 control.element.set( 'blank' !== control.setting() );
2324 control.element.bind( function( to ) {
2326 last = api( 'header_textcolor' ).get();
2328 control.setting.set( to ? last : 'blank' );
2331 control.setting.bind( function( to ) {
2332 control.element.set( 'blank' !== to );
2336 api.trigger( 'ready' );
2338 // Make sure left column gets focus
2339 topFocus = closeBtn;
2341 setTimeout(function () {