1 /* globals _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
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';
20 this._dirty = options.dirty || false;
22 this.bind( this.preview );
25 switch ( this.transport ) {
27 return this.previewer.refresh();
29 return this.previewer.send( 'setting', [ this.id, this() ] );
35 * Utility function namespace
40 * Watch all changes to Value properties, and bubble changes to parent Values instance
44 * @param {wp.customize.Class} instance
45 * @param {Array} properties The names of the Value instances to watch.
47 api.utils.bubbleChildValueChanges = function ( instance, properties ) {
48 $.each( properties, function ( i, key ) {
49 instance[ key ].bind( function ( to, from ) {
50 if ( instance.parent && to !== from ) {
51 instance.parent.trigger( 'change', instance );
58 * Expand a panel, section, or control and focus on the first focusable element.
62 * @param {Object} [params]
63 * @param {Callback} [params.completeCallback]
65 focus = function ( params ) {
66 var construct, completeCallback, focus;
68 params = params || {};
71 if ( construct.extended( api.Panel ) && construct.expanded() ) {
72 focusContainer = construct.container.find( '.control-panel-content:first' );
74 focusContainer = construct.container;
76 focusContainer.find( ':focusable:first' ).focus();
77 focusContainer[0].scrollIntoView( true );
79 if ( params.completeCallback ) {
80 completeCallback = params.completeCallback;
81 params.completeCallback = function () {
86 params.completeCallback = focus;
88 if ( construct.expand ) {
89 construct.expand( params );
91 params.completeCallback();
96 * Stable sort for Panels, Sections, and Controls.
98 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
102 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
103 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
106 api.utils.prioritySort = function ( a, b ) {
107 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
108 return a.params.instanceNumber - b.params.instanceNumber;
110 return a.priority() - b.priority();
115 * Return whether the supplied Event object is for a keydown event but not the Enter key.
119 * @param {jQuery.Event} event
122 api.utils.isKeydownButNotEnterEvent = function ( event ) {
123 return ( 'keydown' === event.type && 13 !== event.which );
127 * Return whether the two lists of elements are the same and are in the same order.
131 * @param {Array|jQuery} listA
132 * @param {Array|jQuery} listB
135 api.utils.areElementListsEqual = function ( listA, listB ) {
137 listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
138 -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
139 _.zip( listA, listB ), // pair up each element between the two lists
141 return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
143 ), false ) // check for presence of false in map's return value
149 * Base class for Panel and Section.
154 * @augments wp.customize.Class
156 Container = api.Class.extend({
157 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
158 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
164 * @param {Object} options
166 initialize: function ( id, options ) {
167 var container = this;
169 container.params = {};
170 $.extend( container, options || {} );
171 container.container = $( container.params.content );
173 container.deferred = {
174 embedded: new $.Deferred()
176 container.priority = new api.Value();
177 container.active = new api.Value();
178 container.activeArgumentsQueue = [];
179 container.expanded = new api.Value();
180 container.expandedArgumentsQueue = [];
182 container.active.bind( function ( active ) {
183 var args = container.activeArgumentsQueue.shift();
184 args = $.extend( {}, container.defaultActiveArguments, args );
185 active = ( active && container.isContextuallyActive() );
186 container.onChangeActive( active, args );
188 container.expanded.bind( function ( expanded ) {
189 var args = container.expandedArgumentsQueue.shift();
190 args = $.extend( {}, container.defaultExpandedArguments, args );
191 container.onChangeExpanded( expanded, args );
194 container.attachEvents();
196 api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
198 container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority );
199 container.active.set( container.params.active );
200 container.expanded.set( false );
208 ready: function() {},
211 * Get the child models associated with this parent, sorting them by their priority Value.
215 * @param {String} parentType
216 * @param {String} childType
219 _children: function ( parentType, childType ) {
222 api[ childType ].each( function ( child ) {
223 if ( child[ parentType ].get() === parent.id ) {
224 children.push( child );
227 children.sort( api.utils.prioritySort );
232 * To override by subclass, to return whether the container has active children.
238 isContextuallyActive: function () {
239 throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
243 * Handle changes to the active state.
245 * This does not change the active state, it merely handles the behavior
246 * for when it does change.
248 * To override by subclass, update the container's UI to reflect the provided active state.
252 * @param {Boolean} active
253 * @param {Object} args
254 * @param {Object} args.duration
255 * @param {Object} args.completeCallback
257 onChangeActive: function ( active, args ) {
258 var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
259 if ( ! $.contains( document, this.container ) ) {
260 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
261 this.container.toggle( active );
262 if ( args.completeCallback ) {
263 args.completeCallback();
265 } else if ( active ) {
266 this.container.stop( true, true ).slideDown( duration, args.completeCallback );
268 this.container.stop( true, true ).slideUp( duration, args.completeCallback );
275 * @params {Boolean} active
276 * @param {Object} [params]
277 * @returns {Boolean} false if state already applied
279 _toggleActive: function ( active, params ) {
281 params = params || {};
282 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
283 params.unchanged = true;
284 self.onChangeActive( self.active.get(), params );
287 params.unchanged = false;
288 this.activeArgumentsQueue.push( params );
289 this.active.set( active );
295 * @param {Object} [params]
296 * @returns {Boolean} false if already active
298 activate: function ( params ) {
299 return this._toggleActive( true, params );
303 * @param {Object} [params]
304 * @returns {Boolean} false if already inactive
306 deactivate: function ( params ) {
307 return this._toggleActive( false, params );
311 * To override by subclass, update the container's UI to reflect the provided active state.
314 onChangeExpanded: function () {
315 throw new Error( 'Must override with subclass.' );
319 * @param {Boolean} expanded
320 * @param {Object} [params]
321 * @returns {Boolean} false if state already applied
323 _toggleExpanded: function ( expanded, params ) {
325 params = params || {};
326 var section = this, previousCompleteCallback = params.completeCallback;
327 params.completeCallback = function () {
328 if ( previousCompleteCallback ) {
329 previousCompleteCallback.apply( section, arguments );
332 section.container.trigger( 'expanded' );
334 section.container.trigger( 'collapsed' );
337 if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
338 params.unchanged = true;
339 self.onChangeExpanded( self.expanded.get(), params );
342 params.unchanged = false;
343 this.expandedArgumentsQueue.push( params );
344 this.expanded.set( expanded );
350 * @param {Object} [params]
351 * @returns {Boolean} false if already expanded
353 expand: function ( params ) {
354 return this._toggleExpanded( true, params );
358 * @param {Object} [params]
359 * @returns {Boolean} false if already collapsed
361 collapse: function ( params ) {
362 return this._toggleExpanded( false, params );
366 * Bring the container into view and then expand this and bring it into view
367 * @param {Object} [params]
376 * @augments wp.customize.Class
378 api.Section = Container.extend({
384 * @param {Array} options
386 initialize: function ( id, options ) {
388 Container.prototype.initialize.call( section, id, options );
391 section.panel = new api.Value();
392 section.panel.bind( function ( id ) {
393 $( section.container ).toggleClass( 'control-subsection', !! id );
395 section.panel.set( section.params.panel || '' );
396 api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
399 section.deferred.embedded.done( function () {
405 * Embed the container in the DOM when any parent panel is ready.
410 var section = this, inject;
412 // Watch for changes to the panel state
413 inject = function ( panelId ) {
416 // The panel has been supplied, so wait until the panel object is registered
417 api.panel( panelId, function ( panel ) {
418 // The panel has been registered, wait for it to become ready/initialized
419 panel.deferred.embedded.done( function () {
420 parentContainer = panel.container.find( 'ul:first' );
421 if ( ! section.container.parent().is( parentContainer ) ) {
422 parentContainer.append( section.container );
424 section.deferred.embedded.resolve();
428 // There is no panel, so embed the section in the root of the customizer
429 parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
430 if ( ! section.container.parent().is( parentContainer ) ) {
431 parentContainer.append( section.container );
433 section.deferred.embedded.resolve();
436 section.panel.bind( inject );
437 inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
441 * Add behaviors for the accordion section.
445 attachEvents: function () {
448 // Expand/Collapse accordion sections on click.
449 section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
450 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
453 event.preventDefault(); // Keep this AFTER the key filter above
455 if ( section.expanded() ) {
464 * Return whether this section has any active controls.
470 isContextuallyActive: function () {
472 controls = section.controls(),
474 _( controls ).each( function ( control ) {
475 if ( control.active() ) {
479 return ( activeCount !== 0 );
483 * Get the controls that are associated with this section, sorted by their priority Value.
489 controls: function () {
490 return this._children( 'section', 'control' );
494 * Update UI to reflect expanded state.
498 * @param {Boolean} expanded
499 * @param {Object} args
501 onChangeExpanded: function ( expanded, args ) {
503 content = section.container.find( '.accordion-section-content' ),
508 if ( args.unchanged ) {
509 expand = args.completeCallback;
511 expand = function () {
512 content.stop().slideDown( args.duration, args.completeCallback );
513 section.container.addClass( 'open' );
517 if ( ! args.allowMultiple ) {
518 api.section.each( function ( otherSection ) {
519 if ( otherSection !== section ) {
520 otherSection.collapse( { duration: args.duration } );
525 if ( section.panel() ) {
526 api.panel( section.panel() ).expand({
527 duration: args.duration,
528 completeCallback: expand
535 section.container.removeClass( 'open' );
536 content.slideUp( args.duration, args.completeCallback );
542 * wp.customize.ThemesSection
544 * Custom section for themes that functions similarly to a backwards panel,
545 * and also handles the theme-details view rendering and navigation.
548 * @augments wp.customize.Section
549 * @augments wp.customize.Container
551 api.ThemesSection = api.Section.extend({
555 screenshotQueue: null,
556 $window: $( window ),
561 initialize: function () {
562 this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
563 return api.Section.prototype.initialize.apply( this, arguments );
571 section.overlay = section.container.find( '.theme-overlay' );
572 section.template = wp.template( 'customize-themes-details-view' );
574 // Bind global keyboard events.
575 $( 'body' ).on( 'keyup', function( event ) {
576 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
580 // Pressing the right arrow key fires a theme:next event
581 if ( 39 === event.keyCode ) {
585 // Pressing the left arrow key fires a theme:previous event
586 if ( 37 === event.keyCode ) {
587 section.previousTheme();
590 // Pressing the escape key fires a theme:collapse event
591 if ( 27 === event.keyCode ) {
592 section.closeDetails();
596 _.bindAll( this, 'renderScreenshots' );
600 * Override Section.isContextuallyActive method.
602 * Ignore the active states' of the contained theme controls, and just
603 * use the section's own active state instead. This ensures empty search
604 * results for themes to cause the section to become inactive.
610 isContextuallyActive: function () {
611 return this.active();
617 attachEvents: function () {
620 // Expand/Collapse section/panel.
621 section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
622 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
625 event.preventDefault(); // Keep this AFTER the key filter above
627 if ( section.expanded() ) {
634 // Theme navigation in details view.
635 section.container.on( 'click keydown', '.left', function( event ) {
636 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
640 event.preventDefault(); // Keep this AFTER the key filter above
642 section.previousTheme();
645 section.container.on( 'click keydown', '.right', function( event ) {
646 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
650 event.preventDefault(); // Keep this AFTER the key filter above
655 section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
656 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
660 event.preventDefault(); // Keep this AFTER the key filter above
662 section.closeDetails();
665 var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
666 section.container.on( 'input', '#themes-filter', function( event ) {
668 term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
669 controls = section.controls();
671 _.each( controls, function( control ) {
672 control.filter( term );
677 // Update theme count.
678 count = section.container.find( 'li.customize-control:visible' ).length;
679 section.container.find( '.theme-count' ).text( count );
682 // Pre-load the first 3 theme screenshots.
683 api.bind( 'ready', function () {
684 _.each( section.controls().slice( 0, 3 ), function ( control ) {
685 var img, src = control.params.theme.screenshot[0];
695 * Update UI to reflect expanded state
699 * @param {Boolean} expanded
700 * @param {Object} args
701 * @param {Boolean} args.unchanged
702 * @param {Callback} args.completeCallback
704 onChangeExpanded: function ( expanded, args ) {
706 // Immediately call the complete callback if there were no changes
707 if ( args.unchanged ) {
708 if ( args.completeCallback ) {
709 args.completeCallback();
714 // Note: there is a second argument 'args' passed
715 var position, scroll,
717 section = panel.container.closest( '.accordion-section' ),
718 overlay = section.closest( '.wp-full-overlay' ),
719 container = section.closest( '.wp-full-overlay-sidebar-content' ),
720 siblings = container.find( '.open' ),
721 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
722 customizeBtn = section.find( '.customize-theme' ),
723 changeBtn = section.find( '.change-theme' ),
724 content = section.find( '.control-panel-content' );
728 // Collapse any sibling sections/panels
729 api.section.each( function ( otherSection ) {
730 if ( otherSection !== panel ) {
731 otherSection.collapse( { duration: args.duration } );
734 api.panel.each( function ( otherPanel ) {
735 otherPanel.collapse( { duration: 0 } );
738 content.show( 0, function() {
739 position = content.offset().top;
740 scroll = container.scrollTop();
741 content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
742 section.addClass( 'current-panel' );
743 overlay.addClass( 'in-themes-panel' );
744 container.scrollTop( 0 );
745 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
746 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
747 if ( args.completeCallback ) {
748 args.completeCallback();
751 topPanel.attr( 'tabindex', '-1' );
752 changeBtn.attr( 'tabindex', '-1' );
753 customizeBtn.focus();
755 siblings.removeClass( 'open' );
756 section.removeClass( 'current-panel' );
757 overlay.removeClass( 'in-themes-panel' );
758 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
759 content.delay( 180 ).hide( 0, function() {
760 content.css( 'margin-top', 'inherit' ); // Reset
761 if ( args.completeCallback ) {
762 args.completeCallback();
765 topPanel.attr( 'tabindex', '0' );
766 customizeBtn.attr( 'tabindex', '0' );
768 container.scrollTop( 0 );
773 * Render control's screenshot if the control comes into view.
777 renderScreenshots: function( ) {
780 // Fill queue initially.
781 if ( section.screenshotQueue === null ) {
782 section.screenshotQueue = section.controls();
785 // Are all screenshots rendered?
786 if ( ! section.screenshotQueue.length ) {
790 section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
791 var $imageWrapper = control.container.find( '.theme-screenshot' ),
792 $image = $imageWrapper.find( 'img' );
794 if ( ! $image.length ) {
798 if ( $image.is( ':hidden' ) ) {
802 // Based on unveil.js.
803 var wt = section.$window.scrollTop(),
804 wb = wt + section.$window.height(),
805 et = $image.offset().top,
806 ih = $imageWrapper.height(),
809 inView = eb >= wt - threshold && et <= wb + threshold;
812 control.container.trigger( 'render-screenshot' );
815 // If the image is in view return false so it's cleared from the queue.
821 * Advance the modal to the next theme.
825 nextTheme: function () {
827 if ( section.getNextTheme() ) {
828 section.showDetails( section.getNextTheme(), function() {
829 section.overlay.find( '.right' ).focus();
835 * Get the next theme model.
839 getNextTheme: function () {
841 control = api.control( 'theme_' + this.currentTheme );
842 next = control.container.next( 'li.customize-control-theme' );
843 if ( ! next.length ) {
846 next = next[0].id.replace( 'customize-control-', '' );
847 control = api.control( next );
849 return control.params.theme;
853 * Advance the modal to the previous theme.
857 previousTheme: function () {
859 if ( section.getPreviousTheme() ) {
860 section.showDetails( section.getPreviousTheme(), function() {
861 section.overlay.find( '.left' ).focus();
867 * Get the previous theme model.
871 getPreviousTheme: function () {
872 var control, previous;
873 control = api.control( 'theme_' + this.currentTheme );
874 previous = control.container.prev( 'li.customize-control-theme' );
875 if ( ! previous.length ) {
878 previous = previous[0].id.replace( 'customize-control-', '' );
879 control = api.control( previous );
881 return control.params.theme;
885 * Disable buttons when we're viewing the first or last theme.
889 updateLimits: function () {
890 if ( ! this.getNextTheme() ) {
891 this.overlay.find( '.right' ).addClass( 'disabled' );
893 if ( ! this.getPreviousTheme() ) {
894 this.overlay.find( '.left' ).addClass( 'disabled' );
899 * Render & show the theme details for a given theme model.
903 * @param {Object} theme
905 showDetails: function ( theme, callback ) {
907 callback = callback || function(){};
908 section.currentTheme = theme.id;
909 section.overlay.html( section.template( theme ) )
912 $( 'body' ).addClass( 'modal-open' );
913 section.containFocus( section.overlay );
914 section.updateLimits();
919 * Close the theme details modal.
923 closeDetails: function () {
924 $( 'body' ).removeClass( 'modal-open' );
925 this.overlay.fadeOut( 'fast' );
926 api.control( 'theme_' + this.currentTheme ).focus();
930 * Keep tab focus within the theme details modal.
934 containFocus: function( el ) {
937 el.on( 'keydown', function( event ) {
939 // Return if it's not the tab key
940 // When navigating with prev/next focus is already handled
941 if ( 9 !== event.keyCode ) {
945 // uses jQuery UI to get the tabbable elements
946 tabbables = $( ':tabbable', el );
948 // Keep focus within the overlay
949 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
950 tabbables.first().focus();
952 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
953 tabbables.last().focus();
964 * @augments wp.customize.Class
966 api.Panel = Container.extend({
971 * @param {Object} options
973 initialize: function ( id, options ) {
975 Container.prototype.initialize.call( panel, id, options );
977 panel.deferred.embedded.done( function () {
983 * Embed the container in the DOM when any parent panel is ready.
989 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
991 if ( ! panel.container.parent().is( parentContainer ) ) {
992 parentContainer.append( panel.container );
994 panel.deferred.embedded.resolve();
1000 attachEvents: function () {
1001 var meta, panel = this;
1003 // Expand/Collapse accordion sections on click.
1004 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
1005 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1008 event.preventDefault(); // Keep this AFTER the key filter above
1010 if ( ! panel.expanded() ) {
1015 meta = panel.container.find( '.panel-meta:first' );
1017 meta.find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
1018 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1021 event.preventDefault(); // Keep this AFTER the key filter above
1023 if ( meta.hasClass( 'cannot-expand' ) ) {
1027 var content = meta.find( '.accordion-section-content:first' );
1028 if ( meta.hasClass( 'open' ) ) {
1029 meta.toggleClass( 'open' );
1030 content.slideUp( panel.defaultExpandedArguments.duration );
1032 content.slideDown( panel.defaultExpandedArguments.duration );
1033 meta.toggleClass( 'open' );
1040 * Get the sections that are associated with this panel, sorted by their priority Value.
1046 sections: function () {
1047 return this._children( 'panel', 'section' );
1051 * Return whether this panel has any active sections.
1055 * @returns {boolean}
1057 isContextuallyActive: function () {
1059 sections = panel.sections(),
1061 _( sections ).each( function ( section ) {
1062 if ( section.active() && section.isContextuallyActive() ) {
1066 return ( activeCount !== 0 );
1070 * Update UI to reflect expanded state
1074 * @param {Boolean} expanded
1075 * @param {Object} args
1076 * @param {Boolean} args.unchanged
1077 * @param {Callback} args.completeCallback
1079 onChangeExpanded: function ( expanded, args ) {
1081 // Immediately call the complete callback if there were no changes
1082 if ( args.unchanged ) {
1083 if ( args.completeCallback ) {
1084 args.completeCallback();
1089 // Note: there is a second argument 'args' passed
1090 var position, scroll,
1092 section = panel.container.closest( '.accordion-section' ),
1093 overlay = section.closest( '.wp-full-overlay' ),
1094 container = section.closest( '.wp-full-overlay-sidebar-content' ),
1095 siblings = container.find( '.open' ),
1096 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
1097 backBtn = overlay.find( '.control-panel-back' ),
1098 panelTitle = section.find( '.accordion-section-title' ).first(),
1099 content = section.find( '.control-panel-content' );
1103 // Collapse any sibling sections/panels
1104 api.section.each( function ( section ) {
1105 if ( ! section.panel() ) {
1106 section.collapse( { duration: 0 } );
1109 api.panel.each( function ( otherPanel ) {
1110 if ( panel !== otherPanel ) {
1111 otherPanel.collapse( { duration: 0 } );
1115 content.show( 0, function() {
1116 content.parent().show();
1117 position = content.offset().top;
1118 scroll = container.scrollTop();
1119 content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
1120 section.addClass( 'current-panel' );
1121 overlay.addClass( 'in-sub-panel' );
1122 container.scrollTop( 0 );
1123 if ( args.completeCallback ) {
1124 args.completeCallback();
1127 topPanel.attr( 'tabindex', '-1' );
1128 backBtn.attr( 'tabindex', '0' );
1131 siblings.removeClass( 'open' );
1132 section.removeClass( 'current-panel' );
1133 overlay.removeClass( 'in-sub-panel' );
1134 content.delay( 180 ).hide( 0, function() {
1135 content.css( 'margin-top', 'inherit' ); // Reset
1136 if ( args.completeCallback ) {
1137 args.completeCallback();
1140 topPanel.attr( 'tabindex', '0' );
1141 backBtn.attr( 'tabindex', '-1' );
1143 container.scrollTop( 0 );
1149 * A Customizer Control.
1151 * A control provides a UI element that allows a user to modify a Customizer Setting.
1153 * @see PHP class WP_Customize_Control.
1156 * @augments wp.customize.Class
1158 * @param {string} id Unique identifier for the control instance.
1159 * @param {object} options Options hash for the control instance.
1160 * @param {object} options.params
1161 * @param {object} options.params.type Type of control (e.g. text, radio, dropdown-pages, etc.)
1162 * @param {string} options.params.content The HTML content for the control.
1163 * @param {string} options.params.priority Order of priority to show the control within the section.
1164 * @param {string} options.params.active
1165 * @param {string} options.params.section
1166 * @param {string} options.params.label
1167 * @param {string} options.params.description
1168 * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
1170 api.Control = api.Class.extend({
1171 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
1173 initialize: function( id, options ) {
1175 nodes, radios, settings;
1177 control.params = {};
1178 $.extend( control, options || {} );
1180 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
1181 control.templateSelector = 'customize-control-' + control.params.type + '-content';
1182 control.container = control.params.content ? $( control.params.content ) : $( control.selector );
1184 control.deferred = {
1185 embedded: new $.Deferred()
1187 control.section = new api.Value();
1188 control.priority = new api.Value();
1189 control.active = new api.Value();
1190 control.activeArgumentsQueue = [];
1192 control.elements = [];
1194 nodes = control.container.find('[data-customize-setting-link]');
1197 nodes.each( function() {
1198 var node = $( this ),
1201 if ( node.is( ':radio' ) ) {
1202 name = node.prop( 'name' );
1203 if ( radios[ name ] ) {
1207 radios[ name ] = true;
1208 node = nodes.filter( '[name="' + name + '"]' );
1211 api( node.data( 'customizeSettingLink' ), function( setting ) {
1212 var element = new api.Element( node );
1213 control.elements.push( element );
1214 element.sync( setting );
1215 element.set( setting() );
1219 control.active.bind( function ( active ) {
1220 var args = control.activeArgumentsQueue.shift();
1221 args = $.extend( {}, control.defaultActiveArguments, args );
1222 control.onChangeActive( active, args );
1225 control.section.set( control.params.section );
1226 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
1227 control.active.set( control.params.active );
1229 api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
1231 // Associate this control with its settings when they are created
1232 settings = $.map( control.params.settings, function( value ) {
1235 api.apply( api, settings.concat( function () {
1238 control.settings = {};
1239 for ( key in control.params.settings ) {
1240 control.settings[ key ] = api( control.params.settings[ key ] );
1243 control.setting = control.settings['default'] || null;
1248 control.deferred.embedded.done( function () {
1254 * Embed the control into the page.
1256 embed: function () {
1260 // Watch for changes to the section state
1261 inject = function ( sectionId ) {
1262 var parentContainer;
1263 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the frontend
1266 // Wait for the section to be registered
1267 api.section( sectionId, function ( section ) {
1268 // Wait for the section to be ready/initialized
1269 section.deferred.embedded.done( function () {
1270 parentContainer = section.container.find( 'ul:first' );
1271 if ( ! control.container.parent().is( parentContainer ) ) {
1272 parentContainer.append( control.container );
1273 control.renderContent();
1275 control.deferred.embedded.resolve();
1279 control.section.bind( inject );
1280 inject( control.section.get() );
1284 * Triggered when the control's markup has been injected into the DOM.
1288 ready: function() {},
1291 * Normal controls do not expand, so just expand its parent
1293 * @param {Object} [params]
1295 expand: function ( params ) {
1296 api.section( this.section() ).expand( params );
1300 * Bring the containing section and panel into view and then
1301 * this control into view, focusing on the first input.
1306 * Update UI in response to a change in the control's active state.
1307 * This does not change the active state, it merely handles the behavior
1308 * for when it does change.
1312 * @param {Boolean} active
1313 * @param {Object} args
1314 * @param {Number} args.duration
1315 * @param {Callback} args.completeCallback
1317 onChangeActive: function ( active, args ) {
1318 if ( ! $.contains( document, this.container ) ) {
1319 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
1320 this.container.toggle( active );
1321 if ( args.completeCallback ) {
1322 args.completeCallback();
1324 } else if ( active ) {
1325 this.container.slideDown( args.duration, args.completeCallback );
1327 this.container.slideUp( args.duration, args.completeCallback );
1332 * @deprecated 4.1.0 Use this.onChangeActive() instead.
1334 toggle: function ( active ) {
1335 return this.onChangeActive( active, this.defaultActiveArguments );
1339 * Shorthand way to enable the active state.
1343 * @param {Object} [params]
1344 * @returns {Boolean} false if already active
1346 activate: Container.prototype.activate,
1349 * Shorthand way to disable the active state.
1353 * @param {Object} [params]
1354 * @returns {Boolean} false if already inactive
1356 deactivate: Container.prototype.deactivate,
1359 * Re-use _toggleActive from Container class.
1363 _toggleActive: Container.prototype._toggleActive,
1365 dropdownInit: function() {
1367 statuses = this.container.find('.dropdown-status'),
1368 params = this.params,
1369 toggleFreeze = false,
1370 update = function( to ) {
1371 if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
1372 statuses.html( params.statuses[ to ] ).show();
1377 // Support the .dropdown class to open/close complex elements
1378 this.container.on( 'click keydown', '.dropdown', function( event ) {
1379 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1383 event.preventDefault();
1386 control.container.toggleClass('open');
1388 if ( control.container.hasClass('open') )
1389 control.container.parent().parent().find('li.library-selected').focus();
1391 // Don't want to fire focus and click at same time
1392 toggleFreeze = true;
1393 setTimeout(function () {
1394 toggleFreeze = false;
1398 this.setting.bind( update );
1399 update( this.setting() );
1403 * Render the control from its JS template, if it exists.
1405 * The control's container must already exist in the DOM.
1409 renderContent: function () {
1413 // Replace the container element's content with the control.
1414 if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
1415 template = wp.template( control.templateSelector );
1416 if ( template && control.container ) {
1417 control.container.html( template( control.params ) );
1424 * A colorpicker control.
1427 * @augments wp.customize.Control
1428 * @augments wp.customize.Class
1430 api.ColorControl = api.Control.extend({
1433 picker = this.container.find('.color-picker-hex');
1435 picker.val( control.setting() ).wpColorPicker({
1436 change: function() {
1437 control.setting.set( picker.wpColorPicker('color') );
1440 control.setting.set( false );
1444 this.setting.bind( function ( value ) {
1445 picker.val( value );
1446 picker.wpColorPicker( 'color', value );
1452 * A control that implements the media modal.
1455 * @augments wp.customize.Control
1456 * @augments wp.customize.Class
1458 api.MediaControl = api.Control.extend({
1461 * When the control's DOM structure is ready,
1462 * set up internal event bindings.
1466 // Shortcut so that we don't have to use _.bind every time we add a callback.
1467 _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
1469 // Bind events, with delegation to facilitate re-rendering.
1470 control.container.on( 'click keydown', '.upload-button', control.openFrame );
1471 control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
1472 control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1473 control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1474 control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
1475 control.container.on( 'click keydown', '.remove-button', control.removeFile );
1476 control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
1478 // Resize the player controls when it becomes visible (ie when section is expanded)
1479 api.section( control.section() ).container
1480 .on( 'expanded', function() {
1481 if ( control.player ) {
1482 control.player.setControlsSize();
1485 .on( 'collapsed', function() {
1486 control.pausePlayer();
1489 // Re-render whenever the control's setting changes.
1490 control.setting.bind( function () { control.renderContent(); } );
1493 pausePlayer: function () {
1494 this.player && this.player.pause();
1497 cleanupPlayer: function () {
1498 this.player && wp.media.mixin.removePlayer( this.player );
1502 * Open the media modal.
1504 openFrame: function( event ) {
1505 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1509 event.preventDefault();
1511 if ( ! this.frame ) {
1519 * Create a media modal select frame, and store it so the instance can be reused when needed.
1521 initFrame: function() {
1522 this.frame = wp.media({
1524 text: this.params.button_labels.frame_button
1527 new wp.media.controller.Library({
1528 title: this.params.button_labels.frame_title,
1529 library: wp.media.query({ type: this.params.mime_type }),
1536 // When a file is selected, run a callback.
1537 this.frame.on( 'select', this.select );
1541 * Callback handler for when an attachment is selected in the media modal.
1542 * Gets the selected image information, and sets it within the control.
1544 select: function() {
1545 // Get the attachment from the modal frame.
1547 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1548 mejsSettings = window._wpmejsSettings || {};
1550 this.params.attachment = attachment;
1552 // Set the Customizer setting; the callback takes care of rendering.
1553 this.setting( attachment.id );
1554 node = this.container.find( 'audio, video' ).get(0);
1556 // Initialize audio/video previews.
1558 this.player = new MediaElementPlayer( node, mejsSettings );
1560 this.cleanupPlayer();
1565 * Reset the setting to the default value.
1567 restoreDefault: function( event ) {
1568 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1571 event.preventDefault();
1573 this.params.attachment = this.params.defaultAttachment;
1574 this.setting( this.params.defaultAttachment.url );
1578 * Called when the "Remove" link is clicked. Empties the setting.
1580 * @param {object} event jQuery Event object
1582 removeFile: function( event ) {
1583 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1586 event.preventDefault();
1588 this.params.attachment = {};
1590 this.renderContent(); // Not bound to setting change when emptying.
1595 * An upload control, which utilizes the media modal.
1598 * @augments wp.customize.MediaControl
1599 * @augments wp.customize.Control
1600 * @augments wp.customize.Class
1602 api.UploadControl = api.MediaControl.extend({
1605 * Callback handler for when an attachment is selected in the media modal.
1606 * Gets the selected image information, and sets it within the control.
1608 select: function() {
1609 // Get the attachment from the modal frame.
1611 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1612 mejsSettings = window._wpmejsSettings || {};
1614 this.params.attachment = attachment;
1616 // Set the Customizer setting; the callback takes care of rendering.
1617 this.setting( attachment.url );
1618 node = this.container.find( 'audio, video' ).get(0);
1620 // Initialize audio/video previews.
1622 this.player = new MediaElementPlayer( node, mejsSettings );
1624 this.cleanupPlayer();
1629 success: function() {},
1632 removerVisibility: function() {}
1636 * A control for uploading images.
1638 * This control no longer needs to do anything more
1639 * than what the upload control does in JS.
1642 * @augments wp.customize.UploadControl
1643 * @augments wp.customize.MediaControl
1644 * @augments wp.customize.Control
1645 * @augments wp.customize.Class
1647 api.ImageControl = api.UploadControl.extend({
1649 thumbnailSrc: function() {}
1653 * A control for uploading background images.
1656 * @augments wp.customize.UploadControl
1657 * @augments wp.customize.MediaControl
1658 * @augments wp.customize.Control
1659 * @augments wp.customize.Class
1661 api.BackgroundControl = api.UploadControl.extend({
1664 * When the control's DOM structure is ready,
1665 * set up internal event bindings.
1668 api.UploadControl.prototype.ready.apply( this, arguments );
1672 * Callback handler for when an attachment is selected in the media modal.
1673 * Does an additional AJAX request for setting the background context.
1675 select: function() {
1676 api.UploadControl.prototype.select.apply( this, arguments );
1678 wp.ajax.post( 'custom-background-add', {
1679 nonce: _wpCustomizeBackground.nonces.add,
1681 theme: api.settings.theme.stylesheet,
1682 attachment_id: this.params.attachment.id
1689 * @augments wp.customize.Control
1690 * @augments wp.customize.Class
1692 api.HeaderControl = api.Control.extend({
1694 this.btnRemove = $('#customize-control-header_image .actions .remove');
1695 this.btnNew = $('#customize-control-header_image .actions .new');
1697 _.bindAll(this, 'openMedia', 'removeImage');
1699 this.btnNew.on( 'click', this.openMedia );
1700 this.btnRemove.on( 'click', this.removeImage );
1702 api.HeaderTool.currentHeader = this.getInitialHeaderImage();
1704 new api.HeaderTool.CurrentView({
1705 model: api.HeaderTool.currentHeader,
1706 el: '#customize-control-header_image .current .container'
1709 new api.HeaderTool.ChoiceListView({
1710 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
1711 el: '#customize-control-header_image .choices .uploaded .list'
1714 new api.HeaderTool.ChoiceListView({
1715 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
1716 el: '#customize-control-header_image .choices .default .list'
1719 api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
1720 api.HeaderTool.UploadsList,
1721 api.HeaderTool.DefaultsList
1726 * Returns a new instance of api.HeaderTool.ImageModel based on the currently
1727 * saved header image (if any).
1731 * @returns {Object} Options
1733 getInitialHeaderImage: function() {
1734 if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
1735 return new api.HeaderTool.ImageModel();
1738 // Get the matching uploaded image object.
1739 var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
1740 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
1742 // Fall back to raw current header image.
1743 if ( ! currentHeaderObject ) {
1744 currentHeaderObject = {
1745 url: api.get().header_image,
1746 thumbnail_url: api.get().header_image,
1747 attachment_id: api.get().header_image_data.attachment_id
1751 return new api.HeaderTool.ImageModel({
1752 header: currentHeaderObject,
1753 choice: currentHeaderObject.url.split( '/' ).pop()
1758 * Returns a set of options, computed from the attached image data and
1759 * theme-specific data, to be fed to the imgAreaSelect plugin in
1760 * wp.media.view.Cropper.
1762 * @param {wp.media.model.Attachment} attachment
1763 * @param {wp.media.controller.Cropper} controller
1764 * @returns {Object} Options
1766 calculateImageSelectOptions: function(attachment, controller) {
1767 var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
1768 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
1769 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
1770 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
1771 ratio, xImg, yImg, realHeight, realWidth,
1774 realWidth = attachment.get('width');
1775 realHeight = attachment.get('height');
1777 this.headerImage = new api.HeaderTool.ImageModel();
1778 this.headerImage.set({
1781 themeFlexWidth: flexWidth,
1782 themeFlexHeight: flexHeight,
1783 imageWidth: realWidth,
1784 imageHeight: realHeight
1787 controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
1789 ratio = xInit / yInit;
1793 if ( xImg / yImg > ratio ) {
1795 xInit = yInit * ratio;
1798 yInit = xInit / ratio;
1801 imgSelectOptions = {
1806 imageWidth: realWidth,
1807 imageHeight: realHeight,
1814 if (flexHeight === false && flexWidth === false) {
1815 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
1817 if (flexHeight === false ) {
1818 imgSelectOptions.maxHeight = yInit;
1820 if (flexWidth === false ) {
1821 imgSelectOptions.maxWidth = xInit;
1824 return imgSelectOptions;
1828 * Sets up and opens the Media Manager in order to select an image.
1829 * Depending on both the size of the image and the properties of the
1830 * current theme, a cropping step after selection may be required or
1833 * @param {event} event
1835 openMedia: function(event) {
1836 var l10n = _wpMediaViewsL10n;
1838 event.preventDefault();
1840 this.frame = wp.media({
1842 text: l10n.selectAndCrop,
1846 new wp.media.controller.Library({
1847 title: l10n.chooseImage,
1848 library: wp.media.query({ type: 'image' }),
1852 suggestedWidth: _wpCustomizeHeader.data.width,
1853 suggestedHeight: _wpCustomizeHeader.data.height
1855 new wp.media.controller.Cropper({
1856 imgSelectOptions: this.calculateImageSelectOptions
1861 this.frame.on('select', this.onSelect, this);
1862 this.frame.on('cropped', this.onCropped, this);
1863 this.frame.on('skippedcrop', this.onSkippedCrop, this);
1869 * After an image is selected in the media modal,
1870 * switch to the cropper state.
1872 onSelect: function() {
1873 this.frame.setState('cropper');
1877 * After the image has been cropped, apply the cropped image data to the setting.
1879 * @param {object} croppedImage Cropped attachment data.
1881 onCropped: function(croppedImage) {
1882 var url = croppedImage.post_content,
1883 attachmentId = croppedImage.attachment_id,
1884 w = croppedImage.width,
1885 h = croppedImage.height;
1886 this.setImageFromURL(url, attachmentId, w, h);
1890 * If cropping was skipped, apply the image data directly to the setting.
1892 * @param {object} selection
1894 onSkippedCrop: function(selection) {
1895 var url = selection.get('url'),
1896 w = selection.get('width'),
1897 h = selection.get('height');
1898 this.setImageFromURL(url, selection.id, w, h);
1902 * Creates a new wp.customize.HeaderTool.ImageModel from provided
1903 * header image data and inserts it into the user-uploaded headers
1906 * @param {String} url
1907 * @param {Number} attachmentId
1908 * @param {Number} width
1909 * @param {Number} height
1911 setImageFromURL: function(url, attachmentId, width, height) {
1912 var choice, data = {};
1915 data.thumbnail_url = url;
1916 data.timestamp = _.now();
1919 data.attachment_id = attachmentId;
1927 data.height = height;
1930 choice = new api.HeaderTool.ImageModel({
1932 choice: url.split('/').pop()
1934 api.HeaderTool.UploadsList.add(choice);
1935 api.HeaderTool.currentHeader.set(choice.toJSON());
1937 choice.importImage();
1941 * Triggers the necessary events to deselect an image which was set as
1942 * the currently selected one.
1944 removeImage: function() {
1945 api.HeaderTool.currentHeader.trigger('hide');
1946 api.HeaderTool.CombinedList.trigger('control:removeImage');
1952 * wp.customize.ThemeControl
1955 * @augments wp.customize.Control
1956 * @augments wp.customize.Class
1958 api.ThemeControl = api.Control.extend({
1964 * Defer rendering the theme control until the section is displayed.
1968 renderContent: function () {
1970 renderContentArgs = arguments;
1972 api.section( control.section(), function( section ) {
1973 if ( section.expanded() ) {
1974 api.Control.prototype.renderContent.apply( control, renderContentArgs );
1975 control.isRendered = true;
1977 section.expanded.bind( function( expanded ) {
1978 if ( expanded && ! control.isRendered ) {
1979 api.Control.prototype.renderContent.apply( control, renderContentArgs );
1980 control.isRendered = true;
1993 control.container.on( 'touchmove', '.theme', function() {
1994 control.touchDrag = true;
1997 // Bind details view trigger.
1998 control.container.on( 'click keydown touchend', '.theme', function( event ) {
1999 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2003 // Bail if the user scrolled on a touch device.
2004 if ( control.touchDrag === true ) {
2005 return control.touchDrag = false;
2008 // Prevent the modal from showing when the user clicks the action button.
2009 if ( $( event.target ).is( '.theme-actions .button' ) ) {
2013 var previewUrl = $( this ).data( 'previewUrl' );
2015 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
2017 window.parent.location = previewUrl;
2020 control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
2021 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2025 event.preventDefault(); // Keep this AFTER the key filter above
2027 api.section( control.section() ).showDetails( control.params.theme );
2030 control.container.on( 'render-screenshot', function() {
2031 var $screenshot = $( this ).find( 'img' ),
2032 source = $screenshot.data( 'src' );
2035 $screenshot.attr( 'src', source );
2041 * Show or hide the theme based on the presence of the term in the title, description, and author.
2045 filter: function( term ) {
2047 haystack = control.params.theme.name + ' ' +
2048 control.params.theme.description + ' ' +
2049 control.params.theme.tags + ' ' +
2050 control.params.theme.author;
2051 haystack = haystack.toLowerCase().replace( '-', ' ' );
2052 if ( -1 !== haystack.search( term ) ) {
2055 control.deactivate();
2060 // Change objects contained within the main customize object to Settings.
2061 api.defaultConstructor = api.Setting;
2063 // Create the collections for Controls, Sections and Panels.
2064 api.control = new api.Values({ defaultConstructor: api.Control });
2065 api.section = new api.Values({ defaultConstructor: api.Section });
2066 api.panel = new api.Values({ defaultConstructor: api.Panel });
2070 * @augments wp.customize.Messenger
2071 * @augments wp.customize.Class
2072 * @mixes wp.customize.Events
2074 api.PreviewFrame = api.Messenger.extend({
2077 initialize: function( params, options ) {
2078 var deferred = $.Deferred();
2080 // This is the promise object.
2081 deferred.promise( this );
2083 this.container = params.container;
2084 this.signature = params.signature;
2086 $.extend( params, { channel: api.PreviewFrame.uuid() });
2088 api.Messenger.prototype.initialize.call( this, params, options );
2090 this.add( 'previewUrl', params.previewUrl );
2092 this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
2094 this.run( deferred );
2097 run: function( deferred ) {
2102 if ( this._ready ) {
2103 this.unbind( 'ready', this._ready );
2106 this._ready = function() {
2110 deferred.resolveWith( self );
2114 this.bind( 'ready', this._ready );
2116 this.bind( 'ready', function ( data ) {
2118 this.container.addClass( 'iframe-ready' );
2125 * Walk over all panels, sections, and controls and set their
2126 * respective active states to true if the preview explicitly
2127 * indicates as such.
2130 panel: data.activePanels,
2131 section: data.activeSections,
2132 control: data.activeControls
2134 _( constructs ).each( function ( activeConstructs, type ) {
2135 api[ type ].each( function ( construct, id ) {
2136 var active = !! ( activeConstructs && activeConstructs[ id ] );
2137 construct.active( active );
2142 this.request = $.ajax( this.previewUrl(), {
2146 withCredentials: true
2150 this.request.fail( function() {
2151 deferred.rejectWith( self, [ 'request failure' ] );
2154 this.request.done( function( response ) {
2155 var location = self.request.getResponseHeader('Location'),
2156 signature = self.signature,
2159 // Check if the location response header differs from the current URL.
2160 // If so, the request was redirected; try loading the requested page.
2161 if ( location && location !== self.previewUrl() ) {
2162 deferred.rejectWith( self, [ 'redirect', location ] );
2166 // Check if the user is not logged in.
2167 if ( '0' === response ) {
2168 self.login( deferred );
2172 // Check for cheaters.
2173 if ( '-1' === response ) {
2174 deferred.rejectWith( self, [ 'cheatin' ] );
2178 // Check for a signature in the request.
2179 index = response.lastIndexOf( signature );
2180 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
2181 deferred.rejectWith( self, [ 'unsigned' ] );
2185 // Strip the signature from the request.
2186 response = response.slice( 0, index ) + response.slice( index + signature.length );
2188 // Create the iframe and inject the html content.
2189 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
2191 // Bind load event after the iframe has been added to the page;
2192 // otherwise it will fire when injected into the DOM.
2193 self.iframe.one( 'load', function() {
2197 deferred.resolveWith( self );
2199 setTimeout( function() {
2200 deferred.rejectWith( self, [ 'ready timeout' ] );
2201 }, self.sensitivity );
2205 self.targetWindow( self.iframe[0].contentWindow );
2207 self.targetWindow().document.open();
2208 self.targetWindow().document.write( response );
2209 self.targetWindow().document.close();
2213 login: function( deferred ) {
2217 reject = function() {
2218 deferred.rejectWith( self, [ 'logged out' ] );
2221 if ( this.triedLogin ) {
2225 // Check if we have an admin cookie.
2226 $.get( api.settings.url.ajax, {
2228 }).fail( reject ).done( function( response ) {
2231 if ( '1' !== response ) {
2235 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
2236 iframe.appendTo( self.container );
2237 iframe.load( function() {
2238 self.triedLogin = true;
2241 self.run( deferred );
2246 destroy: function() {
2247 api.Messenger.prototype.destroy.call( this );
2248 this.request.abort();
2251 this.iframe.remove();
2253 delete this.request;
2255 delete this.targetWindow;
2262 * Create a universally unique identifier.
2266 api.PreviewFrame.uuid = function() {
2267 return 'preview-' + uuid++;
2272 * Set the document title of the customizer.
2276 * @param {string} documentTitle
2278 api.setDocumentTitle = function ( documentTitle ) {
2280 tmpl = api.settings.documentTitleTmpl;
2281 title = tmpl.replace( '%s', documentTitle );
2282 document.title = title;
2283 api.trigger( 'title', title );
2288 * @augments wp.customize.Messenger
2289 * @augments wp.customize.Class
2290 * @mixes wp.customize.Events
2292 api.Previewer = api.Messenger.extend({
2297 * - container - a selector or jQuery element
2298 * - previewUrl - the URL of preview frame
2300 initialize: function( params, options ) {
2302 rscheme = /^https?/;
2304 $.extend( this, options || {} );
2306 active: $.Deferred()
2310 * Wrap this.refresh to prevent it from hammering the servers:
2312 * If refresh is called once and no other refresh requests are
2313 * loading, trigger the request immediately.
2315 * If refresh is called while another refresh request is loading,
2316 * debounce the refresh requests:
2317 * 1. Stop the loading request (as it is instantly outdated).
2318 * 2. Trigger the new request once refresh hasn't been called for
2319 * self.refreshBuffer milliseconds.
2321 this.refresh = (function( self ) {
2322 var refresh = self.refresh,
2323 callback = function() {
2325 refresh.call( self );
2330 if ( typeof timeout !== 'number' ) {
2331 if ( self.loading ) {
2338 clearTimeout( timeout );
2339 timeout = setTimeout( callback, self.refreshBuffer );
2343 this.container = api.ensure( params.container );
2344 this.allowedUrls = params.allowedUrls;
2345 this.signature = params.signature;
2347 params.url = window.location.href;
2349 api.Messenger.prototype.initialize.call( this, params );
2351 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
2352 var match = to.match( rscheme );
2353 return match ? match[0] : '';
2356 // Limit the URL to internal, front-end links.
2358 // If the frontend and the admin are served from the same domain, load the
2359 // preview over ssl if the Customizer is being loaded over ssl. This avoids
2360 // insecure content warnings. This is not attempted if the admin and frontend
2361 // are on different domains to avoid the case where the frontend doesn't have
2364 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
2367 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
2368 // Strip hashes and query strings before testing.
2369 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
2372 // Attempt to match the URL to the control frame's scheme
2373 // and check if it's allowed. If not, try the original URL.
2374 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
2375 $.each( self.allowedUrls, function( i, allowed ) {
2378 allowed = allowed.replace( /\/+$/, '' );
2379 path = url.replace( allowed, '' );
2381 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
2390 // If we found a matching result, return it. If not, bail.
2391 return result ? result : null;
2394 // Refresh the preview when the URL is changed (but not yet).
2395 this.previewUrl.bind( this.refresh );
2398 this.bind( 'scroll', function( distance ) {
2399 this.scroll = distance;
2402 // Update the URL when the iframe sends a URL message.
2403 this.bind( 'url', this.previewUrl );
2405 // Update the document title when the preview changes.
2406 this.bind( 'documentTitle', function ( title ) {
2407 api.setDocumentTitle( title );
2411 query: function() {},
2414 if ( this.loading ) {
2415 this.loading.destroy();
2416 delete this.loading;
2420 refresh: function() {
2423 // Display loading indicator
2424 this.send( 'loading-initiated' );
2428 this.loading = new api.PreviewFrame({
2430 previewUrl: this.previewUrl(),
2431 query: this.query() || {},
2432 container: this.container,
2433 signature: this.signature
2436 this.loading.done( function() {
2437 // 'this' is the loading frame
2438 this.bind( 'synced', function() {
2440 self.preview.destroy();
2441 self.preview = this;
2442 delete self.loading;
2444 self.targetWindow( this.targetWindow() );
2445 self.channel( this.channel() );
2447 self.deferred.active.resolve();
2448 self.send( 'active' );
2451 this.send( 'sync', {
2452 scroll: self.scroll,
2457 this.loading.fail( function( reason, location ) {
2458 self.send( 'loading-failed' );
2459 if ( 'redirect' === reason && location ) {
2460 self.previewUrl( location );
2463 if ( 'logged out' === reason ) {
2464 if ( self.preview ) {
2465 self.preview.destroy();
2466 delete self.preview;
2469 self.login().done( self.refresh );
2472 if ( 'cheatin' === reason ) {
2479 var previewer = this,
2480 deferred, messenger, iframe;
2485 deferred = $.Deferred();
2486 this._login = deferred.promise();
2488 messenger = new api.Messenger({
2490 url: api.settings.url.login
2493 iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
2495 messenger.targetWindow( iframe[0].contentWindow );
2497 messenger.bind( 'login', function () {
2498 var refreshNonces = previewer.refreshNonces();
2500 refreshNonces.always( function() {
2502 messenger.destroy();
2503 delete previewer._login;
2506 refreshNonces.done( function() {
2510 refreshNonces.fail( function() {
2511 previewer.cheatin();
2519 cheatin: function() {
2520 $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
2523 refreshNonces: function() {
2524 var request, deferred = $.Deferred();
2528 request = wp.ajax.post( 'customize_refresh_nonces', {
2530 theme: api.settings.theme.stylesheet
2533 request.done( function( response ) {
2534 api.trigger( 'nonce-refresh', response );
2538 request.fail( function() {
2546 api.controlConstructor = {
2547 color: api.ColorControl,
2548 media: api.MediaControl,
2549 upload: api.UploadControl,
2550 image: api.ImageControl,
2551 header: api.HeaderControl,
2552 background: api.BackgroundControl,
2553 theme: api.ThemeControl
2555 api.panelConstructor = {};
2556 api.sectionConstructor = {
2557 themes: api.ThemesSection
2561 api.settings = window._wpCustomizeSettings;
2562 api.l10n = window._wpCustomizeControlsL10n;
2564 // Check if we can run the Customizer.
2565 if ( ! api.settings ) {
2569 // Redirect to the fallback preview if any incompatibilities are found.
2570 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
2571 return window.location = api.settings.url.fallback;
2573 var parent, topFocus,
2574 body = $( document.body ),
2575 overlay = body.children( '.wp-full-overlay' ),
2576 title = $( '#customize-info .theme-name.site-title' ),
2577 closeBtn = $( '.customize-controls-close' ),
2578 saveBtn = $( '#save' );
2580 // Prevent the form from saving when enter is pressed on an input or select element.
2581 $('#customize-controls').on( 'keydown', function( e ) {
2582 var isEnter = ( 13 === e.which ),
2583 $el = $( e.target );
2585 if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
2590 // Expand/Collapse the main customizer customize info.
2591 $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
2592 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2595 event.preventDefault(); // Keep this AFTER the key filter above
2597 var section = $( this ).parent(),
2598 content = section.find( '.accordion-section-content:first' );
2600 if ( section.hasClass( 'cannot-expand' ) ) {
2604 if ( section.hasClass( 'open' ) ) {
2605 section.toggleClass( 'open' );
2606 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
2608 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
2609 section.toggleClass( 'open' );
2613 // Initialize Previewer
2614 api.previewer = new api.Previewer({
2615 container: '#customize-preview',
2616 form: '#customize-controls',
2617 previewUrl: api.settings.url.preview,
2618 allowedUrls: api.settings.url.allowed,
2619 signature: 'WP_CUSTOMIZER_SIGNATURE'
2622 nonce: api.settings.nonce,
2625 var dirtyCustomized = {};
2626 api.each( function ( value, key ) {
2627 if ( value._dirty ) {
2628 dirtyCustomized[ key ] = value();
2634 theme: api.settings.theme.stylesheet,
2635 customized: JSON.stringify( dirtyCustomized ),
2636 nonce: this.nonce.preview
2642 processing = api.state( 'processing' ),
2643 submitWhenDoneProcessing,
2646 body.addClass( 'saving' );
2648 submit = function () {
2650 query = $.extend( self.query(), {
2651 nonce: self.nonce.save
2653 request = wp.ajax.post( 'customize_save', query );
2655 api.trigger( 'save', request );
2657 request.always( function () {
2658 body.removeClass( 'saving' );
2661 request.fail( function ( response ) {
2662 if ( '0' === response ) {
2663 response = 'not_logged_in';
2664 } else if ( '-1' === response ) {
2665 // Back-compat in case any other check_ajax_referer() call is dying
2666 response = 'invalid_nonce';
2669 if ( 'invalid_nonce' === response ) {
2671 } else if ( 'not_logged_in' === response ) {
2672 self.preview.iframe.hide();
2673 self.login().done( function() {
2675 self.preview.iframe.show();
2678 api.trigger( 'error', response );
2681 request.done( function( response ) {
2682 // Clear setting dirty states
2683 api.each( function ( value ) {
2684 value._dirty = false;
2687 api.trigger( 'saved', response );
2691 if ( 0 === processing() ) {
2694 submitWhenDoneProcessing = function () {
2695 if ( 0 === processing() ) {
2696 api.state.unbind( 'change', submitWhenDoneProcessing );
2700 api.state.bind( 'change', submitWhenDoneProcessing );
2706 // Refresh the nonces if the preview sends updated nonces over.
2707 api.previewer.bind( 'nonce', function( nonce ) {
2708 $.extend( this.nonce, nonce );
2711 // Refresh the nonces if login sends updated nonces over.
2712 api.bind( 'nonce-refresh', function( nonce ) {
2713 $.extend( api.settings.nonce, nonce );
2714 $.extend( api.previewer.nonce, nonce );
2718 $.each( api.settings.settings, function( id, data ) {
2719 api.create( id, id, data.value, {
2720 transport: data.transport,
2721 previewer: api.previewer,
2722 dirty: !! data.dirty
2727 $.each( api.settings.panels, function ( id, data ) {
2728 var constructor = api.panelConstructor[ data.type ] || api.Panel,
2731 panel = new constructor( id, {
2734 api.panel.add( id, panel );
2738 $.each( api.settings.sections, function ( id, data ) {
2739 var constructor = api.sectionConstructor[ data.type ] || api.Section,
2742 section = new constructor( id, {
2745 api.section.add( id, section );
2749 $.each( api.settings.controls, function( id, data ) {
2750 var constructor = api.controlConstructor[ data.type ] || api.Control,
2753 control = new constructor( id, {
2755 previewer: api.previewer
2757 api.control.add( id, control );
2760 // Focus the autofocused element
2761 _.each( [ 'panel', 'section', 'control' ], function ( type ) {
2762 var instance, id = api.settings.autofocus[ type ];
2763 if ( id && api[ type ]( id ) ) {
2764 instance = api[ type ]( id );
2765 // Wait until the element is embedded in the DOM
2766 instance.deferred.embedded.done( function () {
2767 // Wait until the preview has activated and so active panels, sections, controls have been set
2768 api.previewer.deferred.active.done( function () {
2776 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
2780 api.reflowPaneContents = _.bind( function () {
2782 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
2784 if ( document.activeElement ) {
2785 activeElement = $( document.activeElement );
2788 // Sort the sections within each panel
2789 api.panel.each( function ( panel ) {
2790 var sections = panel.sections(),
2791 sectionContainers = _.pluck( sections, 'container' );
2792 rootNodes.push( panel );
2793 appendContainer = panel.container.find( 'ul:first' );
2794 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
2795 _( sections ).each( function ( section ) {
2796 appendContainer.append( section.container );
2802 // Sort the controls within each section
2803 api.section.each( function ( section ) {
2804 var controls = section.controls(),
2805 controlContainers = _.pluck( controls, 'container' );
2806 if ( ! section.panel() ) {
2807 rootNodes.push( section );
2809 appendContainer = section.container.find( 'ul:first' );
2810 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
2811 _( controls ).each( function ( control ) {
2812 appendContainer.append( control.container );
2818 // Sort the root panels and sections
2819 rootNodes.sort( api.utils.prioritySort );
2820 rootContainers = _.pluck( rootNodes, 'container' );
2821 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
2822 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
2823 _( rootNodes ).each( function ( rootNode ) {
2824 appendContainer.append( rootNode.container );
2829 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
2830 api.panel.each( function ( panel ) {
2831 var value = panel.active();
2832 panel.active.callbacks.fireWith( panel.active, [ value, value ] );
2834 api.section.each( function ( section ) {
2835 var value = section.active();
2836 section.active.callbacks.fireWith( section.active, [ value, value ] );
2839 // Restore focus if there was a reflow and there was an active (focused) element
2840 if ( wasReflowed && activeElement ) {
2841 activeElement.focus();
2844 api.bind( 'ready', api.reflowPaneContents );
2845 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
2846 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
2847 values.bind( 'add', api.reflowPaneContents );
2848 values.bind( 'change', api.reflowPaneContents );
2849 values.bind( 'remove', api.reflowPaneContents );
2852 // Check if preview url is valid and load the preview frame.
2853 if ( api.previewer.previewUrl() ) {
2854 api.previewer.refresh();
2856 api.previewer.previewUrl( api.settings.url.home );
2859 // Save and activated states
2861 var state = new api.Values(),
2862 saved = state.create( 'saved' ),
2863 activated = state.create( 'activated' ),
2864 processing = state.create( 'processing' );
2866 state.bind( 'change', function() {
2867 if ( ! activated() ) {
2868 saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
2869 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
2871 } else if ( saved() ) {
2872 saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
2873 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
2876 saveBtn.val( api.l10n.save ).prop( 'disabled', false );
2877 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
2881 // Set default states.
2883 activated( api.settings.theme.active );
2886 api.bind( 'change', function() {
2887 state('saved').set( false );
2890 api.bind( 'saved', function() {
2891 state('saved').set( true );
2892 state('activated').set( true );
2895 activated.bind( function( to ) {
2897 api.trigger( 'activated' );
2900 // Expose states to the API.
2905 saveBtn.click( function( event ) {
2906 api.previewer.save();
2907 event.preventDefault();
2908 }).keydown( function( event ) {
2909 if ( 9 === event.which ) // tab
2911 if ( 13 === event.which ) // enter
2912 api.previewer.save();
2913 event.preventDefault();
2916 // Go back to the top-level Customizer accordion.
2917 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( event ) {
2918 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2922 event.preventDefault(); // Keep this AFTER the key filter above
2923 api.panel.each( function ( panel ) {
2928 closeBtn.keydown( function( event ) {
2929 if ( 9 === event.which ) // tab
2931 if ( 13 === event.which ) // enter
2933 event.preventDefault();
2936 $('.collapse-sidebar').on( 'click keydown', function( event ) {
2937 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2941 overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
2942 event.preventDefault();
2945 $( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) {
2946 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2950 overlay.toggleClass( 'preview-only' );
2951 event.preventDefault();
2954 // Bind site title display to the corresponding field.
2955 if ( title.length ) {
2956 $( '#customize-control-blogname input' ).on( 'input', function() {
2957 title.text( this.value );
2961 // Create a potential postMessage connection with the parent frame.
2962 parent = new api.Messenger({
2963 url: api.settings.url.parent,
2967 // If we receive a 'back' event, we're inside an iframe.
2968 // Send any clicks to the 'Return' link to the parent page.
2969 parent.bind( 'back', function() {
2970 closeBtn.on( 'click.customize-controls-close', function( event ) {
2971 event.preventDefault();
2972 parent.send( 'close' );
2976 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
2977 $( window ).on( 'beforeunload', function () {
2978 if ( ! api.state( 'saved' )() ) {
2979 setTimeout( function() {
2980 overlay.removeClass( 'customize-loading' );
2982 return api.l10n.saveAlert;
2986 // Pass events through to the parent.
2987 $.each( [ 'saved', 'change' ], function ( i, event ) {
2988 api.bind( event, function() {
2989 parent.send( event );
2993 // When activated, let the loader handle redirecting the page.
2994 // If no loader exists, redirect the page ourselves (if a url exists).
2995 api.bind( 'activated', function() {
2996 if ( parent.targetWindow() )
2997 parent.send( 'activated', api.settings.url.activated );
2998 else if ( api.settings.url.activated )
2999 window.location = api.settings.url.activated;
3002 // Pass titles to the parent
3003 api.bind( 'title', function( newTitle ) {
3004 parent.send( 'title', newTitle );
3007 // Initialize the connection with the parent frame.
3008 parent.send( 'ready' );
3010 // Control visibility for default controls
3012 'background_image': {
3013 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
3014 callback: function( to ) { return !! to; }
3017 controls: [ 'page_on_front', 'page_for_posts' ],
3018 callback: function( to ) { return 'page' === to; }
3020 'header_textcolor': {
3021 controls: [ 'header_textcolor' ],
3022 callback: function( to ) { return 'blank' !== to; }
3024 }, function( settingId, o ) {
3025 api( settingId, function( setting ) {
3026 $.each( o.controls, function( i, controlId ) {
3027 api.control( controlId, function( control ) {
3028 var visibility = function( to ) {
3029 control.container.toggle( o.callback( to ) );
3032 visibility( setting.get() );
3033 setting.bind( visibility );
3039 // Juggle the two controls that use header_textcolor
3040 api.control( 'display_header_text', function( control ) {
3043 control.elements[0].unsync( api( 'header_textcolor' ) );
3045 control.element = new api.Element( control.container.find('input') );
3046 control.element.set( 'blank' !== control.setting() );
3048 control.element.bind( function( to ) {
3050 last = api( 'header_textcolor' ).get();
3052 control.setting.set( to ? last : 'blank' );
3055 control.setting.bind( function( to ) {
3056 control.element.set( 'blank' !== to );
3060 api.trigger( 'ready' );
3062 // Make sure left column gets focus
3063 topFocus = closeBtn;
3065 setTimeout(function () {