1 /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
2 (function( exports, $ ){
3 var Container, focus, api = wp.customize;
6 * A Customizer Setting.
8 * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
9 * draft changes to in the Customizer.
11 * @see PHP class WP_Customize_Setting.
14 * @augments wp.customize.Value
15 * @augments wp.customize.Class
17 * @param {object} id The Setting ID.
18 * @param {object} value The initial value of the setting.
19 * @param {object} options.previewer The Previewer instance to sync with.
20 * @param {object} options.transport The transport to use for previewing. Supports 'refresh' and 'postMessage'.
21 * @param {object} options.dirty
23 api.Setting = api.Value.extend({
24 initialize: function( id, value, options ) {
25 api.Value.prototype.initialize.call( this, value, options );
28 this.transport = this.transport || 'refresh';
29 this._dirty = options.dirty || false;
31 // Whenever the setting's value changes, refresh the preview.
32 this.bind( this.preview );
36 * Refresh the preview, respective of the setting's refresh policy.
39 switch ( this.transport ) {
41 return this.previewer.refresh();
43 return this.previewer.send( 'setting', [ this.id, this() ] );
49 * Utility function namespace
54 * Watch all changes to Value properties, and bubble changes to parent Values instance
58 * @param {wp.customize.Class} instance
59 * @param {Array} properties The names of the Value instances to watch.
61 api.utils.bubbleChildValueChanges = function ( instance, properties ) {
62 $.each( properties, function ( i, key ) {
63 instance[ key ].bind( function ( to, from ) {
64 if ( instance.parent && to !== from ) {
65 instance.parent.trigger( 'change', instance );
72 * Expand a panel, section, or control and focus on the first focusable element.
76 * @param {Object} [params]
77 * @param {Callback} [params.completeCallback]
79 focus = function ( params ) {
80 var construct, completeCallback, focus;
82 params = params || {};
85 if ( construct.extended( api.Panel ) && construct.expanded && construct.expanded() ) {
86 focusContainer = construct.container.find( 'ul.control-panel-content' );
87 } else if ( construct.extended( api.Section ) && construct.expanded && construct.expanded() ) {
88 focusContainer = construct.container.find( 'ul.accordion-section-content' );
90 focusContainer = construct.container;
93 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
94 focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first().focus();
96 if ( params.completeCallback ) {
97 completeCallback = params.completeCallback;
98 params.completeCallback = function () {
103 params.completeCallback = focus;
105 if ( construct.expand ) {
106 construct.expand( params );
108 params.completeCallback();
113 * Stable sort for Panels, Sections, and Controls.
115 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
119 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
120 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
123 api.utils.prioritySort = function ( a, b ) {
124 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
125 return a.params.instanceNumber - b.params.instanceNumber;
127 return a.priority() - b.priority();
132 * Return whether the supplied Event object is for a keydown event but not the Enter key.
136 * @param {jQuery.Event} event
139 api.utils.isKeydownButNotEnterEvent = function ( event ) {
140 return ( 'keydown' === event.type && 13 !== event.which );
144 * Return whether the two lists of elements are the same and are in the same order.
148 * @param {Array|jQuery} listA
149 * @param {Array|jQuery} listB
152 api.utils.areElementListsEqual = function ( listA, listB ) {
154 listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
155 -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
156 _.zip( listA, listB ), // pair up each element between the two lists
158 return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
160 ), false ) // check for presence of false in map's return value
166 * Base class for Panel and Section.
171 * @augments wp.customize.Class
173 Container = api.Class.extend({
174 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
175 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
176 containerType: 'container',
190 * @param {string} id - The ID for the container.
191 * @param {object} options - Object containing one property: params.
192 * @param {object} options.params - Object containing the following properties.
193 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
194 * @param {string=} [options.params.description] - Description shown at the top of the panel.
195 * @param {number=100} [options.params.priority] - The sort priority for the panel.
196 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
197 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
198 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
200 initialize: function ( id, options ) {
201 var container = this;
203 options = options || {};
205 options.params = _.defaults(
206 options.params || {},
210 $.extend( container, options );
211 container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
212 container.container = $( container.params.content );
213 if ( 0 === container.container.length ) {
214 container.container = $( container.getContainer() );
217 container.deferred = {
218 embedded: new $.Deferred()
220 container.priority = new api.Value();
221 container.active = new api.Value();
222 container.activeArgumentsQueue = [];
223 container.expanded = new api.Value();
224 container.expandedArgumentsQueue = [];
226 container.active.bind( function ( active ) {
227 var args = container.activeArgumentsQueue.shift();
228 args = $.extend( {}, container.defaultActiveArguments, args );
229 active = ( active && container.isContextuallyActive() );
230 container.onChangeActive( active, args );
232 container.expanded.bind( function ( expanded ) {
233 var args = container.expandedArgumentsQueue.shift();
234 args = $.extend( {}, container.defaultExpandedArguments, args );
235 container.onChangeExpanded( expanded, args );
238 container.deferred.embedded.done( function () {
239 container.attachEvents();
242 api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
244 container.priority.set( container.params.priority );
245 container.active.set( container.params.active );
246 container.expanded.set( false );
254 ready: function() {},
257 * Get the child models associated with this parent, sorting them by their priority Value.
261 * @param {String} parentType
262 * @param {String} childType
265 _children: function ( parentType, childType ) {
268 api[ childType ].each( function ( child ) {
269 if ( child[ parentType ].get() === parent.id ) {
270 children.push( child );
273 children.sort( api.utils.prioritySort );
278 * To override by subclass, to return whether the container has active children.
284 isContextuallyActive: function () {
285 throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
289 * Active state change handler.
291 * Shows the container if it is active, hides it if not.
293 * To override by subclass, update the container's UI to reflect the provided active state.
297 * @param {Boolean} active
298 * @param {Object} args
299 * @param {Object} args.duration
300 * @param {Object} args.completeCallback
302 onChangeActive: function( active, args ) {
303 var duration, construct = this, expandedOtherPanel;
304 if ( args.unchanged ) {
305 if ( args.completeCallback ) {
306 args.completeCallback();
311 duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
313 if ( construct.extended( api.Panel ) ) {
314 // If this is a panel is not currently expanded but another panel is expanded, do not animate.
315 api.panel.each(function ( panel ) {
316 if ( panel !== construct && panel.expanded() ) {
317 expandedOtherPanel = panel;
322 // Collapse any expanded sections inside of this panel first before deactivating.
324 _.each( construct.sections(), function( section ) {
325 section.collapse( { duration: 0 } );
330 if ( ! $.contains( document, construct.container[0] ) ) {
331 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
332 construct.container.toggle( active );
333 if ( args.completeCallback ) {
334 args.completeCallback();
336 } else if ( active ) {
337 construct.container.stop( true, true ).slideDown( duration, args.completeCallback );
339 if ( construct.expanded() ) {
342 completeCallback: function() {
343 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
347 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
351 // Recalculate the margin-top immediately, not waiting for debounced reflow, to prevent momentary (100ms) vertical jiggle.
352 if ( expandedOtherPanel ) {
353 expandedOtherPanel._recalculateTopMargin();
360 * @params {Boolean} active
361 * @param {Object} [params]
362 * @returns {Boolean} false if state already applied
364 _toggleActive: function ( active, params ) {
366 params = params || {};
367 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
368 params.unchanged = true;
369 self.onChangeActive( self.active.get(), params );
372 params.unchanged = false;
373 this.activeArgumentsQueue.push( params );
374 this.active.set( active );
380 * @param {Object} [params]
381 * @returns {Boolean} false if already active
383 activate: function ( params ) {
384 return this._toggleActive( true, params );
388 * @param {Object} [params]
389 * @returns {Boolean} false if already inactive
391 deactivate: function ( params ) {
392 return this._toggleActive( false, params );
396 * To override by subclass, update the container's UI to reflect the provided active state.
399 onChangeExpanded: function () {
400 throw new Error( 'Must override with subclass.' );
404 * Handle the toggle logic for expand/collapse.
406 * @param {Boolean} expanded - The new state to apply.
407 * @param {Object} [params] - Object containing options for expand/collapse.
408 * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
409 * @returns {Boolean} false if state already applied or active state is false
411 _toggleExpanded: function( expanded, params ) {
412 var instance = this, previousCompleteCallback;
413 params = params || {};
414 previousCompleteCallback = params.completeCallback;
416 // Short-circuit expand() if the instance is not active.
417 if ( expanded && ! instance.active() ) {
421 params.completeCallback = function() {
422 if ( previousCompleteCallback ) {
423 previousCompleteCallback.apply( instance, arguments );
426 instance.container.trigger( 'expanded' );
428 instance.container.trigger( 'collapsed' );
431 if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
432 params.unchanged = true;
433 instance.onChangeExpanded( instance.expanded.get(), params );
436 params.unchanged = false;
437 instance.expandedArgumentsQueue.push( params );
438 instance.expanded.set( expanded );
444 * @param {Object} [params]
445 * @returns {Boolean} false if already expanded or if inactive.
447 expand: function ( params ) {
448 return this._toggleExpanded( true, params );
452 * @param {Object} [params]
453 * @returns {Boolean} false if already collapsed.
455 collapse: function ( params ) {
456 return this._toggleExpanded( false, params );
460 * Bring the container into view and then expand this and bring it into view
461 * @param {Object} [params]
466 * Return the container html, generated from its JS template, if it exists.
470 getContainer: function () {
474 if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
475 template = wp.template( container.templateSelector );
477 template = wp.template( 'customize-' + container.containerType + '-default' );
479 if ( template && container.container ) {
480 return $.trim( template( container.params ) );
491 * @augments wp.customize.Class
493 api.Section = Container.extend({
494 containerType: 'section',
502 instanceNumber: null,
510 * @param {string} id - The ID for the section.
511 * @param {object} options - Object containing one property: params.
512 * @param {object} options.params - Object containing the following properties.
513 * @param {string} options.params.title - Title shown when section is collapsed and expanded.
514 * @param {string=} [options.params.description] - Description shown at the top of the section.
515 * @param {number=100} [options.params.priority] - The sort priority for the section.
516 * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
517 * @param {string=} [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
518 * @param {boolean=true} [options.params.active] - Whether the section is active or not.
519 * @param {string} options.params.panel - The ID for the panel this section is associated with.
520 * @param {string=} [options.params.customizeAction] - Additional context information shown before the section title when expanded.
522 initialize: function ( id, options ) {
524 Container.prototype.initialize.call( section, id, options );
527 section.panel = new api.Value();
528 section.panel.bind( function ( id ) {
529 $( section.container ).toggleClass( 'control-subsection', !! id );
531 section.panel.set( section.params.panel || '' );
532 api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
535 section.deferred.embedded.done( function () {
541 * Embed the container in the DOM when any parent panel is ready.
546 var section = this, inject;
548 // Watch for changes to the panel state
549 inject = function ( panelId ) {
552 // The panel has been supplied, so wait until the panel object is registered
553 api.panel( panelId, function ( panel ) {
554 // The panel has been registered, wait for it to become ready/initialized
555 panel.deferred.embedded.done( function () {
556 parentContainer = panel.container.find( 'ul:first' );
557 if ( ! section.container.parent().is( parentContainer ) ) {
558 parentContainer.append( section.container );
560 section.deferred.embedded.resolve();
564 // There is no panel, so embed the section in the root of the customizer
565 parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
566 if ( ! section.container.parent().is( parentContainer ) ) {
567 parentContainer.append( section.container );
569 section.deferred.embedded.resolve();
572 section.panel.bind( inject );
573 inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
575 section.deferred.embedded.done(function() {
576 // Fix the top margin after reflow.
577 api.bind( 'pane-contents-reflowed', _.debounce( function() {
578 section._recalculateTopMargin();
584 * Add behaviors for the accordion section.
588 attachEvents: function () {
591 // Expand/Collapse accordion sections on click.
592 section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
593 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
596 event.preventDefault(); // Keep this AFTER the key filter above
598 if ( section.expanded() ) {
607 * Return whether this section has any active controls.
613 isContextuallyActive: function () {
615 controls = section.controls(),
617 _( controls ).each( function ( control ) {
618 if ( control.active() ) {
622 return ( activeCount !== 0 );
626 * Get the controls that are associated with this section, sorted by their priority Value.
632 controls: function () {
633 return this._children( 'section', 'control' );
637 * Update UI to reflect expanded state.
641 * @param {Boolean} expanded
642 * @param {Object} args
644 onChangeExpanded: function ( expanded, args ) {
646 container = section.container.closest( '.wp-full-overlay-sidebar-content' ),
647 content = section.container.find( '.accordion-section-content' ),
648 overlay = section.container.closest( '.wp-full-overlay' ),
649 backBtn = section.container.find( '.customize-section-back' ),
650 sectionTitle = section.container.find( '.accordion-section-title' ).first(),
651 headerActionsHeight = $( '#customize-header-actions' ).height(),
652 resizeContentHeight, expand, position, scroll;
654 if ( expanded && ! section.container.hasClass( 'open' ) ) {
656 if ( args.unchanged ) {
657 expand = args.completeCallback;
659 container.scrollTop( 0 );
660 resizeContentHeight = function() {
661 var matchMedia, offset;
662 matchMedia = window.matchMedia || window.msMatchMedia;
663 offset = 90; // 45px for customize header actions + 45px for footer actions.
665 // No footer on small screens.
666 if ( matchMedia && matchMedia( '(max-width: 640px)' ).matches ) {
669 content.css( 'height', ( window.innerHeight - offset ) );
671 expand = function() {
672 section.container.addClass( 'open' );
673 overlay.addClass( 'section-open' );
674 position = content.offset().top;
675 scroll = container.scrollTop();
676 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
677 resizeContentHeight();
678 sectionTitle.attr( 'tabindex', '-1' );
679 backBtn.attr( 'tabindex', '0' );
681 if ( args.completeCallback ) {
682 args.completeCallback();
685 // Fix the height after browser resize.
686 $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
688 section._recalculateTopMargin();
692 if ( ! args.allowMultiple ) {
693 api.section.each( function ( otherSection ) {
694 if ( otherSection !== section ) {
695 otherSection.collapse( { duration: args.duration } );
700 if ( section.panel() ) {
701 api.panel( section.panel() ).expand({
702 duration: args.duration,
703 completeCallback: expand
706 api.panel.each( function( panel ) {
712 } else if ( ! expanded && section.container.hasClass( 'open' ) ) {
713 section.container.removeClass( 'open' );
714 overlay.removeClass( 'section-open' );
715 content.css( 'margin-top', '' );
716 container.scrollTop( 0 );
717 backBtn.attr( 'tabindex', '-1' );
718 sectionTitle.attr( 'tabindex', '0' );
719 sectionTitle.focus();
720 if ( args.completeCallback ) {
721 args.completeCallback();
723 $( window ).off( 'resize.customizer-section' );
725 if ( args.completeCallback ) {
726 args.completeCallback();
732 * Recalculate the top margin.
737 _recalculateTopMargin: function() {
738 var section = this, content, offset, headerActionsHeight;
739 content = section.container.find( '.accordion-section-content' );
740 if ( 0 === content.length ) {
743 headerActionsHeight = $( '#customize-header-actions' ).height();
744 offset = ( content.offset().top - headerActionsHeight );
746 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
752 * wp.customize.ThemesSection
754 * Custom section for themes that functions similarly to a backwards panel,
755 * and also handles the theme-details view rendering and navigation.
758 * @augments wp.customize.Section
759 * @augments wp.customize.Container
761 api.ThemesSection = api.Section.extend({
765 screenshotQueue: null,
766 $window: $( window ),
771 initialize: function () {
772 this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
773 return api.Section.prototype.initialize.apply( this, arguments );
781 section.overlay = section.container.find( '.theme-overlay' );
782 section.template = wp.template( 'customize-themes-details-view' );
784 // Bind global keyboard events.
785 $( 'body' ).on( 'keyup', function( event ) {
786 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
790 // Pressing the right arrow key fires a theme:next event
791 if ( 39 === event.keyCode ) {
795 // Pressing the left arrow key fires a theme:previous event
796 if ( 37 === event.keyCode ) {
797 section.previousTheme();
800 // Pressing the escape key fires a theme:collapse event
801 if ( 27 === event.keyCode ) {
802 section.closeDetails();
806 _.bindAll( this, 'renderScreenshots' );
810 * Override Section.isContextuallyActive method.
812 * Ignore the active states' of the contained theme controls, and just
813 * use the section's own active state instead. This ensures empty search
814 * results for themes to cause the section to become inactive.
820 isContextuallyActive: function () {
821 return this.active();
827 attachEvents: function () {
830 // Expand/Collapse section/panel.
831 section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
832 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
835 event.preventDefault(); // Keep this AFTER the key filter above
837 if ( section.expanded() ) {
844 // Theme navigation in details view.
845 section.container.on( 'click keydown', '.left', function( event ) {
846 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
850 event.preventDefault(); // Keep this AFTER the key filter above
852 section.previousTheme();
855 section.container.on( 'click keydown', '.right', function( event ) {
856 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
860 event.preventDefault(); // Keep this AFTER the key filter above
865 section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
866 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
870 event.preventDefault(); // Keep this AFTER the key filter above
872 section.closeDetails();
875 var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
876 section.container.on( 'input', '#themes-filter', function( event ) {
878 term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
879 controls = section.controls();
881 _.each( controls, function( control ) {
882 control.filter( term );
887 // Update theme count.
888 count = section.container.find( 'li.customize-control:visible' ).length;
889 section.container.find( '.theme-count' ).text( count );
892 // Pre-load the first 3 theme screenshots.
893 api.bind( 'ready', function () {
894 _.each( section.controls().slice( 0, 3 ), function ( control ) {
895 var img, src = control.params.theme.screenshot[0];
905 * Update UI to reflect expanded state
909 * @param {Boolean} expanded
910 * @param {Object} args
911 * @param {Boolean} args.unchanged
912 * @param {Callback} args.completeCallback
914 onChangeExpanded: function ( expanded, args ) {
916 // Immediately call the complete callback if there were no changes
917 if ( args.unchanged ) {
918 if ( args.completeCallback ) {
919 args.completeCallback();
924 // Note: there is a second argument 'args' passed
925 var position, scroll,
927 section = panel.container.closest( '.accordion-section' ),
928 overlay = section.closest( '.wp-full-overlay' ),
929 container = section.closest( '.wp-full-overlay-sidebar-content' ),
930 siblings = container.find( '.open' ),
931 customizeBtn = section.find( '.customize-theme' ),
932 changeBtn = section.find( '.change-theme' ),
933 content = section.find( '.control-panel-content' );
937 // Collapse any sibling sections/panels
938 api.section.each( function ( otherSection ) {
939 if ( otherSection !== panel ) {
940 otherSection.collapse( { duration: args.duration } );
943 api.panel.each( function ( otherPanel ) {
944 otherPanel.collapse( { duration: 0 } );
947 content.show( 0, function() {
948 position = content.offset().top;
949 scroll = container.scrollTop();
950 content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
951 section.addClass( 'current-panel' );
952 overlay.addClass( 'in-themes-panel' );
953 container.scrollTop( 0 );
954 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
955 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
956 if ( args.completeCallback ) {
957 args.completeCallback();
960 customizeBtn.focus();
962 siblings.removeClass( 'open' );
963 section.removeClass( 'current-panel' );
964 overlay.removeClass( 'in-themes-panel' );
965 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
966 content.delay( 180 ).hide( 0, function() {
967 content.css( 'margin-top', 'inherit' ); // Reset
968 if ( args.completeCallback ) {
969 args.completeCallback();
972 customizeBtn.attr( 'tabindex', '0' );
974 container.scrollTop( 0 );
979 * Recalculate the top margin.
984 _recalculateTopMargin: function() {
985 api.Panel.prototype._recalculateTopMargin.call( this );
989 * Render control's screenshot if the control comes into view.
993 renderScreenshots: function( ) {
996 // Fill queue initially.
997 if ( section.screenshotQueue === null ) {
998 section.screenshotQueue = section.controls();
1001 // Are all screenshots rendered?
1002 if ( ! section.screenshotQueue.length ) {
1006 section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
1007 var $imageWrapper = control.container.find( '.theme-screenshot' ),
1008 $image = $imageWrapper.find( 'img' );
1010 if ( ! $image.length ) {
1014 if ( $image.is( ':hidden' ) ) {
1018 // Based on unveil.js.
1019 var wt = section.$window.scrollTop(),
1020 wb = wt + section.$window.height(),
1021 et = $image.offset().top,
1022 ih = $imageWrapper.height(),
1025 inView = eb >= wt - threshold && et <= wb + threshold;
1028 control.container.trigger( 'render-screenshot' );
1031 // If the image is in view return false so it's cleared from the queue.
1037 * Advance the modal to the next theme.
1041 nextTheme: function () {
1043 if ( section.getNextTheme() ) {
1044 section.showDetails( section.getNextTheme(), function() {
1045 section.overlay.find( '.right' ).focus();
1051 * Get the next theme model.
1055 getNextTheme: function () {
1057 control = api.control( 'theme_' + this.currentTheme );
1058 next = control.container.next( 'li.customize-control-theme' );
1059 if ( ! next.length ) {
1062 next = next[0].id.replace( 'customize-control-', '' );
1063 control = api.control( next );
1065 return control.params.theme;
1069 * Advance the modal to the previous theme.
1073 previousTheme: function () {
1075 if ( section.getPreviousTheme() ) {
1076 section.showDetails( section.getPreviousTheme(), function() {
1077 section.overlay.find( '.left' ).focus();
1083 * Get the previous theme model.
1087 getPreviousTheme: function () {
1088 var control, previous;
1089 control = api.control( 'theme_' + this.currentTheme );
1090 previous = control.container.prev( 'li.customize-control-theme' );
1091 if ( ! previous.length ) {
1094 previous = previous[0].id.replace( 'customize-control-', '' );
1095 control = api.control( previous );
1097 return control.params.theme;
1101 * Disable buttons when we're viewing the first or last theme.
1105 updateLimits: function () {
1106 if ( ! this.getNextTheme() ) {
1107 this.overlay.find( '.right' ).addClass( 'disabled' );
1109 if ( ! this.getPreviousTheme() ) {
1110 this.overlay.find( '.left' ).addClass( 'disabled' );
1115 * Render & show the theme details for a given theme model.
1119 * @param {Object} theme
1121 showDetails: function ( theme, callback ) {
1123 callback = callback || function(){};
1124 section.currentTheme = theme.id;
1125 section.overlay.html( section.template( theme ) )
1128 $( 'body' ).addClass( 'modal-open' );
1129 section.containFocus( section.overlay );
1130 section.updateLimits();
1135 * Close the theme details modal.
1139 closeDetails: function () {
1140 $( 'body' ).removeClass( 'modal-open' );
1141 this.overlay.fadeOut( 'fast' );
1142 api.control( 'theme_' + this.currentTheme ).focus();
1146 * Keep tab focus within the theme details modal.
1150 containFocus: function( el ) {
1153 el.on( 'keydown', function( event ) {
1155 // Return if it's not the tab key
1156 // When navigating with prev/next focus is already handled
1157 if ( 9 !== event.keyCode ) {
1161 // uses jQuery UI to get the tabbable elements
1162 tabbables = $( ':tabbable', el );
1164 // Keep focus within the overlay
1165 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
1166 tabbables.first().focus();
1168 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
1169 tabbables.last().focus();
1180 * @augments wp.customize.Class
1182 api.Panel = Container.extend({
1183 containerType: 'panel',
1188 * @param {string} id - The ID for the panel.
1189 * @param {object} options - Object containing one property: params.
1190 * @param {object} options.params - Object containing the following properties.
1191 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
1192 * @param {string=} [options.params.description] - Description shown at the top of the panel.
1193 * @param {number=100} [options.params.priority] - The sort priority for the panel.
1194 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
1195 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
1196 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
1198 initialize: function ( id, options ) {
1200 Container.prototype.initialize.call( panel, id, options );
1202 panel.deferred.embedded.done( function () {
1208 * Embed the container in the DOM when any parent panel is ready.
1212 embed: function () {
1214 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
1216 if ( ! panel.container.parent().is( parentContainer ) ) {
1217 parentContainer.append( panel.container );
1218 panel.renderContent();
1221 api.bind( 'pane-contents-reflowed', _.debounce( function() {
1222 panel._recalculateTopMargin();
1225 panel.deferred.embedded.resolve();
1231 attachEvents: function () {
1232 var meta, panel = this;
1234 // Expand/Collapse accordion sections on click.
1235 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
1236 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1239 event.preventDefault(); // Keep this AFTER the key filter above
1241 if ( ! panel.expanded() ) {
1247 panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
1248 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1251 event.preventDefault(); // Keep this AFTER the key filter above
1253 if ( panel.expanded() ) {
1258 meta = panel.container.find( '.panel-meta:first' );
1260 meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
1261 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1264 event.preventDefault(); // Keep this AFTER the key filter above
1266 meta = panel.container.find( '.panel-meta' );
1267 if ( meta.hasClass( 'cannot-expand' ) ) {
1271 var content = meta.find( '.customize-panel-description:first' );
1272 if ( meta.hasClass( 'open' ) ) {
1273 meta.toggleClass( 'open' );
1274 content.slideUp( panel.defaultExpandedArguments.duration );
1275 $( this ).attr( 'aria-expanded', false );
1277 content.slideDown( panel.defaultExpandedArguments.duration );
1278 meta.toggleClass( 'open' );
1279 $( this ).attr( 'aria-expanded', true );
1286 * Get the sections that are associated with this panel, sorted by their priority Value.
1292 sections: function () {
1293 return this._children( 'panel', 'section' );
1297 * Return whether this panel has any active sections.
1301 * @returns {boolean}
1303 isContextuallyActive: function () {
1305 sections = panel.sections(),
1307 _( sections ).each( function ( section ) {
1308 if ( section.active() && section.isContextuallyActive() ) {
1312 return ( activeCount !== 0 );
1316 * Update UI to reflect expanded state
1320 * @param {Boolean} expanded
1321 * @param {Object} args
1322 * @param {Boolean} args.unchanged
1323 * @param {Function} args.completeCallback
1325 onChangeExpanded: function ( expanded, args ) {
1327 // Immediately call the complete callback if there were no changes
1328 if ( args.unchanged ) {
1329 if ( args.completeCallback ) {
1330 args.completeCallback();
1335 // Note: there is a second argument 'args' passed
1336 var position, scroll,
1338 accordionSection = panel.container.closest( '.accordion-section' ),
1339 overlay = accordionSection.closest( '.wp-full-overlay' ),
1340 container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
1341 siblings = container.find( '.open' ),
1342 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
1343 backBtn = accordionSection.find( '.customize-panel-back' ),
1344 panelTitle = accordionSection.find( '.accordion-section-title' ).first(),
1345 content = accordionSection.find( '.control-panel-content' ),
1346 headerActionsHeight = $( '#customize-header-actions' ).height();
1350 // Collapse any sibling sections/panels
1351 api.section.each( function ( section ) {
1352 if ( panel.id !== section.panel() ) {
1353 section.collapse( { duration: 0 } );
1356 api.panel.each( function ( otherPanel ) {
1357 if ( panel !== otherPanel ) {
1358 otherPanel.collapse( { duration: 0 } );
1362 content.show( 0, function() {
1363 content.parent().show();
1364 position = content.offset().top;
1365 scroll = container.scrollTop();
1366 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
1367 accordionSection.addClass( 'current-panel' );
1368 overlay.addClass( 'in-sub-panel' );
1369 container.scrollTop( 0 );
1370 if ( args.completeCallback ) {
1371 args.completeCallback();
1374 topPanel.attr( 'tabindex', '-1' );
1375 backBtn.attr( 'tabindex', '0' );
1377 panel._recalculateTopMargin();
1379 siblings.removeClass( 'open' );
1380 accordionSection.removeClass( 'current-panel' );
1381 overlay.removeClass( 'in-sub-panel' );
1382 content.delay( 180 ).hide( 0, function() {
1383 content.css( 'margin-top', 'inherit' ); // Reset
1384 if ( args.completeCallback ) {
1385 args.completeCallback();
1388 topPanel.attr( 'tabindex', '0' );
1389 backBtn.attr( 'tabindex', '-1' );
1391 container.scrollTop( 0 );
1396 * Recalculate the top margin.
1401 _recalculateTopMargin: function() {
1402 var panel = this, headerActionsHeight, content, accordionSection;
1403 headerActionsHeight = $( '#customize-header-actions' ).height();
1404 accordionSection = panel.container.closest( '.accordion-section' );
1405 content = accordionSection.find( '.control-panel-content' );
1406 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
1410 * Render the panel from its JS template, if it exists.
1412 * The panel's container must already exist in the DOM.
1416 renderContent: function () {
1420 // Add the content to the container.
1421 if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
1422 template = wp.template( panel.templateSelector + '-content' );
1424 template = wp.template( 'customize-panel-default-content' );
1426 if ( template && panel.container ) {
1427 panel.container.find( '.accordion-sub-container' ).html( template( panel.params ) );
1433 * A Customizer Control.
1435 * A control provides a UI element that allows a user to modify a Customizer Setting.
1437 * @see PHP class WP_Customize_Control.
1440 * @augments wp.customize.Class
1442 * @param {string} id Unique identifier for the control instance.
1443 * @param {object} options Options hash for the control instance.
1444 * @param {object} options.params
1445 * @param {object} options.params.type Type of control (e.g. text, radio, dropdown-pages, etc.)
1446 * @param {string} options.params.content The HTML content for the control.
1447 * @param {string} options.params.priority Order of priority to show the control within the section.
1448 * @param {string} options.params.active
1449 * @param {string} options.params.section The ID of the section the control belongs to.
1450 * @param {string} options.params.settings.default The ID of the setting the control relates to.
1451 * @param {string} options.params.settings.data
1452 * @param {string} options.params.label
1453 * @param {string} options.params.description
1454 * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
1456 api.Control = api.Class.extend({
1457 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
1459 initialize: function( id, options ) {
1461 nodes, radios, settings;
1463 control.params = {};
1464 $.extend( control, options || {} );
1466 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
1467 control.templateSelector = 'customize-control-' + control.params.type + '-content';
1468 control.container = control.params.content ? $( control.params.content ) : $( control.selector );
1470 control.deferred = {
1471 embedded: new $.Deferred()
1473 control.section = new api.Value();
1474 control.priority = new api.Value();
1475 control.active = new api.Value();
1476 control.activeArgumentsQueue = [];
1478 control.elements = [];
1480 nodes = control.container.find('[data-customize-setting-link]');
1483 nodes.each( function() {
1484 var node = $( this ),
1487 if ( node.is( ':radio' ) ) {
1488 name = node.prop( 'name' );
1489 if ( radios[ name ] ) {
1493 radios[ name ] = true;
1494 node = nodes.filter( '[name="' + name + '"]' );
1497 api( node.data( 'customizeSettingLink' ), function( setting ) {
1498 var element = new api.Element( node );
1499 control.elements.push( element );
1500 element.sync( setting );
1501 element.set( setting() );
1505 control.active.bind( function ( active ) {
1506 var args = control.activeArgumentsQueue.shift();
1507 args = $.extend( {}, control.defaultActiveArguments, args );
1508 control.onChangeActive( active, args );
1511 control.section.set( control.params.section );
1512 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
1513 control.active.set( control.params.active );
1515 api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
1518 * After all settings related to the control are available,
1519 * make them available on the control and embed the control into the page.
1521 settings = $.map( control.params.settings, function( value ) {
1524 api.apply( api, settings.concat( function () {
1527 control.settings = {};
1528 for ( key in control.params.settings ) {
1529 control.settings[ key ] = api( control.params.settings[ key ] );
1532 control.setting = control.settings['default'] || null;
1537 // After the control is embedded on the page, invoke the "ready" method.
1538 control.deferred.embedded.done( function () {
1544 * Embed the control into the page.
1546 embed: function () {
1550 // Watch for changes to the section state
1551 inject = function ( sectionId ) {
1552 var parentContainer;
1553 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the frontend
1556 // Wait for the section to be registered
1557 api.section( sectionId, function ( section ) {
1558 // Wait for the section to be ready/initialized
1559 section.deferred.embedded.done( function () {
1560 parentContainer = section.container.find( 'ul:first' );
1561 if ( ! control.container.parent().is( parentContainer ) ) {
1562 parentContainer.append( control.container );
1563 control.renderContent();
1565 control.deferred.embedded.resolve();
1569 control.section.bind( inject );
1570 inject( control.section.get() );
1574 * Triggered when the control's markup has been injected into the DOM.
1578 ready: function() {},
1581 * Normal controls do not expand, so just expand its parent
1583 * @param {Object} [params]
1585 expand: function ( params ) {
1586 api.section( this.section() ).expand( params );
1590 * Bring the containing section and panel into view and then
1591 * this control into view, focusing on the first input.
1596 * Update UI in response to a change in the control's active state.
1597 * This does not change the active state, it merely handles the behavior
1598 * for when it does change.
1602 * @param {Boolean} active
1603 * @param {Object} args
1604 * @param {Number} args.duration
1605 * @param {Callback} args.completeCallback
1607 onChangeActive: function ( active, args ) {
1608 if ( args.unchanged ) {
1609 if ( args.completeCallback ) {
1610 args.completeCallback();
1615 if ( ! $.contains( document, this.container[0] ) ) {
1616 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
1617 this.container.toggle( active );
1618 if ( args.completeCallback ) {
1619 args.completeCallback();
1621 } else if ( active ) {
1622 this.container.slideDown( args.duration, args.completeCallback );
1624 this.container.slideUp( args.duration, args.completeCallback );
1629 * @deprecated 4.1.0 Use this.onChangeActive() instead.
1631 toggle: function ( active ) {
1632 return this.onChangeActive( active, this.defaultActiveArguments );
1636 * Shorthand way to enable the active state.
1640 * @param {Object} [params]
1641 * @returns {Boolean} false if already active
1643 activate: Container.prototype.activate,
1646 * Shorthand way to disable the active state.
1650 * @param {Object} [params]
1651 * @returns {Boolean} false if already inactive
1653 deactivate: Container.prototype.deactivate,
1656 * Re-use _toggleActive from Container class.
1660 _toggleActive: Container.prototype._toggleActive,
1662 dropdownInit: function() {
1664 statuses = this.container.find('.dropdown-status'),
1665 params = this.params,
1666 toggleFreeze = false,
1667 update = function( to ) {
1668 if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
1669 statuses.html( params.statuses[ to ] ).show();
1674 // Support the .dropdown class to open/close complex elements
1675 this.container.on( 'click keydown', '.dropdown', function( event ) {
1676 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1680 event.preventDefault();
1683 control.container.toggleClass('open');
1685 if ( control.container.hasClass('open') )
1686 control.container.parent().parent().find('li.library-selected').focus();
1688 // Don't want to fire focus and click at same time
1689 toggleFreeze = true;
1690 setTimeout(function () {
1691 toggleFreeze = false;
1695 this.setting.bind( update );
1696 update( this.setting() );
1700 * Render the control from its JS template, if it exists.
1702 * The control's container must already exist in the DOM.
1706 renderContent: function () {
1710 // Replace the container element's content with the control.
1711 if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
1712 template = wp.template( control.templateSelector );
1713 if ( template && control.container ) {
1714 control.container.html( template( control.params ) );
1721 * A colorpicker control.
1724 * @augments wp.customize.Control
1725 * @augments wp.customize.Class
1727 api.ColorControl = api.Control.extend({
1730 picker = this.container.find('.color-picker-hex');
1732 picker.val( control.setting() ).wpColorPicker({
1733 change: function() {
1734 control.setting.set( picker.wpColorPicker('color') );
1737 control.setting.set( '' );
1741 this.setting.bind( function ( value ) {
1742 picker.val( value );
1743 picker.wpColorPicker( 'color', value );
1749 * A control that implements the media modal.
1752 * @augments wp.customize.Control
1753 * @augments wp.customize.Class
1755 api.MediaControl = api.Control.extend({
1758 * When the control's DOM structure is ready,
1759 * set up internal event bindings.
1763 // Shortcut so that we don't have to use _.bind every time we add a callback.
1764 _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
1766 // Bind events, with delegation to facilitate re-rendering.
1767 control.container.on( 'click keydown', '.upload-button', control.openFrame );
1768 control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
1769 control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1770 control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1771 control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
1772 control.container.on( 'click keydown', '.remove-button', control.removeFile );
1773 control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
1775 // Resize the player controls when it becomes visible (ie when section is expanded)
1776 api.section( control.section() ).container
1777 .on( 'expanded', function() {
1778 if ( control.player ) {
1779 control.player.setControlsSize();
1782 .on( 'collapsed', function() {
1783 control.pausePlayer();
1786 // Re-render whenever the control's setting changes.
1787 control.setting.bind( function () { control.renderContent(); } );
1790 pausePlayer: function () {
1791 this.player && this.player.pause();
1794 cleanupPlayer: function () {
1795 this.player && wp.media.mixin.removePlayer( this.player );
1799 * Open the media modal.
1801 openFrame: function( event ) {
1802 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1806 event.preventDefault();
1808 if ( ! this.frame ) {
1816 * Create a media modal select frame, and store it so the instance can be reused when needed.
1818 initFrame: function() {
1819 this.frame = wp.media({
1821 text: this.params.button_labels.frame_button
1824 new wp.media.controller.Library({
1825 title: this.params.button_labels.frame_title,
1826 library: wp.media.query({ type: this.params.mime_type }),
1833 // When a file is selected, run a callback.
1834 this.frame.on( 'select', this.select );
1838 * Callback handler for when an attachment is selected in the media modal.
1839 * Gets the selected image information, and sets it within the control.
1841 select: function() {
1842 // Get the attachment from the modal frame.
1844 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1845 mejsSettings = window._wpmejsSettings || {};
1847 this.params.attachment = attachment;
1849 // Set the Customizer setting; the callback takes care of rendering.
1850 this.setting( attachment.id );
1851 node = this.container.find( 'audio, video' ).get(0);
1853 // Initialize audio/video previews.
1855 this.player = new MediaElementPlayer( node, mejsSettings );
1857 this.cleanupPlayer();
1862 * Reset the setting to the default value.
1864 restoreDefault: function( event ) {
1865 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1868 event.preventDefault();
1870 this.params.attachment = this.params.defaultAttachment;
1871 this.setting( this.params.defaultAttachment.url );
1875 * Called when the "Remove" link is clicked. Empties the setting.
1877 * @param {object} event jQuery Event object
1879 removeFile: function( event ) {
1880 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1883 event.preventDefault();
1885 this.params.attachment = {};
1887 this.renderContent(); // Not bound to setting change when emptying.
1892 * An upload control, which utilizes the media modal.
1895 * @augments wp.customize.MediaControl
1896 * @augments wp.customize.Control
1897 * @augments wp.customize.Class
1899 api.UploadControl = api.MediaControl.extend({
1902 * Callback handler for when an attachment is selected in the media modal.
1903 * Gets the selected image information, and sets it within the control.
1905 select: function() {
1906 // Get the attachment from the modal frame.
1908 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1909 mejsSettings = window._wpmejsSettings || {};
1911 this.params.attachment = attachment;
1913 // Set the Customizer setting; the callback takes care of rendering.
1914 this.setting( attachment.url );
1915 node = this.container.find( 'audio, video' ).get(0);
1917 // Initialize audio/video previews.
1919 this.player = new MediaElementPlayer( node, mejsSettings );
1921 this.cleanupPlayer();
1926 success: function() {},
1929 removerVisibility: function() {}
1933 * A control for uploading images.
1935 * This control no longer needs to do anything more
1936 * than what the upload control does in JS.
1939 * @augments wp.customize.UploadControl
1940 * @augments wp.customize.MediaControl
1941 * @augments wp.customize.Control
1942 * @augments wp.customize.Class
1944 api.ImageControl = api.UploadControl.extend({
1946 thumbnailSrc: function() {}
1950 * A control for uploading background images.
1953 * @augments wp.customize.UploadControl
1954 * @augments wp.customize.MediaControl
1955 * @augments wp.customize.Control
1956 * @augments wp.customize.Class
1958 api.BackgroundControl = api.UploadControl.extend({
1961 * When the control's DOM structure is ready,
1962 * set up internal event bindings.
1965 api.UploadControl.prototype.ready.apply( this, arguments );
1969 * Callback handler for when an attachment is selected in the media modal.
1970 * Does an additional AJAX request for setting the background context.
1972 select: function() {
1973 api.UploadControl.prototype.select.apply( this, arguments );
1975 wp.ajax.post( 'custom-background-add', {
1976 nonce: _wpCustomizeBackground.nonces.add,
1978 theme: api.settings.theme.stylesheet,
1979 attachment_id: this.params.attachment.id
1985 * A control for selecting and cropping an image.
1988 * @augments wp.customize.MediaControl
1989 * @augments wp.customize.Control
1990 * @augments wp.customize.Class
1992 api.CroppedImageControl = api.MediaControl.extend({
1995 * Open the media modal to the library state.
1997 openFrame: function( event ) {
1998 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2003 this.frame.setState( 'library' ).open();
2007 * Create a media modal select frame, and store it so the instance can be reused when needed.
2009 initFrame: function() {
2010 var l10n = _wpMediaViewsL10n;
2012 this.frame = wp.media({
2018 new wp.media.controller.Library({
2019 title: this.params.button_labels.frame_title,
2020 library: wp.media.query({ type: 'image' }),
2024 suggestedWidth: this.params.width,
2025 suggestedHeight: this.params.height
2027 new wp.media.controller.CustomizeImageCropper({
2028 imgSelectOptions: this.calculateImageSelectOptions,
2034 this.frame.on( 'select', this.onSelect, this );
2035 this.frame.on( 'cropped', this.onCropped, this );
2036 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2040 * After an image is selected in the media modal, switch to the cropper
2041 * state if the image isn't the right size.
2043 onSelect: function() {
2044 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2046 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2047 this.setImageFromAttachment( attachment );
2050 this.frame.setState( 'cropper' );
2055 * After the image has been cropped, apply the cropped image data to the setting.
2057 * @param {object} croppedImage Cropped attachment data.
2059 onCropped: function( croppedImage ) {
2060 this.setImageFromAttachment( croppedImage );
2064 * Returns a set of options, computed from the attached image data and
2065 * control-specific data, to be fed to the imgAreaSelect plugin in
2066 * wp.media.view.Cropper.
2068 * @param {wp.media.model.Attachment} attachment
2069 * @param {wp.media.controller.Cropper} controller
2070 * @returns {Object} Options
2072 calculateImageSelectOptions: function( attachment, controller ) {
2073 var control = controller.get( 'control' ),
2074 flexWidth = !! parseInt( control.params.flex_width, 10 ),
2075 flexHeight = !! parseInt( control.params.flex_height, 10 ),
2076 realWidth = attachment.get( 'width' ),
2077 realHeight = attachment.get( 'height' ),
2078 xInit = parseInt( control.params.width, 10 ),
2079 yInit = parseInt( control.params.height, 10 ),
2080 ratio = xInit / yInit,
2083 x1, y1, imgSelectOptions;
2085 controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
2087 if ( xImg / yImg > ratio ) {
2089 xInit = yInit * ratio;
2092 yInit = xInit / ratio;
2095 x1 = ( xImg - xInit ) / 2;
2096 y1 = ( yImg - yInit ) / 2;
2098 imgSelectOptions = {
2103 imageWidth: realWidth,
2104 imageHeight: realHeight,
2111 if ( flexHeight === false && flexWidth === false ) {
2112 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2114 if ( flexHeight === false ) {
2115 imgSelectOptions.maxHeight = yInit;
2117 if ( flexWidth === false ) {
2118 imgSelectOptions.maxWidth = xInit;
2121 return imgSelectOptions;
2125 * Return whether the image must be cropped, based on required dimensions.
2127 * @param {bool} flexW
2128 * @param {bool} flexH
2135 mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
2136 if ( true === flexW && true === flexH ) {
2140 if ( true === flexW && dstH === imgH ) {
2144 if ( true === flexH && dstW === imgW ) {
2148 if ( dstW === imgW && dstH === imgH ) {
2152 if ( imgW <= dstW ) {
2160 * If cropping was skipped, apply the image data directly to the setting.
2162 onSkippedCrop: function() {
2163 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2164 this.setImageFromAttachment( attachment );
2168 * Updates the setting and re-renders the control UI.
2170 * @param {object} attachment
2172 setImageFromAttachment: function( attachment ) {
2173 this.params.attachment = attachment;
2175 // Set the Customizer setting; the callback takes care of rendering.
2176 this.setting( attachment.id );
2181 * A control for selecting and cropping Site Icons.
2184 * @augments wp.customize.CroppedImageControl
2185 * @augments wp.customize.MediaControl
2186 * @augments wp.customize.Control
2187 * @augments wp.customize.Class
2189 api.SiteIconControl = api.CroppedImageControl.extend({
2192 * Create a media modal select frame, and store it so the instance can be reused when needed.
2194 initFrame: function() {
2195 var l10n = _wpMediaViewsL10n;
2197 this.frame = wp.media({
2203 new wp.media.controller.Library({
2204 title: this.params.button_labels.frame_title,
2205 library: wp.media.query({ type: 'image' }),
2209 suggestedWidth: this.params.width,
2210 suggestedHeight: this.params.height
2212 new wp.media.controller.SiteIconCropper({
2213 imgSelectOptions: this.calculateImageSelectOptions,
2219 this.frame.on( 'select', this.onSelect, this );
2220 this.frame.on( 'cropped', this.onCropped, this );
2221 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2225 * After an image is selected in the media modal, switch to the cropper
2226 * state if the image isn't the right size.
2228 onSelect: function() {
2229 var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2232 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2233 wp.ajax.post( 'crop-image', {
2234 nonce: attachment.nonces.edit,
2236 context: 'site-icon',
2240 width: this.params.width,
2241 height: this.params.height,
2242 dst_width: this.params.width,
2243 dst_height: this.params.height
2245 } ).done( function( croppedImage ) {
2246 controller.setImageFromAttachment( croppedImage );
2247 controller.frame.close();
2248 } ).fail( function() {
2249 controller.trigger('content:error:crop');
2252 this.frame.setState( 'cropper' );
2257 * Updates the setting and re-renders the control UI.
2259 * @param {object} attachment
2261 setImageFromAttachment: function( attachment ) {
2262 var sizes = [ 'site_icon-32', 'thumbnail', 'full' ],
2265 _.each( sizes, function( size ) {
2266 if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
2267 icon = attachment.sizes[ size ];
2271 this.params.attachment = attachment;
2273 // Set the Customizer setting; the callback takes care of rendering.
2274 this.setting( attachment.id );
2276 // Update the icon in-browser.
2277 $( 'link[sizes="32x32"]' ).attr( 'href', icon.url );
2281 * Called when the "Remove" link is clicked. Empties the setting.
2283 * @param {object} event jQuery Event object
2285 removeFile: function( event ) {
2286 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2289 event.preventDefault();
2291 this.params.attachment = {};
2293 this.renderContent(); // Not bound to setting change when emptying.
2294 $( 'link[rel="icon"]' ).attr( 'href', '' );
2300 * @augments wp.customize.Control
2301 * @augments wp.customize.Class
2303 api.HeaderControl = api.Control.extend({
2305 this.btnRemove = $('#customize-control-header_image .actions .remove');
2306 this.btnNew = $('#customize-control-header_image .actions .new');
2308 _.bindAll(this, 'openMedia', 'removeImage');
2310 this.btnNew.on( 'click', this.openMedia );
2311 this.btnRemove.on( 'click', this.removeImage );
2313 api.HeaderTool.currentHeader = this.getInitialHeaderImage();
2315 new api.HeaderTool.CurrentView({
2316 model: api.HeaderTool.currentHeader,
2317 el: '#customize-control-header_image .current .container'
2320 new api.HeaderTool.ChoiceListView({
2321 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
2322 el: '#customize-control-header_image .choices .uploaded .list'
2325 new api.HeaderTool.ChoiceListView({
2326 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
2327 el: '#customize-control-header_image .choices .default .list'
2330 api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
2331 api.HeaderTool.UploadsList,
2332 api.HeaderTool.DefaultsList
2337 * Returns a new instance of api.HeaderTool.ImageModel based on the currently
2338 * saved header image (if any).
2342 * @returns {Object} Options
2344 getInitialHeaderImage: function() {
2345 if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
2346 return new api.HeaderTool.ImageModel();
2349 // Get the matching uploaded image object.
2350 var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
2351 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
2353 // Fall back to raw current header image.
2354 if ( ! currentHeaderObject ) {
2355 currentHeaderObject = {
2356 url: api.get().header_image,
2357 thumbnail_url: api.get().header_image,
2358 attachment_id: api.get().header_image_data.attachment_id
2362 return new api.HeaderTool.ImageModel({
2363 header: currentHeaderObject,
2364 choice: currentHeaderObject.url.split( '/' ).pop()
2369 * Returns a set of options, computed from the attached image data and
2370 * theme-specific data, to be fed to the imgAreaSelect plugin in
2371 * wp.media.view.Cropper.
2373 * @param {wp.media.model.Attachment} attachment
2374 * @param {wp.media.controller.Cropper} controller
2375 * @returns {Object} Options
2377 calculateImageSelectOptions: function(attachment, controller) {
2378 var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
2379 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
2380 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
2381 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
2382 ratio, xImg, yImg, realHeight, realWidth,
2385 realWidth = attachment.get('width');
2386 realHeight = attachment.get('height');
2388 this.headerImage = new api.HeaderTool.ImageModel();
2389 this.headerImage.set({
2392 themeFlexWidth: flexWidth,
2393 themeFlexHeight: flexHeight,
2394 imageWidth: realWidth,
2395 imageHeight: realHeight
2398 controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
2400 ratio = xInit / yInit;
2404 if ( xImg / yImg > ratio ) {
2406 xInit = yInit * ratio;
2409 yInit = xInit / ratio;
2412 imgSelectOptions = {
2417 imageWidth: realWidth,
2418 imageHeight: realHeight,
2425 if (flexHeight === false && flexWidth === false) {
2426 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2428 if (flexHeight === false ) {
2429 imgSelectOptions.maxHeight = yInit;
2431 if (flexWidth === false ) {
2432 imgSelectOptions.maxWidth = xInit;
2435 return imgSelectOptions;
2439 * Sets up and opens the Media Manager in order to select an image.
2440 * Depending on both the size of the image and the properties of the
2441 * current theme, a cropping step after selection may be required or
2444 * @param {event} event
2446 openMedia: function(event) {
2447 var l10n = _wpMediaViewsL10n;
2449 event.preventDefault();
2451 this.frame = wp.media({
2453 text: l10n.selectAndCrop,
2457 new wp.media.controller.Library({
2458 title: l10n.chooseImage,
2459 library: wp.media.query({ type: 'image' }),
2463 suggestedWidth: _wpCustomizeHeader.data.width,
2464 suggestedHeight: _wpCustomizeHeader.data.height
2466 new wp.media.controller.Cropper({
2467 imgSelectOptions: this.calculateImageSelectOptions
2472 this.frame.on('select', this.onSelect, this);
2473 this.frame.on('cropped', this.onCropped, this);
2474 this.frame.on('skippedcrop', this.onSkippedCrop, this);
2480 * After an image is selected in the media modal,
2481 * switch to the cropper state.
2483 onSelect: function() {
2484 this.frame.setState('cropper');
2488 * After the image has been cropped, apply the cropped image data to the setting.
2490 * @param {object} croppedImage Cropped attachment data.
2492 onCropped: function(croppedImage) {
2493 var url = croppedImage.url,
2494 attachmentId = croppedImage.attachment_id,
2495 w = croppedImage.width,
2496 h = croppedImage.height;
2497 this.setImageFromURL(url, attachmentId, w, h);
2501 * If cropping was skipped, apply the image data directly to the setting.
2503 * @param {object} selection
2505 onSkippedCrop: function(selection) {
2506 var url = selection.get('url'),
2507 w = selection.get('width'),
2508 h = selection.get('height');
2509 this.setImageFromURL(url, selection.id, w, h);
2513 * Creates a new wp.customize.HeaderTool.ImageModel from provided
2514 * header image data and inserts it into the user-uploaded headers
2517 * @param {String} url
2518 * @param {Number} attachmentId
2519 * @param {Number} width
2520 * @param {Number} height
2522 setImageFromURL: function(url, attachmentId, width, height) {
2523 var choice, data = {};
2526 data.thumbnail_url = url;
2527 data.timestamp = _.now();
2530 data.attachment_id = attachmentId;
2538 data.height = height;
2541 choice = new api.HeaderTool.ImageModel({
2543 choice: url.split('/').pop()
2545 api.HeaderTool.UploadsList.add(choice);
2546 api.HeaderTool.currentHeader.set(choice.toJSON());
2548 choice.importImage();
2552 * Triggers the necessary events to deselect an image which was set as
2553 * the currently selected one.
2555 removeImage: function() {
2556 api.HeaderTool.currentHeader.trigger('hide');
2557 api.HeaderTool.CombinedList.trigger('control:removeImage');
2563 * wp.customize.ThemeControl
2566 * @augments wp.customize.Control
2567 * @augments wp.customize.Class
2569 api.ThemeControl = api.Control.extend({
2575 * Defer rendering the theme control until the section is displayed.
2579 renderContent: function () {
2581 renderContentArgs = arguments;
2583 api.section( control.section(), function( section ) {
2584 if ( section.expanded() ) {
2585 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2586 control.isRendered = true;
2588 section.expanded.bind( function( expanded ) {
2589 if ( expanded && ! control.isRendered ) {
2590 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2591 control.isRendered = true;
2604 control.container.on( 'touchmove', '.theme', function() {
2605 control.touchDrag = true;
2608 // Bind details view trigger.
2609 control.container.on( 'click keydown touchend', '.theme', function( event ) {
2610 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2614 // Bail if the user scrolled on a touch device.
2615 if ( control.touchDrag === true ) {
2616 return control.touchDrag = false;
2619 // Prevent the modal from showing when the user clicks the action button.
2620 if ( $( event.target ).is( '.theme-actions .button' ) ) {
2624 var previewUrl = $( this ).data( 'previewUrl' );
2626 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
2628 window.parent.location = previewUrl;
2631 control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
2632 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2636 event.preventDefault(); // Keep this AFTER the key filter above
2638 api.section( control.section() ).showDetails( control.params.theme );
2641 control.container.on( 'render-screenshot', function() {
2642 var $screenshot = $( this ).find( 'img' ),
2643 source = $screenshot.data( 'src' );
2646 $screenshot.attr( 'src', source );
2652 * Show or hide the theme based on the presence of the term in the title, description, and author.
2656 filter: function( term ) {
2658 haystack = control.params.theme.name + ' ' +
2659 control.params.theme.description + ' ' +
2660 control.params.theme.tags + ' ' +
2661 control.params.theme.author;
2662 haystack = haystack.toLowerCase().replace( '-', ' ' );
2663 if ( -1 !== haystack.search( term ) ) {
2666 control.deactivate();
2671 // Change objects contained within the main customize object to Settings.
2672 api.defaultConstructor = api.Setting;
2674 // Create the collections for Controls, Sections and Panels.
2675 api.control = new api.Values({ defaultConstructor: api.Control });
2676 api.section = new api.Values({ defaultConstructor: api.Section });
2677 api.panel = new api.Values({ defaultConstructor: api.Panel });
2680 * An object that fetches a preview in the background of the document, which
2681 * allows for seamless replacement of an existing preview.
2684 * @augments wp.customize.Messenger
2685 * @augments wp.customize.Class
2686 * @mixes wp.customize.Events
2688 api.PreviewFrame = api.Messenger.extend({
2692 * Initialize the PreviewFrame.
2694 * @param {object} params.container
2695 * @param {object} params.signature
2696 * @param {object} params.previewUrl
2697 * @param {object} params.query
2698 * @param {object} options
2700 initialize: function( params, options ) {
2701 var deferred = $.Deferred();
2704 * Make the instance of the PreviewFrame the promise object
2705 * so other objects can easily interact with it.
2707 deferred.promise( this );
2709 this.container = params.container;
2710 this.signature = params.signature;
2712 $.extend( params, { channel: api.PreviewFrame.uuid() });
2714 api.Messenger.prototype.initialize.call( this, params, options );
2716 this.add( 'previewUrl', params.previewUrl );
2718 this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
2720 this.run( deferred );
2724 * Run the preview request.
2726 * @param {object} deferred jQuery Deferred object to be resolved with
2729 run: function( deferred ) {
2734 if ( this._ready ) {
2735 this.unbind( 'ready', this._ready );
2738 this._ready = function() {
2742 deferred.resolveWith( self );
2746 this.bind( 'ready', this._ready );
2748 this.bind( 'ready', function ( data ) {
2750 this.container.addClass( 'iframe-ready' );
2757 * Walk over all panels, sections, and controls and set their
2758 * respective active states to true if the preview explicitly
2759 * indicates as such.
2762 panel: data.activePanels,
2763 section: data.activeSections,
2764 control: data.activeControls
2766 _( constructs ).each( function ( activeConstructs, type ) {
2767 api[ type ].each( function ( construct, id ) {
2768 var active = !! ( activeConstructs && activeConstructs[ id ] );
2770 construct.activate();
2772 construct.deactivate();
2778 this.request = $.ajax( this.previewUrl(), {
2782 withCredentials: true
2786 this.request.fail( function() {
2787 deferred.rejectWith( self, [ 'request failure' ] );
2790 this.request.done( function( response ) {
2791 var location = self.request.getResponseHeader('Location'),
2792 signature = self.signature,
2795 // Check if the location response header differs from the current URL.
2796 // If so, the request was redirected; try loading the requested page.
2797 if ( location && location !== self.previewUrl() ) {
2798 deferred.rejectWith( self, [ 'redirect', location ] );
2802 // Check if the user is not logged in.
2803 if ( '0' === response ) {
2804 self.login( deferred );
2808 // Check for cheaters.
2809 if ( '-1' === response ) {
2810 deferred.rejectWith( self, [ 'cheatin' ] );
2814 // Check for a signature in the request.
2815 index = response.lastIndexOf( signature );
2816 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
2817 deferred.rejectWith( self, [ 'unsigned' ] );
2821 // Strip the signature from the request.
2822 response = response.slice( 0, index ) + response.slice( index + signature.length );
2824 // Create the iframe and inject the html content.
2825 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
2827 // Bind load event after the iframe has been added to the page;
2828 // otherwise it will fire when injected into the DOM.
2829 self.iframe.one( 'load', function() {
2833 deferred.resolveWith( self );
2835 setTimeout( function() {
2836 deferred.rejectWith( self, [ 'ready timeout' ] );
2837 }, self.sensitivity );
2841 self.targetWindow( self.iframe[0].contentWindow );
2843 self.targetWindow().document.open();
2844 self.targetWindow().document.write( response );
2845 self.targetWindow().document.close();
2849 login: function( deferred ) {
2853 reject = function() {
2854 deferred.rejectWith( self, [ 'logged out' ] );
2857 if ( this.triedLogin ) {
2861 // Check if we have an admin cookie.
2862 $.get( api.settings.url.ajax, {
2864 }).fail( reject ).done( function( response ) {
2867 if ( '1' !== response ) {
2871 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
2872 iframe.appendTo( self.container );
2873 iframe.load( function() {
2874 self.triedLogin = true;
2877 self.run( deferred );
2882 destroy: function() {
2883 api.Messenger.prototype.destroy.call( this );
2884 this.request.abort();
2887 this.iframe.remove();
2889 delete this.request;
2891 delete this.targetWindow;
2898 * Create a universally unique identifier.
2902 api.PreviewFrame.uuid = function() {
2903 return 'preview-' + uuid++;
2908 * Set the document title of the customizer.
2912 * @param {string} documentTitle
2914 api.setDocumentTitle = function ( documentTitle ) {
2916 tmpl = api.settings.documentTitleTmpl;
2917 title = tmpl.replace( '%s', documentTitle );
2918 document.title = title;
2919 api.trigger( 'title', title );
2924 * @augments wp.customize.Messenger
2925 * @augments wp.customize.Class
2926 * @mixes wp.customize.Events
2928 api.Previewer = api.Messenger.extend({
2932 * @param {array} params.allowedUrls
2933 * @param {string} params.container A selector or jQuery element for the preview
2934 * frame to be placed.
2935 * @param {string} params.form
2936 * @param {string} params.previewUrl The URL to preview.
2937 * @param {string} params.signature
2938 * @param {object} options
2940 initialize: function( params, options ) {
2942 rscheme = /^https?/;
2944 $.extend( this, options || {} );
2946 active: $.Deferred()
2950 * Wrap this.refresh to prevent it from hammering the servers:
2952 * If refresh is called once and no other refresh requests are
2953 * loading, trigger the request immediately.
2955 * If refresh is called while another refresh request is loading,
2956 * debounce the refresh requests:
2957 * 1. Stop the loading request (as it is instantly outdated).
2958 * 2. Trigger the new request once refresh hasn't been called for
2959 * self.refreshBuffer milliseconds.
2961 this.refresh = (function( self ) {
2962 var refresh = self.refresh,
2963 callback = function() {
2965 refresh.call( self );
2970 if ( typeof timeout !== 'number' ) {
2971 if ( self.loading ) {
2978 clearTimeout( timeout );
2979 timeout = setTimeout( callback, self.refreshBuffer );
2983 this.container = api.ensure( params.container );
2984 this.allowedUrls = params.allowedUrls;
2985 this.signature = params.signature;
2987 params.url = window.location.href;
2989 api.Messenger.prototype.initialize.call( this, params );
2991 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
2992 var match = to.match( rscheme );
2993 return match ? match[0] : '';
2996 // Limit the URL to internal, front-end links.
2998 // If the frontend and the admin are served from the same domain, load the
2999 // preview over ssl if the Customizer is being loaded over ssl. This avoids
3000 // insecure content warnings. This is not attempted if the admin and frontend
3001 // are on different domains to avoid the case where the frontend doesn't have
3004 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
3007 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
3008 // Strip hashes and query strings before testing.
3009 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
3012 // Attempt to match the URL to the control frame's scheme
3013 // and check if it's allowed. If not, try the original URL.
3014 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
3015 $.each( self.allowedUrls, function( i, allowed ) {
3018 allowed = allowed.replace( /\/+$/, '' );
3019 path = url.replace( allowed, '' );
3021 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
3030 // If we found a matching result, return it. If not, bail.
3031 return result ? result : null;
3034 // Refresh the preview when the URL is changed (but not yet).
3035 this.previewUrl.bind( this.refresh );
3038 this.bind( 'scroll', function( distance ) {
3039 this.scroll = distance;
3042 // Update the URL when the iframe sends a URL message.
3043 this.bind( 'url', this.previewUrl );
3045 // Update the document title when the preview changes.
3046 this.bind( 'documentTitle', function ( title ) {
3047 api.setDocumentTitle( title );
3052 * Query string data sent with each preview request.
3056 query: function() {},
3059 if ( this.loading ) {
3060 this.loading.destroy();
3061 delete this.loading;
3066 * Refresh the preview.
3068 refresh: function() {
3071 // Display loading indicator
3072 this.send( 'loading-initiated' );
3076 this.loading = new api.PreviewFrame({
3078 previewUrl: this.previewUrl(),
3079 query: this.query() || {},
3080 container: this.container,
3081 signature: this.signature
3084 this.loading.done( function() {
3085 // 'this' is the loading frame
3086 this.bind( 'synced', function() {
3088 self.preview.destroy();
3089 self.preview = this;
3090 delete self.loading;
3092 self.targetWindow( this.targetWindow() );
3093 self.channel( this.channel() );
3095 self.deferred.active.resolve();
3096 self.send( 'active' );
3099 this.send( 'sync', {
3100 scroll: self.scroll,
3105 this.loading.fail( function( reason, location ) {
3106 self.send( 'loading-failed' );
3107 if ( 'redirect' === reason && location ) {
3108 self.previewUrl( location );
3111 if ( 'logged out' === reason ) {
3112 if ( self.preview ) {
3113 self.preview.destroy();
3114 delete self.preview;
3117 self.login().done( self.refresh );
3120 if ( 'cheatin' === reason ) {
3127 var previewer = this,
3128 deferred, messenger, iframe;
3133 deferred = $.Deferred();
3134 this._login = deferred.promise();
3136 messenger = new api.Messenger({
3138 url: api.settings.url.login
3141 iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
3143 messenger.targetWindow( iframe[0].contentWindow );
3145 messenger.bind( 'login', function () {
3146 var refreshNonces = previewer.refreshNonces();
3148 refreshNonces.always( function() {
3150 messenger.destroy();
3151 delete previewer._login;
3154 refreshNonces.done( function() {
3158 refreshNonces.fail( function() {
3159 previewer.cheatin();
3167 cheatin: function() {
3168 $( document.body ).empty().addClass( 'cheatin' ).append(
3169 '<h1>' + api.l10n.cheatin + '</h1>' +
3170 '<p>' + api.l10n.notAllowed + '</p>'
3174 refreshNonces: function() {
3175 var request, deferred = $.Deferred();
3179 request = wp.ajax.post( 'customize_refresh_nonces', {
3181 theme: api.settings.theme.stylesheet
3184 request.done( function( response ) {
3185 api.trigger( 'nonce-refresh', response );
3189 request.fail( function() {
3197 api.controlConstructor = {
3198 color: api.ColorControl,
3199 media: api.MediaControl,
3200 upload: api.UploadControl,
3201 image: api.ImageControl,
3202 cropped_image: api.CroppedImageControl,
3203 site_icon: api.SiteIconControl,
3204 header: api.HeaderControl,
3205 background: api.BackgroundControl,
3206 theme: api.ThemeControl
3208 api.panelConstructor = {};
3209 api.sectionConstructor = {
3210 themes: api.ThemesSection
3214 api.settings = window._wpCustomizeSettings;
3215 api.l10n = window._wpCustomizeControlsL10n;
3217 // Check if we can run the Customizer.
3218 if ( ! api.settings ) {
3222 // Bail if any incompatibilities are found.
3223 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
3227 var parent, topFocus,
3228 body = $( document.body ),
3229 overlay = body.children( '.wp-full-overlay' ),
3230 title = $( '#customize-info .panel-title.site-title' ),
3231 closeBtn = $( '.customize-controls-close' ),
3232 saveBtn = $( '#save' );
3234 // Prevent the form from saving when enter is pressed on an input or select element.
3235 $('#customize-controls').on( 'keydown', function( e ) {
3236 var isEnter = ( 13 === e.which ),
3237 $el = $( e.target );
3239 if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
3244 // Expand/Collapse the main customizer customize info.
3245 $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
3246 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3249 event.preventDefault(); // Keep this AFTER the key filter above
3251 var section = $( this ).closest( '.accordion-section' ),
3252 content = section.find( '.customize-panel-description:first' );
3254 if ( section.hasClass( 'cannot-expand' ) ) {
3258 if ( section.hasClass( 'open' ) ) {
3259 section.toggleClass( 'open' );
3260 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
3261 $( this ).attr( 'aria-expanded', false );
3263 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
3264 section.toggleClass( 'open' );
3265 $( this ).attr( 'aria-expanded', true );
3269 // Initialize Previewer
3270 api.previewer = new api.Previewer({
3271 container: '#customize-preview',
3272 form: '#customize-controls',
3273 previewUrl: api.settings.url.preview,
3274 allowedUrls: api.settings.url.allowed,
3275 signature: 'WP_CUSTOMIZER_SIGNATURE'
3278 nonce: api.settings.nonce,
3281 * Build the query to send along with the Preview request.
3286 var dirtyCustomized = {};
3287 api.each( function ( value, key ) {
3288 if ( value._dirty ) {
3289 dirtyCustomized[ key ] = value();
3295 theme: api.settings.theme.stylesheet,
3296 customized: JSON.stringify( dirtyCustomized ),
3297 nonce: this.nonce.preview
3303 processing = api.state( 'processing' ),
3304 submitWhenDoneProcessing,
3307 body.addClass( 'saving' );
3309 submit = function () {
3311 query = $.extend( self.query(), {
3312 nonce: self.nonce.save
3314 request = wp.ajax.post( 'customize_save', query );
3316 api.trigger( 'save', request );
3318 request.always( function () {
3319 body.removeClass( 'saving' );
3322 request.fail( function ( response ) {
3323 if ( '0' === response ) {
3324 response = 'not_logged_in';
3325 } else if ( '-1' === response ) {
3326 // Back-compat in case any other check_ajax_referer() call is dying
3327 response = 'invalid_nonce';
3330 if ( 'invalid_nonce' === response ) {
3332 } else if ( 'not_logged_in' === response ) {
3333 self.preview.iframe.hide();
3334 self.login().done( function() {
3336 self.preview.iframe.show();
3339 api.trigger( 'error', response );
3342 request.done( function( response ) {
3343 // Clear setting dirty states
3344 api.each( function ( value ) {
3345 value._dirty = false;
3348 api.trigger( 'saved', response );
3352 if ( 0 === processing() ) {
3355 submitWhenDoneProcessing = function () {
3356 if ( 0 === processing() ) {
3357 api.state.unbind( 'change', submitWhenDoneProcessing );
3361 api.state.bind( 'change', submitWhenDoneProcessing );
3367 // Refresh the nonces if the preview sends updated nonces over.
3368 api.previewer.bind( 'nonce', function( nonce ) {
3369 $.extend( this.nonce, nonce );
3372 // Refresh the nonces if login sends updated nonces over.
3373 api.bind( 'nonce-refresh', function( nonce ) {
3374 $.extend( api.settings.nonce, nonce );
3375 $.extend( api.previewer.nonce, nonce );
3379 $.each( api.settings.settings, function( id, data ) {
3380 api.create( id, id, data.value, {
3381 transport: data.transport,
3382 previewer: api.previewer,
3383 dirty: !! data.dirty
3388 $.each( api.settings.panels, function ( id, data ) {
3389 var constructor = api.panelConstructor[ data.type ] || api.Panel,
3392 panel = new constructor( id, {
3395 api.panel.add( id, panel );
3399 $.each( api.settings.sections, function ( id, data ) {
3400 var constructor = api.sectionConstructor[ data.type ] || api.Section,
3403 section = new constructor( id, {
3406 api.section.add( id, section );
3410 $.each( api.settings.controls, function( id, data ) {
3411 var constructor = api.controlConstructor[ data.type ] || api.Control,
3414 control = new constructor( id, {
3416 previewer: api.previewer
3418 api.control.add( id, control );
3421 // Focus the autofocused element
3422 _.each( [ 'panel', 'section', 'control' ], function ( type ) {
3423 var instance, id = api.settings.autofocus[ type ];
3424 if ( id && api[ type ]( id ) ) {
3425 instance = api[ type ]( id );
3426 // Wait until the element is embedded in the DOM
3427 instance.deferred.embedded.done( function () {
3428 // Wait until the preview has activated and so active panels, sections, controls have been set
3429 api.previewer.deferred.active.done( function () {
3437 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
3441 api.reflowPaneContents = _.bind( function () {
3443 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3445 if ( document.activeElement ) {
3446 activeElement = $( document.activeElement );
3449 // Sort the sections within each panel
3450 api.panel.each( function ( panel ) {
3451 var sections = panel.sections(),
3452 sectionContainers = _.pluck( sections, 'container' );
3453 rootNodes.push( panel );
3454 appendContainer = panel.container.find( 'ul:first' );
3455 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
3456 _( sections ).each( function ( section ) {
3457 appendContainer.append( section.container );
3463 // Sort the controls within each section
3464 api.section.each( function ( section ) {
3465 var controls = section.controls(),
3466 controlContainers = _.pluck( controls, 'container' );
3467 if ( ! section.panel() ) {
3468 rootNodes.push( section );
3470 appendContainer = section.container.find( 'ul:first' );
3471 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
3472 _( controls ).each( function ( control ) {
3473 appendContainer.append( control.container );
3479 // Sort the root panels and sections
3480 rootNodes.sort( api.utils.prioritySort );
3481 rootContainers = _.pluck( rootNodes, 'container' );
3482 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
3483 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
3484 _( rootNodes ).each( function ( rootNode ) {
3485 appendContainer.append( rootNode.container );
3490 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
3491 api.panel.each( function ( panel ) {
3492 var value = panel.active();
3493 panel.active.callbacks.fireWith( panel.active, [ value, value ] );
3495 api.section.each( function ( section ) {
3496 var value = section.active();
3497 section.active.callbacks.fireWith( section.active, [ value, value ] );
3500 // Restore focus if there was a reflow and there was an active (focused) element
3501 if ( wasReflowed && activeElement ) {
3502 activeElement.focus();
3504 api.trigger( 'pane-contents-reflowed' );
3506 api.bind( 'ready', api.reflowPaneContents );
3507 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
3508 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
3509 values.bind( 'add', api.reflowPaneContents );
3510 values.bind( 'change', api.reflowPaneContents );
3511 values.bind( 'remove', api.reflowPaneContents );
3514 // Check if preview url is valid and load the preview frame.
3515 if ( api.previewer.previewUrl() ) {
3516 api.previewer.refresh();
3518 api.previewer.previewUrl( api.settings.url.home );
3521 // Save and activated states
3523 var state = new api.Values(),
3524 saved = state.create( 'saved' ),
3525 activated = state.create( 'activated' ),
3526 processing = state.create( 'processing' );
3528 state.bind( 'change', function() {
3529 if ( ! activated() ) {
3530 saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
3531 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3533 } else if ( saved() ) {
3534 saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
3535 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
3538 saveBtn.val( api.l10n.save ).prop( 'disabled', false );
3539 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3543 // Set default states.
3545 activated( api.settings.theme.active );
3548 api.bind( 'change', function() {
3549 state('saved').set( false );
3552 api.bind( 'saved', function() {
3553 state('saved').set( true );
3554 state('activated').set( true );
3557 activated.bind( function( to ) {
3559 api.trigger( 'activated' );
3562 // Expose states to the API.
3567 saveBtn.click( function( event ) {
3568 api.previewer.save();
3569 event.preventDefault();
3570 }).keydown( function( event ) {
3571 if ( 9 === event.which ) // tab
3573 if ( 13 === event.which ) // enter
3574 api.previewer.save();
3575 event.preventDefault();
3578 closeBtn.keydown( function( event ) {
3579 if ( 9 === event.which ) // tab
3581 if ( 13 === event.which ) // enter
3583 event.preventDefault();
3586 $( '.collapse-sidebar' ).on( 'click', function() {
3587 if ( 'true' === $( this ).attr( 'aria-expanded' ) ) {
3588 $( this ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
3590 $( this ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
3593 overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
3596 $( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) {
3597 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3601 overlay.toggleClass( 'preview-only' );
3602 event.preventDefault();
3605 // Bind site title display to the corresponding field.
3606 if ( title.length ) {
3607 $( '#customize-control-blogname input' ).on( 'input', function() {
3608 title.text( this.value );
3613 * Create a postMessage connection with a parent frame,
3614 * in case the Customizer frame was opened with the Customize loader.
3616 * @see wp.customize.Loader
3618 parent = new api.Messenger({
3619 url: api.settings.url.parent,
3624 * If we receive a 'back' event, we're inside an iframe.
3625 * Send any clicks to the 'Return' link to the parent page.
3627 parent.bind( 'back', function() {
3628 closeBtn.on( 'click.customize-controls-close', function( event ) {
3629 event.preventDefault();
3630 parent.send( 'close' );
3634 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
3635 $( window ).on( 'beforeunload', function () {
3636 if ( ! api.state( 'saved' )() ) {
3637 setTimeout( function() {
3638 overlay.removeClass( 'customize-loading' );
3640 return api.l10n.saveAlert;
3644 // Pass events through to the parent.
3645 $.each( [ 'saved', 'change' ], function ( i, event ) {
3646 api.bind( event, function() {
3647 parent.send( event );
3652 * When activated, let the loader handle redirecting the page.
3653 * If no loader exists, redirect the page ourselves (if a url exists).
3655 api.bind( 'activated', function() {
3656 if ( parent.targetWindow() )
3657 parent.send( 'activated', api.settings.url.activated );
3658 else if ( api.settings.url.activated )
3659 window.location = api.settings.url.activated;
3662 // Pass titles to the parent
3663 api.bind( 'title', function( newTitle ) {
3664 parent.send( 'title', newTitle );
3667 // Initialize the connection with the parent frame.
3668 parent.send( 'ready' );
3670 // Control visibility for default controls
3672 'background_image': {
3673 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
3674 callback: function( to ) { return !! to; }
3677 controls: [ 'page_on_front', 'page_for_posts' ],
3678 callback: function( to ) { return 'page' === to; }
3680 'header_textcolor': {
3681 controls: [ 'header_textcolor' ],
3682 callback: function( to ) { return 'blank' !== to; }
3684 }, function( settingId, o ) {
3685 api( settingId, function( setting ) {
3686 $.each( o.controls, function( i, controlId ) {
3687 api.control( controlId, function( control ) {
3688 var visibility = function( to ) {
3689 control.container.toggle( o.callback( to ) );
3692 visibility( setting.get() );
3693 setting.bind( visibility );
3699 // Juggle the two controls that use header_textcolor
3700 api.control( 'display_header_text', function( control ) {
3703 control.elements[0].unsync( api( 'header_textcolor' ) );
3705 control.element = new api.Element( control.container.find('input') );
3706 control.element.set( 'blank' !== control.setting() );
3708 control.element.bind( function( to ) {
3710 last = api( 'header_textcolor' ).get();
3712 control.setting.set( to ? last : 'blank' );
3715 control.setting.bind( function( to ) {
3716 control.element.set( 'blank' !== to );
3720 // Change previewed URL to the homepage when changing the page_on_front.
3721 api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
3722 var updatePreviewUrl = function() {
3723 if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
3724 api.previewer.previewUrl.set( api.settings.url.home );
3727 showOnFront.bind( updatePreviewUrl );
3728 pageOnFront.bind( updatePreviewUrl );
3731 // Change the previewed URL to the selected page when changing the page_for_posts.
3732 api( 'page_for_posts', function( setting ) {
3733 setting.bind(function( pageId ) {
3734 pageId = parseInt( pageId, 10 );
3736 api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
3741 api.trigger( 'ready' );
3743 // Make sure left column gets focus
3744 topFocus = closeBtn;
3746 setTimeout(function () {