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;
30 this.notifications = new api.Values({ defaultConstructor: api.Notification });
32 // Whenever the setting's value changes, refresh the preview.
33 this.bind( this.preview );
37 * Refresh the preview, respective of the setting's refresh policy.
40 switch ( this.transport ) {
42 return this.previewer.refresh();
44 return this.previewer.send( 'setting', [ this.id, this() ] );
49 * Find controls associated with this setting.
52 * @returns {wp.customize.Control[]} Controls associated with setting.
54 findControls: function() {
55 var setting = this, controls = [];
56 api.control.each( function( control ) {
57 _.each( control.settings, function( controlSetting ) {
58 if ( controlSetting.id === setting.id ) {
59 controls.push( control );
68 * Utility function namespace
73 * Watch all changes to Value properties, and bubble changes to parent Values instance
77 * @param {wp.customize.Class} instance
78 * @param {Array} properties The names of the Value instances to watch.
80 api.utils.bubbleChildValueChanges = function ( instance, properties ) {
81 $.each( properties, function ( i, key ) {
82 instance[ key ].bind( function ( to, from ) {
83 if ( instance.parent && to !== from ) {
84 instance.parent.trigger( 'change', instance );
91 * Expand a panel, section, or control and focus on the first focusable element.
95 * @param {Object} [params]
96 * @param {Function} [params.completeCallback]
98 focus = function ( params ) {
99 var construct, completeCallback, focus, focusElement;
101 params = params || {};
102 focus = function () {
104 if ( construct.extended( api.Panel ) && construct.expanded && construct.expanded() ) {
105 focusContainer = construct.container.find( 'ul.control-panel-content' );
106 } else if ( construct.extended( api.Section ) && construct.expanded && construct.expanded() ) {
107 focusContainer = construct.container.find( 'ul.accordion-section-content' );
109 focusContainer = construct.container;
112 focusElement = focusContainer.find( '.control-focus:first' );
113 if ( 0 === focusElement.length ) {
114 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
115 focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
117 focusElement.focus();
119 if ( params.completeCallback ) {
120 completeCallback = params.completeCallback;
121 params.completeCallback = function () {
126 params.completeCallback = focus;
128 if ( construct.expand ) {
129 construct.expand( params );
131 params.completeCallback();
136 * Stable sort for Panels, Sections, and Controls.
138 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
142 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
143 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
146 api.utils.prioritySort = function ( a, b ) {
147 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
148 return a.params.instanceNumber - b.params.instanceNumber;
150 return a.priority() - b.priority();
155 * Return whether the supplied Event object is for a keydown event but not the Enter key.
159 * @param {jQuery.Event} event
162 api.utils.isKeydownButNotEnterEvent = function ( event ) {
163 return ( 'keydown' === event.type && 13 !== event.which );
167 * Return whether the two lists of elements are the same and are in the same order.
171 * @param {Array|jQuery} listA
172 * @param {Array|jQuery} listB
175 api.utils.areElementListsEqual = function ( listA, listB ) {
177 listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
178 -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
179 _.zip( listA, listB ), // pair up each element between the two lists
181 return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
183 ), false ) // check for presence of false in map's return value
189 * Base class for Panel and Section.
194 * @augments wp.customize.Class
196 Container = api.Class.extend({
197 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
198 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
199 containerType: 'container',
213 * @param {string} id - The ID for the container.
214 * @param {object} options - Object containing one property: params.
215 * @param {object} options.params - Object containing the following properties.
216 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
217 * @param {string=} [options.params.description] - Description shown at the top of the panel.
218 * @param {number=100} [options.params.priority] - The sort priority for the panel.
219 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
220 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
221 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
223 initialize: function ( id, options ) {
224 var container = this;
226 options = options || {};
228 options.params = _.defaults(
229 options.params || {},
233 $.extend( container, options );
234 container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
235 container.container = $( container.params.content );
236 if ( 0 === container.container.length ) {
237 container.container = $( container.getContainer() );
240 container.deferred = {
241 embedded: new $.Deferred()
243 container.priority = new api.Value();
244 container.active = new api.Value();
245 container.activeArgumentsQueue = [];
246 container.expanded = new api.Value();
247 container.expandedArgumentsQueue = [];
249 container.active.bind( function ( active ) {
250 var args = container.activeArgumentsQueue.shift();
251 args = $.extend( {}, container.defaultActiveArguments, args );
252 active = ( active && container.isContextuallyActive() );
253 container.onChangeActive( active, args );
255 container.expanded.bind( function ( expanded ) {
256 var args = container.expandedArgumentsQueue.shift();
257 args = $.extend( {}, container.defaultExpandedArguments, args );
258 container.onChangeExpanded( expanded, args );
261 container.deferred.embedded.done( function () {
262 container.attachEvents();
265 api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
267 container.priority.set( container.params.priority );
268 container.active.set( container.params.active );
269 container.expanded.set( false );
277 ready: function() {},
280 * Get the child models associated with this parent, sorting them by their priority Value.
284 * @param {String} parentType
285 * @param {String} childType
288 _children: function ( parentType, childType ) {
291 api[ childType ].each( function ( child ) {
292 if ( child[ parentType ].get() === parent.id ) {
293 children.push( child );
296 children.sort( api.utils.prioritySort );
301 * To override by subclass, to return whether the container has active children.
307 isContextuallyActive: function () {
308 throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
312 * Active state change handler.
314 * Shows the container if it is active, hides it if not.
316 * To override by subclass, update the container's UI to reflect the provided active state.
320 * @param {Boolean} active
321 * @param {Object} args
322 * @param {Object} args.duration
323 * @param {Object} args.completeCallback
325 onChangeActive: function( active, args ) {
326 var duration, construct = this, expandedOtherPanel;
327 if ( args.unchanged ) {
328 if ( args.completeCallback ) {
329 args.completeCallback();
334 duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
336 if ( construct.extended( api.Panel ) ) {
337 // If this is a panel is not currently expanded but another panel is expanded, do not animate.
338 api.panel.each(function ( panel ) {
339 if ( panel !== construct && panel.expanded() ) {
340 expandedOtherPanel = panel;
345 // Collapse any expanded sections inside of this panel first before deactivating.
347 _.each( construct.sections(), function( section ) {
348 section.collapse( { duration: 0 } );
353 if ( ! $.contains( document, construct.container[0] ) ) {
354 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
355 construct.container.toggle( active );
356 if ( args.completeCallback ) {
357 args.completeCallback();
359 } else if ( active ) {
360 construct.container.stop( true, true ).slideDown( duration, args.completeCallback );
362 if ( construct.expanded() ) {
365 completeCallback: function() {
366 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
370 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
374 // Recalculate the margin-top immediately, not waiting for debounced reflow, to prevent momentary (100ms) vertical jiggle.
375 if ( expandedOtherPanel ) {
376 expandedOtherPanel._recalculateTopMargin();
383 * @params {Boolean} active
384 * @param {Object} [params]
385 * @returns {Boolean} false if state already applied
387 _toggleActive: function ( active, params ) {
389 params = params || {};
390 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
391 params.unchanged = true;
392 self.onChangeActive( self.active.get(), params );
395 params.unchanged = false;
396 this.activeArgumentsQueue.push( params );
397 this.active.set( active );
403 * @param {Object} [params]
404 * @returns {Boolean} false if already active
406 activate: function ( params ) {
407 return this._toggleActive( true, params );
411 * @param {Object} [params]
412 * @returns {Boolean} false if already inactive
414 deactivate: function ( params ) {
415 return this._toggleActive( false, params );
419 * To override by subclass, update the container's UI to reflect the provided active state.
422 onChangeExpanded: function () {
423 throw new Error( 'Must override with subclass.' );
427 * Handle the toggle logic for expand/collapse.
429 * @param {Boolean} expanded - The new state to apply.
430 * @param {Object} [params] - Object containing options for expand/collapse.
431 * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
432 * @returns {Boolean} false if state already applied or active state is false
434 _toggleExpanded: function( expanded, params ) {
435 var instance = this, previousCompleteCallback;
436 params = params || {};
437 previousCompleteCallback = params.completeCallback;
439 // Short-circuit expand() if the instance is not active.
440 if ( expanded && ! instance.active() ) {
444 params.completeCallback = function() {
445 if ( previousCompleteCallback ) {
446 previousCompleteCallback.apply( instance, arguments );
449 instance.container.trigger( 'expanded' );
451 instance.container.trigger( 'collapsed' );
454 if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
455 params.unchanged = true;
456 instance.onChangeExpanded( instance.expanded.get(), params );
459 params.unchanged = false;
460 instance.expandedArgumentsQueue.push( params );
461 instance.expanded.set( expanded );
467 * @param {Object} [params]
468 * @returns {Boolean} false if already expanded or if inactive.
470 expand: function ( params ) {
471 return this._toggleExpanded( true, params );
475 * @param {Object} [params]
476 * @returns {Boolean} false if already collapsed.
478 collapse: function ( params ) {
479 return this._toggleExpanded( false, params );
483 * Bring the container into view and then expand this and bring it into view
484 * @param {Object} [params]
489 * Return the container html, generated from its JS template, if it exists.
493 getContainer: function () {
497 if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
498 template = wp.template( container.templateSelector );
500 template = wp.template( 'customize-' + container.containerType + '-default' );
502 if ( template && container.container ) {
503 return $.trim( template( container.params ) );
514 * @augments wp.customize.Class
516 api.Section = Container.extend({
517 containerType: 'section',
525 instanceNumber: null,
533 * @param {string} id - The ID for the section.
534 * @param {object} options - Object containing one property: params.
535 * @param {object} options.params - Object containing the following properties.
536 * @param {string} options.params.title - Title shown when section is collapsed and expanded.
537 * @param {string=} [options.params.description] - Description shown at the top of the section.
538 * @param {number=100} [options.params.priority] - The sort priority for the section.
539 * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
540 * @param {string=} [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
541 * @param {boolean=true} [options.params.active] - Whether the section is active or not.
542 * @param {string} options.params.panel - The ID for the panel this section is associated with.
543 * @param {string=} [options.params.customizeAction] - Additional context information shown before the section title when expanded.
545 initialize: function ( id, options ) {
547 Container.prototype.initialize.call( section, id, options );
550 section.panel = new api.Value();
551 section.panel.bind( function ( id ) {
552 $( section.container ).toggleClass( 'control-subsection', !! id );
554 section.panel.set( section.params.panel || '' );
555 api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
558 section.deferred.embedded.done( function () {
564 * Embed the container in the DOM when any parent panel is ready.
569 var section = this, inject;
571 // Watch for changes to the panel state
572 inject = function ( panelId ) {
575 // The panel has been supplied, so wait until the panel object is registered
576 api.panel( panelId, function ( panel ) {
577 // The panel has been registered, wait for it to become ready/initialized
578 panel.deferred.embedded.done( function () {
579 parentContainer = panel.container.find( 'ul:first' );
580 if ( ! section.container.parent().is( parentContainer ) ) {
581 parentContainer.append( section.container );
583 section.deferred.embedded.resolve();
587 // There is no panel, so embed the section in the root of the customizer
588 parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
589 if ( ! section.container.parent().is( parentContainer ) ) {
590 parentContainer.append( section.container );
592 section.deferred.embedded.resolve();
595 section.panel.bind( inject );
596 inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
598 section.deferred.embedded.done(function() {
599 // Fix the top margin after reflow.
600 api.bind( 'pane-contents-reflowed', _.debounce( function() {
601 section._recalculateTopMargin();
607 * Add behaviors for the accordion section.
611 attachEvents: function () {
614 // Expand/Collapse accordion sections on click.
615 section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
616 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
619 event.preventDefault(); // Keep this AFTER the key filter above
621 if ( section.expanded() ) {
630 * Return whether this section has any active controls.
636 isContextuallyActive: function () {
638 controls = section.controls(),
640 _( controls ).each( function ( control ) {
641 if ( control.active() ) {
645 return ( activeCount !== 0 );
649 * Get the controls that are associated with this section, sorted by their priority Value.
655 controls: function () {
656 return this._children( 'section', 'control' );
660 * Update UI to reflect expanded state.
664 * @param {Boolean} expanded
665 * @param {Object} args
667 onChangeExpanded: function ( expanded, args ) {
669 container = section.container.closest( '.wp-full-overlay-sidebar-content' ),
670 content = section.container.find( '.accordion-section-content' ),
671 overlay = section.container.closest( '.wp-full-overlay' ),
672 backBtn = section.container.find( '.customize-section-back' ),
673 sectionTitle = section.container.find( '.accordion-section-title' ).first(),
674 headerActionsHeight = $( '#customize-header-actions' ).height(),
675 resizeContentHeight, expand, position, scroll;
677 if ( expanded && ! section.container.hasClass( 'open' ) ) {
679 if ( args.unchanged ) {
680 expand = args.completeCallback;
682 container.scrollTop( 0 );
683 resizeContentHeight = function() {
684 var matchMedia, offset;
685 matchMedia = window.matchMedia || window.msMatchMedia;
686 offset = 90; // 45px for customize header actions + 45px for footer actions.
688 // No footer on small screens.
689 if ( matchMedia && matchMedia( '(max-width: 640px)' ).matches ) {
692 content.css( 'height', ( window.innerHeight - offset ) );
694 expand = function() {
695 section.container.addClass( 'open' );
696 overlay.addClass( 'section-open' );
697 position = content.offset().top;
698 scroll = container.scrollTop();
699 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
700 resizeContentHeight();
701 sectionTitle.attr( 'tabindex', '-1' );
702 backBtn.attr( 'tabindex', '0' );
704 if ( args.completeCallback ) {
705 args.completeCallback();
708 // Fix the height after browser resize.
709 $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
711 setTimeout( _.bind( section._recalculateTopMargin, section ), 0 );
715 if ( ! args.allowMultiple ) {
716 api.section.each( function ( otherSection ) {
717 if ( otherSection !== section ) {
718 otherSection.collapse( { duration: args.duration } );
723 if ( section.panel() ) {
724 api.panel( section.panel() ).expand({
725 duration: args.duration,
726 completeCallback: expand
729 api.panel.each( function( panel ) {
735 } else if ( ! expanded && section.container.hasClass( 'open' ) ) {
736 section.container.removeClass( 'open' );
737 overlay.removeClass( 'section-open' );
738 content.css( 'margin-top', '' );
739 container.scrollTop( 0 );
740 backBtn.attr( 'tabindex', '-1' );
741 sectionTitle.attr( 'tabindex', '0' );
742 sectionTitle.focus();
743 if ( args.completeCallback ) {
744 args.completeCallback();
746 $( window ).off( 'resize.customizer-section' );
748 if ( args.completeCallback ) {
749 args.completeCallback();
755 * Recalculate the top margin.
760 _recalculateTopMargin: function() {
761 var section = this, content, offset, headerActionsHeight;
762 content = section.container.find( '.accordion-section-content' );
763 if ( 0 === content.length ) {
766 headerActionsHeight = $( '#customize-header-actions' ).height();
767 offset = ( content.offset().top - headerActionsHeight );
769 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
775 * wp.customize.ThemesSection
777 * Custom section for themes that functions similarly to a backwards panel,
778 * and also handles the theme-details view rendering and navigation.
781 * @augments wp.customize.Section
782 * @augments wp.customize.Container
784 api.ThemesSection = api.Section.extend({
788 screenshotQueue: null,
789 $window: $( window ),
794 initialize: function () {
795 this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
796 return api.Section.prototype.initialize.apply( this, arguments );
804 section.overlay = section.container.find( '.theme-overlay' );
805 section.template = wp.template( 'customize-themes-details-view' );
807 // Bind global keyboard events.
808 $( 'body' ).on( 'keyup', function( event ) {
809 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
813 // Pressing the right arrow key fires a theme:next event
814 if ( 39 === event.keyCode ) {
818 // Pressing the left arrow key fires a theme:previous event
819 if ( 37 === event.keyCode ) {
820 section.previousTheme();
823 // Pressing the escape key fires a theme:collapse event
824 if ( 27 === event.keyCode ) {
825 section.closeDetails();
829 _.bindAll( this, 'renderScreenshots' );
833 * Override Section.isContextuallyActive method.
835 * Ignore the active states' of the contained theme controls, and just
836 * use the section's own active state instead. This ensures empty search
837 * results for themes to cause the section to become inactive.
843 isContextuallyActive: function () {
844 return this.active();
850 attachEvents: function () {
853 // Expand/Collapse section/panel.
854 section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
855 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
858 event.preventDefault(); // Keep this AFTER the key filter above
860 if ( section.expanded() ) {
867 // Theme navigation in details view.
868 section.container.on( 'click keydown', '.left', function( event ) {
869 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
873 event.preventDefault(); // Keep this AFTER the key filter above
875 section.previousTheme();
878 section.container.on( 'click keydown', '.right', function( event ) {
879 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
883 event.preventDefault(); // Keep this AFTER the key filter above
888 section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
889 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
893 event.preventDefault(); // Keep this AFTER the key filter above
895 section.closeDetails();
898 var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
899 section.container.on( 'input', '#themes-filter', function( event ) {
901 term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
902 controls = section.controls();
904 _.each( controls, function( control ) {
905 control.filter( term );
910 // Update theme count.
911 count = section.container.find( 'li.customize-control:visible' ).length;
912 section.container.find( '.theme-count' ).text( count );
915 // Pre-load the first 3 theme screenshots.
916 api.bind( 'ready', function () {
917 _.each( section.controls().slice( 0, 3 ), function ( control ) {
918 var img, src = control.params.theme.screenshot[0];
928 * Update UI to reflect expanded state
932 * @param {Boolean} expanded
933 * @param {Object} args
934 * @param {Boolean} args.unchanged
935 * @param {Callback} args.completeCallback
937 onChangeExpanded: function ( expanded, args ) {
939 // Immediately call the complete callback if there were no changes
940 if ( args.unchanged ) {
941 if ( args.completeCallback ) {
942 args.completeCallback();
947 // Note: there is a second argument 'args' passed
948 var position, scroll,
950 section = panel.container.closest( '.accordion-section' ),
951 overlay = section.closest( '.wp-full-overlay' ),
952 container = section.closest( '.wp-full-overlay-sidebar-content' ),
953 siblings = container.find( '.open' ),
954 customizeBtn = section.find( '.customize-theme' ),
955 changeBtn = section.find( '.change-theme' ),
956 content = section.find( '.control-panel-content' );
960 // Collapse any sibling sections/panels
961 api.section.each( function ( otherSection ) {
962 if ( otherSection !== panel ) {
963 otherSection.collapse( { duration: args.duration } );
966 api.panel.each( function ( otherPanel ) {
967 otherPanel.collapse( { duration: 0 } );
970 content.show( 0, function() {
971 position = content.offset().top;
972 scroll = container.scrollTop();
973 content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
974 section.addClass( 'current-panel' );
975 overlay.addClass( 'in-themes-panel' );
976 container.scrollTop( 0 );
977 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
978 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
979 if ( args.completeCallback ) {
980 args.completeCallback();
983 customizeBtn.focus();
985 siblings.removeClass( 'open' );
986 section.removeClass( 'current-panel' );
987 overlay.removeClass( 'in-themes-panel' );
988 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
989 content.delay( 180 ).hide( 0, function() {
990 content.css( 'margin-top', 'inherit' ); // Reset
991 if ( args.completeCallback ) {
992 args.completeCallback();
995 customizeBtn.attr( 'tabindex', '0' );
997 container.scrollTop( 0 );
1002 * Recalculate the top margin.
1007 _recalculateTopMargin: function() {
1008 api.Panel.prototype._recalculateTopMargin.call( this );
1012 * Render control's screenshot if the control comes into view.
1016 renderScreenshots: function( ) {
1019 // Fill queue initially.
1020 if ( section.screenshotQueue === null ) {
1021 section.screenshotQueue = section.controls();
1024 // Are all screenshots rendered?
1025 if ( ! section.screenshotQueue.length ) {
1029 section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
1030 var $imageWrapper = control.container.find( '.theme-screenshot' ),
1031 $image = $imageWrapper.find( 'img' );
1033 if ( ! $image.length ) {
1037 if ( $image.is( ':hidden' ) ) {
1041 // Based on unveil.js.
1042 var wt = section.$window.scrollTop(),
1043 wb = wt + section.$window.height(),
1044 et = $image.offset().top,
1045 ih = $imageWrapper.height(),
1048 inView = eb >= wt - threshold && et <= wb + threshold;
1051 control.container.trigger( 'render-screenshot' );
1054 // If the image is in view return false so it's cleared from the queue.
1060 * Advance the modal to the next theme.
1064 nextTheme: function () {
1066 if ( section.getNextTheme() ) {
1067 section.showDetails( section.getNextTheme(), function() {
1068 section.overlay.find( '.right' ).focus();
1074 * Get the next theme model.
1078 getNextTheme: function () {
1080 control = api.control( 'theme_' + this.currentTheme );
1081 next = control.container.next( 'li.customize-control-theme' );
1082 if ( ! next.length ) {
1085 next = next[0].id.replace( 'customize-control-', '' );
1086 control = api.control( next );
1088 return control.params.theme;
1092 * Advance the modal to the previous theme.
1096 previousTheme: function () {
1098 if ( section.getPreviousTheme() ) {
1099 section.showDetails( section.getPreviousTheme(), function() {
1100 section.overlay.find( '.left' ).focus();
1106 * Get the previous theme model.
1110 getPreviousTheme: function () {
1111 var control, previous;
1112 control = api.control( 'theme_' + this.currentTheme );
1113 previous = control.container.prev( 'li.customize-control-theme' );
1114 if ( ! previous.length ) {
1117 previous = previous[0].id.replace( 'customize-control-', '' );
1118 control = api.control( previous );
1120 return control.params.theme;
1124 * Disable buttons when we're viewing the first or last theme.
1128 updateLimits: function () {
1129 if ( ! this.getNextTheme() ) {
1130 this.overlay.find( '.right' ).addClass( 'disabled' );
1132 if ( ! this.getPreviousTheme() ) {
1133 this.overlay.find( '.left' ).addClass( 'disabled' );
1138 * Render & show the theme details for a given theme model.
1142 * @param {Object} theme
1144 showDetails: function ( theme, callback ) {
1146 callback = callback || function(){};
1147 section.currentTheme = theme.id;
1148 section.overlay.html( section.template( theme ) )
1151 $( 'body' ).addClass( 'modal-open' );
1152 section.containFocus( section.overlay );
1153 section.updateLimits();
1158 * Close the theme details modal.
1162 closeDetails: function () {
1163 $( 'body' ).removeClass( 'modal-open' );
1164 this.overlay.fadeOut( 'fast' );
1165 api.control( 'theme_' + this.currentTheme ).focus();
1169 * Keep tab focus within the theme details modal.
1173 containFocus: function( el ) {
1176 el.on( 'keydown', function( event ) {
1178 // Return if it's not the tab key
1179 // When navigating with prev/next focus is already handled
1180 if ( 9 !== event.keyCode ) {
1184 // uses jQuery UI to get the tabbable elements
1185 tabbables = $( ':tabbable', el );
1187 // Keep focus within the overlay
1188 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
1189 tabbables.first().focus();
1191 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
1192 tabbables.last().focus();
1203 * @augments wp.customize.Class
1205 api.Panel = Container.extend({
1206 containerType: 'panel',
1211 * @param {string} id - The ID for the panel.
1212 * @param {object} options - Object containing one property: params.
1213 * @param {object} options.params - Object containing the following properties.
1214 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
1215 * @param {string=} [options.params.description] - Description shown at the top of the panel.
1216 * @param {number=100} [options.params.priority] - The sort priority for the panel.
1217 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
1218 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
1219 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
1221 initialize: function ( id, options ) {
1223 Container.prototype.initialize.call( panel, id, options );
1225 panel.deferred.embedded.done( function () {
1231 * Embed the container in the DOM when any parent panel is ready.
1235 embed: function () {
1237 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
1239 if ( ! panel.container.parent().is( parentContainer ) ) {
1240 parentContainer.append( panel.container );
1241 panel.renderContent();
1244 api.bind( 'pane-contents-reflowed', _.debounce( function() {
1245 panel._recalculateTopMargin();
1248 panel.deferred.embedded.resolve();
1254 attachEvents: function () {
1255 var meta, panel = this;
1257 // Expand/Collapse accordion sections on click.
1258 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
1259 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1262 event.preventDefault(); // Keep this AFTER the key filter above
1264 if ( ! panel.expanded() ) {
1270 panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
1271 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1274 event.preventDefault(); // Keep this AFTER the key filter above
1276 if ( panel.expanded() ) {
1281 meta = panel.container.find( '.panel-meta:first' );
1283 meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
1284 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1287 event.preventDefault(); // Keep this AFTER the key filter above
1289 meta = panel.container.find( '.panel-meta' );
1290 if ( meta.hasClass( 'cannot-expand' ) ) {
1294 var content = meta.find( '.customize-panel-description:first' );
1295 if ( meta.hasClass( 'open' ) ) {
1296 meta.toggleClass( 'open' );
1297 content.slideUp( panel.defaultExpandedArguments.duration );
1298 $( this ).attr( 'aria-expanded', false );
1300 content.slideDown( panel.defaultExpandedArguments.duration );
1301 meta.toggleClass( 'open' );
1302 $( this ).attr( 'aria-expanded', true );
1309 * Get the sections that are associated with this panel, sorted by their priority Value.
1315 sections: function () {
1316 return this._children( 'panel', 'section' );
1320 * Return whether this panel has any active sections.
1324 * @returns {boolean}
1326 isContextuallyActive: function () {
1328 sections = panel.sections(),
1330 _( sections ).each( function ( section ) {
1331 if ( section.active() && section.isContextuallyActive() ) {
1335 return ( activeCount !== 0 );
1339 * Update UI to reflect expanded state
1343 * @param {Boolean} expanded
1344 * @param {Object} args
1345 * @param {Boolean} args.unchanged
1346 * @param {Function} args.completeCallback
1348 onChangeExpanded: function ( expanded, args ) {
1350 // Immediately call the complete callback if there were no changes
1351 if ( args.unchanged ) {
1352 if ( args.completeCallback ) {
1353 args.completeCallback();
1358 // Note: there is a second argument 'args' passed
1359 var position, scroll,
1361 accordionSection = panel.container.closest( '.accordion-section' ),
1362 overlay = accordionSection.closest( '.wp-full-overlay' ),
1363 container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
1364 siblings = container.find( '.open' ),
1365 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
1366 backBtn = accordionSection.find( '.customize-panel-back' ),
1367 panelTitle = accordionSection.find( '.accordion-section-title' ).first(),
1368 content = accordionSection.find( '.control-panel-content' ),
1369 headerActionsHeight = $( '#customize-header-actions' ).height();
1373 // Collapse any sibling sections/panels
1374 api.section.each( function ( section ) {
1375 if ( panel.id !== section.panel() ) {
1376 section.collapse( { duration: 0 } );
1379 api.panel.each( function ( otherPanel ) {
1380 if ( panel !== otherPanel ) {
1381 otherPanel.collapse( { duration: 0 } );
1385 content.show( 0, function() {
1386 content.parent().show();
1387 position = content.offset().top;
1388 scroll = container.scrollTop();
1389 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
1390 accordionSection.addClass( 'current-panel' );
1391 overlay.addClass( 'in-sub-panel' );
1392 container.scrollTop( 0 );
1393 if ( args.completeCallback ) {
1394 args.completeCallback();
1397 topPanel.attr( 'tabindex', '-1' );
1398 backBtn.attr( 'tabindex', '0' );
1400 panel._recalculateTopMargin();
1402 siblings.removeClass( 'open' );
1403 accordionSection.removeClass( 'current-panel' );
1404 overlay.removeClass( 'in-sub-panel' );
1405 content.delay( 180 ).hide( 0, function() {
1406 content.css( 'margin-top', 'inherit' ); // Reset
1407 if ( args.completeCallback ) {
1408 args.completeCallback();
1411 topPanel.attr( 'tabindex', '0' );
1412 backBtn.attr( 'tabindex', '-1' );
1414 container.scrollTop( 0 );
1419 * Recalculate the top margin.
1424 _recalculateTopMargin: function() {
1425 var panel = this, headerActionsHeight, content, accordionSection;
1426 headerActionsHeight = $( '#customize-header-actions' ).height();
1427 accordionSection = panel.container.closest( '.accordion-section' );
1428 content = accordionSection.find( '.control-panel-content' );
1429 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
1433 * Render the panel from its JS template, if it exists.
1435 * The panel's container must already exist in the DOM.
1439 renderContent: function () {
1443 // Add the content to the container.
1444 if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
1445 template = wp.template( panel.templateSelector + '-content' );
1447 template = wp.template( 'customize-panel-default-content' );
1449 if ( template && panel.container ) {
1450 panel.container.find( '.accordion-sub-container' ).html( template( panel.params ) );
1456 * A Customizer Control.
1458 * A control provides a UI element that allows a user to modify a Customizer Setting.
1460 * @see PHP class WP_Customize_Control.
1463 * @augments wp.customize.Class
1465 * @param {string} id Unique identifier for the control instance.
1466 * @param {object} options Options hash for the control instance.
1467 * @param {object} options.params
1468 * @param {object} options.params.type Type of control (e.g. text, radio, dropdown-pages, etc.)
1469 * @param {string} options.params.content The HTML content for the control.
1470 * @param {string} options.params.priority Order of priority to show the control within the section.
1471 * @param {string} options.params.active
1472 * @param {string} options.params.section The ID of the section the control belongs to.
1473 * @param {string} options.params.settings.default The ID of the setting the control relates to.
1474 * @param {string} options.params.settings.data
1475 * @param {string} options.params.label
1476 * @param {string} options.params.description
1477 * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
1479 api.Control = api.Class.extend({
1480 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
1482 initialize: function( id, options ) {
1484 nodes, radios, settings;
1486 control.params = {};
1487 $.extend( control, options || {} );
1489 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
1490 control.templateSelector = 'customize-control-' + control.params.type + '-content';
1491 control.container = control.params.content ? $( control.params.content ) : $( control.selector );
1493 control.deferred = {
1494 embedded: new $.Deferred()
1496 control.section = new api.Value();
1497 control.priority = new api.Value();
1498 control.active = new api.Value();
1499 control.activeArgumentsQueue = [];
1500 control.notifications = new api.Values({ defaultConstructor: api.Notification });
1502 control.elements = [];
1504 nodes = control.container.find('[data-customize-setting-link]');
1507 nodes.each( function() {
1508 var node = $( this ),
1511 if ( node.is( ':radio' ) ) {
1512 name = node.prop( 'name' );
1513 if ( radios[ name ] ) {
1517 radios[ name ] = true;
1518 node = nodes.filter( '[name="' + name + '"]' );
1521 api( node.data( 'customizeSettingLink' ), function( setting ) {
1522 var element = new api.Element( node );
1523 control.elements.push( element );
1524 element.sync( setting );
1525 element.set( setting() );
1529 control.active.bind( function ( active ) {
1530 var args = control.activeArgumentsQueue.shift();
1531 args = $.extend( {}, control.defaultActiveArguments, args );
1532 control.onChangeActive( active, args );
1535 control.section.set( control.params.section );
1536 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
1537 control.active.set( control.params.active );
1539 api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
1542 * After all settings related to the control are available,
1543 * make them available on the control and embed the control into the page.
1545 settings = $.map( control.params.settings, function( value ) {
1549 if ( 0 === settings.length ) {
1550 control.setting = null;
1551 control.settings = {};
1554 api.apply( api, settings.concat( function() {
1557 control.settings = {};
1558 for ( key in control.params.settings ) {
1559 control.settings[ key ] = api( control.params.settings[ key ] );
1562 control.setting = control.settings['default'] || null;
1564 // Add setting notifications to the control notification.
1565 _.each( control.settings, function( setting ) {
1566 setting.notifications.bind( 'add', function( settingNotification ) {
1567 var controlNotification, code, params;
1568 code = setting.id + ':' + settingNotification.code;
1571 settingNotification,
1576 controlNotification = new api.Notification( code, params );
1577 control.notifications.add( controlNotification.code, controlNotification );
1579 setting.notifications.bind( 'remove', function( settingNotification ) {
1580 control.notifications.remove( setting.id + ':' + settingNotification.code );
1588 // After the control is embedded on the page, invoke the "ready" method.
1589 control.deferred.embedded.done( function () {
1591 * Note that this debounced/deferred rendering is needed for two reasons:
1592 * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
1593 * 2) Improve performance when adding/removing multiple notifications at a time.
1595 var debouncedRenderNotifications = _.debounce( function renderNotifications() {
1596 control.renderNotifications();
1598 control.notifications.bind( 'add', function( notification ) {
1599 wp.a11y.speak( notification.message, 'assertive' );
1600 debouncedRenderNotifications();
1602 control.notifications.bind( 'remove', debouncedRenderNotifications );
1603 control.renderNotifications();
1610 * Embed the control into the page.
1612 embed: function () {
1616 // Watch for changes to the section state
1617 inject = function ( sectionId ) {
1618 var parentContainer;
1619 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the front end.
1622 // Wait for the section to be registered
1623 api.section( sectionId, function ( section ) {
1624 // Wait for the section to be ready/initialized
1625 section.deferred.embedded.done( function () {
1626 parentContainer = section.container.find( 'ul:first' );
1627 if ( ! control.container.parent().is( parentContainer ) ) {
1628 parentContainer.append( control.container );
1629 control.renderContent();
1631 control.deferred.embedded.resolve();
1635 control.section.bind( inject );
1636 inject( control.section.get() );
1640 * Triggered when the control's markup has been injected into the DOM.
1644 ready: function() {},
1647 * Get the element inside of a control's container that contains the validation error message.
1649 * Control subclasses may override this to return the proper container to render notifications into.
1650 * Injects the notification container for existing controls that lack the necessary container,
1651 * including special handling for nav menu items and widgets.
1654 * @returns {jQuery} Setting validation message element.
1655 * @this {wp.customize.Control}
1657 getNotificationsContainerElement: function() {
1658 var control = this, controlTitle, notificationsContainer;
1660 notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
1661 if ( notificationsContainer.length ) {
1662 return notificationsContainer;
1665 notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
1667 if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
1668 control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
1669 } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
1670 control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
1672 controlTitle = control.container.find( '.customize-control-title' );
1673 if ( controlTitle.length ) {
1674 controlTitle.after( notificationsContainer );
1676 control.container.prepend( notificationsContainer );
1679 return notificationsContainer;
1683 * Render notifications.
1685 * Renders the `control.notifications` into the control's container.
1686 * Control subclasses may override this method to do their own handling
1687 * of rendering notifications.
1690 * @this {wp.customize.Control}
1692 renderNotifications: function() {
1693 var control = this, container, notifications, hasError = false;
1694 container = control.getNotificationsContainerElement();
1695 if ( ! container || ! container.length ) {
1699 control.notifications.each( function( notification ) {
1700 notifications.push( notification );
1701 if ( 'error' === notification.type ) {
1706 if ( 0 === notifications.length ) {
1707 container.stop().slideUp( 'fast' );
1709 container.stop().slideDown( 'fast', null, function() {
1710 $( this ).css( 'height', 'auto' );
1714 if ( ! control.notificationsTemplate ) {
1715 control.notificationsTemplate = wp.template( 'customize-control-notifications' );
1718 control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
1719 control.container.toggleClass( 'has-error', hasError );
1720 container.empty().append( $.trim(
1721 control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } )
1726 * Normal controls do not expand, so just expand its parent
1728 * @param {Object} [params]
1730 expand: function ( params ) {
1731 api.section( this.section() ).expand( params );
1735 * Bring the containing section and panel into view and then
1736 * this control into view, focusing on the first input.
1741 * Update UI in response to a change in the control's active state.
1742 * This does not change the active state, it merely handles the behavior
1743 * for when it does change.
1747 * @param {Boolean} active
1748 * @param {Object} args
1749 * @param {Number} args.duration
1750 * @param {Callback} args.completeCallback
1752 onChangeActive: function ( active, args ) {
1753 if ( args.unchanged ) {
1754 if ( args.completeCallback ) {
1755 args.completeCallback();
1760 if ( ! $.contains( document, this.container[0] ) ) {
1761 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
1762 this.container.toggle( active );
1763 if ( args.completeCallback ) {
1764 args.completeCallback();
1766 } else if ( active ) {
1767 this.container.slideDown( args.duration, args.completeCallback );
1769 this.container.slideUp( args.duration, args.completeCallback );
1774 * @deprecated 4.1.0 Use this.onChangeActive() instead.
1776 toggle: function ( active ) {
1777 return this.onChangeActive( active, this.defaultActiveArguments );
1781 * Shorthand way to enable the active state.
1785 * @param {Object} [params]
1786 * @returns {Boolean} false if already active
1788 activate: Container.prototype.activate,
1791 * Shorthand way to disable the active state.
1795 * @param {Object} [params]
1796 * @returns {Boolean} false if already inactive
1798 deactivate: Container.prototype.deactivate,
1801 * Re-use _toggleActive from Container class.
1805 _toggleActive: Container.prototype._toggleActive,
1807 dropdownInit: function() {
1809 statuses = this.container.find('.dropdown-status'),
1810 params = this.params,
1811 toggleFreeze = false,
1812 update = function( to ) {
1813 if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
1814 statuses.html( params.statuses[ to ] ).show();
1819 // Support the .dropdown class to open/close complex elements
1820 this.container.on( 'click keydown', '.dropdown', function( event ) {
1821 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1825 event.preventDefault();
1828 control.container.toggleClass('open');
1830 if ( control.container.hasClass('open') )
1831 control.container.parent().parent().find('li.library-selected').focus();
1833 // Don't want to fire focus and click at same time
1834 toggleFreeze = true;
1835 setTimeout(function () {
1836 toggleFreeze = false;
1840 this.setting.bind( update );
1841 update( this.setting() );
1845 * Render the control from its JS template, if it exists.
1847 * The control's container must already exist in the DOM.
1851 renderContent: function () {
1855 // Replace the container element's content with the control.
1856 if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
1857 template = wp.template( control.templateSelector );
1858 if ( template && control.container ) {
1859 control.container.html( template( control.params ) );
1866 * A colorpicker control.
1869 * @augments wp.customize.Control
1870 * @augments wp.customize.Class
1872 api.ColorControl = api.Control.extend({
1875 picker = this.container.find('.color-picker-hex');
1877 picker.val( control.setting() ).wpColorPicker({
1878 change: function() {
1879 control.setting.set( picker.wpColorPicker('color') );
1882 control.setting.set( '' );
1886 this.setting.bind( function ( value ) {
1887 picker.val( value );
1888 picker.wpColorPicker( 'color', value );
1894 * A control that implements the media modal.
1897 * @augments wp.customize.Control
1898 * @augments wp.customize.Class
1900 api.MediaControl = api.Control.extend({
1903 * When the control's DOM structure is ready,
1904 * set up internal event bindings.
1908 // Shortcut so that we don't have to use _.bind every time we add a callback.
1909 _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
1911 // Bind events, with delegation to facilitate re-rendering.
1912 control.container.on( 'click keydown', '.upload-button', control.openFrame );
1913 control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
1914 control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1915 control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1916 control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
1917 control.container.on( 'click keydown', '.remove-button', control.removeFile );
1918 control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
1920 // Resize the player controls when it becomes visible (ie when section is expanded)
1921 api.section( control.section() ).container
1922 .on( 'expanded', function() {
1923 if ( control.player ) {
1924 control.player.setControlsSize();
1927 .on( 'collapsed', function() {
1928 control.pausePlayer();
1932 * Set attachment data and render content.
1934 * Note that BackgroundImage.prototype.ready applies this ready method
1935 * to itself. Since BackgroundImage is an UploadControl, the value
1936 * is the attachment URL instead of the attachment ID. In this case
1937 * we skip fetching the attachment data because we have no ID available,
1938 * and it is the responsibility of the UploadControl to set the control's
1939 * attachmentData before calling the renderContent method.
1941 * @param {number|string} value Attachment
1943 function setAttachmentDataAndRenderContent( value ) {
1944 var hasAttachmentData = $.Deferred();
1946 if ( control.extended( api.UploadControl ) ) {
1947 hasAttachmentData.resolve();
1949 value = parseInt( value, 10 );
1950 if ( _.isNaN( value ) || value <= 0 ) {
1951 delete control.params.attachment;
1952 hasAttachmentData.resolve();
1953 } else if ( control.params.attachment && control.params.attachment.id === value ) {
1954 hasAttachmentData.resolve();
1958 // Fetch the attachment data.
1959 if ( 'pending' === hasAttachmentData.state() ) {
1960 wp.media.attachment( value ).fetch().done( function() {
1961 control.params.attachment = this.attributes;
1962 hasAttachmentData.resolve();
1964 // Send attachment information to the preview for possible use in `postMessage` transport.
1965 wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
1969 hasAttachmentData.done( function() {
1970 control.renderContent();
1974 // Ensure attachment data is initially set (for dynamically-instantiated controls).
1975 setAttachmentDataAndRenderContent( control.setting() );
1977 // Update the attachment data and re-render the control when the setting changes.
1978 control.setting.bind( setAttachmentDataAndRenderContent );
1981 pausePlayer: function () {
1982 this.player && this.player.pause();
1985 cleanupPlayer: function () {
1986 this.player && wp.media.mixin.removePlayer( this.player );
1990 * Open the media modal.
1992 openFrame: function( event ) {
1993 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1997 event.preventDefault();
1999 if ( ! this.frame ) {
2007 * Create a media modal select frame, and store it so the instance can be reused when needed.
2009 initFrame: function() {
2010 this.frame = wp.media({
2012 text: this.params.button_labels.frame_button
2015 new wp.media.controller.Library({
2016 title: this.params.button_labels.frame_title,
2017 library: wp.media.query({ type: this.params.mime_type }),
2024 // When a file is selected, run a callback.
2025 this.frame.on( 'select', this.select );
2029 * Callback handler for when an attachment is selected in the media modal.
2030 * Gets the selected image information, and sets it within the control.
2032 select: function() {
2033 // Get the attachment from the modal frame.
2035 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2036 mejsSettings = window._wpmejsSettings || {};
2038 this.params.attachment = attachment;
2040 // Set the Customizer setting; the callback takes care of rendering.
2041 this.setting( attachment.id );
2042 node = this.container.find( 'audio, video' ).get(0);
2044 // Initialize audio/video previews.
2046 this.player = new MediaElementPlayer( node, mejsSettings );
2048 this.cleanupPlayer();
2053 * Reset the setting to the default value.
2055 restoreDefault: function( event ) {
2056 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2059 event.preventDefault();
2061 this.params.attachment = this.params.defaultAttachment;
2062 this.setting( this.params.defaultAttachment.url );
2066 * Called when the "Remove" link is clicked. Empties the setting.
2068 * @param {object} event jQuery Event object
2070 removeFile: function( event ) {
2071 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2074 event.preventDefault();
2076 this.params.attachment = {};
2078 this.renderContent(); // Not bound to setting change when emptying.
2083 * An upload control, which utilizes the media modal.
2086 * @augments wp.customize.MediaControl
2087 * @augments wp.customize.Control
2088 * @augments wp.customize.Class
2090 api.UploadControl = api.MediaControl.extend({
2093 * Callback handler for when an attachment is selected in the media modal.
2094 * Gets the selected image information, and sets it within the control.
2096 select: function() {
2097 // Get the attachment from the modal frame.
2099 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2100 mejsSettings = window._wpmejsSettings || {};
2102 this.params.attachment = attachment;
2104 // Set the Customizer setting; the callback takes care of rendering.
2105 this.setting( attachment.url );
2106 node = this.container.find( 'audio, video' ).get(0);
2108 // Initialize audio/video previews.
2110 this.player = new MediaElementPlayer( node, mejsSettings );
2112 this.cleanupPlayer();
2117 success: function() {},
2120 removerVisibility: function() {}
2124 * A control for uploading images.
2126 * This control no longer needs to do anything more
2127 * than what the upload control does in JS.
2130 * @augments wp.customize.UploadControl
2131 * @augments wp.customize.MediaControl
2132 * @augments wp.customize.Control
2133 * @augments wp.customize.Class
2135 api.ImageControl = api.UploadControl.extend({
2137 thumbnailSrc: function() {}
2141 * A control for uploading background images.
2144 * @augments wp.customize.UploadControl
2145 * @augments wp.customize.MediaControl
2146 * @augments wp.customize.Control
2147 * @augments wp.customize.Class
2149 api.BackgroundControl = api.UploadControl.extend({
2152 * When the control's DOM structure is ready,
2153 * set up internal event bindings.
2156 api.UploadControl.prototype.ready.apply( this, arguments );
2160 * Callback handler for when an attachment is selected in the media modal.
2161 * Does an additional AJAX request for setting the background context.
2163 select: function() {
2164 api.UploadControl.prototype.select.apply( this, arguments );
2166 wp.ajax.post( 'custom-background-add', {
2167 nonce: _wpCustomizeBackground.nonces.add,
2169 theme: api.settings.theme.stylesheet,
2170 attachment_id: this.params.attachment.id
2176 * A control for selecting and cropping an image.
2179 * @augments wp.customize.MediaControl
2180 * @augments wp.customize.Control
2181 * @augments wp.customize.Class
2183 api.CroppedImageControl = api.MediaControl.extend({
2186 * Open the media modal to the library state.
2188 openFrame: function( event ) {
2189 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2194 this.frame.setState( 'library' ).open();
2198 * Create a media modal select frame, and store it so the instance can be reused when needed.
2200 initFrame: function() {
2201 var l10n = _wpMediaViewsL10n;
2203 this.frame = wp.media({
2209 new wp.media.controller.Library({
2210 title: this.params.button_labels.frame_title,
2211 library: wp.media.query({ type: 'image' }),
2215 suggestedWidth: this.params.width,
2216 suggestedHeight: this.params.height
2218 new wp.media.controller.CustomizeImageCropper({
2219 imgSelectOptions: this.calculateImageSelectOptions,
2225 this.frame.on( 'select', this.onSelect, this );
2226 this.frame.on( 'cropped', this.onCropped, this );
2227 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2231 * After an image is selected in the media modal, switch to the cropper
2232 * state if the image isn't the right size.
2234 onSelect: function() {
2235 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2237 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2238 this.setImageFromAttachment( attachment );
2241 this.frame.setState( 'cropper' );
2246 * After the image has been cropped, apply the cropped image data to the setting.
2248 * @param {object} croppedImage Cropped attachment data.
2250 onCropped: function( croppedImage ) {
2251 this.setImageFromAttachment( croppedImage );
2255 * Returns a set of options, computed from the attached image data and
2256 * control-specific data, to be fed to the imgAreaSelect plugin in
2257 * wp.media.view.Cropper.
2259 * @param {wp.media.model.Attachment} attachment
2260 * @param {wp.media.controller.Cropper} controller
2261 * @returns {Object} Options
2263 calculateImageSelectOptions: function( attachment, controller ) {
2264 var control = controller.get( 'control' ),
2265 flexWidth = !! parseInt( control.params.flex_width, 10 ),
2266 flexHeight = !! parseInt( control.params.flex_height, 10 ),
2267 realWidth = attachment.get( 'width' ),
2268 realHeight = attachment.get( 'height' ),
2269 xInit = parseInt( control.params.width, 10 ),
2270 yInit = parseInt( control.params.height, 10 ),
2271 ratio = xInit / yInit,
2274 x1, y1, imgSelectOptions;
2276 controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
2278 if ( realWidth / realHeight > ratio ) {
2280 xInit = yInit * ratio;
2283 yInit = xInit / ratio;
2286 x1 = ( realWidth - xInit ) / 2;
2287 y1 = ( realHeight - yInit ) / 2;
2289 imgSelectOptions = {
2294 imageWidth: realWidth,
2295 imageHeight: realHeight,
2296 minWidth: xImg > xInit ? xInit : xImg,
2297 minHeight: yImg > yInit ? yInit : yImg,
2304 if ( flexHeight === false && flexWidth === false ) {
2305 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2308 if ( true === flexHeight ) {
2309 delete imgSelectOptions.minHeight;
2310 imgSelectOptions.maxWidth = realWidth;
2313 if ( true === flexWidth ) {
2314 delete imgSelectOptions.minWidth;
2315 imgSelectOptions.maxHeight = realHeight;
2318 return imgSelectOptions;
2322 * Return whether the image must be cropped, based on required dimensions.
2324 * @param {bool} flexW
2325 * @param {bool} flexH
2332 mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
2333 if ( true === flexW && true === flexH ) {
2337 if ( true === flexW && dstH === imgH ) {
2341 if ( true === flexH && dstW === imgW ) {
2345 if ( dstW === imgW && dstH === imgH ) {
2349 if ( imgW <= dstW ) {
2357 * If cropping was skipped, apply the image data directly to the setting.
2359 onSkippedCrop: function() {
2360 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2361 this.setImageFromAttachment( attachment );
2365 * Updates the setting and re-renders the control UI.
2367 * @param {object} attachment
2369 setImageFromAttachment: function( attachment ) {
2370 this.params.attachment = attachment;
2372 // Set the Customizer setting; the callback takes care of rendering.
2373 this.setting( attachment.id );
2378 * A control for selecting and cropping Site Icons.
2381 * @augments wp.customize.CroppedImageControl
2382 * @augments wp.customize.MediaControl
2383 * @augments wp.customize.Control
2384 * @augments wp.customize.Class
2386 api.SiteIconControl = api.CroppedImageControl.extend({
2389 * Create a media modal select frame, and store it so the instance can be reused when needed.
2391 initFrame: function() {
2392 var l10n = _wpMediaViewsL10n;
2394 this.frame = wp.media({
2400 new wp.media.controller.Library({
2401 title: this.params.button_labels.frame_title,
2402 library: wp.media.query({ type: 'image' }),
2406 suggestedWidth: this.params.width,
2407 suggestedHeight: this.params.height
2409 new wp.media.controller.SiteIconCropper({
2410 imgSelectOptions: this.calculateImageSelectOptions,
2416 this.frame.on( 'select', this.onSelect, this );
2417 this.frame.on( 'cropped', this.onCropped, this );
2418 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2422 * After an image is selected in the media modal, switch to the cropper
2423 * state if the image isn't the right size.
2425 onSelect: function() {
2426 var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2429 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2430 wp.ajax.post( 'crop-image', {
2431 nonce: attachment.nonces.edit,
2433 context: 'site-icon',
2437 width: this.params.width,
2438 height: this.params.height,
2439 dst_width: this.params.width,
2440 dst_height: this.params.height
2442 } ).done( function( croppedImage ) {
2443 controller.setImageFromAttachment( croppedImage );
2444 controller.frame.close();
2445 } ).fail( function() {
2446 controller.frame.trigger('content:error:crop');
2449 this.frame.setState( 'cropper' );
2454 * Updates the setting and re-renders the control UI.
2456 * @param {object} attachment
2458 setImageFromAttachment: function( attachment ) {
2459 var sizes = [ 'site_icon-32', 'thumbnail', 'full' ],
2462 _.each( sizes, function( size ) {
2463 if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
2464 icon = attachment.sizes[ size ];
2468 this.params.attachment = attachment;
2470 // Set the Customizer setting; the callback takes care of rendering.
2471 this.setting( attachment.id );
2473 // Update the icon in-browser.
2474 $( 'link[sizes="32x32"]' ).attr( 'href', icon.url );
2478 * Called when the "Remove" link is clicked. Empties the setting.
2480 * @param {object} event jQuery Event object
2482 removeFile: function( event ) {
2483 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2486 event.preventDefault();
2488 this.params.attachment = {};
2490 this.renderContent(); // Not bound to setting change when emptying.
2491 $( 'link[rel="icon"]' ).attr( 'href', '' );
2497 * @augments wp.customize.Control
2498 * @augments wp.customize.Class
2500 api.HeaderControl = api.Control.extend({
2502 this.btnRemove = $('#customize-control-header_image .actions .remove');
2503 this.btnNew = $('#customize-control-header_image .actions .new');
2505 _.bindAll(this, 'openMedia', 'removeImage');
2507 this.btnNew.on( 'click', this.openMedia );
2508 this.btnRemove.on( 'click', this.removeImage );
2510 api.HeaderTool.currentHeader = this.getInitialHeaderImage();
2512 new api.HeaderTool.CurrentView({
2513 model: api.HeaderTool.currentHeader,
2514 el: '#customize-control-header_image .current .container'
2517 new api.HeaderTool.ChoiceListView({
2518 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
2519 el: '#customize-control-header_image .choices .uploaded .list'
2522 new api.HeaderTool.ChoiceListView({
2523 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
2524 el: '#customize-control-header_image .choices .default .list'
2527 api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
2528 api.HeaderTool.UploadsList,
2529 api.HeaderTool.DefaultsList
2532 // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
2533 wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
2534 wp.media.controller.Cropper.prototype.defaults.doCropArgs.theme = api.settings.theme.stylesheet;
2538 * Returns a new instance of api.HeaderTool.ImageModel based on the currently
2539 * saved header image (if any).
2543 * @returns {Object} Options
2545 getInitialHeaderImage: function() {
2546 if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
2547 return new api.HeaderTool.ImageModel();
2550 // Get the matching uploaded image object.
2551 var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
2552 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
2554 // Fall back to raw current header image.
2555 if ( ! currentHeaderObject ) {
2556 currentHeaderObject = {
2557 url: api.get().header_image,
2558 thumbnail_url: api.get().header_image,
2559 attachment_id: api.get().header_image_data.attachment_id
2563 return new api.HeaderTool.ImageModel({
2564 header: currentHeaderObject,
2565 choice: currentHeaderObject.url.split( '/' ).pop()
2570 * Returns a set of options, computed from the attached image data and
2571 * theme-specific data, to be fed to the imgAreaSelect plugin in
2572 * wp.media.view.Cropper.
2574 * @param {wp.media.model.Attachment} attachment
2575 * @param {wp.media.controller.Cropper} controller
2576 * @returns {Object} Options
2578 calculateImageSelectOptions: function(attachment, controller) {
2579 var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
2580 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
2581 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
2582 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
2583 ratio, xImg, yImg, realHeight, realWidth,
2586 realWidth = attachment.get('width');
2587 realHeight = attachment.get('height');
2589 this.headerImage = new api.HeaderTool.ImageModel();
2590 this.headerImage.set({
2593 themeFlexWidth: flexWidth,
2594 themeFlexHeight: flexHeight,
2595 imageWidth: realWidth,
2596 imageHeight: realHeight
2599 controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
2601 ratio = xInit / yInit;
2605 if ( xImg / yImg > ratio ) {
2607 xInit = yInit * ratio;
2610 yInit = xInit / ratio;
2613 imgSelectOptions = {
2618 imageWidth: realWidth,
2619 imageHeight: realHeight,
2626 if (flexHeight === false && flexWidth === false) {
2627 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2629 if (flexHeight === false ) {
2630 imgSelectOptions.maxHeight = yInit;
2632 if (flexWidth === false ) {
2633 imgSelectOptions.maxWidth = xInit;
2636 return imgSelectOptions;
2640 * Sets up and opens the Media Manager in order to select an image.
2641 * Depending on both the size of the image and the properties of the
2642 * current theme, a cropping step after selection may be required or
2645 * @param {event} event
2647 openMedia: function(event) {
2648 var l10n = _wpMediaViewsL10n;
2650 event.preventDefault();
2652 this.frame = wp.media({
2654 text: l10n.selectAndCrop,
2658 new wp.media.controller.Library({
2659 title: l10n.chooseImage,
2660 library: wp.media.query({ type: 'image' }),
2664 suggestedWidth: _wpCustomizeHeader.data.width,
2665 suggestedHeight: _wpCustomizeHeader.data.height
2667 new wp.media.controller.Cropper({
2668 imgSelectOptions: this.calculateImageSelectOptions
2673 this.frame.on('select', this.onSelect, this);
2674 this.frame.on('cropped', this.onCropped, this);
2675 this.frame.on('skippedcrop', this.onSkippedCrop, this);
2681 * After an image is selected in the media modal,
2682 * switch to the cropper state.
2684 onSelect: function() {
2685 this.frame.setState('cropper');
2689 * After the image has been cropped, apply the cropped image data to the setting.
2691 * @param {object} croppedImage Cropped attachment data.
2693 onCropped: function(croppedImage) {
2694 var url = croppedImage.url,
2695 attachmentId = croppedImage.attachment_id,
2696 w = croppedImage.width,
2697 h = croppedImage.height;
2698 this.setImageFromURL(url, attachmentId, w, h);
2702 * If cropping was skipped, apply the image data directly to the setting.
2704 * @param {object} selection
2706 onSkippedCrop: function(selection) {
2707 var url = selection.get('url'),
2708 w = selection.get('width'),
2709 h = selection.get('height');
2710 this.setImageFromURL(url, selection.id, w, h);
2714 * Creates a new wp.customize.HeaderTool.ImageModel from provided
2715 * header image data and inserts it into the user-uploaded headers
2718 * @param {String} url
2719 * @param {Number} attachmentId
2720 * @param {Number} width
2721 * @param {Number} height
2723 setImageFromURL: function(url, attachmentId, width, height) {
2724 var choice, data = {};
2727 data.thumbnail_url = url;
2728 data.timestamp = _.now();
2731 data.attachment_id = attachmentId;
2739 data.height = height;
2742 choice = new api.HeaderTool.ImageModel({
2744 choice: url.split('/').pop()
2746 api.HeaderTool.UploadsList.add(choice);
2747 api.HeaderTool.currentHeader.set(choice.toJSON());
2749 choice.importImage();
2753 * Triggers the necessary events to deselect an image which was set as
2754 * the currently selected one.
2756 removeImage: function() {
2757 api.HeaderTool.currentHeader.trigger('hide');
2758 api.HeaderTool.CombinedList.trigger('control:removeImage');
2764 * wp.customize.ThemeControl
2767 * @augments wp.customize.Control
2768 * @augments wp.customize.Class
2770 api.ThemeControl = api.Control.extend({
2776 * Defer rendering the theme control until the section is displayed.
2780 renderContent: function () {
2782 renderContentArgs = arguments;
2784 api.section( control.section(), function( section ) {
2785 if ( section.expanded() ) {
2786 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2787 control.isRendered = true;
2789 section.expanded.bind( function( expanded ) {
2790 if ( expanded && ! control.isRendered ) {
2791 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2792 control.isRendered = true;
2805 control.container.on( 'touchmove', '.theme', function() {
2806 control.touchDrag = true;
2809 // Bind details view trigger.
2810 control.container.on( 'click keydown touchend', '.theme', function( event ) {
2811 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2815 // Bail if the user scrolled on a touch device.
2816 if ( control.touchDrag === true ) {
2817 return control.touchDrag = false;
2820 // Prevent the modal from showing when the user clicks the action button.
2821 if ( $( event.target ).is( '.theme-actions .button' ) ) {
2825 var previewUrl = $( this ).data( 'previewUrl' );
2827 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
2829 window.parent.location = previewUrl;
2832 control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
2833 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2837 event.preventDefault(); // Keep this AFTER the key filter above
2839 api.section( control.section() ).showDetails( control.params.theme );
2842 control.container.on( 'render-screenshot', function() {
2843 var $screenshot = $( this ).find( 'img' ),
2844 source = $screenshot.data( 'src' );
2847 $screenshot.attr( 'src', source );
2853 * Show or hide the theme based on the presence of the term in the title, description, and author.
2857 filter: function( term ) {
2859 haystack = control.params.theme.name + ' ' +
2860 control.params.theme.description + ' ' +
2861 control.params.theme.tags + ' ' +
2862 control.params.theme.author;
2863 haystack = haystack.toLowerCase().replace( '-', ' ' );
2864 if ( -1 !== haystack.search( term ) ) {
2867 control.deactivate();
2872 // Change objects contained within the main customize object to Settings.
2873 api.defaultConstructor = api.Setting;
2875 // Create the collections for Controls, Sections and Panels.
2876 api.control = new api.Values({ defaultConstructor: api.Control });
2877 api.section = new api.Values({ defaultConstructor: api.Section });
2878 api.panel = new api.Values({ defaultConstructor: api.Panel });
2881 * An object that fetches a preview in the background of the document, which
2882 * allows for seamless replacement of an existing preview.
2885 * @augments wp.customize.Messenger
2886 * @augments wp.customize.Class
2887 * @mixes wp.customize.Events
2889 api.PreviewFrame = api.Messenger.extend({
2893 * Initialize the PreviewFrame.
2895 * @param {object} params.container
2896 * @param {object} params.signature
2897 * @param {object} params.previewUrl
2898 * @param {object} params.query
2899 * @param {object} options
2901 initialize: function( params, options ) {
2902 var deferred = $.Deferred();
2905 * Make the instance of the PreviewFrame the promise object
2906 * so other objects can easily interact with it.
2908 deferred.promise( this );
2910 this.container = params.container;
2911 this.signature = params.signature;
2913 $.extend( params, { channel: api.PreviewFrame.uuid() });
2915 api.Messenger.prototype.initialize.call( this, params, options );
2917 this.add( 'previewUrl', params.previewUrl );
2919 this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
2921 this.run( deferred );
2925 * Run the preview request.
2927 * @param {object} deferred jQuery Deferred object to be resolved with
2930 run: function( deferred ) {
2935 if ( this._ready ) {
2936 this.unbind( 'ready', this._ready );
2939 this._ready = function() {
2943 deferred.resolveWith( self );
2947 this.bind( 'ready', this._ready );
2949 this.bind( 'ready', function ( data ) {
2951 this.container.addClass( 'iframe-ready' );
2958 * Walk over all panels, sections, and controls and set their
2959 * respective active states to true if the preview explicitly
2960 * indicates as such.
2963 panel: data.activePanels,
2964 section: data.activeSections,
2965 control: data.activeControls
2967 _( constructs ).each( function ( activeConstructs, type ) {
2968 api[ type ].each( function ( construct, id ) {
2969 var active = !! ( activeConstructs && activeConstructs[ id ] );
2971 construct.activate();
2973 construct.deactivate();
2978 if ( data.settingValidities ) {
2979 api._handleSettingValidities( {
2980 settingValidities: data.settingValidities,
2981 focusInvalidControl: false
2986 this.request = $.ajax( this.previewUrl(), {
2990 withCredentials: true
2994 this.request.fail( function() {
2995 deferred.rejectWith( self, [ 'request failure' ] );
2998 this.request.done( function( response ) {
2999 var location = self.request.getResponseHeader('Location'),
3000 signature = self.signature,
3003 // Check if the location response header differs from the current URL.
3004 // If so, the request was redirected; try loading the requested page.
3005 if ( location && location !== self.previewUrl() ) {
3006 deferred.rejectWith( self, [ 'redirect', location ] );
3010 // Check if the user is not logged in.
3011 if ( '0' === response ) {
3012 self.login( deferred );
3016 // Check for cheaters.
3017 if ( '-1' === response ) {
3018 deferred.rejectWith( self, [ 'cheatin' ] );
3022 // Check for a signature in the request.
3023 index = response.lastIndexOf( signature );
3024 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
3025 deferred.rejectWith( self, [ 'unsigned' ] );
3029 // Strip the signature from the request.
3030 response = response.slice( 0, index ) + response.slice( index + signature.length );
3032 // Create the iframe and inject the html content.
3033 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
3035 // Bind load event after the iframe has been added to the page;
3036 // otherwise it will fire when injected into the DOM.
3037 self.iframe.one( 'load', function() {
3041 deferred.resolveWith( self );
3043 setTimeout( function() {
3044 deferred.rejectWith( self, [ 'ready timeout' ] );
3045 }, self.sensitivity );
3049 self.targetWindow( self.iframe[0].contentWindow );
3051 self.targetWindow().document.open();
3052 self.targetWindow().document.write( response );
3053 self.targetWindow().document.close();
3057 login: function( deferred ) {
3061 reject = function() {
3062 deferred.rejectWith( self, [ 'logged out' ] );
3065 if ( this.triedLogin ) {
3069 // Check if we have an admin cookie.
3070 $.get( api.settings.url.ajax, {
3072 }).fail( reject ).done( function( response ) {
3075 if ( '1' !== response ) {
3079 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
3080 iframe.appendTo( self.container );
3081 iframe.on( 'load', function() {
3082 self.triedLogin = true;
3085 self.run( deferred );
3090 destroy: function() {
3091 api.Messenger.prototype.destroy.call( this );
3092 this.request.abort();
3095 this.iframe.remove();
3097 delete this.request;
3099 delete this.targetWindow;
3106 * Create a universally unique identifier.
3110 api.PreviewFrame.uuid = function() {
3111 return 'preview-' + uuid++;
3116 * Set the document title of the customizer.
3120 * @param {string} documentTitle
3122 api.setDocumentTitle = function ( documentTitle ) {
3124 tmpl = api.settings.documentTitleTmpl;
3125 title = tmpl.replace( '%s', documentTitle );
3126 document.title = title;
3127 api.trigger( 'title', title );
3132 * @augments wp.customize.Messenger
3133 * @augments wp.customize.Class
3134 * @mixes wp.customize.Events
3136 api.Previewer = api.Messenger.extend({
3140 * @param {array} params.allowedUrls
3141 * @param {string} params.container A selector or jQuery element for the preview
3142 * frame to be placed.
3143 * @param {string} params.form
3144 * @param {string} params.previewUrl The URL to preview.
3145 * @param {string} params.signature
3146 * @param {object} options
3148 initialize: function( params, options ) {
3150 rscheme = /^https?/;
3152 $.extend( this, options || {} );
3154 active: $.Deferred()
3158 * Wrap this.refresh to prevent it from hammering the servers:
3160 * If refresh is called once and no other refresh requests are
3161 * loading, trigger the request immediately.
3163 * If refresh is called while another refresh request is loading,
3164 * debounce the refresh requests:
3165 * 1. Stop the loading request (as it is instantly outdated).
3166 * 2. Trigger the new request once refresh hasn't been called for
3167 * self.refreshBuffer milliseconds.
3169 this.refresh = (function( self ) {
3170 var refresh = self.refresh,
3171 callback = function() {
3173 refresh.call( self );
3178 if ( typeof timeout !== 'number' ) {
3179 if ( self.loading ) {
3186 clearTimeout( timeout );
3187 timeout = setTimeout( callback, self.refreshBuffer );
3191 this.container = api.ensure( params.container );
3192 this.allowedUrls = params.allowedUrls;
3193 this.signature = params.signature;
3195 params.url = window.location.href;
3197 api.Messenger.prototype.initialize.call( this, params );
3199 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
3200 var match = to.match( rscheme );
3201 return match ? match[0] : '';
3204 // Limit the URL to internal, front-end links.
3206 // If the front end and the admin are served from the same domain, load the
3207 // preview over ssl if the Customizer is being loaded over ssl. This avoids
3208 // insecure content warnings. This is not attempted if the admin and front end
3209 // are on different domains to avoid the case where the front end doesn't have
3212 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
3215 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
3216 // Strip hashes and query strings before testing.
3217 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
3220 // Attempt to match the URL to the control frame's scheme
3221 // and check if it's allowed. If not, try the original URL.
3222 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
3223 $.each( self.allowedUrls, function( i, allowed ) {
3226 allowed = allowed.replace( /\/+$/, '' );
3227 path = url.replace( allowed, '' );
3229 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
3238 // If we found a matching result, return it. If not, bail.
3239 return result ? result : null;
3242 // Refresh the preview when the URL is changed (but not yet).
3243 this.previewUrl.bind( this.refresh );
3246 this.bind( 'scroll', function( distance ) {
3247 this.scroll = distance;
3250 // Update the URL when the iframe sends a URL message.
3251 this.bind( 'url', this.previewUrl );
3253 // Update the document title when the preview changes.
3254 this.bind( 'documentTitle', function ( title ) {
3255 api.setDocumentTitle( title );
3260 * Query string data sent with each preview request.
3264 query: function() {},
3267 if ( this.loading ) {
3268 this.loading.destroy();
3269 delete this.loading;
3274 * Refresh the preview.
3276 refresh: function() {
3279 // Display loading indicator
3280 this.send( 'loading-initiated' );
3284 this.loading = new api.PreviewFrame({
3286 previewUrl: this.previewUrl(),
3287 query: this.query() || {},
3288 container: this.container,
3289 signature: this.signature
3292 this.loading.done( function() {
3293 // 'this' is the loading frame
3294 this.bind( 'synced', function() {
3296 self.preview.destroy();
3297 self.preview = this;
3298 delete self.loading;
3300 self.targetWindow( this.targetWindow() );
3301 self.channel( this.channel() );
3303 self.deferred.active.resolve();
3304 self.send( 'active' );
3307 this.send( 'sync', {
3308 scroll: self.scroll,
3313 this.loading.fail( function( reason, location ) {
3314 self.send( 'loading-failed' );
3315 if ( 'redirect' === reason && location ) {
3316 self.previewUrl( location );
3319 if ( 'logged out' === reason ) {
3320 if ( self.preview ) {
3321 self.preview.destroy();
3322 delete self.preview;
3325 self.login().done( self.refresh );
3328 if ( 'cheatin' === reason ) {
3335 var previewer = this,
3336 deferred, messenger, iframe;
3341 deferred = $.Deferred();
3342 this._login = deferred.promise();
3344 messenger = new api.Messenger({
3346 url: api.settings.url.login
3349 iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
3351 messenger.targetWindow( iframe[0].contentWindow );
3353 messenger.bind( 'login', function () {
3354 var refreshNonces = previewer.refreshNonces();
3356 refreshNonces.always( function() {
3358 messenger.destroy();
3359 delete previewer._login;
3362 refreshNonces.done( function() {
3366 refreshNonces.fail( function() {
3367 previewer.cheatin();
3375 cheatin: function() {
3376 $( document.body ).empty().addClass( 'cheatin' ).append(
3377 '<h1>' + api.l10n.cheatin + '</h1>' +
3378 '<p>' + api.l10n.notAllowed + '</p>'
3382 refreshNonces: function() {
3383 var request, deferred = $.Deferred();
3387 request = wp.ajax.post( 'customize_refresh_nonces', {
3389 theme: api.settings.theme.stylesheet
3392 request.done( function( response ) {
3393 api.trigger( 'nonce-refresh', response );
3397 request.fail( function() {
3405 api.settingConstructor = {};
3406 api.controlConstructor = {
3407 color: api.ColorControl,
3408 media: api.MediaControl,
3409 upload: api.UploadControl,
3410 image: api.ImageControl,
3411 cropped_image: api.CroppedImageControl,
3412 site_icon: api.SiteIconControl,
3413 header: api.HeaderControl,
3414 background: api.BackgroundControl,
3415 theme: api.ThemeControl
3417 api.panelConstructor = {};
3418 api.sectionConstructor = {
3419 themes: api.ThemesSection
3423 * Handle setting_validities in an error response for the customize-save request.
3425 * Add notifications to the settings and focus on the first control that has an invalid setting.
3430 * @param {object} args
3431 * @param {object} args.settingValidities
3432 * @param {boolean} [args.focusInvalidControl=false]
3435 api._handleSettingValidities = function handleSettingValidities( args ) {
3436 var invalidSettingControls, invalidSettings = [], wasFocused = false;
3438 // Find the controls that correspond to each invalid setting.
3439 _.each( args.settingValidities, function( validity, settingId ) {
3440 var setting = api( settingId );
3443 // Add notifications for invalidities.
3444 if ( _.isObject( validity ) ) {
3445 _.each( validity, function( params, code ) {
3446 var notification = new api.Notification( code, params ), existingNotification, needsReplacement = false;
3448 // Remove existing notification if already exists for code but differs in parameters.
3449 existingNotification = setting.notifications( notification.code );
3450 if ( existingNotification ) {
3451 needsReplacement = ( notification.type !== existingNotification.type ) || ! _.isEqual( notification.data, existingNotification.data );
3453 if ( needsReplacement ) {
3454 setting.notifications.remove( code );
3457 if ( ! setting.notifications.has( notification.code ) ) {
3458 setting.notifications.add( code, notification );
3460 invalidSettings.push( setting.id );
3464 // Remove notification errors that are no longer valid.
3465 setting.notifications.each( function( notification ) {
3466 if ( 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
3467 setting.notifications.remove( notification.code );
3473 if ( args.focusInvalidControl ) {
3474 invalidSettingControls = api.findControlsForSettings( invalidSettings );
3476 // Focus on the first control that is inside of an expanded section (one that is visible).
3477 _( _.values( invalidSettingControls ) ).find( function( controls ) {
3478 return _( controls ).find( function( control ) {
3479 var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
3480 if ( isExpanded && control.expanded ) {
3481 isExpanded = control.expanded();
3491 // Focus on the first invalid control.
3492 if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
3493 _.values( invalidSettingControls )[0][0].focus();
3499 * Find all controls associated with the given settings.
3502 * @param {string[]} settingIds Setting IDs.
3503 * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
3505 api.findControlsForSettings = function findControlsForSettings( settingIds ) {
3506 var controls = {}, settingControls;
3507 _.each( _.unique( settingIds ), function( settingId ) {
3508 var setting = api( settingId );
3510 settingControls = setting.findControls();
3511 if ( settingControls && settingControls.length > 0 ) {
3512 controls[ settingId ] = settingControls;
3520 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
3524 api.reflowPaneContents = _.bind( function () {
3526 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3528 if ( document.activeElement ) {
3529 activeElement = $( document.activeElement );
3532 // Sort the sections within each panel
3533 api.panel.each( function ( panel ) {
3534 var sections = panel.sections(),
3535 sectionContainers = _.pluck( sections, 'container' );
3536 rootNodes.push( panel );
3537 appendContainer = panel.container.find( 'ul:first' );
3538 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
3539 _( sections ).each( function ( section ) {
3540 appendContainer.append( section.container );
3546 // Sort the controls within each section
3547 api.section.each( function ( section ) {
3548 var controls = section.controls(),
3549 controlContainers = _.pluck( controls, 'container' );
3550 if ( ! section.panel() ) {
3551 rootNodes.push( section );
3553 appendContainer = section.container.find( 'ul:first' );
3554 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
3555 _( controls ).each( function ( control ) {
3556 appendContainer.append( control.container );
3562 // Sort the root panels and sections
3563 rootNodes.sort( api.utils.prioritySort );
3564 rootContainers = _.pluck( rootNodes, 'container' );
3565 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
3566 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
3567 _( rootNodes ).each( function ( rootNode ) {
3568 appendContainer.append( rootNode.container );
3573 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
3574 api.panel.each( function ( panel ) {
3575 var value = panel.active();
3576 panel.active.callbacks.fireWith( panel.active, [ value, value ] );
3578 api.section.each( function ( section ) {
3579 var value = section.active();
3580 section.active.callbacks.fireWith( section.active, [ value, value ] );
3583 // Restore focus if there was a reflow and there was an active (focused) element
3584 if ( wasReflowed && activeElement ) {
3585 activeElement.focus();
3587 api.trigger( 'pane-contents-reflowed' );
3591 api.settings = window._wpCustomizeSettings;
3592 api.l10n = window._wpCustomizeControlsL10n;
3594 // Check if we can run the Customizer.
3595 if ( ! api.settings ) {
3599 // Bail if any incompatibilities are found.
3600 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
3604 var parent, topFocus,
3605 body = $( document.body ),
3606 overlay = body.children( '.wp-full-overlay' ),
3607 title = $( '#customize-info .panel-title.site-title' ),
3608 closeBtn = $( '.customize-controls-close' ),
3609 saveBtn = $( '#save' ),
3610 footerActions = $( '#customize-footer-actions' );
3612 // Prevent the form from saving when enter is pressed on an input or select element.
3613 $('#customize-controls').on( 'keydown', function( e ) {
3614 var isEnter = ( 13 === e.which ),
3615 $el = $( e.target );
3617 if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
3622 // Expand/Collapse the main customizer customize info.
3623 $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
3624 var section = $( this ).closest( '.accordion-section' ),
3625 content = section.find( '.customize-panel-description:first' );
3627 if ( section.hasClass( 'cannot-expand' ) ) {
3631 if ( section.hasClass( 'open' ) ) {
3632 section.toggleClass( 'open' );
3633 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
3634 $( this ).attr( 'aria-expanded', false );
3636 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
3637 section.toggleClass( 'open' );
3638 $( this ).attr( 'aria-expanded', true );
3642 // Initialize Previewer
3643 api.previewer = new api.Previewer({
3644 container: '#customize-preview',
3645 form: '#customize-controls',
3646 previewUrl: api.settings.url.preview,
3647 allowedUrls: api.settings.url.allowed,
3648 signature: 'WP_CUSTOMIZER_SIGNATURE'
3651 nonce: api.settings.nonce,
3654 * Build the query to send along with the Preview request.
3659 var dirtyCustomized = {};
3660 api.each( function ( value, key ) {
3661 if ( value._dirty ) {
3662 dirtyCustomized[ key ] = value();
3668 theme: api.settings.theme.stylesheet,
3669 customized: JSON.stringify( dirtyCustomized ),
3670 nonce: this.nonce.preview
3676 processing = api.state( 'processing' ),
3677 submitWhenDoneProcessing,
3679 modifiedWhileSaving = {},
3680 invalidSettings = [],
3683 body.addClass( 'saving' );
3685 function captureSettingModifiedDuringSave( setting ) {
3686 modifiedWhileSaving[ setting.id ] = true;
3688 api.bind( 'change', captureSettingModifiedDuringSave );
3690 submit = function () {
3694 * Block saving if there are any settings that are marked as
3695 * invalid from the client (not from the server). Focus on
3698 api.each( function( setting ) {
3699 setting.notifications.each( function( notification ) {
3700 if ( 'error' === notification.type && ( ! notification.data || ! notification.data.from_server ) ) {
3701 invalidSettings.push( setting.id );
3705 invalidControls = api.findControlsForSettings( invalidSettings );
3706 if ( ! _.isEmpty( invalidControls ) ) {
3707 _.values( invalidControls )[0][0].focus();
3708 body.removeClass( 'saving' );
3709 api.unbind( 'change', captureSettingModifiedDuringSave );
3713 query = $.extend( self.query(), {
3714 nonce: self.nonce.save
3716 request = wp.ajax.post( 'customize_save', query );
3718 // Disable save button during the save request.
3719 saveBtn.prop( 'disabled', true );
3721 api.trigger( 'save', request );
3723 request.always( function () {
3724 body.removeClass( 'saving' );
3725 saveBtn.prop( 'disabled', false );
3726 api.unbind( 'change', captureSettingModifiedDuringSave );
3729 request.fail( function ( response ) {
3730 if ( '0' === response ) {
3731 response = 'not_logged_in';
3732 } else if ( '-1' === response ) {
3733 // Back-compat in case any other check_ajax_referer() call is dying
3734 response = 'invalid_nonce';
3737 if ( 'invalid_nonce' === response ) {
3739 } else if ( 'not_logged_in' === response ) {
3740 self.preview.iframe.hide();
3741 self.login().done( function() {
3743 self.preview.iframe.show();
3747 if ( response.setting_validities ) {
3748 api._handleSettingValidities( {
3749 settingValidities: response.setting_validities,
3750 focusInvalidControl: true
3754 api.trigger( 'error', response );
3757 request.done( function( response ) {
3759 // Clear setting dirty states, if setting wasn't modified while saving.
3760 api.each( function( setting ) {
3761 if ( ! modifiedWhileSaving[ setting.id ] ) {
3762 setting._dirty = false;
3766 api.previewer.send( 'saved', response );
3768 if ( response.setting_validities ) {
3769 api._handleSettingValidities( {
3770 settingValidities: response.setting_validities,
3771 focusInvalidControl: true
3775 api.trigger( 'saved', response );
3777 // Restore the global dirty state if any settings were modified during save.
3778 if ( ! _.isEmpty( modifiedWhileSaving ) ) {
3779 api.state( 'saved' ).set( false );
3784 if ( 0 === processing() ) {
3787 submitWhenDoneProcessing = function () {
3788 if ( 0 === processing() ) {
3789 api.state.unbind( 'change', submitWhenDoneProcessing );
3793 api.state.bind( 'change', submitWhenDoneProcessing );
3799 // Refresh the nonces if the preview sends updated nonces over.
3800 api.previewer.bind( 'nonce', function( nonce ) {
3801 $.extend( this.nonce, nonce );
3804 // Refresh the nonces if login sends updated nonces over.
3805 api.bind( 'nonce-refresh', function( nonce ) {
3806 $.extend( api.settings.nonce, nonce );
3807 $.extend( api.previewer.nonce, nonce );
3808 api.previewer.send( 'nonce-refresh', nonce );
3812 $.each( api.settings.settings, function( id, data ) {
3813 var constructor = api.settingConstructor[ data.type ] || api.Setting,
3816 setting = new constructor( id, data.value, {
3817 transport: data.transport,
3818 previewer: api.previewer,
3819 dirty: !! data.dirty
3821 api.add( id, setting );
3825 $.each( api.settings.panels, function ( id, data ) {
3826 var constructor = api.panelConstructor[ data.type ] || api.Panel,
3829 panel = new constructor( id, {
3832 api.panel.add( id, panel );
3836 $.each( api.settings.sections, function ( id, data ) {
3837 var constructor = api.sectionConstructor[ data.type ] || api.Section,
3840 section = new constructor( id, {
3843 api.section.add( id, section );
3847 $.each( api.settings.controls, function( id, data ) {
3848 var constructor = api.controlConstructor[ data.type ] || api.Control,
3851 control = new constructor( id, {
3853 previewer: api.previewer
3855 api.control.add( id, control );
3858 // Focus the autofocused element
3859 _.each( [ 'panel', 'section', 'control' ], function( type ) {
3860 var id = api.settings.autofocus[ type ];
3866 * Defer focus until:
3867 * 1. The panel, section, or control exists (especially for dynamically-created ones).
3868 * 2. The instance is embedded in the document (and so is focusable).
3869 * 3. The preview has finished loading so that the active states have been set.
3871 api[ type ]( id, function( instance ) {
3872 instance.deferred.embedded.done( function() {
3873 api.previewer.deferred.active.done( function() {
3880 api.bind( 'ready', api.reflowPaneContents );
3881 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
3882 var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
3883 values.bind( 'add', debouncedReflowPaneContents );
3884 values.bind( 'change', debouncedReflowPaneContents );
3885 values.bind( 'remove', debouncedReflowPaneContents );
3888 // Check if preview url is valid and load the preview frame.
3889 if ( api.previewer.previewUrl() ) {
3890 api.previewer.refresh();
3892 api.previewer.previewUrl( api.settings.url.home );
3895 // Save and activated states
3897 var state = new api.Values(),
3898 saved = state.create( 'saved' ),
3899 activated = state.create( 'activated' ),
3900 processing = state.create( 'processing' );
3902 state.bind( 'change', function() {
3903 if ( ! activated() ) {
3904 saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
3905 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3907 } else if ( saved() ) {
3908 saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
3909 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
3912 saveBtn.val( api.l10n.save ).prop( 'disabled', false );
3913 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3917 // Set default states.
3919 activated( api.settings.theme.active );
3922 api.bind( 'change', function() {
3923 state('saved').set( false );
3926 api.bind( 'saved', function() {
3927 state('saved').set( true );
3928 state('activated').set( true );
3931 activated.bind( function( to ) {
3933 api.trigger( 'activated' );
3937 // Expose states to the API.
3942 saveBtn.click( function( event ) {
3943 api.previewer.save();
3944 event.preventDefault();
3945 }).keydown( function( event ) {
3946 if ( 9 === event.which ) // tab
3948 if ( 13 === event.which ) // enter
3949 api.previewer.save();
3950 event.preventDefault();
3953 closeBtn.keydown( function( event ) {
3954 if ( 9 === event.which ) // tab
3956 if ( 13 === event.which ) // enter
3958 event.preventDefault();
3961 $( '.collapse-sidebar' ).on( 'click', function() {
3962 if ( 'true' === $( this ).attr( 'aria-expanded' ) ) {
3963 $( this ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
3965 $( this ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
3968 overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
3971 // Keyboard shortcuts - esc to exit section/panel.
3972 $( 'body' ).on( 'keydown', function( event ) {
3973 var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
3975 if ( 27 !== event.which ) { // Esc.
3979 // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
3980 api.control.each( function( control ) {
3981 if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
3982 expandedControls.push( control );
3985 api.section.each( function( section ) {
3986 if ( section.expanded() ) {
3987 expandedSections.push( section );
3990 api.panel.each( function( panel ) {
3991 if ( panel.expanded() ) {
3992 expandedPanels.push( panel );
3996 // Skip collapsing expanded controls if there are no expanded sections.
3997 if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
3998 expandedControls.length = 0;
4001 // Collapse the most granular expanded object.
4002 collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
4003 if ( collapsedObject ) {
4004 collapsedObject.collapse();
4005 event.preventDefault();
4009 $( '.customize-controls-preview-toggle' ).on( 'click', function() {
4010 overlay.toggleClass( 'preview-only' );
4013 // Previewed device bindings.
4014 api.previewedDevice = new api.Value();
4016 // Set the default device.
4017 api.bind( 'ready', function() {
4018 _.find( api.settings.previewableDevices, function( value, key ) {
4019 if ( true === value['default'] ) {
4020 api.previewedDevice.set( key );
4026 // Set the toggled device.
4027 footerActions.find( '.devices button' ).on( 'click', function( event ) {
4028 api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
4031 // Bind device changes.
4032 api.previewedDevice.bind( function( newDevice ) {
4033 var overlay = $( '.wp-full-overlay' ),
4036 footerActions.find( '.devices button' )
4037 .removeClass( 'active' )
4038 .attr( 'aria-pressed', false );
4040 footerActions.find( '.devices .preview-' + newDevice )
4041 .addClass( 'active' )
4042 .attr( 'aria-pressed', true );
4044 $.each( api.settings.previewableDevices, function( device ) {
4045 devices += ' preview-' + device;
4049 .removeClass( devices )
4050 .addClass( 'preview-' + newDevice );
4053 // Bind site title display to the corresponding field.
4054 if ( title.length ) {
4055 api( 'blogname', function( setting ) {
4056 var updateTitle = function() {
4057 title.text( $.trim( setting() ) || api.l10n.untitledBlogName );
4059 setting.bind( updateTitle );
4065 * Create a postMessage connection with a parent frame,
4066 * in case the Customizer frame was opened with the Customize loader.
4068 * @see wp.customize.Loader
4070 parent = new api.Messenger({
4071 url: api.settings.url.parent,
4076 * If we receive a 'back' event, we're inside an iframe.
4077 * Send any clicks to the 'Return' link to the parent page.
4079 parent.bind( 'back', function() {
4080 closeBtn.on( 'click.customize-controls-close', function( event ) {
4081 event.preventDefault();
4082 parent.send( 'close' );
4086 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
4087 $( window ).on( 'beforeunload', function () {
4088 if ( ! api.state( 'saved' )() ) {
4089 setTimeout( function() {
4090 overlay.removeClass( 'customize-loading' );
4092 return api.l10n.saveAlert;
4096 // Pass events through to the parent.
4097 $.each( [ 'saved', 'change' ], function ( i, event ) {
4098 api.bind( event, function() {
4099 parent.send( event );
4103 // Pass titles to the parent
4104 api.bind( 'title', function( newTitle ) {
4105 parent.send( 'title', newTitle );
4108 // Initialize the connection with the parent frame.
4109 parent.send( 'ready' );
4111 // Control visibility for default controls
4113 'background_image': {
4114 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
4115 callback: function( to ) { return !! to; }
4118 controls: [ 'page_on_front', 'page_for_posts' ],
4119 callback: function( to ) { return 'page' === to; }
4121 'header_textcolor': {
4122 controls: [ 'header_textcolor' ],
4123 callback: function( to ) { return 'blank' !== to; }
4125 }, function( settingId, o ) {
4126 api( settingId, function( setting ) {
4127 $.each( o.controls, function( i, controlId ) {
4128 api.control( controlId, function( control ) {
4129 var visibility = function( to ) {
4130 control.container.toggle( o.callback( to ) );
4133 visibility( setting.get() );
4134 setting.bind( visibility );
4140 // Juggle the two controls that use header_textcolor
4141 api.control( 'display_header_text', function( control ) {
4144 control.elements[0].unsync( api( 'header_textcolor' ) );
4146 control.element = new api.Element( control.container.find('input') );
4147 control.element.set( 'blank' !== control.setting() );
4149 control.element.bind( function( to ) {
4151 last = api( 'header_textcolor' ).get();
4153 control.setting.set( to ? last : 'blank' );
4156 control.setting.bind( function( to ) {
4157 control.element.set( 'blank' !== to );
4161 // Change previewed URL to the homepage when changing the page_on_front.
4162 api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
4163 var updatePreviewUrl = function() {
4164 if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
4165 api.previewer.previewUrl.set( api.settings.url.home );
4168 showOnFront.bind( updatePreviewUrl );
4169 pageOnFront.bind( updatePreviewUrl );
4172 // Change the previewed URL to the selected page when changing the page_for_posts.
4173 api( 'page_for_posts', function( setting ) {
4174 setting.bind(function( pageId ) {
4175 pageId = parseInt( pageId, 10 );
4177 api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
4182 // Update the setting validities.
4183 api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
4184 api._handleSettingValidities( {
4185 settingValidities: settingValidities,
4186 focusInvalidControl: false
4190 // Focus on the control that is associated with the given setting.
4191 api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
4193 api.control.each( function( control ) {
4194 var settingIds = _.pluck( control.settings, 'id' );
4195 if ( -1 !== _.indexOf( settingIds, settingId ) ) {
4196 matchedControl = control;
4200 if ( matchedControl ) {
4201 matchedControl.focus();
4205 // Refresh the preview when it requests.
4206 api.previewer.bind( 'refresh', function() {
4207 api.previewer.refresh();
4210 api.trigger( 'ready' );
4212 // Make sure left column gets focus
4213 topFocus = closeBtn;
4215 setTimeout(function () {