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 {Function} [params.completeCallback]
79 focus = function ( params ) {
80 var construct, completeCallback, focus, focusElement;
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 focusElement = focusContainer.find( '.control-focus:first' );
94 if ( 0 === focusElement.length ) {
95 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
96 focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
100 if ( params.completeCallback ) {
101 completeCallback = params.completeCallback;
102 params.completeCallback = function () {
107 params.completeCallback = focus;
109 if ( construct.expand ) {
110 construct.expand( params );
112 params.completeCallback();
117 * Stable sort for Panels, Sections, and Controls.
119 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
123 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
124 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
127 api.utils.prioritySort = function ( a, b ) {
128 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
129 return a.params.instanceNumber - b.params.instanceNumber;
131 return a.priority() - b.priority();
136 * Return whether the supplied Event object is for a keydown event but not the Enter key.
140 * @param {jQuery.Event} event
143 api.utils.isKeydownButNotEnterEvent = function ( event ) {
144 return ( 'keydown' === event.type && 13 !== event.which );
148 * Return whether the two lists of elements are the same and are in the same order.
152 * @param {Array|jQuery} listA
153 * @param {Array|jQuery} listB
156 api.utils.areElementListsEqual = function ( listA, listB ) {
158 listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
159 -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
160 _.zip( listA, listB ), // pair up each element between the two lists
162 return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
164 ), false ) // check for presence of false in map's return value
170 * Base class for Panel and Section.
175 * @augments wp.customize.Class
177 Container = api.Class.extend({
178 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
179 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
180 containerType: 'container',
194 * @param {string} id - The ID for the container.
195 * @param {object} options - Object containing one property: params.
196 * @param {object} options.params - Object containing the following properties.
197 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
198 * @param {string=} [options.params.description] - Description shown at the top of the panel.
199 * @param {number=100} [options.params.priority] - The sort priority for the panel.
200 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
201 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
202 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
204 initialize: function ( id, options ) {
205 var container = this;
207 options = options || {};
209 options.params = _.defaults(
210 options.params || {},
214 $.extend( container, options );
215 container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
216 container.container = $( container.params.content );
217 if ( 0 === container.container.length ) {
218 container.container = $( container.getContainer() );
221 container.deferred = {
222 embedded: new $.Deferred()
224 container.priority = new api.Value();
225 container.active = new api.Value();
226 container.activeArgumentsQueue = [];
227 container.expanded = new api.Value();
228 container.expandedArgumentsQueue = [];
230 container.active.bind( function ( active ) {
231 var args = container.activeArgumentsQueue.shift();
232 args = $.extend( {}, container.defaultActiveArguments, args );
233 active = ( active && container.isContextuallyActive() );
234 container.onChangeActive( active, args );
236 container.expanded.bind( function ( expanded ) {
237 var args = container.expandedArgumentsQueue.shift();
238 args = $.extend( {}, container.defaultExpandedArguments, args );
239 container.onChangeExpanded( expanded, args );
242 container.deferred.embedded.done( function () {
243 container.attachEvents();
246 api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
248 container.priority.set( container.params.priority );
249 container.active.set( container.params.active );
250 container.expanded.set( false );
258 ready: function() {},
261 * Get the child models associated with this parent, sorting them by their priority Value.
265 * @param {String} parentType
266 * @param {String} childType
269 _children: function ( parentType, childType ) {
272 api[ childType ].each( function ( child ) {
273 if ( child[ parentType ].get() === parent.id ) {
274 children.push( child );
277 children.sort( api.utils.prioritySort );
282 * To override by subclass, to return whether the container has active children.
288 isContextuallyActive: function () {
289 throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
293 * Active state change handler.
295 * Shows the container if it is active, hides it if not.
297 * To override by subclass, update the container's UI to reflect the provided active state.
301 * @param {Boolean} active
302 * @param {Object} args
303 * @param {Object} args.duration
304 * @param {Object} args.completeCallback
306 onChangeActive: function( active, args ) {
307 var duration, construct = this, expandedOtherPanel;
308 if ( args.unchanged ) {
309 if ( args.completeCallback ) {
310 args.completeCallback();
315 duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
317 if ( construct.extended( api.Panel ) ) {
318 // If this is a panel is not currently expanded but another panel is expanded, do not animate.
319 api.panel.each(function ( panel ) {
320 if ( panel !== construct && panel.expanded() ) {
321 expandedOtherPanel = panel;
326 // Collapse any expanded sections inside of this panel first before deactivating.
328 _.each( construct.sections(), function( section ) {
329 section.collapse( { duration: 0 } );
334 if ( ! $.contains( document, construct.container[0] ) ) {
335 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
336 construct.container.toggle( active );
337 if ( args.completeCallback ) {
338 args.completeCallback();
340 } else if ( active ) {
341 construct.container.stop( true, true ).slideDown( duration, args.completeCallback );
343 if ( construct.expanded() ) {
346 completeCallback: function() {
347 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
351 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
355 // Recalculate the margin-top immediately, not waiting for debounced reflow, to prevent momentary (100ms) vertical jiggle.
356 if ( expandedOtherPanel ) {
357 expandedOtherPanel._recalculateTopMargin();
364 * @params {Boolean} active
365 * @param {Object} [params]
366 * @returns {Boolean} false if state already applied
368 _toggleActive: function ( active, params ) {
370 params = params || {};
371 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
372 params.unchanged = true;
373 self.onChangeActive( self.active.get(), params );
376 params.unchanged = false;
377 this.activeArgumentsQueue.push( params );
378 this.active.set( active );
384 * @param {Object} [params]
385 * @returns {Boolean} false if already active
387 activate: function ( params ) {
388 return this._toggleActive( true, params );
392 * @param {Object} [params]
393 * @returns {Boolean} false if already inactive
395 deactivate: function ( params ) {
396 return this._toggleActive( false, params );
400 * To override by subclass, update the container's UI to reflect the provided active state.
403 onChangeExpanded: function () {
404 throw new Error( 'Must override with subclass.' );
408 * Handle the toggle logic for expand/collapse.
410 * @param {Boolean} expanded - The new state to apply.
411 * @param {Object} [params] - Object containing options for expand/collapse.
412 * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
413 * @returns {Boolean} false if state already applied or active state is false
415 _toggleExpanded: function( expanded, params ) {
416 var instance = this, previousCompleteCallback;
417 params = params || {};
418 previousCompleteCallback = params.completeCallback;
420 // Short-circuit expand() if the instance is not active.
421 if ( expanded && ! instance.active() ) {
425 params.completeCallback = function() {
426 if ( previousCompleteCallback ) {
427 previousCompleteCallback.apply( instance, arguments );
430 instance.container.trigger( 'expanded' );
432 instance.container.trigger( 'collapsed' );
435 if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
436 params.unchanged = true;
437 instance.onChangeExpanded( instance.expanded.get(), params );
440 params.unchanged = false;
441 instance.expandedArgumentsQueue.push( params );
442 instance.expanded.set( expanded );
448 * @param {Object} [params]
449 * @returns {Boolean} false if already expanded or if inactive.
451 expand: function ( params ) {
452 return this._toggleExpanded( true, params );
456 * @param {Object} [params]
457 * @returns {Boolean} false if already collapsed.
459 collapse: function ( params ) {
460 return this._toggleExpanded( false, params );
464 * Bring the container into view and then expand this and bring it into view
465 * @param {Object} [params]
470 * Return the container html, generated from its JS template, if it exists.
474 getContainer: function () {
478 if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
479 template = wp.template( container.templateSelector );
481 template = wp.template( 'customize-' + container.containerType + '-default' );
483 if ( template && container.container ) {
484 return $.trim( template( container.params ) );
495 * @augments wp.customize.Class
497 api.Section = Container.extend({
498 containerType: 'section',
506 instanceNumber: null,
514 * @param {string} id - The ID for the section.
515 * @param {object} options - Object containing one property: params.
516 * @param {object} options.params - Object containing the following properties.
517 * @param {string} options.params.title - Title shown when section is collapsed and expanded.
518 * @param {string=} [options.params.description] - Description shown at the top of the section.
519 * @param {number=100} [options.params.priority] - The sort priority for the section.
520 * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
521 * @param {string=} [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
522 * @param {boolean=true} [options.params.active] - Whether the section is active or not.
523 * @param {string} options.params.panel - The ID for the panel this section is associated with.
524 * @param {string=} [options.params.customizeAction] - Additional context information shown before the section title when expanded.
526 initialize: function ( id, options ) {
528 Container.prototype.initialize.call( section, id, options );
531 section.panel = new api.Value();
532 section.panel.bind( function ( id ) {
533 $( section.container ).toggleClass( 'control-subsection', !! id );
535 section.panel.set( section.params.panel || '' );
536 api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
539 section.deferred.embedded.done( function () {
545 * Embed the container in the DOM when any parent panel is ready.
550 var section = this, inject;
552 // Watch for changes to the panel state
553 inject = function ( panelId ) {
556 // The panel has been supplied, so wait until the panel object is registered
557 api.panel( panelId, function ( panel ) {
558 // The panel has been registered, wait for it to become ready/initialized
559 panel.deferred.embedded.done( function () {
560 parentContainer = panel.container.find( 'ul:first' );
561 if ( ! section.container.parent().is( parentContainer ) ) {
562 parentContainer.append( section.container );
564 section.deferred.embedded.resolve();
568 // There is no panel, so embed the section in the root of the customizer
569 parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
570 if ( ! section.container.parent().is( parentContainer ) ) {
571 parentContainer.append( section.container );
573 section.deferred.embedded.resolve();
576 section.panel.bind( inject );
577 inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
579 section.deferred.embedded.done(function() {
580 // Fix the top margin after reflow.
581 api.bind( 'pane-contents-reflowed', _.debounce( function() {
582 section._recalculateTopMargin();
588 * Add behaviors for the accordion section.
592 attachEvents: function () {
595 // Expand/Collapse accordion sections on click.
596 section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
597 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
600 event.preventDefault(); // Keep this AFTER the key filter above
602 if ( section.expanded() ) {
611 * Return whether this section has any active controls.
617 isContextuallyActive: function () {
619 controls = section.controls(),
621 _( controls ).each( function ( control ) {
622 if ( control.active() ) {
626 return ( activeCount !== 0 );
630 * Get the controls that are associated with this section, sorted by their priority Value.
636 controls: function () {
637 return this._children( 'section', 'control' );
641 * Update UI to reflect expanded state.
645 * @param {Boolean} expanded
646 * @param {Object} args
648 onChangeExpanded: function ( expanded, args ) {
650 container = section.container.closest( '.wp-full-overlay-sidebar-content' ),
651 content = section.container.find( '.accordion-section-content' ),
652 overlay = section.container.closest( '.wp-full-overlay' ),
653 backBtn = section.container.find( '.customize-section-back' ),
654 sectionTitle = section.container.find( '.accordion-section-title' ).first(),
655 headerActionsHeight = $( '#customize-header-actions' ).height(),
656 resizeContentHeight, expand, position, scroll;
658 if ( expanded && ! section.container.hasClass( 'open' ) ) {
660 if ( args.unchanged ) {
661 expand = args.completeCallback;
663 container.scrollTop( 0 );
664 resizeContentHeight = function() {
665 var matchMedia, offset;
666 matchMedia = window.matchMedia || window.msMatchMedia;
667 offset = 90; // 45px for customize header actions + 45px for footer actions.
669 // No footer on small screens.
670 if ( matchMedia && matchMedia( '(max-width: 640px)' ).matches ) {
673 content.css( 'height', ( window.innerHeight - offset ) );
675 expand = function() {
676 section.container.addClass( 'open' );
677 overlay.addClass( 'section-open' );
678 position = content.offset().top;
679 scroll = container.scrollTop();
680 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
681 resizeContentHeight();
682 sectionTitle.attr( 'tabindex', '-1' );
683 backBtn.attr( 'tabindex', '0' );
685 if ( args.completeCallback ) {
686 args.completeCallback();
689 // Fix the height after browser resize.
690 $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
692 setTimeout( _.bind( section._recalculateTopMargin, section ), 0 );
696 if ( ! args.allowMultiple ) {
697 api.section.each( function ( otherSection ) {
698 if ( otherSection !== section ) {
699 otherSection.collapse( { duration: args.duration } );
704 if ( section.panel() ) {
705 api.panel( section.panel() ).expand({
706 duration: args.duration,
707 completeCallback: expand
710 api.panel.each( function( panel ) {
716 } else if ( ! expanded && section.container.hasClass( 'open' ) ) {
717 section.container.removeClass( 'open' );
718 overlay.removeClass( 'section-open' );
719 content.css( 'margin-top', '' );
720 container.scrollTop( 0 );
721 backBtn.attr( 'tabindex', '-1' );
722 sectionTitle.attr( 'tabindex', '0' );
723 sectionTitle.focus();
724 if ( args.completeCallback ) {
725 args.completeCallback();
727 $( window ).off( 'resize.customizer-section' );
729 if ( args.completeCallback ) {
730 args.completeCallback();
736 * Recalculate the top margin.
741 _recalculateTopMargin: function() {
742 var section = this, content, offset, headerActionsHeight;
743 content = section.container.find( '.accordion-section-content' );
744 if ( 0 === content.length ) {
747 headerActionsHeight = $( '#customize-header-actions' ).height();
748 offset = ( content.offset().top - headerActionsHeight );
750 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
756 * wp.customize.ThemesSection
758 * Custom section for themes that functions similarly to a backwards panel,
759 * and also handles the theme-details view rendering and navigation.
762 * @augments wp.customize.Section
763 * @augments wp.customize.Container
765 api.ThemesSection = api.Section.extend({
769 screenshotQueue: null,
770 $window: $( window ),
775 initialize: function () {
776 this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
777 return api.Section.prototype.initialize.apply( this, arguments );
785 section.overlay = section.container.find( '.theme-overlay' );
786 section.template = wp.template( 'customize-themes-details-view' );
788 // Bind global keyboard events.
789 $( 'body' ).on( 'keyup', function( event ) {
790 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
794 // Pressing the right arrow key fires a theme:next event
795 if ( 39 === event.keyCode ) {
799 // Pressing the left arrow key fires a theme:previous event
800 if ( 37 === event.keyCode ) {
801 section.previousTheme();
804 // Pressing the escape key fires a theme:collapse event
805 if ( 27 === event.keyCode ) {
806 section.closeDetails();
810 _.bindAll( this, 'renderScreenshots' );
814 * Override Section.isContextuallyActive method.
816 * Ignore the active states' of the contained theme controls, and just
817 * use the section's own active state instead. This ensures empty search
818 * results for themes to cause the section to become inactive.
824 isContextuallyActive: function () {
825 return this.active();
831 attachEvents: function () {
834 // Expand/Collapse section/panel.
835 section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
836 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
839 event.preventDefault(); // Keep this AFTER the key filter above
841 if ( section.expanded() ) {
848 // Theme navigation in details view.
849 section.container.on( 'click keydown', '.left', function( event ) {
850 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
854 event.preventDefault(); // Keep this AFTER the key filter above
856 section.previousTheme();
859 section.container.on( 'click keydown', '.right', function( event ) {
860 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
864 event.preventDefault(); // Keep this AFTER the key filter above
869 section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
870 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
874 event.preventDefault(); // Keep this AFTER the key filter above
876 section.closeDetails();
879 var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
880 section.container.on( 'input', '#themes-filter', function( event ) {
882 term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
883 controls = section.controls();
885 _.each( controls, function( control ) {
886 control.filter( term );
891 // Update theme count.
892 count = section.container.find( 'li.customize-control:visible' ).length;
893 section.container.find( '.theme-count' ).text( count );
896 // Pre-load the first 3 theme screenshots.
897 api.bind( 'ready', function () {
898 _.each( section.controls().slice( 0, 3 ), function ( control ) {
899 var img, src = control.params.theme.screenshot[0];
909 * Update UI to reflect expanded state
913 * @param {Boolean} expanded
914 * @param {Object} args
915 * @param {Boolean} args.unchanged
916 * @param {Callback} args.completeCallback
918 onChangeExpanded: function ( expanded, args ) {
920 // Immediately call the complete callback if there were no changes
921 if ( args.unchanged ) {
922 if ( args.completeCallback ) {
923 args.completeCallback();
928 // Note: there is a second argument 'args' passed
929 var position, scroll,
931 section = panel.container.closest( '.accordion-section' ),
932 overlay = section.closest( '.wp-full-overlay' ),
933 container = section.closest( '.wp-full-overlay-sidebar-content' ),
934 siblings = container.find( '.open' ),
935 customizeBtn = section.find( '.customize-theme' ),
936 changeBtn = section.find( '.change-theme' ),
937 content = section.find( '.control-panel-content' );
941 // Collapse any sibling sections/panels
942 api.section.each( function ( otherSection ) {
943 if ( otherSection !== panel ) {
944 otherSection.collapse( { duration: args.duration } );
947 api.panel.each( function ( otherPanel ) {
948 otherPanel.collapse( { duration: 0 } );
951 content.show( 0, function() {
952 position = content.offset().top;
953 scroll = container.scrollTop();
954 content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
955 section.addClass( 'current-panel' );
956 overlay.addClass( 'in-themes-panel' );
957 container.scrollTop( 0 );
958 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
959 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
960 if ( args.completeCallback ) {
961 args.completeCallback();
964 customizeBtn.focus();
966 siblings.removeClass( 'open' );
967 section.removeClass( 'current-panel' );
968 overlay.removeClass( 'in-themes-panel' );
969 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
970 content.delay( 180 ).hide( 0, function() {
971 content.css( 'margin-top', 'inherit' ); // Reset
972 if ( args.completeCallback ) {
973 args.completeCallback();
976 customizeBtn.attr( 'tabindex', '0' );
978 container.scrollTop( 0 );
983 * Recalculate the top margin.
988 _recalculateTopMargin: function() {
989 api.Panel.prototype._recalculateTopMargin.call( this );
993 * Render control's screenshot if the control comes into view.
997 renderScreenshots: function( ) {
1000 // Fill queue initially.
1001 if ( section.screenshotQueue === null ) {
1002 section.screenshotQueue = section.controls();
1005 // Are all screenshots rendered?
1006 if ( ! section.screenshotQueue.length ) {
1010 section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
1011 var $imageWrapper = control.container.find( '.theme-screenshot' ),
1012 $image = $imageWrapper.find( 'img' );
1014 if ( ! $image.length ) {
1018 if ( $image.is( ':hidden' ) ) {
1022 // Based on unveil.js.
1023 var wt = section.$window.scrollTop(),
1024 wb = wt + section.$window.height(),
1025 et = $image.offset().top,
1026 ih = $imageWrapper.height(),
1029 inView = eb >= wt - threshold && et <= wb + threshold;
1032 control.container.trigger( 'render-screenshot' );
1035 // If the image is in view return false so it's cleared from the queue.
1041 * Advance the modal to the next theme.
1045 nextTheme: function () {
1047 if ( section.getNextTheme() ) {
1048 section.showDetails( section.getNextTheme(), function() {
1049 section.overlay.find( '.right' ).focus();
1055 * Get the next theme model.
1059 getNextTheme: function () {
1061 control = api.control( 'theme_' + this.currentTheme );
1062 next = control.container.next( 'li.customize-control-theme' );
1063 if ( ! next.length ) {
1066 next = next[0].id.replace( 'customize-control-', '' );
1067 control = api.control( next );
1069 return control.params.theme;
1073 * Advance the modal to the previous theme.
1077 previousTheme: function () {
1079 if ( section.getPreviousTheme() ) {
1080 section.showDetails( section.getPreviousTheme(), function() {
1081 section.overlay.find( '.left' ).focus();
1087 * Get the previous theme model.
1091 getPreviousTheme: function () {
1092 var control, previous;
1093 control = api.control( 'theme_' + this.currentTheme );
1094 previous = control.container.prev( 'li.customize-control-theme' );
1095 if ( ! previous.length ) {
1098 previous = previous[0].id.replace( 'customize-control-', '' );
1099 control = api.control( previous );
1101 return control.params.theme;
1105 * Disable buttons when we're viewing the first or last theme.
1109 updateLimits: function () {
1110 if ( ! this.getNextTheme() ) {
1111 this.overlay.find( '.right' ).addClass( 'disabled' );
1113 if ( ! this.getPreviousTheme() ) {
1114 this.overlay.find( '.left' ).addClass( 'disabled' );
1119 * Render & show the theme details for a given theme model.
1123 * @param {Object} theme
1125 showDetails: function ( theme, callback ) {
1127 callback = callback || function(){};
1128 section.currentTheme = theme.id;
1129 section.overlay.html( section.template( theme ) )
1132 $( 'body' ).addClass( 'modal-open' );
1133 section.containFocus( section.overlay );
1134 section.updateLimits();
1139 * Close the theme details modal.
1143 closeDetails: function () {
1144 $( 'body' ).removeClass( 'modal-open' );
1145 this.overlay.fadeOut( 'fast' );
1146 api.control( 'theme_' + this.currentTheme ).focus();
1150 * Keep tab focus within the theme details modal.
1154 containFocus: function( el ) {
1157 el.on( 'keydown', function( event ) {
1159 // Return if it's not the tab key
1160 // When navigating with prev/next focus is already handled
1161 if ( 9 !== event.keyCode ) {
1165 // uses jQuery UI to get the tabbable elements
1166 tabbables = $( ':tabbable', el );
1168 // Keep focus within the overlay
1169 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
1170 tabbables.first().focus();
1172 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
1173 tabbables.last().focus();
1184 * @augments wp.customize.Class
1186 api.Panel = Container.extend({
1187 containerType: 'panel',
1192 * @param {string} id - The ID for the panel.
1193 * @param {object} options - Object containing one property: params.
1194 * @param {object} options.params - Object containing the following properties.
1195 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
1196 * @param {string=} [options.params.description] - Description shown at the top of the panel.
1197 * @param {number=100} [options.params.priority] - The sort priority for the panel.
1198 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
1199 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
1200 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
1202 initialize: function ( id, options ) {
1204 Container.prototype.initialize.call( panel, id, options );
1206 panel.deferred.embedded.done( function () {
1212 * Embed the container in the DOM when any parent panel is ready.
1216 embed: function () {
1218 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
1220 if ( ! panel.container.parent().is( parentContainer ) ) {
1221 parentContainer.append( panel.container );
1222 panel.renderContent();
1225 api.bind( 'pane-contents-reflowed', _.debounce( function() {
1226 panel._recalculateTopMargin();
1229 panel.deferred.embedded.resolve();
1235 attachEvents: function () {
1236 var meta, panel = this;
1238 // Expand/Collapse accordion sections on click.
1239 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
1240 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1243 event.preventDefault(); // Keep this AFTER the key filter above
1245 if ( ! panel.expanded() ) {
1251 panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
1252 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1255 event.preventDefault(); // Keep this AFTER the key filter above
1257 if ( panel.expanded() ) {
1262 meta = panel.container.find( '.panel-meta:first' );
1264 meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
1265 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1268 event.preventDefault(); // Keep this AFTER the key filter above
1270 meta = panel.container.find( '.panel-meta' );
1271 if ( meta.hasClass( 'cannot-expand' ) ) {
1275 var content = meta.find( '.customize-panel-description:first' );
1276 if ( meta.hasClass( 'open' ) ) {
1277 meta.toggleClass( 'open' );
1278 content.slideUp( panel.defaultExpandedArguments.duration );
1279 $( this ).attr( 'aria-expanded', false );
1281 content.slideDown( panel.defaultExpandedArguments.duration );
1282 meta.toggleClass( 'open' );
1283 $( this ).attr( 'aria-expanded', true );
1290 * Get the sections that are associated with this panel, sorted by their priority Value.
1296 sections: function () {
1297 return this._children( 'panel', 'section' );
1301 * Return whether this panel has any active sections.
1305 * @returns {boolean}
1307 isContextuallyActive: function () {
1309 sections = panel.sections(),
1311 _( sections ).each( function ( section ) {
1312 if ( section.active() && section.isContextuallyActive() ) {
1316 return ( activeCount !== 0 );
1320 * Update UI to reflect expanded state
1324 * @param {Boolean} expanded
1325 * @param {Object} args
1326 * @param {Boolean} args.unchanged
1327 * @param {Function} args.completeCallback
1329 onChangeExpanded: function ( expanded, args ) {
1331 // Immediately call the complete callback if there were no changes
1332 if ( args.unchanged ) {
1333 if ( args.completeCallback ) {
1334 args.completeCallback();
1339 // Note: there is a second argument 'args' passed
1340 var position, scroll,
1342 accordionSection = panel.container.closest( '.accordion-section' ),
1343 overlay = accordionSection.closest( '.wp-full-overlay' ),
1344 container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
1345 siblings = container.find( '.open' ),
1346 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
1347 backBtn = accordionSection.find( '.customize-panel-back' ),
1348 panelTitle = accordionSection.find( '.accordion-section-title' ).first(),
1349 content = accordionSection.find( '.control-panel-content' ),
1350 headerActionsHeight = $( '#customize-header-actions' ).height();
1354 // Collapse any sibling sections/panels
1355 api.section.each( function ( section ) {
1356 if ( panel.id !== section.panel() ) {
1357 section.collapse( { duration: 0 } );
1360 api.panel.each( function ( otherPanel ) {
1361 if ( panel !== otherPanel ) {
1362 otherPanel.collapse( { duration: 0 } );
1366 content.show( 0, function() {
1367 content.parent().show();
1368 position = content.offset().top;
1369 scroll = container.scrollTop();
1370 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
1371 accordionSection.addClass( 'current-panel' );
1372 overlay.addClass( 'in-sub-panel' );
1373 container.scrollTop( 0 );
1374 if ( args.completeCallback ) {
1375 args.completeCallback();
1378 topPanel.attr( 'tabindex', '-1' );
1379 backBtn.attr( 'tabindex', '0' );
1381 panel._recalculateTopMargin();
1383 siblings.removeClass( 'open' );
1384 accordionSection.removeClass( 'current-panel' );
1385 overlay.removeClass( 'in-sub-panel' );
1386 content.delay( 180 ).hide( 0, function() {
1387 content.css( 'margin-top', 'inherit' ); // Reset
1388 if ( args.completeCallback ) {
1389 args.completeCallback();
1392 topPanel.attr( 'tabindex', '0' );
1393 backBtn.attr( 'tabindex', '-1' );
1395 container.scrollTop( 0 );
1400 * Recalculate the top margin.
1405 _recalculateTopMargin: function() {
1406 var panel = this, headerActionsHeight, content, accordionSection;
1407 headerActionsHeight = $( '#customize-header-actions' ).height();
1408 accordionSection = panel.container.closest( '.accordion-section' );
1409 content = accordionSection.find( '.control-panel-content' );
1410 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
1414 * Render the panel from its JS template, if it exists.
1416 * The panel's container must already exist in the DOM.
1420 renderContent: function () {
1424 // Add the content to the container.
1425 if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
1426 template = wp.template( panel.templateSelector + '-content' );
1428 template = wp.template( 'customize-panel-default-content' );
1430 if ( template && panel.container ) {
1431 panel.container.find( '.accordion-sub-container' ).html( template( panel.params ) );
1437 * A Customizer Control.
1439 * A control provides a UI element that allows a user to modify a Customizer Setting.
1441 * @see PHP class WP_Customize_Control.
1444 * @augments wp.customize.Class
1446 * @param {string} id Unique identifier for the control instance.
1447 * @param {object} options Options hash for the control instance.
1448 * @param {object} options.params
1449 * @param {object} options.params.type Type of control (e.g. text, radio, dropdown-pages, etc.)
1450 * @param {string} options.params.content The HTML content for the control.
1451 * @param {string} options.params.priority Order of priority to show the control within the section.
1452 * @param {string} options.params.active
1453 * @param {string} options.params.section The ID of the section the control belongs to.
1454 * @param {string} options.params.settings.default The ID of the setting the control relates to.
1455 * @param {string} options.params.settings.data
1456 * @param {string} options.params.label
1457 * @param {string} options.params.description
1458 * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
1460 api.Control = api.Class.extend({
1461 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
1463 initialize: function( id, options ) {
1465 nodes, radios, settings;
1467 control.params = {};
1468 $.extend( control, options || {} );
1470 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
1471 control.templateSelector = 'customize-control-' + control.params.type + '-content';
1472 control.container = control.params.content ? $( control.params.content ) : $( control.selector );
1474 control.deferred = {
1475 embedded: new $.Deferred()
1477 control.section = new api.Value();
1478 control.priority = new api.Value();
1479 control.active = new api.Value();
1480 control.activeArgumentsQueue = [];
1482 control.elements = [];
1484 nodes = control.container.find('[data-customize-setting-link]');
1487 nodes.each( function() {
1488 var node = $( this ),
1491 if ( node.is( ':radio' ) ) {
1492 name = node.prop( 'name' );
1493 if ( radios[ name ] ) {
1497 radios[ name ] = true;
1498 node = nodes.filter( '[name="' + name + '"]' );
1501 api( node.data( 'customizeSettingLink' ), function( setting ) {
1502 var element = new api.Element( node );
1503 control.elements.push( element );
1504 element.sync( setting );
1505 element.set( setting() );
1509 control.active.bind( function ( active ) {
1510 var args = control.activeArgumentsQueue.shift();
1511 args = $.extend( {}, control.defaultActiveArguments, args );
1512 control.onChangeActive( active, args );
1515 control.section.set( control.params.section );
1516 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
1517 control.active.set( control.params.active );
1519 api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
1522 * After all settings related to the control are available,
1523 * make them available on the control and embed the control into the page.
1525 settings = $.map( control.params.settings, function( value ) {
1529 if ( 0 === settings.length ) {
1530 control.setting = null;
1531 control.settings = {};
1534 api.apply( api, settings.concat( function() {
1537 control.settings = {};
1538 for ( key in control.params.settings ) {
1539 control.settings[ key ] = api( control.params.settings[ key ] );
1542 control.setting = control.settings['default'] || null;
1548 // After the control is embedded on the page, invoke the "ready" method.
1549 control.deferred.embedded.done( function () {
1555 * Embed the control into the page.
1557 embed: function () {
1561 // Watch for changes to the section state
1562 inject = function ( sectionId ) {
1563 var parentContainer;
1564 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the front end.
1567 // Wait for the section to be registered
1568 api.section( sectionId, function ( section ) {
1569 // Wait for the section to be ready/initialized
1570 section.deferred.embedded.done( function () {
1571 parentContainer = section.container.find( 'ul:first' );
1572 if ( ! control.container.parent().is( parentContainer ) ) {
1573 parentContainer.append( control.container );
1574 control.renderContent();
1576 control.deferred.embedded.resolve();
1580 control.section.bind( inject );
1581 inject( control.section.get() );
1585 * Triggered when the control's markup has been injected into the DOM.
1589 ready: function() {},
1592 * Normal controls do not expand, so just expand its parent
1594 * @param {Object} [params]
1596 expand: function ( params ) {
1597 api.section( this.section() ).expand( params );
1601 * Bring the containing section and panel into view and then
1602 * this control into view, focusing on the first input.
1607 * Update UI in response to a change in the control's active state.
1608 * This does not change the active state, it merely handles the behavior
1609 * for when it does change.
1613 * @param {Boolean} active
1614 * @param {Object} args
1615 * @param {Number} args.duration
1616 * @param {Callback} args.completeCallback
1618 onChangeActive: function ( active, args ) {
1619 if ( args.unchanged ) {
1620 if ( args.completeCallback ) {
1621 args.completeCallback();
1626 if ( ! $.contains( document, this.container[0] ) ) {
1627 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
1628 this.container.toggle( active );
1629 if ( args.completeCallback ) {
1630 args.completeCallback();
1632 } else if ( active ) {
1633 this.container.slideDown( args.duration, args.completeCallback );
1635 this.container.slideUp( args.duration, args.completeCallback );
1640 * @deprecated 4.1.0 Use this.onChangeActive() instead.
1642 toggle: function ( active ) {
1643 return this.onChangeActive( active, this.defaultActiveArguments );
1647 * Shorthand way to enable the active state.
1651 * @param {Object} [params]
1652 * @returns {Boolean} false if already active
1654 activate: Container.prototype.activate,
1657 * Shorthand way to disable the active state.
1661 * @param {Object} [params]
1662 * @returns {Boolean} false if already inactive
1664 deactivate: Container.prototype.deactivate,
1667 * Re-use _toggleActive from Container class.
1671 _toggleActive: Container.prototype._toggleActive,
1673 dropdownInit: function() {
1675 statuses = this.container.find('.dropdown-status'),
1676 params = this.params,
1677 toggleFreeze = false,
1678 update = function( to ) {
1679 if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
1680 statuses.html( params.statuses[ to ] ).show();
1685 // Support the .dropdown class to open/close complex elements
1686 this.container.on( 'click keydown', '.dropdown', function( event ) {
1687 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1691 event.preventDefault();
1694 control.container.toggleClass('open');
1696 if ( control.container.hasClass('open') )
1697 control.container.parent().parent().find('li.library-selected').focus();
1699 // Don't want to fire focus and click at same time
1700 toggleFreeze = true;
1701 setTimeout(function () {
1702 toggleFreeze = false;
1706 this.setting.bind( update );
1707 update( this.setting() );
1711 * Render the control from its JS template, if it exists.
1713 * The control's container must already exist in the DOM.
1717 renderContent: function () {
1721 // Replace the container element's content with the control.
1722 if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
1723 template = wp.template( control.templateSelector );
1724 if ( template && control.container ) {
1725 control.container.html( template( control.params ) );
1732 * A colorpicker control.
1735 * @augments wp.customize.Control
1736 * @augments wp.customize.Class
1738 api.ColorControl = api.Control.extend({
1741 picker = this.container.find('.color-picker-hex');
1743 picker.val( control.setting() ).wpColorPicker({
1744 change: function() {
1745 control.setting.set( picker.wpColorPicker('color') );
1748 control.setting.set( '' );
1752 this.setting.bind( function ( value ) {
1753 picker.val( value );
1754 picker.wpColorPicker( 'color', value );
1760 * A control that implements the media modal.
1763 * @augments wp.customize.Control
1764 * @augments wp.customize.Class
1766 api.MediaControl = api.Control.extend({
1769 * When the control's DOM structure is ready,
1770 * set up internal event bindings.
1774 // Shortcut so that we don't have to use _.bind every time we add a callback.
1775 _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
1777 // Bind events, with delegation to facilitate re-rendering.
1778 control.container.on( 'click keydown', '.upload-button', control.openFrame );
1779 control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
1780 control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1781 control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1782 control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
1783 control.container.on( 'click keydown', '.remove-button', control.removeFile );
1784 control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
1786 // Resize the player controls when it becomes visible (ie when section is expanded)
1787 api.section( control.section() ).container
1788 .on( 'expanded', function() {
1789 if ( control.player ) {
1790 control.player.setControlsSize();
1793 .on( 'collapsed', function() {
1794 control.pausePlayer();
1797 control.setting.bind( function( value ) {
1799 // Send attachment information to the preview for possible use in `postMessage` transport.
1800 wp.media.attachment( value ).fetch().done( function() {
1801 wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
1804 // Re-render whenever the control's setting changes.
1805 control.renderContent();
1809 pausePlayer: function () {
1810 this.player && this.player.pause();
1813 cleanupPlayer: function () {
1814 this.player && wp.media.mixin.removePlayer( this.player );
1818 * Open the media modal.
1820 openFrame: function( event ) {
1821 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1825 event.preventDefault();
1827 if ( ! this.frame ) {
1835 * Create a media modal select frame, and store it so the instance can be reused when needed.
1837 initFrame: function() {
1838 this.frame = wp.media({
1840 text: this.params.button_labels.frame_button
1843 new wp.media.controller.Library({
1844 title: this.params.button_labels.frame_title,
1845 library: wp.media.query({ type: this.params.mime_type }),
1852 // When a file is selected, run a callback.
1853 this.frame.on( 'select', this.select );
1857 * Callback handler for when an attachment is selected in the media modal.
1858 * Gets the selected image information, and sets it within the control.
1860 select: function() {
1861 // Get the attachment from the modal frame.
1863 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1864 mejsSettings = window._wpmejsSettings || {};
1866 this.params.attachment = attachment;
1868 // Set the Customizer setting; the callback takes care of rendering.
1869 this.setting( attachment.id );
1870 node = this.container.find( 'audio, video' ).get(0);
1872 // Initialize audio/video previews.
1874 this.player = new MediaElementPlayer( node, mejsSettings );
1876 this.cleanupPlayer();
1881 * Reset the setting to the default value.
1883 restoreDefault: function( event ) {
1884 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1887 event.preventDefault();
1889 this.params.attachment = this.params.defaultAttachment;
1890 this.setting( this.params.defaultAttachment.url );
1894 * Called when the "Remove" link is clicked. Empties the setting.
1896 * @param {object} event jQuery Event object
1898 removeFile: function( event ) {
1899 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1902 event.preventDefault();
1904 this.params.attachment = {};
1906 this.renderContent(); // Not bound to setting change when emptying.
1911 * An upload control, which utilizes the media modal.
1914 * @augments wp.customize.MediaControl
1915 * @augments wp.customize.Control
1916 * @augments wp.customize.Class
1918 api.UploadControl = api.MediaControl.extend({
1921 * Callback handler for when an attachment is selected in the media modal.
1922 * Gets the selected image information, and sets it within the control.
1924 select: function() {
1925 // Get the attachment from the modal frame.
1927 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1928 mejsSettings = window._wpmejsSettings || {};
1930 this.params.attachment = attachment;
1932 // Set the Customizer setting; the callback takes care of rendering.
1933 this.setting( attachment.url );
1934 node = this.container.find( 'audio, video' ).get(0);
1936 // Initialize audio/video previews.
1938 this.player = new MediaElementPlayer( node, mejsSettings );
1940 this.cleanupPlayer();
1945 success: function() {},
1948 removerVisibility: function() {}
1952 * A control for uploading images.
1954 * This control no longer needs to do anything more
1955 * than what the upload control does in JS.
1958 * @augments wp.customize.UploadControl
1959 * @augments wp.customize.MediaControl
1960 * @augments wp.customize.Control
1961 * @augments wp.customize.Class
1963 api.ImageControl = api.UploadControl.extend({
1965 thumbnailSrc: function() {}
1969 * A control for uploading background images.
1972 * @augments wp.customize.UploadControl
1973 * @augments wp.customize.MediaControl
1974 * @augments wp.customize.Control
1975 * @augments wp.customize.Class
1977 api.BackgroundControl = api.UploadControl.extend({
1980 * When the control's DOM structure is ready,
1981 * set up internal event bindings.
1984 api.UploadControl.prototype.ready.apply( this, arguments );
1988 * Callback handler for when an attachment is selected in the media modal.
1989 * Does an additional AJAX request for setting the background context.
1991 select: function() {
1992 api.UploadControl.prototype.select.apply( this, arguments );
1994 wp.ajax.post( 'custom-background-add', {
1995 nonce: _wpCustomizeBackground.nonces.add,
1997 theme: api.settings.theme.stylesheet,
1998 attachment_id: this.params.attachment.id
2004 * A control for selecting and cropping an image.
2007 * @augments wp.customize.MediaControl
2008 * @augments wp.customize.Control
2009 * @augments wp.customize.Class
2011 api.CroppedImageControl = api.MediaControl.extend({
2014 * Open the media modal to the library state.
2016 openFrame: function( event ) {
2017 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2022 this.frame.setState( 'library' ).open();
2026 * Create a media modal select frame, and store it so the instance can be reused when needed.
2028 initFrame: function() {
2029 var l10n = _wpMediaViewsL10n;
2031 this.frame = wp.media({
2037 new wp.media.controller.Library({
2038 title: this.params.button_labels.frame_title,
2039 library: wp.media.query({ type: 'image' }),
2043 suggestedWidth: this.params.width,
2044 suggestedHeight: this.params.height
2046 new wp.media.controller.CustomizeImageCropper({
2047 imgSelectOptions: this.calculateImageSelectOptions,
2053 this.frame.on( 'select', this.onSelect, this );
2054 this.frame.on( 'cropped', this.onCropped, this );
2055 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2059 * After an image is selected in the media modal, switch to the cropper
2060 * state if the image isn't the right size.
2062 onSelect: function() {
2063 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2065 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2066 this.setImageFromAttachment( attachment );
2069 this.frame.setState( 'cropper' );
2074 * After the image has been cropped, apply the cropped image data to the setting.
2076 * @param {object} croppedImage Cropped attachment data.
2078 onCropped: function( croppedImage ) {
2079 this.setImageFromAttachment( croppedImage );
2083 * Returns a set of options, computed from the attached image data and
2084 * control-specific data, to be fed to the imgAreaSelect plugin in
2085 * wp.media.view.Cropper.
2087 * @param {wp.media.model.Attachment} attachment
2088 * @param {wp.media.controller.Cropper} controller
2089 * @returns {Object} Options
2091 calculateImageSelectOptions: function( attachment, controller ) {
2092 var control = controller.get( 'control' ),
2093 flexWidth = !! parseInt( control.params.flex_width, 10 ),
2094 flexHeight = !! parseInt( control.params.flex_height, 10 ),
2095 realWidth = attachment.get( 'width' ),
2096 realHeight = attachment.get( 'height' ),
2097 xInit = parseInt( control.params.width, 10 ),
2098 yInit = parseInt( control.params.height, 10 ),
2099 ratio = xInit / yInit,
2102 x1, y1, imgSelectOptions;
2104 controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
2106 if ( realWidth / realHeight > ratio ) {
2108 xInit = yInit * ratio;
2111 yInit = xInit / ratio;
2114 x1 = ( realWidth - xInit ) / 2;
2115 y1 = ( realHeight - yInit ) / 2;
2117 imgSelectOptions = {
2122 imageWidth: realWidth,
2123 imageHeight: realHeight,
2124 minWidth: xImg > xInit ? xInit : xImg,
2125 minHeight: yImg > yInit ? yInit : yImg,
2132 if ( flexHeight === false && flexWidth === false ) {
2133 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2136 if ( true === flexHeight ) {
2137 delete imgSelectOptions.minHeight;
2138 imgSelectOptions.maxWidth = realWidth;
2141 if ( true === flexWidth ) {
2142 delete imgSelectOptions.minWidth;
2143 imgSelectOptions.maxHeight = realHeight;
2146 return imgSelectOptions;
2150 * Return whether the image must be cropped, based on required dimensions.
2152 * @param {bool} flexW
2153 * @param {bool} flexH
2160 mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
2161 if ( true === flexW && true === flexH ) {
2165 if ( true === flexW && dstH === imgH ) {
2169 if ( true === flexH && dstW === imgW ) {
2173 if ( dstW === imgW && dstH === imgH ) {
2177 if ( imgW <= dstW ) {
2185 * If cropping was skipped, apply the image data directly to the setting.
2187 onSkippedCrop: function() {
2188 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2189 this.setImageFromAttachment( attachment );
2193 * Updates the setting and re-renders the control UI.
2195 * @param {object} attachment
2197 setImageFromAttachment: function( attachment ) {
2198 this.params.attachment = attachment;
2200 // Set the Customizer setting; the callback takes care of rendering.
2201 this.setting( attachment.id );
2206 * A control for selecting and cropping Site Icons.
2209 * @augments wp.customize.CroppedImageControl
2210 * @augments wp.customize.MediaControl
2211 * @augments wp.customize.Control
2212 * @augments wp.customize.Class
2214 api.SiteIconControl = api.CroppedImageControl.extend({
2217 * Create a media modal select frame, and store it so the instance can be reused when needed.
2219 initFrame: function() {
2220 var l10n = _wpMediaViewsL10n;
2222 this.frame = wp.media({
2228 new wp.media.controller.Library({
2229 title: this.params.button_labels.frame_title,
2230 library: wp.media.query({ type: 'image' }),
2234 suggestedWidth: this.params.width,
2235 suggestedHeight: this.params.height
2237 new wp.media.controller.SiteIconCropper({
2238 imgSelectOptions: this.calculateImageSelectOptions,
2244 this.frame.on( 'select', this.onSelect, this );
2245 this.frame.on( 'cropped', this.onCropped, this );
2246 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2250 * After an image is selected in the media modal, switch to the cropper
2251 * state if the image isn't the right size.
2253 onSelect: function() {
2254 var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2257 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2258 wp.ajax.post( 'crop-image', {
2259 nonce: attachment.nonces.edit,
2261 context: 'site-icon',
2265 width: this.params.width,
2266 height: this.params.height,
2267 dst_width: this.params.width,
2268 dst_height: this.params.height
2270 } ).done( function( croppedImage ) {
2271 controller.setImageFromAttachment( croppedImage );
2272 controller.frame.close();
2273 } ).fail( function() {
2274 controller.trigger('content:error:crop');
2277 this.frame.setState( 'cropper' );
2282 * Updates the setting and re-renders the control UI.
2284 * @param {object} attachment
2286 setImageFromAttachment: function( attachment ) {
2287 var sizes = [ 'site_icon-32', 'thumbnail', 'full' ],
2290 _.each( sizes, function( size ) {
2291 if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
2292 icon = attachment.sizes[ size ];
2296 this.params.attachment = attachment;
2298 // Set the Customizer setting; the callback takes care of rendering.
2299 this.setting( attachment.id );
2301 // Update the icon in-browser.
2302 $( 'link[sizes="32x32"]' ).attr( 'href', icon.url );
2306 * Called when the "Remove" link is clicked. Empties the setting.
2308 * @param {object} event jQuery Event object
2310 removeFile: function( event ) {
2311 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2314 event.preventDefault();
2316 this.params.attachment = {};
2318 this.renderContent(); // Not bound to setting change when emptying.
2319 $( 'link[rel="icon"]' ).attr( 'href', '' );
2325 * @augments wp.customize.Control
2326 * @augments wp.customize.Class
2328 api.HeaderControl = api.Control.extend({
2330 this.btnRemove = $('#customize-control-header_image .actions .remove');
2331 this.btnNew = $('#customize-control-header_image .actions .new');
2333 _.bindAll(this, 'openMedia', 'removeImage');
2335 this.btnNew.on( 'click', this.openMedia );
2336 this.btnRemove.on( 'click', this.removeImage );
2338 api.HeaderTool.currentHeader = this.getInitialHeaderImage();
2340 new api.HeaderTool.CurrentView({
2341 model: api.HeaderTool.currentHeader,
2342 el: '#customize-control-header_image .current .container'
2345 new api.HeaderTool.ChoiceListView({
2346 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
2347 el: '#customize-control-header_image .choices .uploaded .list'
2350 new api.HeaderTool.ChoiceListView({
2351 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
2352 el: '#customize-control-header_image .choices .default .list'
2355 api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
2356 api.HeaderTool.UploadsList,
2357 api.HeaderTool.DefaultsList
2360 // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
2361 wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
2362 wp.media.controller.Cropper.prototype.defaults.doCropArgs.theme = api.settings.theme.stylesheet;
2366 * Returns a new instance of api.HeaderTool.ImageModel based on the currently
2367 * saved header image (if any).
2371 * @returns {Object} Options
2373 getInitialHeaderImage: function() {
2374 if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
2375 return new api.HeaderTool.ImageModel();
2378 // Get the matching uploaded image object.
2379 var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
2380 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
2382 // Fall back to raw current header image.
2383 if ( ! currentHeaderObject ) {
2384 currentHeaderObject = {
2385 url: api.get().header_image,
2386 thumbnail_url: api.get().header_image,
2387 attachment_id: api.get().header_image_data.attachment_id
2391 return new api.HeaderTool.ImageModel({
2392 header: currentHeaderObject,
2393 choice: currentHeaderObject.url.split( '/' ).pop()
2398 * Returns a set of options, computed from the attached image data and
2399 * theme-specific data, to be fed to the imgAreaSelect plugin in
2400 * wp.media.view.Cropper.
2402 * @param {wp.media.model.Attachment} attachment
2403 * @param {wp.media.controller.Cropper} controller
2404 * @returns {Object} Options
2406 calculateImageSelectOptions: function(attachment, controller) {
2407 var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
2408 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
2409 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
2410 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
2411 ratio, xImg, yImg, realHeight, realWidth,
2414 realWidth = attachment.get('width');
2415 realHeight = attachment.get('height');
2417 this.headerImage = new api.HeaderTool.ImageModel();
2418 this.headerImage.set({
2421 themeFlexWidth: flexWidth,
2422 themeFlexHeight: flexHeight,
2423 imageWidth: realWidth,
2424 imageHeight: realHeight
2427 controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
2429 ratio = xInit / yInit;
2433 if ( xImg / yImg > ratio ) {
2435 xInit = yInit * ratio;
2438 yInit = xInit / ratio;
2441 imgSelectOptions = {
2446 imageWidth: realWidth,
2447 imageHeight: realHeight,
2454 if (flexHeight === false && flexWidth === false) {
2455 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2457 if (flexHeight === false ) {
2458 imgSelectOptions.maxHeight = yInit;
2460 if (flexWidth === false ) {
2461 imgSelectOptions.maxWidth = xInit;
2464 return imgSelectOptions;
2468 * Sets up and opens the Media Manager in order to select an image.
2469 * Depending on both the size of the image and the properties of the
2470 * current theme, a cropping step after selection may be required or
2473 * @param {event} event
2475 openMedia: function(event) {
2476 var l10n = _wpMediaViewsL10n;
2478 event.preventDefault();
2480 this.frame = wp.media({
2482 text: l10n.selectAndCrop,
2486 new wp.media.controller.Library({
2487 title: l10n.chooseImage,
2488 library: wp.media.query({ type: 'image' }),
2492 suggestedWidth: _wpCustomizeHeader.data.width,
2493 suggestedHeight: _wpCustomizeHeader.data.height
2495 new wp.media.controller.Cropper({
2496 imgSelectOptions: this.calculateImageSelectOptions
2501 this.frame.on('select', this.onSelect, this);
2502 this.frame.on('cropped', this.onCropped, this);
2503 this.frame.on('skippedcrop', this.onSkippedCrop, this);
2509 * After an image is selected in the media modal,
2510 * switch to the cropper state.
2512 onSelect: function() {
2513 this.frame.setState('cropper');
2517 * After the image has been cropped, apply the cropped image data to the setting.
2519 * @param {object} croppedImage Cropped attachment data.
2521 onCropped: function(croppedImage) {
2522 var url = croppedImage.url,
2523 attachmentId = croppedImage.attachment_id,
2524 w = croppedImage.width,
2525 h = croppedImage.height;
2526 this.setImageFromURL(url, attachmentId, w, h);
2530 * If cropping was skipped, apply the image data directly to the setting.
2532 * @param {object} selection
2534 onSkippedCrop: function(selection) {
2535 var url = selection.get('url'),
2536 w = selection.get('width'),
2537 h = selection.get('height');
2538 this.setImageFromURL(url, selection.id, w, h);
2542 * Creates a new wp.customize.HeaderTool.ImageModel from provided
2543 * header image data and inserts it into the user-uploaded headers
2546 * @param {String} url
2547 * @param {Number} attachmentId
2548 * @param {Number} width
2549 * @param {Number} height
2551 setImageFromURL: function(url, attachmentId, width, height) {
2552 var choice, data = {};
2555 data.thumbnail_url = url;
2556 data.timestamp = _.now();
2559 data.attachment_id = attachmentId;
2567 data.height = height;
2570 choice = new api.HeaderTool.ImageModel({
2572 choice: url.split('/').pop()
2574 api.HeaderTool.UploadsList.add(choice);
2575 api.HeaderTool.currentHeader.set(choice.toJSON());
2577 choice.importImage();
2581 * Triggers the necessary events to deselect an image which was set as
2582 * the currently selected one.
2584 removeImage: function() {
2585 api.HeaderTool.currentHeader.trigger('hide');
2586 api.HeaderTool.CombinedList.trigger('control:removeImage');
2592 * wp.customize.ThemeControl
2595 * @augments wp.customize.Control
2596 * @augments wp.customize.Class
2598 api.ThemeControl = api.Control.extend({
2604 * Defer rendering the theme control until the section is displayed.
2608 renderContent: function () {
2610 renderContentArgs = arguments;
2612 api.section( control.section(), function( section ) {
2613 if ( section.expanded() ) {
2614 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2615 control.isRendered = true;
2617 section.expanded.bind( function( expanded ) {
2618 if ( expanded && ! control.isRendered ) {
2619 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2620 control.isRendered = true;
2633 control.container.on( 'touchmove', '.theme', function() {
2634 control.touchDrag = true;
2637 // Bind details view trigger.
2638 control.container.on( 'click keydown touchend', '.theme', function( event ) {
2639 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2643 // Bail if the user scrolled on a touch device.
2644 if ( control.touchDrag === true ) {
2645 return control.touchDrag = false;
2648 // Prevent the modal from showing when the user clicks the action button.
2649 if ( $( event.target ).is( '.theme-actions .button' ) ) {
2653 var previewUrl = $( this ).data( 'previewUrl' );
2655 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
2657 window.parent.location = previewUrl;
2660 control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
2661 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2665 event.preventDefault(); // Keep this AFTER the key filter above
2667 api.section( control.section() ).showDetails( control.params.theme );
2670 control.container.on( 'render-screenshot', function() {
2671 var $screenshot = $( this ).find( 'img' ),
2672 source = $screenshot.data( 'src' );
2675 $screenshot.attr( 'src', source );
2681 * Show or hide the theme based on the presence of the term in the title, description, and author.
2685 filter: function( term ) {
2687 haystack = control.params.theme.name + ' ' +
2688 control.params.theme.description + ' ' +
2689 control.params.theme.tags + ' ' +
2690 control.params.theme.author;
2691 haystack = haystack.toLowerCase().replace( '-', ' ' );
2692 if ( -1 !== haystack.search( term ) ) {
2695 control.deactivate();
2700 // Change objects contained within the main customize object to Settings.
2701 api.defaultConstructor = api.Setting;
2703 // Create the collections for Controls, Sections and Panels.
2704 api.control = new api.Values({ defaultConstructor: api.Control });
2705 api.section = new api.Values({ defaultConstructor: api.Section });
2706 api.panel = new api.Values({ defaultConstructor: api.Panel });
2709 * An object that fetches a preview in the background of the document, which
2710 * allows for seamless replacement of an existing preview.
2713 * @augments wp.customize.Messenger
2714 * @augments wp.customize.Class
2715 * @mixes wp.customize.Events
2717 api.PreviewFrame = api.Messenger.extend({
2721 * Initialize the PreviewFrame.
2723 * @param {object} params.container
2724 * @param {object} params.signature
2725 * @param {object} params.previewUrl
2726 * @param {object} params.query
2727 * @param {object} options
2729 initialize: function( params, options ) {
2730 var deferred = $.Deferred();
2733 * Make the instance of the PreviewFrame the promise object
2734 * so other objects can easily interact with it.
2736 deferred.promise( this );
2738 this.container = params.container;
2739 this.signature = params.signature;
2741 $.extend( params, { channel: api.PreviewFrame.uuid() });
2743 api.Messenger.prototype.initialize.call( this, params, options );
2745 this.add( 'previewUrl', params.previewUrl );
2747 this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
2749 this.run( deferred );
2753 * Run the preview request.
2755 * @param {object} deferred jQuery Deferred object to be resolved with
2758 run: function( deferred ) {
2763 if ( this._ready ) {
2764 this.unbind( 'ready', this._ready );
2767 this._ready = function() {
2771 deferred.resolveWith( self );
2775 this.bind( 'ready', this._ready );
2777 this.bind( 'ready', function ( data ) {
2779 this.container.addClass( 'iframe-ready' );
2786 * Walk over all panels, sections, and controls and set their
2787 * respective active states to true if the preview explicitly
2788 * indicates as such.
2791 panel: data.activePanels,
2792 section: data.activeSections,
2793 control: data.activeControls
2795 _( constructs ).each( function ( activeConstructs, type ) {
2796 api[ type ].each( function ( construct, id ) {
2797 var active = !! ( activeConstructs && activeConstructs[ id ] );
2799 construct.activate();
2801 construct.deactivate();
2807 this.request = $.ajax( this.previewUrl(), {
2811 withCredentials: true
2815 this.request.fail( function() {
2816 deferred.rejectWith( self, [ 'request failure' ] );
2819 this.request.done( function( response ) {
2820 var location = self.request.getResponseHeader('Location'),
2821 signature = self.signature,
2824 // Check if the location response header differs from the current URL.
2825 // If so, the request was redirected; try loading the requested page.
2826 if ( location && location !== self.previewUrl() ) {
2827 deferred.rejectWith( self, [ 'redirect', location ] );
2831 // Check if the user is not logged in.
2832 if ( '0' === response ) {
2833 self.login( deferred );
2837 // Check for cheaters.
2838 if ( '-1' === response ) {
2839 deferred.rejectWith( self, [ 'cheatin' ] );
2843 // Check for a signature in the request.
2844 index = response.lastIndexOf( signature );
2845 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
2846 deferred.rejectWith( self, [ 'unsigned' ] );
2850 // Strip the signature from the request.
2851 response = response.slice( 0, index ) + response.slice( index + signature.length );
2853 // Create the iframe and inject the html content.
2854 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
2856 // Bind load event after the iframe has been added to the page;
2857 // otherwise it will fire when injected into the DOM.
2858 self.iframe.one( 'load', function() {
2862 deferred.resolveWith( self );
2864 setTimeout( function() {
2865 deferred.rejectWith( self, [ 'ready timeout' ] );
2866 }, self.sensitivity );
2870 self.targetWindow( self.iframe[0].contentWindow );
2872 self.targetWindow().document.open();
2873 self.targetWindow().document.write( response );
2874 self.targetWindow().document.close();
2878 login: function( deferred ) {
2882 reject = function() {
2883 deferred.rejectWith( self, [ 'logged out' ] );
2886 if ( this.triedLogin ) {
2890 // Check if we have an admin cookie.
2891 $.get( api.settings.url.ajax, {
2893 }).fail( reject ).done( function( response ) {
2896 if ( '1' !== response ) {
2900 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
2901 iframe.appendTo( self.container );
2902 iframe.on( 'load', function() {
2903 self.triedLogin = true;
2906 self.run( deferred );
2911 destroy: function() {
2912 api.Messenger.prototype.destroy.call( this );
2913 this.request.abort();
2916 this.iframe.remove();
2918 delete this.request;
2920 delete this.targetWindow;
2927 * Create a universally unique identifier.
2931 api.PreviewFrame.uuid = function() {
2932 return 'preview-' + uuid++;
2937 * Set the document title of the customizer.
2941 * @param {string} documentTitle
2943 api.setDocumentTitle = function ( documentTitle ) {
2945 tmpl = api.settings.documentTitleTmpl;
2946 title = tmpl.replace( '%s', documentTitle );
2947 document.title = title;
2948 api.trigger( 'title', title );
2953 * @augments wp.customize.Messenger
2954 * @augments wp.customize.Class
2955 * @mixes wp.customize.Events
2957 api.Previewer = api.Messenger.extend({
2961 * @param {array} params.allowedUrls
2962 * @param {string} params.container A selector or jQuery element for the preview
2963 * frame to be placed.
2964 * @param {string} params.form
2965 * @param {string} params.previewUrl The URL to preview.
2966 * @param {string} params.signature
2967 * @param {object} options
2969 initialize: function( params, options ) {
2971 rscheme = /^https?/;
2973 $.extend( this, options || {} );
2975 active: $.Deferred()
2979 * Wrap this.refresh to prevent it from hammering the servers:
2981 * If refresh is called once and no other refresh requests are
2982 * loading, trigger the request immediately.
2984 * If refresh is called while another refresh request is loading,
2985 * debounce the refresh requests:
2986 * 1. Stop the loading request (as it is instantly outdated).
2987 * 2. Trigger the new request once refresh hasn't been called for
2988 * self.refreshBuffer milliseconds.
2990 this.refresh = (function( self ) {
2991 var refresh = self.refresh,
2992 callback = function() {
2994 refresh.call( self );
2999 if ( typeof timeout !== 'number' ) {
3000 if ( self.loading ) {
3007 clearTimeout( timeout );
3008 timeout = setTimeout( callback, self.refreshBuffer );
3012 this.container = api.ensure( params.container );
3013 this.allowedUrls = params.allowedUrls;
3014 this.signature = params.signature;
3016 params.url = window.location.href;
3018 api.Messenger.prototype.initialize.call( this, params );
3020 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
3021 var match = to.match( rscheme );
3022 return match ? match[0] : '';
3025 // Limit the URL to internal, front-end links.
3027 // If the front end and the admin are served from the same domain, load the
3028 // preview over ssl if the Customizer is being loaded over ssl. This avoids
3029 // insecure content warnings. This is not attempted if the admin and front end
3030 // are on different domains to avoid the case where the front end doesn't have
3033 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
3036 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
3037 // Strip hashes and query strings before testing.
3038 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
3041 // Attempt to match the URL to the control frame's scheme
3042 // and check if it's allowed. If not, try the original URL.
3043 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
3044 $.each( self.allowedUrls, function( i, allowed ) {
3047 allowed = allowed.replace( /\/+$/, '' );
3048 path = url.replace( allowed, '' );
3050 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
3059 // If we found a matching result, return it. If not, bail.
3060 return result ? result : null;
3063 // Refresh the preview when the URL is changed (but not yet).
3064 this.previewUrl.bind( this.refresh );
3067 this.bind( 'scroll', function( distance ) {
3068 this.scroll = distance;
3071 // Update the URL when the iframe sends a URL message.
3072 this.bind( 'url', this.previewUrl );
3074 // Update the document title when the preview changes.
3075 this.bind( 'documentTitle', function ( title ) {
3076 api.setDocumentTitle( title );
3081 * Query string data sent with each preview request.
3085 query: function() {},
3088 if ( this.loading ) {
3089 this.loading.destroy();
3090 delete this.loading;
3095 * Refresh the preview.
3097 refresh: function() {
3100 // Display loading indicator
3101 this.send( 'loading-initiated' );
3105 this.loading = new api.PreviewFrame({
3107 previewUrl: this.previewUrl(),
3108 query: this.query() || {},
3109 container: this.container,
3110 signature: this.signature
3113 this.loading.done( function() {
3114 // 'this' is the loading frame
3115 this.bind( 'synced', function() {
3117 self.preview.destroy();
3118 self.preview = this;
3119 delete self.loading;
3121 self.targetWindow( this.targetWindow() );
3122 self.channel( this.channel() );
3124 self.deferred.active.resolve();
3125 self.send( 'active' );
3128 this.send( 'sync', {
3129 scroll: self.scroll,
3134 this.loading.fail( function( reason, location ) {
3135 self.send( 'loading-failed' );
3136 if ( 'redirect' === reason && location ) {
3137 self.previewUrl( location );
3140 if ( 'logged out' === reason ) {
3141 if ( self.preview ) {
3142 self.preview.destroy();
3143 delete self.preview;
3146 self.login().done( self.refresh );
3149 if ( 'cheatin' === reason ) {
3156 var previewer = this,
3157 deferred, messenger, iframe;
3162 deferred = $.Deferred();
3163 this._login = deferred.promise();
3165 messenger = new api.Messenger({
3167 url: api.settings.url.login
3170 iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
3172 messenger.targetWindow( iframe[0].contentWindow );
3174 messenger.bind( 'login', function () {
3175 var refreshNonces = previewer.refreshNonces();
3177 refreshNonces.always( function() {
3179 messenger.destroy();
3180 delete previewer._login;
3183 refreshNonces.done( function() {
3187 refreshNonces.fail( function() {
3188 previewer.cheatin();
3196 cheatin: function() {
3197 $( document.body ).empty().addClass( 'cheatin' ).append(
3198 '<h1>' + api.l10n.cheatin + '</h1>' +
3199 '<p>' + api.l10n.notAllowed + '</p>'
3203 refreshNonces: function() {
3204 var request, deferred = $.Deferred();
3208 request = wp.ajax.post( 'customize_refresh_nonces', {
3210 theme: api.settings.theme.stylesheet
3213 request.done( function( response ) {
3214 api.trigger( 'nonce-refresh', response );
3218 request.fail( function() {
3226 api.controlConstructor = {
3227 color: api.ColorControl,
3228 media: api.MediaControl,
3229 upload: api.UploadControl,
3230 image: api.ImageControl,
3231 cropped_image: api.CroppedImageControl,
3232 site_icon: api.SiteIconControl,
3233 header: api.HeaderControl,
3234 background: api.BackgroundControl,
3235 theme: api.ThemeControl
3237 api.panelConstructor = {};
3238 api.sectionConstructor = {
3239 themes: api.ThemesSection
3243 api.settings = window._wpCustomizeSettings;
3244 api.l10n = window._wpCustomizeControlsL10n;
3246 // Check if we can run the Customizer.
3247 if ( ! api.settings ) {
3251 // Bail if any incompatibilities are found.
3252 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
3256 var parent, topFocus,
3257 body = $( document.body ),
3258 overlay = body.children( '.wp-full-overlay' ),
3259 title = $( '#customize-info .panel-title.site-title' ),
3260 closeBtn = $( '.customize-controls-close' ),
3261 saveBtn = $( '#save' ),
3262 footerActions = $( '#customize-footer-actions' );
3264 // Prevent the form from saving when enter is pressed on an input or select element.
3265 $('#customize-controls').on( 'keydown', function( e ) {
3266 var isEnter = ( 13 === e.which ),
3267 $el = $( e.target );
3269 if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
3274 // Expand/Collapse the main customizer customize info.
3275 $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
3276 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3279 event.preventDefault(); // Keep this AFTER the key filter above
3281 var section = $( this ).closest( '.accordion-section' ),
3282 content = section.find( '.customize-panel-description:first' );
3284 if ( section.hasClass( 'cannot-expand' ) ) {
3288 if ( section.hasClass( 'open' ) ) {
3289 section.toggleClass( 'open' );
3290 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
3291 $( this ).attr( 'aria-expanded', false );
3293 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
3294 section.toggleClass( 'open' );
3295 $( this ).attr( 'aria-expanded', true );
3299 // Initialize Previewer
3300 api.previewer = new api.Previewer({
3301 container: '#customize-preview',
3302 form: '#customize-controls',
3303 previewUrl: api.settings.url.preview,
3304 allowedUrls: api.settings.url.allowed,
3305 signature: 'WP_CUSTOMIZER_SIGNATURE'
3308 nonce: api.settings.nonce,
3311 * Build the query to send along with the Preview request.
3316 var dirtyCustomized = {};
3317 api.each( function ( value, key ) {
3318 if ( value._dirty ) {
3319 dirtyCustomized[ key ] = value();
3325 theme: api.settings.theme.stylesheet,
3326 customized: JSON.stringify( dirtyCustomized ),
3327 nonce: this.nonce.preview
3333 processing = api.state( 'processing' ),
3334 submitWhenDoneProcessing,
3337 body.addClass( 'saving' );
3339 submit = function () {
3341 query = $.extend( self.query(), {
3342 nonce: self.nonce.save
3344 request = wp.ajax.post( 'customize_save', query );
3346 api.trigger( 'save', request );
3348 request.always( function () {
3349 body.removeClass( 'saving' );
3352 request.fail( function ( response ) {
3353 if ( '0' === response ) {
3354 response = 'not_logged_in';
3355 } else if ( '-1' === response ) {
3356 // Back-compat in case any other check_ajax_referer() call is dying
3357 response = 'invalid_nonce';
3360 if ( 'invalid_nonce' === response ) {
3362 } else if ( 'not_logged_in' === response ) {
3363 self.preview.iframe.hide();
3364 self.login().done( function() {
3366 self.preview.iframe.show();
3369 api.trigger( 'error', response );
3372 request.done( function( response ) {
3373 // Clear setting dirty states
3374 api.each( function ( value ) {
3375 value._dirty = false;
3378 api.previewer.send( 'saved', response );
3380 api.trigger( 'saved', response );
3384 if ( 0 === processing() ) {
3387 submitWhenDoneProcessing = function () {
3388 if ( 0 === processing() ) {
3389 api.state.unbind( 'change', submitWhenDoneProcessing );
3393 api.state.bind( 'change', submitWhenDoneProcessing );
3399 // Refresh the nonces if the preview sends updated nonces over.
3400 api.previewer.bind( 'nonce', function( nonce ) {
3401 $.extend( this.nonce, nonce );
3404 // Refresh the nonces if login sends updated nonces over.
3405 api.bind( 'nonce-refresh', function( nonce ) {
3406 $.extend( api.settings.nonce, nonce );
3407 $.extend( api.previewer.nonce, nonce );
3408 api.previewer.send( 'nonce-refresh', nonce );
3412 $.each( api.settings.settings, function( id, data ) {
3413 api.create( id, id, data.value, {
3414 transport: data.transport,
3415 previewer: api.previewer,
3416 dirty: !! data.dirty
3421 $.each( api.settings.panels, function ( id, data ) {
3422 var constructor = api.panelConstructor[ data.type ] || api.Panel,
3425 panel = new constructor( id, {
3428 api.panel.add( id, panel );
3432 $.each( api.settings.sections, function ( id, data ) {
3433 var constructor = api.sectionConstructor[ data.type ] || api.Section,
3436 section = new constructor( id, {
3439 api.section.add( id, section );
3443 $.each( api.settings.controls, function( id, data ) {
3444 var constructor = api.controlConstructor[ data.type ] || api.Control,
3447 control = new constructor( id, {
3449 previewer: api.previewer
3451 api.control.add( id, control );
3454 // Focus the autofocused element
3455 _.each( [ 'panel', 'section', 'control' ], function( type ) {
3456 var id = api.settings.autofocus[ type ];
3462 * Defer focus until:
3463 * 1. The panel, section, or control exists (especially for dynamically-created ones).
3464 * 2. The instance is embedded in the document (and so is focusable).
3465 * 3. The preview has finished loading so that the active states have been set.
3467 api[ type ]( id, function( instance ) {
3468 instance.deferred.embedded.done( function() {
3469 api.previewer.deferred.active.done( function() {
3477 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
3481 api.reflowPaneContents = _.bind( function () {
3483 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3485 if ( document.activeElement ) {
3486 activeElement = $( document.activeElement );
3489 // Sort the sections within each panel
3490 api.panel.each( function ( panel ) {
3491 var sections = panel.sections(),
3492 sectionContainers = _.pluck( sections, 'container' );
3493 rootNodes.push( panel );
3494 appendContainer = panel.container.find( 'ul:first' );
3495 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
3496 _( sections ).each( function ( section ) {
3497 appendContainer.append( section.container );
3503 // Sort the controls within each section
3504 api.section.each( function ( section ) {
3505 var controls = section.controls(),
3506 controlContainers = _.pluck( controls, 'container' );
3507 if ( ! section.panel() ) {
3508 rootNodes.push( section );
3510 appendContainer = section.container.find( 'ul:first' );
3511 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
3512 _( controls ).each( function ( control ) {
3513 appendContainer.append( control.container );
3519 // Sort the root panels and sections
3520 rootNodes.sort( api.utils.prioritySort );
3521 rootContainers = _.pluck( rootNodes, 'container' );
3522 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
3523 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
3524 _( rootNodes ).each( function ( rootNode ) {
3525 appendContainer.append( rootNode.container );
3530 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
3531 api.panel.each( function ( panel ) {
3532 var value = panel.active();
3533 panel.active.callbacks.fireWith( panel.active, [ value, value ] );
3535 api.section.each( function ( section ) {
3536 var value = section.active();
3537 section.active.callbacks.fireWith( section.active, [ value, value ] );
3540 // Restore focus if there was a reflow and there was an active (focused) element
3541 if ( wasReflowed && activeElement ) {
3542 activeElement.focus();
3544 api.trigger( 'pane-contents-reflowed' );
3546 api.bind( 'ready', api.reflowPaneContents );
3547 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
3548 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
3549 values.bind( 'add', api.reflowPaneContents );
3550 values.bind( 'change', api.reflowPaneContents );
3551 values.bind( 'remove', api.reflowPaneContents );
3554 // Check if preview url is valid and load the preview frame.
3555 if ( api.previewer.previewUrl() ) {
3556 api.previewer.refresh();
3558 api.previewer.previewUrl( api.settings.url.home );
3561 // Save and activated states
3563 var state = new api.Values(),
3564 saved = state.create( 'saved' ),
3565 activated = state.create( 'activated' ),
3566 processing = state.create( 'processing' );
3568 state.bind( 'change', function() {
3569 if ( ! activated() ) {
3570 saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
3571 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3573 } else if ( saved() ) {
3574 saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
3575 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
3578 saveBtn.val( api.l10n.save ).prop( 'disabled', false );
3579 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3583 // Set default states.
3585 activated( api.settings.theme.active );
3588 api.bind( 'change', function() {
3589 state('saved').set( false );
3592 api.bind( 'saved', function() {
3593 state('saved').set( true );
3594 state('activated').set( true );
3597 activated.bind( function( to ) {
3599 api.trigger( 'activated' );
3602 // Expose states to the API.
3607 saveBtn.click( function( event ) {
3608 api.previewer.save();
3609 event.preventDefault();
3610 }).keydown( function( event ) {
3611 if ( 9 === event.which ) // tab
3613 if ( 13 === event.which ) // enter
3614 api.previewer.save();
3615 event.preventDefault();
3618 closeBtn.keydown( function( event ) {
3619 if ( 9 === event.which ) // tab
3621 if ( 13 === event.which ) // enter
3623 event.preventDefault();
3626 $( '.collapse-sidebar' ).on( 'click', function() {
3627 if ( 'true' === $( this ).attr( 'aria-expanded' ) ) {
3628 $( this ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
3630 $( this ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
3633 overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
3636 $( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) {
3637 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3641 overlay.toggleClass( 'preview-only' );
3642 event.preventDefault();
3645 // Previewed device bindings.
3646 api.previewedDevice = new api.Value();
3648 // Set the default device.
3649 api.bind( 'ready', function() {
3650 _.find( api.settings.previewableDevices, function( value, key ) {
3651 if ( true === value['default'] ) {
3652 api.previewedDevice.set( key );
3658 // Set the toggled device.
3659 footerActions.find( '.devices button' ).on( 'click', function( event ) {
3660 api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
3663 // Bind device changes.
3664 api.previewedDevice.bind( function( newDevice ) {
3665 var overlay = $( '.wp-full-overlay' ),
3668 footerActions.find( '.devices button' )
3669 .removeClass( 'active' )
3670 .attr( 'aria-pressed', false );
3672 footerActions.find( '.devices .preview-' + newDevice )
3673 .addClass( 'active' )
3674 .attr( 'aria-pressed', true );
3676 $.each( api.settings.previewableDevices, function( device ) {
3677 devices += ' preview-' + device;
3681 .removeClass( devices )
3682 .addClass( 'preview-' + newDevice );
3685 // Bind site title display to the corresponding field.
3686 if ( title.length ) {
3687 api( 'blogname', function( setting ) {
3688 var updateTitle = function() {
3689 title.text( $.trim( setting() ) || api.l10n.untitledBlogName );
3691 setting.bind( updateTitle );
3697 * Create a postMessage connection with a parent frame,
3698 * in case the Customizer frame was opened with the Customize loader.
3700 * @see wp.customize.Loader
3702 parent = new api.Messenger({
3703 url: api.settings.url.parent,
3708 * If we receive a 'back' event, we're inside an iframe.
3709 * Send any clicks to the 'Return' link to the parent page.
3711 parent.bind( 'back', function() {
3712 closeBtn.on( 'click.customize-controls-close', function( event ) {
3713 event.preventDefault();
3714 parent.send( 'close' );
3718 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
3719 $( window ).on( 'beforeunload', function () {
3720 if ( ! api.state( 'saved' )() ) {
3721 setTimeout( function() {
3722 overlay.removeClass( 'customize-loading' );
3724 return api.l10n.saveAlert;
3728 // Pass events through to the parent.
3729 $.each( [ 'saved', 'change' ], function ( i, event ) {
3730 api.bind( event, function() {
3731 parent.send( event );
3736 * When activated, let the loader handle redirecting the page.
3737 * If no loader exists, redirect the page ourselves (if a url exists).
3739 api.bind( 'activated', function() {
3740 if ( parent.targetWindow() )
3741 parent.send( 'activated', api.settings.url.activated );
3742 else if ( api.settings.url.activated )
3743 window.location = api.settings.url.activated;
3746 // Pass titles to the parent
3747 api.bind( 'title', function( newTitle ) {
3748 parent.send( 'title', newTitle );
3751 // Initialize the connection with the parent frame.
3752 parent.send( 'ready' );
3754 // Control visibility for default controls
3756 'background_image': {
3757 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
3758 callback: function( to ) { return !! to; }
3761 controls: [ 'page_on_front', 'page_for_posts' ],
3762 callback: function( to ) { return 'page' === to; }
3764 'header_textcolor': {
3765 controls: [ 'header_textcolor' ],
3766 callback: function( to ) { return 'blank' !== to; }
3768 }, function( settingId, o ) {
3769 api( settingId, function( setting ) {
3770 $.each( o.controls, function( i, controlId ) {
3771 api.control( controlId, function( control ) {
3772 var visibility = function( to ) {
3773 control.container.toggle( o.callback( to ) );
3776 visibility( setting.get() );
3777 setting.bind( visibility );
3783 // Juggle the two controls that use header_textcolor
3784 api.control( 'display_header_text', function( control ) {
3787 control.elements[0].unsync( api( 'header_textcolor' ) );
3789 control.element = new api.Element( control.container.find('input') );
3790 control.element.set( 'blank' !== control.setting() );
3792 control.element.bind( function( to ) {
3794 last = api( 'header_textcolor' ).get();
3796 control.setting.set( to ? last : 'blank' );
3799 control.setting.bind( function( to ) {
3800 control.element.set( 'blank' !== to );
3804 // Change previewed URL to the homepage when changing the page_on_front.
3805 api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
3806 var updatePreviewUrl = function() {
3807 if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
3808 api.previewer.previewUrl.set( api.settings.url.home );
3811 showOnFront.bind( updatePreviewUrl );
3812 pageOnFront.bind( updatePreviewUrl );
3815 // Change the previewed URL to the selected page when changing the page_for_posts.
3816 api( 'page_for_posts', function( setting ) {
3817 setting.bind(function( pageId ) {
3818 pageId = parseInt( pageId, 10 );
3820 api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
3825 // Focus on the control that is associated with the given setting.
3826 api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
3828 api.control.each( function( control ) {
3829 var settingIds = _.pluck( control.settings, 'id' );
3830 if ( -1 !== _.indexOf( settingIds, settingId ) ) {
3831 matchedControl = control;
3835 if ( matchedControl ) {
3836 matchedControl.focus();
3840 // Refresh the preview when it requests.
3841 api.previewer.bind( 'refresh', function() {
3842 api.previewer.refresh();
3845 api.trigger( 'ready' );
3847 // Make sure left column gets focus
3848 topFocus = closeBtn;
3850 setTimeout(function () {