1 /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
2 (function( exports, $ ){
3 var Container, focus, api = wp.customize;
7 * @augments wp.customize.Value
8 * @augments wp.customize.Class
11 * - previewer - The Previewer instance to sync with.
12 * - transport - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
14 api.Setting = api.Value.extend({
15 initialize: function( id, value, options ) {
16 api.Value.prototype.initialize.call( this, value, options );
19 this.transport = this.transport || 'refresh';
20 this._dirty = options.dirty || false;
22 this.bind( this.preview );
25 switch ( this.transport ) {
27 return this.previewer.refresh();
29 return this.previewer.send( 'setting', [ this.id, this() ] );
35 * Utility function namespace
40 * Watch all changes to Value properties, and bubble changes to parent Values instance
44 * @param {wp.customize.Class} instance
45 * @param {Array} properties The names of the Value instances to watch.
47 api.utils.bubbleChildValueChanges = function ( instance, properties ) {
48 $.each( properties, function ( i, key ) {
49 instance[ key ].bind( function ( to, from ) {
50 if ( instance.parent && to !== from ) {
51 instance.parent.trigger( 'change', instance );
58 * Expand a panel, section, or control and focus on the first focusable element.
62 * @param {Object} [params]
63 * @param {Callback} [params.completeCallback]
65 focus = function ( params ) {
66 var construct, completeCallback, focus;
68 params = params || {};
71 if ( construct.extended( api.Panel ) && construct.expanded && construct.expanded() ) {
72 focusContainer = construct.container.find( 'ul.control-panel-content' );
73 } else if ( construct.extended( api.Section ) && construct.expanded && construct.expanded() ) {
74 focusContainer = construct.container.find( 'ul.accordion-section-content' );
76 focusContainer = construct.container;
79 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
80 focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first().focus();
82 if ( params.completeCallback ) {
83 completeCallback = params.completeCallback;
84 params.completeCallback = function () {
89 params.completeCallback = focus;
91 if ( construct.expand ) {
92 construct.expand( params );
94 params.completeCallback();
99 * Stable sort for Panels, Sections, and Controls.
101 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
105 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
106 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
109 api.utils.prioritySort = function ( a, b ) {
110 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
111 return a.params.instanceNumber - b.params.instanceNumber;
113 return a.priority() - b.priority();
118 * Return whether the supplied Event object is for a keydown event but not the Enter key.
122 * @param {jQuery.Event} event
125 api.utils.isKeydownButNotEnterEvent = function ( event ) {
126 return ( 'keydown' === event.type && 13 !== event.which );
130 * Return whether the two lists of elements are the same and are in the same order.
134 * @param {Array|jQuery} listA
135 * @param {Array|jQuery} listB
138 api.utils.areElementListsEqual = function ( listA, listB ) {
140 listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
141 -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
142 _.zip( listA, listB ), // pair up each element between the two lists
144 return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
146 ), false ) // check for presence of false in map's return value
152 * Base class for Panel and Section.
157 * @augments wp.customize.Class
159 Container = api.Class.extend({
160 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
161 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
162 containerType: 'container',
176 * @param {string} id - The ID for the container.
177 * @param {object} options - Object containing one property: params.
178 * @param {object} options.params - Object containing the following properties.
179 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
180 * @param {string=} [options.params.description] - Description shown at the top of the panel.
181 * @param {number=100} [options.params.priority] - The sort priority for the panel.
182 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
183 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
184 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
186 initialize: function ( id, options ) {
187 var container = this;
189 options = options || {};
191 options.params = _.defaults(
192 options.params || {},
196 $.extend( container, options );
197 container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
198 container.container = $( container.params.content );
199 if ( 0 === container.container.length ) {
200 container.container = $( container.getContainer() );
203 container.deferred = {
204 embedded: new $.Deferred()
206 container.priority = new api.Value();
207 container.active = new api.Value();
208 container.activeArgumentsQueue = [];
209 container.expanded = new api.Value();
210 container.expandedArgumentsQueue = [];
212 container.active.bind( function ( active ) {
213 var args = container.activeArgumentsQueue.shift();
214 args = $.extend( {}, container.defaultActiveArguments, args );
215 active = ( active && container.isContextuallyActive() );
216 container.onChangeActive( active, args );
218 container.expanded.bind( function ( expanded ) {
219 var args = container.expandedArgumentsQueue.shift();
220 args = $.extend( {}, container.defaultExpandedArguments, args );
221 container.onChangeExpanded( expanded, args );
224 container.deferred.embedded.done( function () {
225 container.attachEvents();
228 api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
230 container.priority.set( container.params.priority );
231 container.active.set( container.params.active );
232 container.expanded.set( false );
240 ready: function() {},
243 * Get the child models associated with this parent, sorting them by their priority Value.
247 * @param {String} parentType
248 * @param {String} childType
251 _children: function ( parentType, childType ) {
254 api[ childType ].each( function ( child ) {
255 if ( child[ parentType ].get() === parent.id ) {
256 children.push( child );
259 children.sort( api.utils.prioritySort );
264 * To override by subclass, to return whether the container has active children.
270 isContextuallyActive: function () {
271 throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
275 * Handle changes to the active state.
277 * This does not change the active state, it merely handles the behavior
278 * for when it does change.
280 * To override by subclass, update the container's UI to reflect the provided active state.
284 * @param {Boolean} active
285 * @param {Object} args
286 * @param {Object} args.duration
287 * @param {Object} args.completeCallback
289 onChangeActive: function( active, args ) {
290 var duration, construct = this;
291 if ( args.unchanged ) {
292 if ( args.completeCallback ) {
293 args.completeCallback();
298 duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
299 if ( ! $.contains( document, construct.container[0] ) ) {
300 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
301 construct.container.toggle( active );
302 if ( args.completeCallback ) {
303 args.completeCallback();
305 } else if ( active ) {
306 construct.container.stop( true, true ).slideDown( duration, args.completeCallback );
308 if ( construct.expanded() ) {
311 completeCallback: function() {
312 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
316 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
324 * @params {Boolean} active
325 * @param {Object} [params]
326 * @returns {Boolean} false if state already applied
328 _toggleActive: function ( active, params ) {
330 params = params || {};
331 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
332 params.unchanged = true;
333 self.onChangeActive( self.active.get(), params );
336 params.unchanged = false;
337 this.activeArgumentsQueue.push( params );
338 this.active.set( active );
344 * @param {Object} [params]
345 * @returns {Boolean} false if already active
347 activate: function ( params ) {
348 return this._toggleActive( true, params );
352 * @param {Object} [params]
353 * @returns {Boolean} false if already inactive
355 deactivate: function ( params ) {
356 return this._toggleActive( false, params );
360 * To override by subclass, update the container's UI to reflect the provided active state.
363 onChangeExpanded: function () {
364 throw new Error( 'Must override with subclass.' );
368 * @param {Boolean} expanded
369 * @param {Object} [params]
370 * @returns {Boolean} false if state already applied
372 _toggleExpanded: function ( expanded, params ) {
374 params = params || {};
375 var section = this, previousCompleteCallback = params.completeCallback;
376 params.completeCallback = function () {
377 if ( previousCompleteCallback ) {
378 previousCompleteCallback.apply( section, arguments );
381 section.container.trigger( 'expanded' );
383 section.container.trigger( 'collapsed' );
386 if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
387 params.unchanged = true;
388 self.onChangeExpanded( self.expanded.get(), params );
391 params.unchanged = false;
392 this.expandedArgumentsQueue.push( params );
393 this.expanded.set( expanded );
399 * @param {Object} [params]
400 * @returns {Boolean} false if already expanded
402 expand: function ( params ) {
403 return this._toggleExpanded( true, params );
407 * @param {Object} [params]
408 * @returns {Boolean} false if already collapsed
410 collapse: function ( params ) {
411 return this._toggleExpanded( false, params );
415 * Bring the container into view and then expand this and bring it into view
416 * @param {Object} [params]
421 * Return the container html, generated from its JS template, if it exists.
425 getContainer: function () {
429 if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
430 template = wp.template( container.templateSelector );
432 template = wp.template( 'customize-' + container.containerType + '-default' );
434 if ( template && container.container ) {
435 return $.trim( template( container.params ) );
446 * @augments wp.customize.Class
448 api.Section = Container.extend({
449 containerType: 'section',
457 instanceNumber: null,
465 * @param {string} id - The ID for the section.
466 * @param {object} options - Object containing one property: params.
467 * @param {object} options.params - Object containing the following properties.
468 * @param {string} options.params.title - Title shown when section is collapsed and expanded.
469 * @param {string=} [options.params.description] - Description shown at the top of the section.
470 * @param {number=100} [options.params.priority] - The sort priority for the section.
471 * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
472 * @param {string=} [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
473 * @param {boolean=true} [options.params.active] - Whether the section is active or not.
474 * @param {string} options.params.panel - The ID for the panel this section is associated with.
475 * @param {string=} [options.params.customizeAction] - Additional context information shown before the section title when expanded.
477 initialize: function ( id, options ) {
479 Container.prototype.initialize.call( section, id, options );
482 section.panel = new api.Value();
483 section.panel.bind( function ( id ) {
484 $( section.container ).toggleClass( 'control-subsection', !! id );
486 section.panel.set( section.params.panel || '' );
487 api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
490 section.deferred.embedded.done( function () {
496 * Embed the container in the DOM when any parent panel is ready.
501 var section = this, inject;
503 // Watch for changes to the panel state
504 inject = function ( panelId ) {
507 // The panel has been supplied, so wait until the panel object is registered
508 api.panel( panelId, function ( panel ) {
509 // The panel has been registered, wait for it to become ready/initialized
510 panel.deferred.embedded.done( function () {
511 parentContainer = panel.container.find( 'ul:first' );
512 if ( ! section.container.parent().is( parentContainer ) ) {
513 parentContainer.append( section.container );
515 section.deferred.embedded.resolve();
519 // There is no panel, so embed the section in the root of the customizer
520 parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
521 if ( ! section.container.parent().is( parentContainer ) ) {
522 parentContainer.append( section.container );
524 section.deferred.embedded.resolve();
527 section.panel.bind( inject );
528 inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
532 * Add behaviors for the accordion section.
536 attachEvents: function () {
539 // Expand/Collapse accordion sections on click.
540 section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
541 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
544 event.preventDefault(); // Keep this AFTER the key filter above
546 if ( section.expanded() ) {
555 * Return whether this section has any active controls.
561 isContextuallyActive: function () {
563 controls = section.controls(),
565 _( controls ).each( function ( control ) {
566 if ( control.active() ) {
570 return ( activeCount !== 0 );
574 * Get the controls that are associated with this section, sorted by their priority Value.
580 controls: function () {
581 return this._children( 'section', 'control' );
585 * Update UI to reflect expanded state.
589 * @param {Boolean} expanded
590 * @param {Object} args
592 onChangeExpanded: function ( expanded, args ) {
594 container = section.container.closest( '.wp-full-overlay-sidebar-content' ),
595 content = section.container.find( '.accordion-section-content' ),
596 overlay = section.container.closest( '.wp-full-overlay' ),
597 backBtn = section.container.find( '.customize-section-back' ),
598 sectionTitle = section.container.find( '.accordion-section-title' ).first(),
599 headerActionsHeight = $( '#customize-header-actions' ).height(),
600 resizeContentHeight, expand, position, scroll;
602 if ( expanded && ! section.container.hasClass( 'open' ) ) {
604 if ( args.unchanged ) {
605 expand = args.completeCallback;
607 container.scrollTop( 0 );
608 resizeContentHeight = function() {
609 var matchMedia, offset;
610 matchMedia = window.matchMedia || window.msMatchMedia;
611 offset = 90; // 45px for customize header actions + 45px for footer actions.
613 // No footer on small screens.
614 if ( matchMedia && matchMedia( '(max-width: 640px)' ).matches ) {
617 content.css( 'height', ( window.innerHeight - offset ) );
619 expand = function() {
620 section.container.addClass( 'open' );
621 overlay.addClass( 'section-open' );
622 position = content.offset().top;
623 scroll = container.scrollTop();
624 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
625 resizeContentHeight();
626 sectionTitle.attr( 'tabindex', '-1' );
627 backBtn.attr( 'tabindex', '0' );
629 if ( args.completeCallback ) {
630 args.completeCallback();
633 // Fix the height after browser resize.
634 $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
636 // Fix the top margin after reflow.
637 api.bind( 'pane-contents-reflowed', _.debounce( function() {
638 var offset = ( content.offset().top - headerActionsHeight );
640 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
646 if ( ! args.allowMultiple ) {
647 api.section.each( function ( otherSection ) {
648 if ( otherSection !== section ) {
649 otherSection.collapse( { duration: args.duration } );
654 if ( section.panel() ) {
655 api.panel( section.panel() ).expand({
656 duration: args.duration,
657 completeCallback: expand
660 api.panel.each( function( panel ) {
666 } else if ( ! expanded && section.container.hasClass( 'open' ) ) {
667 section.container.removeClass( 'open' );
668 overlay.removeClass( 'section-open' );
669 content.css( 'margin-top', '' );
670 container.scrollTop( 0 );
671 backBtn.attr( 'tabindex', '-1' );
672 sectionTitle.attr( 'tabindex', '0' );
673 sectionTitle.focus();
674 if ( args.completeCallback ) {
675 args.completeCallback();
677 $( window ).off( 'resize.customizer-section' );
679 if ( args.completeCallback ) {
680 args.completeCallback();
687 * wp.customize.ThemesSection
689 * Custom section for themes that functions similarly to a backwards panel,
690 * and also handles the theme-details view rendering and navigation.
693 * @augments wp.customize.Section
694 * @augments wp.customize.Container
696 api.ThemesSection = api.Section.extend({
700 screenshotQueue: null,
701 $window: $( window ),
706 initialize: function () {
707 this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
708 return api.Section.prototype.initialize.apply( this, arguments );
716 section.overlay = section.container.find( '.theme-overlay' );
717 section.template = wp.template( 'customize-themes-details-view' );
719 // Bind global keyboard events.
720 $( 'body' ).on( 'keyup', function( event ) {
721 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
725 // Pressing the right arrow key fires a theme:next event
726 if ( 39 === event.keyCode ) {
730 // Pressing the left arrow key fires a theme:previous event
731 if ( 37 === event.keyCode ) {
732 section.previousTheme();
735 // Pressing the escape key fires a theme:collapse event
736 if ( 27 === event.keyCode ) {
737 section.closeDetails();
741 _.bindAll( this, 'renderScreenshots' );
745 * Override Section.isContextuallyActive method.
747 * Ignore the active states' of the contained theme controls, and just
748 * use the section's own active state instead. This ensures empty search
749 * results for themes to cause the section to become inactive.
755 isContextuallyActive: function () {
756 return this.active();
762 attachEvents: function () {
765 // Expand/Collapse section/panel.
766 section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
767 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
770 event.preventDefault(); // Keep this AFTER the key filter above
772 if ( section.expanded() ) {
779 // Theme navigation in details view.
780 section.container.on( 'click keydown', '.left', function( event ) {
781 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
785 event.preventDefault(); // Keep this AFTER the key filter above
787 section.previousTheme();
790 section.container.on( 'click keydown', '.right', function( event ) {
791 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
795 event.preventDefault(); // Keep this AFTER the key filter above
800 section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
801 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
805 event.preventDefault(); // Keep this AFTER the key filter above
807 section.closeDetails();
810 var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
811 section.container.on( 'input', '#themes-filter', function( event ) {
813 term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
814 controls = section.controls();
816 _.each( controls, function( control ) {
817 control.filter( term );
822 // Update theme count.
823 count = section.container.find( 'li.customize-control:visible' ).length;
824 section.container.find( '.theme-count' ).text( count );
827 // Pre-load the first 3 theme screenshots.
828 api.bind( 'ready', function () {
829 _.each( section.controls().slice( 0, 3 ), function ( control ) {
830 var img, src = control.params.theme.screenshot[0];
840 * Update UI to reflect expanded state
844 * @param {Boolean} expanded
845 * @param {Object} args
846 * @param {Boolean} args.unchanged
847 * @param {Callback} args.completeCallback
849 onChangeExpanded: function ( expanded, args ) {
851 // Immediately call the complete callback if there were no changes
852 if ( args.unchanged ) {
853 if ( args.completeCallback ) {
854 args.completeCallback();
859 // Note: there is a second argument 'args' passed
860 var position, scroll,
862 section = panel.container.closest( '.accordion-section' ),
863 overlay = section.closest( '.wp-full-overlay' ),
864 container = section.closest( '.wp-full-overlay-sidebar-content' ),
865 siblings = container.find( '.open' ),
866 customizeBtn = section.find( '.customize-theme' ),
867 changeBtn = section.find( '.change-theme' ),
868 content = section.find( '.control-panel-content' );
872 // Collapse any sibling sections/panels
873 api.section.each( function ( otherSection ) {
874 if ( otherSection !== panel ) {
875 otherSection.collapse( { duration: args.duration } );
878 api.panel.each( function ( otherPanel ) {
879 otherPanel.collapse( { duration: 0 } );
882 content.show( 0, function() {
883 position = content.offset().top;
884 scroll = container.scrollTop();
885 content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
886 section.addClass( 'current-panel' );
887 overlay.addClass( 'in-themes-panel' );
888 container.scrollTop( 0 );
889 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
890 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
891 if ( args.completeCallback ) {
892 args.completeCallback();
895 customizeBtn.focus();
897 siblings.removeClass( 'open' );
898 section.removeClass( 'current-panel' );
899 overlay.removeClass( 'in-themes-panel' );
900 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
901 content.delay( 180 ).hide( 0, function() {
902 content.css( 'margin-top', 'inherit' ); // Reset
903 if ( args.completeCallback ) {
904 args.completeCallback();
907 customizeBtn.attr( 'tabindex', '0' );
909 container.scrollTop( 0 );
914 * Render control's screenshot if the control comes into view.
918 renderScreenshots: function( ) {
921 // Fill queue initially.
922 if ( section.screenshotQueue === null ) {
923 section.screenshotQueue = section.controls();
926 // Are all screenshots rendered?
927 if ( ! section.screenshotQueue.length ) {
931 section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
932 var $imageWrapper = control.container.find( '.theme-screenshot' ),
933 $image = $imageWrapper.find( 'img' );
935 if ( ! $image.length ) {
939 if ( $image.is( ':hidden' ) ) {
943 // Based on unveil.js.
944 var wt = section.$window.scrollTop(),
945 wb = wt + section.$window.height(),
946 et = $image.offset().top,
947 ih = $imageWrapper.height(),
950 inView = eb >= wt - threshold && et <= wb + threshold;
953 control.container.trigger( 'render-screenshot' );
956 // If the image is in view return false so it's cleared from the queue.
962 * Advance the modal to the next theme.
966 nextTheme: function () {
968 if ( section.getNextTheme() ) {
969 section.showDetails( section.getNextTheme(), function() {
970 section.overlay.find( '.right' ).focus();
976 * Get the next theme model.
980 getNextTheme: function () {
982 control = api.control( 'theme_' + this.currentTheme );
983 next = control.container.next( 'li.customize-control-theme' );
984 if ( ! next.length ) {
987 next = next[0].id.replace( 'customize-control-', '' );
988 control = api.control( next );
990 return control.params.theme;
994 * Advance the modal to the previous theme.
998 previousTheme: function () {
1000 if ( section.getPreviousTheme() ) {
1001 section.showDetails( section.getPreviousTheme(), function() {
1002 section.overlay.find( '.left' ).focus();
1008 * Get the previous theme model.
1012 getPreviousTheme: function () {
1013 var control, previous;
1014 control = api.control( 'theme_' + this.currentTheme );
1015 previous = control.container.prev( 'li.customize-control-theme' );
1016 if ( ! previous.length ) {
1019 previous = previous[0].id.replace( 'customize-control-', '' );
1020 control = api.control( previous );
1022 return control.params.theme;
1026 * Disable buttons when we're viewing the first or last theme.
1030 updateLimits: function () {
1031 if ( ! this.getNextTheme() ) {
1032 this.overlay.find( '.right' ).addClass( 'disabled' );
1034 if ( ! this.getPreviousTheme() ) {
1035 this.overlay.find( '.left' ).addClass( 'disabled' );
1040 * Render & show the theme details for a given theme model.
1044 * @param {Object} theme
1046 showDetails: function ( theme, callback ) {
1048 callback = callback || function(){};
1049 section.currentTheme = theme.id;
1050 section.overlay.html( section.template( theme ) )
1053 $( 'body' ).addClass( 'modal-open' );
1054 section.containFocus( section.overlay );
1055 section.updateLimits();
1060 * Close the theme details modal.
1064 closeDetails: function () {
1065 $( 'body' ).removeClass( 'modal-open' );
1066 this.overlay.fadeOut( 'fast' );
1067 api.control( 'theme_' + this.currentTheme ).focus();
1071 * Keep tab focus within the theme details modal.
1075 containFocus: function( el ) {
1078 el.on( 'keydown', function( event ) {
1080 // Return if it's not the tab key
1081 // When navigating with prev/next focus is already handled
1082 if ( 9 !== event.keyCode ) {
1086 // uses jQuery UI to get the tabbable elements
1087 tabbables = $( ':tabbable', el );
1089 // Keep focus within the overlay
1090 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
1091 tabbables.first().focus();
1093 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
1094 tabbables.last().focus();
1105 * @augments wp.customize.Class
1107 api.Panel = Container.extend({
1108 containerType: 'panel',
1113 * @param {string} id - The ID for the panel.
1114 * @param {object} options - Object containing one property: params.
1115 * @param {object} options.params - Object containing the following properties.
1116 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
1117 * @param {string=} [options.params.description] - Description shown at the top of the panel.
1118 * @param {number=100} [options.params.priority] - The sort priority for the panel.
1119 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
1120 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
1121 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
1123 initialize: function ( id, options ) {
1125 Container.prototype.initialize.call( panel, id, options );
1127 panel.deferred.embedded.done( function () {
1133 * Embed the container in the DOM when any parent panel is ready.
1137 embed: function () {
1139 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
1141 if ( ! panel.container.parent().is( parentContainer ) ) {
1142 parentContainer.append( panel.container );
1143 panel.renderContent();
1145 panel.deferred.embedded.resolve();
1151 attachEvents: function () {
1152 var meta, panel = this;
1154 // Expand/Collapse accordion sections on click.
1155 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
1156 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1159 event.preventDefault(); // Keep this AFTER the key filter above
1161 if ( ! panel.expanded() ) {
1167 panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
1168 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1171 event.preventDefault(); // Keep this AFTER the key filter above
1173 if ( panel.expanded() ) {
1178 meta = panel.container.find( '.panel-meta:first' );
1180 meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
1181 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1184 event.preventDefault(); // Keep this AFTER the key filter above
1186 meta = panel.container.find( '.panel-meta' );
1187 if ( meta.hasClass( 'cannot-expand' ) ) {
1191 var content = meta.find( '.customize-panel-description:first' );
1192 if ( meta.hasClass( 'open' ) ) {
1193 meta.toggleClass( 'open' );
1194 content.slideUp( panel.defaultExpandedArguments.duration );
1195 $( this ).attr( 'aria-expanded', false );
1197 content.slideDown( panel.defaultExpandedArguments.duration );
1198 meta.toggleClass( 'open' );
1199 $( this ).attr( 'aria-expanded', true );
1206 * Get the sections that are associated with this panel, sorted by their priority Value.
1212 sections: function () {
1213 return this._children( 'panel', 'section' );
1217 * Return whether this panel has any active sections.
1221 * @returns {boolean}
1223 isContextuallyActive: function () {
1225 sections = panel.sections(),
1227 _( sections ).each( function ( section ) {
1228 if ( section.active() && section.isContextuallyActive() ) {
1232 return ( activeCount !== 0 );
1236 * Update UI to reflect expanded state
1240 * @param {Boolean} expanded
1241 * @param {Object} args
1242 * @param {Boolean} args.unchanged
1243 * @param {Callback} args.completeCallback
1245 onChangeExpanded: function ( expanded, args ) {
1247 // Immediately call the complete callback if there were no changes
1248 if ( args.unchanged ) {
1249 if ( args.completeCallback ) {
1250 args.completeCallback();
1255 // Note: there is a second argument 'args' passed
1256 var position, scroll,
1258 section = panel.container.closest( '.accordion-section' ), // This is actually the panel.
1259 overlay = section.closest( '.wp-full-overlay' ),
1260 container = section.closest( '.wp-full-overlay-sidebar-content' ),
1261 siblings = container.find( '.open' ),
1262 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
1263 backBtn = section.find( '.customize-panel-back' ),
1264 panelTitle = section.find( '.accordion-section-title' ).first(),
1265 content = section.find( '.control-panel-content' ),
1266 headerActionsHeight = $( '#customize-header-actions' ).height();
1270 // Collapse any sibling sections/panels
1271 api.section.each( function ( section ) {
1272 if ( panel.id !== section.panel() ) {
1273 section.collapse( { duration: 0 } );
1276 api.panel.each( function ( otherPanel ) {
1277 if ( panel !== otherPanel ) {
1278 otherPanel.collapse( { duration: 0 } );
1282 content.show( 0, function() {
1283 content.parent().show();
1284 position = content.offset().top;
1285 scroll = container.scrollTop();
1286 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
1287 section.addClass( 'current-panel' );
1288 overlay.addClass( 'in-sub-panel' );
1289 container.scrollTop( 0 );
1290 if ( args.completeCallback ) {
1291 args.completeCallback();
1294 topPanel.attr( 'tabindex', '-1' );
1295 backBtn.attr( 'tabindex', '0' );
1298 // Fix the top margin after reflow.
1299 api.bind( 'pane-contents-reflowed', _.debounce( function() {
1300 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
1303 siblings.removeClass( 'open' );
1304 section.removeClass( 'current-panel' );
1305 overlay.removeClass( 'in-sub-panel' );
1306 content.delay( 180 ).hide( 0, function() {
1307 content.css( 'margin-top', 'inherit' ); // Reset
1308 if ( args.completeCallback ) {
1309 args.completeCallback();
1312 topPanel.attr( 'tabindex', '0' );
1313 backBtn.attr( 'tabindex', '-1' );
1315 container.scrollTop( 0 );
1320 * Render the panel from its JS template, if it exists.
1322 * The panel's container must already exist in the DOM.
1326 renderContent: function () {
1330 // Add the content to the container.
1331 if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
1332 template = wp.template( panel.templateSelector + '-content' );
1334 template = wp.template( 'customize-panel-default-content' );
1336 if ( template && panel.container ) {
1337 panel.container.find( '.accordion-sub-container' ).html( template( panel.params ) );
1343 * A Customizer Control.
1345 * A control provides a UI element that allows a user to modify a Customizer Setting.
1347 * @see PHP class WP_Customize_Control.
1350 * @augments wp.customize.Class
1352 * @param {string} id Unique identifier for the control instance.
1353 * @param {object} options Options hash for the control instance.
1354 * @param {object} options.params
1355 * @param {object} options.params.type Type of control (e.g. text, radio, dropdown-pages, etc.)
1356 * @param {string} options.params.content The HTML content for the control.
1357 * @param {string} options.params.priority Order of priority to show the control within the section.
1358 * @param {string} options.params.active
1359 * @param {string} options.params.section
1360 * @param {string} options.params.label
1361 * @param {string} options.params.description
1362 * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
1364 api.Control = api.Class.extend({
1365 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
1367 initialize: function( id, options ) {
1369 nodes, radios, settings;
1371 control.params = {};
1372 $.extend( control, options || {} );
1374 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
1375 control.templateSelector = 'customize-control-' + control.params.type + '-content';
1376 control.container = control.params.content ? $( control.params.content ) : $( control.selector );
1378 control.deferred = {
1379 embedded: new $.Deferred()
1381 control.section = new api.Value();
1382 control.priority = new api.Value();
1383 control.active = new api.Value();
1384 control.activeArgumentsQueue = [];
1386 control.elements = [];
1388 nodes = control.container.find('[data-customize-setting-link]');
1391 nodes.each( function() {
1392 var node = $( this ),
1395 if ( node.is( ':radio' ) ) {
1396 name = node.prop( 'name' );
1397 if ( radios[ name ] ) {
1401 radios[ name ] = true;
1402 node = nodes.filter( '[name="' + name + '"]' );
1405 api( node.data( 'customizeSettingLink' ), function( setting ) {
1406 var element = new api.Element( node );
1407 control.elements.push( element );
1408 element.sync( setting );
1409 element.set( setting() );
1413 control.active.bind( function ( active ) {
1414 var args = control.activeArgumentsQueue.shift();
1415 args = $.extend( {}, control.defaultActiveArguments, args );
1416 control.onChangeActive( active, args );
1419 control.section.set( control.params.section );
1420 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
1421 control.active.set( control.params.active );
1423 api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
1425 // Associate this control with its settings when they are created
1426 settings = $.map( control.params.settings, function( value ) {
1429 api.apply( api, settings.concat( function () {
1432 control.settings = {};
1433 for ( key in control.params.settings ) {
1434 control.settings[ key ] = api( control.params.settings[ key ] );
1437 control.setting = control.settings['default'] || null;
1442 control.deferred.embedded.done( function () {
1448 * Embed the control into the page.
1450 embed: function () {
1454 // Watch for changes to the section state
1455 inject = function ( sectionId ) {
1456 var parentContainer;
1457 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the frontend
1460 // Wait for the section to be registered
1461 api.section( sectionId, function ( section ) {
1462 // Wait for the section to be ready/initialized
1463 section.deferred.embedded.done( function () {
1464 parentContainer = section.container.find( 'ul:first' );
1465 if ( ! control.container.parent().is( parentContainer ) ) {
1466 parentContainer.append( control.container );
1467 control.renderContent();
1469 control.deferred.embedded.resolve();
1473 control.section.bind( inject );
1474 inject( control.section.get() );
1478 * Triggered when the control's markup has been injected into the DOM.
1482 ready: function() {},
1485 * Normal controls do not expand, so just expand its parent
1487 * @param {Object} [params]
1489 expand: function ( params ) {
1490 api.section( this.section() ).expand( params );
1494 * Bring the containing section and panel into view and then
1495 * this control into view, focusing on the first input.
1500 * Update UI in response to a change in the control's active state.
1501 * This does not change the active state, it merely handles the behavior
1502 * for when it does change.
1506 * @param {Boolean} active
1507 * @param {Object} args
1508 * @param {Number} args.duration
1509 * @param {Callback} args.completeCallback
1511 onChangeActive: function ( active, args ) {
1512 if ( args.unchanged ) {
1513 if ( args.completeCallback ) {
1514 args.completeCallback();
1519 if ( ! $.contains( document, this.container ) ) {
1520 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
1521 this.container.toggle( active );
1522 if ( args.completeCallback ) {
1523 args.completeCallback();
1525 } else if ( active ) {
1526 this.container.slideDown( args.duration, args.completeCallback );
1528 this.container.slideUp( args.duration, args.completeCallback );
1533 * @deprecated 4.1.0 Use this.onChangeActive() instead.
1535 toggle: function ( active ) {
1536 return this.onChangeActive( active, this.defaultActiveArguments );
1540 * Shorthand way to enable the active state.
1544 * @param {Object} [params]
1545 * @returns {Boolean} false if already active
1547 activate: Container.prototype.activate,
1550 * Shorthand way to disable the active state.
1554 * @param {Object} [params]
1555 * @returns {Boolean} false if already inactive
1557 deactivate: Container.prototype.deactivate,
1560 * Re-use _toggleActive from Container class.
1564 _toggleActive: Container.prototype._toggleActive,
1566 dropdownInit: function() {
1568 statuses = this.container.find('.dropdown-status'),
1569 params = this.params,
1570 toggleFreeze = false,
1571 update = function( to ) {
1572 if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
1573 statuses.html( params.statuses[ to ] ).show();
1578 // Support the .dropdown class to open/close complex elements
1579 this.container.on( 'click keydown', '.dropdown', function( event ) {
1580 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1584 event.preventDefault();
1587 control.container.toggleClass('open');
1589 if ( control.container.hasClass('open') )
1590 control.container.parent().parent().find('li.library-selected').focus();
1592 // Don't want to fire focus and click at same time
1593 toggleFreeze = true;
1594 setTimeout(function () {
1595 toggleFreeze = false;
1599 this.setting.bind( update );
1600 update( this.setting() );
1604 * Render the control from its JS template, if it exists.
1606 * The control's container must already exist in the DOM.
1610 renderContent: function () {
1614 // Replace the container element's content with the control.
1615 if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
1616 template = wp.template( control.templateSelector );
1617 if ( template && control.container ) {
1618 control.container.html( template( control.params ) );
1625 * A colorpicker control.
1628 * @augments wp.customize.Control
1629 * @augments wp.customize.Class
1631 api.ColorControl = api.Control.extend({
1634 picker = this.container.find('.color-picker-hex');
1636 picker.val( control.setting() ).wpColorPicker({
1637 change: function() {
1638 control.setting.set( picker.wpColorPicker('color') );
1641 control.setting.set( false );
1645 this.setting.bind( function ( value ) {
1646 picker.val( value );
1647 picker.wpColorPicker( 'color', value );
1653 * A control that implements the media modal.
1656 * @augments wp.customize.Control
1657 * @augments wp.customize.Class
1659 api.MediaControl = api.Control.extend({
1662 * When the control's DOM structure is ready,
1663 * set up internal event bindings.
1667 // Shortcut so that we don't have to use _.bind every time we add a callback.
1668 _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
1670 // Bind events, with delegation to facilitate re-rendering.
1671 control.container.on( 'click keydown', '.upload-button', control.openFrame );
1672 control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
1673 control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1674 control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1675 control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
1676 control.container.on( 'click keydown', '.remove-button', control.removeFile );
1677 control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
1679 // Resize the player controls when it becomes visible (ie when section is expanded)
1680 api.section( control.section() ).container
1681 .on( 'expanded', function() {
1682 if ( control.player ) {
1683 control.player.setControlsSize();
1686 .on( 'collapsed', function() {
1687 control.pausePlayer();
1690 // Re-render whenever the control's setting changes.
1691 control.setting.bind( function () { control.renderContent(); } );
1694 pausePlayer: function () {
1695 this.player && this.player.pause();
1698 cleanupPlayer: function () {
1699 this.player && wp.media.mixin.removePlayer( this.player );
1703 * Open the media modal.
1705 openFrame: function( event ) {
1706 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1710 event.preventDefault();
1712 if ( ! this.frame ) {
1720 * Create a media modal select frame, and store it so the instance can be reused when needed.
1722 initFrame: function() {
1723 this.frame = wp.media({
1725 text: this.params.button_labels.frame_button
1728 new wp.media.controller.Library({
1729 title: this.params.button_labels.frame_title,
1730 library: wp.media.query({ type: this.params.mime_type }),
1737 // When a file is selected, run a callback.
1738 this.frame.on( 'select', this.select );
1742 * Callback handler for when an attachment is selected in the media modal.
1743 * Gets the selected image information, and sets it within the control.
1745 select: function() {
1746 // Get the attachment from the modal frame.
1748 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1749 mejsSettings = window._wpmejsSettings || {};
1751 this.params.attachment = attachment;
1753 // Set the Customizer setting; the callback takes care of rendering.
1754 this.setting( attachment.id );
1755 node = this.container.find( 'audio, video' ).get(0);
1757 // Initialize audio/video previews.
1759 this.player = new MediaElementPlayer( node, mejsSettings );
1761 this.cleanupPlayer();
1766 * Reset the setting to the default value.
1768 restoreDefault: function( event ) {
1769 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1772 event.preventDefault();
1774 this.params.attachment = this.params.defaultAttachment;
1775 this.setting( this.params.defaultAttachment.url );
1779 * Called when the "Remove" link is clicked. Empties the setting.
1781 * @param {object} event jQuery Event object
1783 removeFile: function( event ) {
1784 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1787 event.preventDefault();
1789 this.params.attachment = {};
1791 this.renderContent(); // Not bound to setting change when emptying.
1796 * An upload control, which utilizes the media modal.
1799 * @augments wp.customize.MediaControl
1800 * @augments wp.customize.Control
1801 * @augments wp.customize.Class
1803 api.UploadControl = api.MediaControl.extend({
1806 * Callback handler for when an attachment is selected in the media modal.
1807 * Gets the selected image information, and sets it within the control.
1809 select: function() {
1810 // Get the attachment from the modal frame.
1812 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1813 mejsSettings = window._wpmejsSettings || {};
1815 this.params.attachment = attachment;
1817 // Set the Customizer setting; the callback takes care of rendering.
1818 this.setting( attachment.url );
1819 node = this.container.find( 'audio, video' ).get(0);
1821 // Initialize audio/video previews.
1823 this.player = new MediaElementPlayer( node, mejsSettings );
1825 this.cleanupPlayer();
1830 success: function() {},
1833 removerVisibility: function() {}
1837 * A control for uploading images.
1839 * This control no longer needs to do anything more
1840 * than what the upload control does in JS.
1843 * @augments wp.customize.UploadControl
1844 * @augments wp.customize.MediaControl
1845 * @augments wp.customize.Control
1846 * @augments wp.customize.Class
1848 api.ImageControl = api.UploadControl.extend({
1850 thumbnailSrc: function() {}
1854 * A control for uploading background images.
1857 * @augments wp.customize.UploadControl
1858 * @augments wp.customize.MediaControl
1859 * @augments wp.customize.Control
1860 * @augments wp.customize.Class
1862 api.BackgroundControl = api.UploadControl.extend({
1865 * When the control's DOM structure is ready,
1866 * set up internal event bindings.
1869 api.UploadControl.prototype.ready.apply( this, arguments );
1873 * Callback handler for when an attachment is selected in the media modal.
1874 * Does an additional AJAX request for setting the background context.
1876 select: function() {
1877 api.UploadControl.prototype.select.apply( this, arguments );
1879 wp.ajax.post( 'custom-background-add', {
1880 nonce: _wpCustomizeBackground.nonces.add,
1882 theme: api.settings.theme.stylesheet,
1883 attachment_id: this.params.attachment.id
1889 * A control for selecting and cropping an image.
1892 * @augments wp.customize.MediaControl
1893 * @augments wp.customize.Control
1894 * @augments wp.customize.Class
1896 api.CroppedImageControl = api.MediaControl.extend({
1899 * Open the media modal to the library state.
1901 openFrame: function( event ) {
1902 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1907 this.frame.setState( 'library' ).open();
1911 * Create a media modal select frame, and store it so the instance can be reused when needed.
1913 initFrame: function() {
1914 var l10n = _wpMediaViewsL10n;
1916 this.frame = wp.media({
1922 new wp.media.controller.Library({
1923 title: this.params.button_labels.frame_title,
1924 library: wp.media.query({ type: 'image' }),
1928 suggestedWidth: this.params.width,
1929 suggestedHeight: this.params.height
1931 new wp.media.controller.CustomizeImageCropper({
1932 imgSelectOptions: this.calculateImageSelectOptions,
1938 this.frame.on( 'select', this.onSelect, this );
1939 this.frame.on( 'cropped', this.onCropped, this );
1940 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
1944 * After an image is selected in the media modal, switch to the cropper
1945 * state if the image isn't the right size.
1947 onSelect: function() {
1948 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
1950 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
1951 this.setImageFromAttachment( attachment );
1954 this.frame.setState( 'cropper' );
1959 * After the image has been cropped, apply the cropped image data to the setting.
1961 * @param {object} croppedImage Cropped attachment data.
1963 onCropped: function( croppedImage ) {
1964 this.setImageFromAttachment( croppedImage );
1968 * Returns a set of options, computed from the attached image data and
1969 * control-specific data, to be fed to the imgAreaSelect plugin in
1970 * wp.media.view.Cropper.
1972 * @param {wp.media.model.Attachment} attachment
1973 * @param {wp.media.controller.Cropper} controller
1974 * @returns {Object} Options
1976 calculateImageSelectOptions: function( attachment, controller ) {
1977 var control = controller.get( 'control' ),
1978 flexWidth = !! parseInt( control.params.flex_width, 10 ),
1979 flexHeight = !! parseInt( control.params.flex_height, 10 ),
1980 realWidth = attachment.get( 'width' ),
1981 realHeight = attachment.get( 'height' ),
1982 xInit = parseInt( control.params.width, 10 ),
1983 yInit = parseInt( control.params.height, 10 ),
1984 ratio = xInit / yInit,
1987 x1, y1, imgSelectOptions;
1989 controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
1991 if ( xImg / yImg > ratio ) {
1993 xInit = yInit * ratio;
1996 yInit = xInit / ratio;
1999 x1 = ( xImg - xInit ) / 2;
2000 y1 = ( yImg - yInit ) / 2;
2002 imgSelectOptions = {
2007 imageWidth: realWidth,
2008 imageHeight: realHeight,
2015 if ( flexHeight === false && flexWidth === false ) {
2016 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2018 if ( flexHeight === false ) {
2019 imgSelectOptions.maxHeight = yInit;
2021 if ( flexWidth === false ) {
2022 imgSelectOptions.maxWidth = xInit;
2025 return imgSelectOptions;
2029 * Return whether the image must be cropped, based on required dimensions.
2031 * @param {bool} flexW
2032 * @param {bool} flexH
2039 mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
2040 if ( true === flexW && true === flexH ) {
2044 if ( true === flexW && dstH === imgH ) {
2048 if ( true === flexH && dstW === imgW ) {
2052 if ( dstW === imgW && dstH === imgH ) {
2056 if ( imgW <= dstW ) {
2064 * If cropping was skipped, apply the image data directly to the setting.
2066 onSkippedCrop: function() {
2067 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2068 this.setImageFromAttachment( attachment );
2072 * Updates the setting and re-renders the control UI.
2074 * @param {object} attachment
2076 setImageFromAttachment: function( attachment ) {
2077 this.params.attachment = attachment;
2079 // Set the Customizer setting; the callback takes care of rendering.
2080 this.setting( attachment.id );
2085 * A control for selecting and cropping Site Icons.
2088 * @augments wp.customize.CroppedImageControl
2089 * @augments wp.customize.MediaControl
2090 * @augments wp.customize.Control
2091 * @augments wp.customize.Class
2093 api.SiteIconControl = api.CroppedImageControl.extend({
2096 * Create a media modal select frame, and store it so the instance can be reused when needed.
2098 initFrame: function() {
2099 var l10n = _wpMediaViewsL10n;
2101 this.frame = wp.media({
2107 new wp.media.controller.Library({
2108 title: this.params.button_labels.frame_title,
2109 library: wp.media.query({ type: 'image' }),
2113 suggestedWidth: this.params.width,
2114 suggestedHeight: this.params.height
2116 new wp.media.controller.SiteIconCropper({
2117 imgSelectOptions: this.calculateImageSelectOptions,
2123 this.frame.on( 'select', this.onSelect, this );
2124 this.frame.on( 'cropped', this.onCropped, this );
2125 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2129 * After an image is selected in the media modal, switch to the cropper
2130 * state if the image isn't the right size.
2132 onSelect: function() {
2133 var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2136 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2137 wp.ajax.post( 'crop-image', {
2138 nonce: attachment.nonces.edit,
2140 context: 'site-icon',
2144 width: this.params.width,
2145 height: this.params.height,
2146 dst_width: this.params.width,
2147 dst_height: this.params.height
2149 } ).done( function( croppedImage ) {
2150 controller.setImageFromAttachment( croppedImage );
2151 controller.frame.close();
2152 } ).fail( function() {
2153 controller.trigger('content:error:crop');
2156 this.frame.setState( 'cropper' );
2161 * Updates the setting and re-renders the control UI.
2163 * @param {object} attachment
2165 setImageFromAttachment: function( attachment ) {
2166 var sizes = [ 'site_icon-32', 'thumbnail', 'full' ],
2169 _.each( sizes, function( size ) {
2170 if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
2171 icon = attachment.sizes[ size ];
2175 this.params.attachment = attachment;
2177 // Set the Customizer setting; the callback takes care of rendering.
2178 this.setting( attachment.id );
2180 // Update the icon in-browser.
2181 $( 'link[sizes="32x32"]' ).attr( 'href', icon.url );
2185 * Called when the "Remove" link is clicked. Empties the setting.
2187 * @param {object} event jQuery Event object
2189 removeFile: function( event ) {
2190 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2193 event.preventDefault();
2195 this.params.attachment = {};
2197 this.renderContent(); // Not bound to setting change when emptying.
2198 $( 'link[rel="icon"]' ).attr( 'href', '' );
2204 * @augments wp.customize.Control
2205 * @augments wp.customize.Class
2207 api.HeaderControl = api.Control.extend({
2209 this.btnRemove = $('#customize-control-header_image .actions .remove');
2210 this.btnNew = $('#customize-control-header_image .actions .new');
2212 _.bindAll(this, 'openMedia', 'removeImage');
2214 this.btnNew.on( 'click', this.openMedia );
2215 this.btnRemove.on( 'click', this.removeImage );
2217 api.HeaderTool.currentHeader = this.getInitialHeaderImage();
2219 new api.HeaderTool.CurrentView({
2220 model: api.HeaderTool.currentHeader,
2221 el: '#customize-control-header_image .current .container'
2224 new api.HeaderTool.ChoiceListView({
2225 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
2226 el: '#customize-control-header_image .choices .uploaded .list'
2229 new api.HeaderTool.ChoiceListView({
2230 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
2231 el: '#customize-control-header_image .choices .default .list'
2234 api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
2235 api.HeaderTool.UploadsList,
2236 api.HeaderTool.DefaultsList
2241 * Returns a new instance of api.HeaderTool.ImageModel based on the currently
2242 * saved header image (if any).
2246 * @returns {Object} Options
2248 getInitialHeaderImage: function() {
2249 if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
2250 return new api.HeaderTool.ImageModel();
2253 // Get the matching uploaded image object.
2254 var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
2255 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
2257 // Fall back to raw current header image.
2258 if ( ! currentHeaderObject ) {
2259 currentHeaderObject = {
2260 url: api.get().header_image,
2261 thumbnail_url: api.get().header_image,
2262 attachment_id: api.get().header_image_data.attachment_id
2266 return new api.HeaderTool.ImageModel({
2267 header: currentHeaderObject,
2268 choice: currentHeaderObject.url.split( '/' ).pop()
2273 * Returns a set of options, computed from the attached image data and
2274 * theme-specific data, to be fed to the imgAreaSelect plugin in
2275 * wp.media.view.Cropper.
2277 * @param {wp.media.model.Attachment} attachment
2278 * @param {wp.media.controller.Cropper} controller
2279 * @returns {Object} Options
2281 calculateImageSelectOptions: function(attachment, controller) {
2282 var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
2283 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
2284 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
2285 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
2286 ratio, xImg, yImg, realHeight, realWidth,
2289 realWidth = attachment.get('width');
2290 realHeight = attachment.get('height');
2292 this.headerImage = new api.HeaderTool.ImageModel();
2293 this.headerImage.set({
2296 themeFlexWidth: flexWidth,
2297 themeFlexHeight: flexHeight,
2298 imageWidth: realWidth,
2299 imageHeight: realHeight
2302 controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
2304 ratio = xInit / yInit;
2308 if ( xImg / yImg > ratio ) {
2310 xInit = yInit * ratio;
2313 yInit = xInit / ratio;
2316 imgSelectOptions = {
2321 imageWidth: realWidth,
2322 imageHeight: realHeight,
2329 if (flexHeight === false && flexWidth === false) {
2330 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2332 if (flexHeight === false ) {
2333 imgSelectOptions.maxHeight = yInit;
2335 if (flexWidth === false ) {
2336 imgSelectOptions.maxWidth = xInit;
2339 return imgSelectOptions;
2343 * Sets up and opens the Media Manager in order to select an image.
2344 * Depending on both the size of the image and the properties of the
2345 * current theme, a cropping step after selection may be required or
2348 * @param {event} event
2350 openMedia: function(event) {
2351 var l10n = _wpMediaViewsL10n;
2353 event.preventDefault();
2355 this.frame = wp.media({
2357 text: l10n.selectAndCrop,
2361 new wp.media.controller.Library({
2362 title: l10n.chooseImage,
2363 library: wp.media.query({ type: 'image' }),
2367 suggestedWidth: _wpCustomizeHeader.data.width,
2368 suggestedHeight: _wpCustomizeHeader.data.height
2370 new wp.media.controller.Cropper({
2371 imgSelectOptions: this.calculateImageSelectOptions
2376 this.frame.on('select', this.onSelect, this);
2377 this.frame.on('cropped', this.onCropped, this);
2378 this.frame.on('skippedcrop', this.onSkippedCrop, this);
2384 * After an image is selected in the media modal,
2385 * switch to the cropper state.
2387 onSelect: function() {
2388 this.frame.setState('cropper');
2392 * After the image has been cropped, apply the cropped image data to the setting.
2394 * @param {object} croppedImage Cropped attachment data.
2396 onCropped: function(croppedImage) {
2397 var url = croppedImage.post_content,
2398 attachmentId = croppedImage.attachment_id,
2399 w = croppedImage.width,
2400 h = croppedImage.height;
2401 this.setImageFromURL(url, attachmentId, w, h);
2405 * If cropping was skipped, apply the image data directly to the setting.
2407 * @param {object} selection
2409 onSkippedCrop: function(selection) {
2410 var url = selection.get('url'),
2411 w = selection.get('width'),
2412 h = selection.get('height');
2413 this.setImageFromURL(url, selection.id, w, h);
2417 * Creates a new wp.customize.HeaderTool.ImageModel from provided
2418 * header image data and inserts it into the user-uploaded headers
2421 * @param {String} url
2422 * @param {Number} attachmentId
2423 * @param {Number} width
2424 * @param {Number} height
2426 setImageFromURL: function(url, attachmentId, width, height) {
2427 var choice, data = {};
2430 data.thumbnail_url = url;
2431 data.timestamp = _.now();
2434 data.attachment_id = attachmentId;
2442 data.height = height;
2445 choice = new api.HeaderTool.ImageModel({
2447 choice: url.split('/').pop()
2449 api.HeaderTool.UploadsList.add(choice);
2450 api.HeaderTool.currentHeader.set(choice.toJSON());
2452 choice.importImage();
2456 * Triggers the necessary events to deselect an image which was set as
2457 * the currently selected one.
2459 removeImage: function() {
2460 api.HeaderTool.currentHeader.trigger('hide');
2461 api.HeaderTool.CombinedList.trigger('control:removeImage');
2467 * wp.customize.ThemeControl
2470 * @augments wp.customize.Control
2471 * @augments wp.customize.Class
2473 api.ThemeControl = api.Control.extend({
2479 * Defer rendering the theme control until the section is displayed.
2483 renderContent: function () {
2485 renderContentArgs = arguments;
2487 api.section( control.section(), function( section ) {
2488 if ( section.expanded() ) {
2489 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2490 control.isRendered = true;
2492 section.expanded.bind( function( expanded ) {
2493 if ( expanded && ! control.isRendered ) {
2494 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2495 control.isRendered = true;
2508 control.container.on( 'touchmove', '.theme', function() {
2509 control.touchDrag = true;
2512 // Bind details view trigger.
2513 control.container.on( 'click keydown touchend', '.theme', function( event ) {
2514 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2518 // Bail if the user scrolled on a touch device.
2519 if ( control.touchDrag === true ) {
2520 return control.touchDrag = false;
2523 // Prevent the modal from showing when the user clicks the action button.
2524 if ( $( event.target ).is( '.theme-actions .button' ) ) {
2528 var previewUrl = $( this ).data( 'previewUrl' );
2530 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
2532 window.parent.location = previewUrl;
2535 control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
2536 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2540 event.preventDefault(); // Keep this AFTER the key filter above
2542 api.section( control.section() ).showDetails( control.params.theme );
2545 control.container.on( 'render-screenshot', function() {
2546 var $screenshot = $( this ).find( 'img' ),
2547 source = $screenshot.data( 'src' );
2550 $screenshot.attr( 'src', source );
2556 * Show or hide the theme based on the presence of the term in the title, description, and author.
2560 filter: function( term ) {
2562 haystack = control.params.theme.name + ' ' +
2563 control.params.theme.description + ' ' +
2564 control.params.theme.tags + ' ' +
2565 control.params.theme.author;
2566 haystack = haystack.toLowerCase().replace( '-', ' ' );
2567 if ( -1 !== haystack.search( term ) ) {
2570 control.deactivate();
2575 // Change objects contained within the main customize object to Settings.
2576 api.defaultConstructor = api.Setting;
2578 // Create the collections for Controls, Sections and Panels.
2579 api.control = new api.Values({ defaultConstructor: api.Control });
2580 api.section = new api.Values({ defaultConstructor: api.Section });
2581 api.panel = new api.Values({ defaultConstructor: api.Panel });
2585 * @augments wp.customize.Messenger
2586 * @augments wp.customize.Class
2587 * @mixes wp.customize.Events
2589 api.PreviewFrame = api.Messenger.extend({
2592 initialize: function( params, options ) {
2593 var deferred = $.Deferred();
2595 // This is the promise object.
2596 deferred.promise( this );
2598 this.container = params.container;
2599 this.signature = params.signature;
2601 $.extend( params, { channel: api.PreviewFrame.uuid() });
2603 api.Messenger.prototype.initialize.call( this, params, options );
2605 this.add( 'previewUrl', params.previewUrl );
2607 this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
2609 this.run( deferred );
2612 run: function( deferred ) {
2617 if ( this._ready ) {
2618 this.unbind( 'ready', this._ready );
2621 this._ready = function() {
2625 deferred.resolveWith( self );
2629 this.bind( 'ready', this._ready );
2631 this.bind( 'ready', function ( data ) {
2633 this.container.addClass( 'iframe-ready' );
2640 * Walk over all panels, sections, and controls and set their
2641 * respective active states to true if the preview explicitly
2642 * indicates as such.
2645 panel: data.activePanels,
2646 section: data.activeSections,
2647 control: data.activeControls
2649 _( constructs ).each( function ( activeConstructs, type ) {
2650 api[ type ].each( function ( construct, id ) {
2651 var active = !! ( activeConstructs && activeConstructs[ id ] );
2653 construct.activate();
2655 construct.deactivate();
2661 this.request = $.ajax( this.previewUrl(), {
2665 withCredentials: true
2669 this.request.fail( function() {
2670 deferred.rejectWith( self, [ 'request failure' ] );
2673 this.request.done( function( response ) {
2674 var location = self.request.getResponseHeader('Location'),
2675 signature = self.signature,
2678 // Check if the location response header differs from the current URL.
2679 // If so, the request was redirected; try loading the requested page.
2680 if ( location && location !== self.previewUrl() ) {
2681 deferred.rejectWith( self, [ 'redirect', location ] );
2685 // Check if the user is not logged in.
2686 if ( '0' === response ) {
2687 self.login( deferred );
2691 // Check for cheaters.
2692 if ( '-1' === response ) {
2693 deferred.rejectWith( self, [ 'cheatin' ] );
2697 // Check for a signature in the request.
2698 index = response.lastIndexOf( signature );
2699 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
2700 deferred.rejectWith( self, [ 'unsigned' ] );
2704 // Strip the signature from the request.
2705 response = response.slice( 0, index ) + response.slice( index + signature.length );
2707 // Create the iframe and inject the html content.
2708 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
2710 // Bind load event after the iframe has been added to the page;
2711 // otherwise it will fire when injected into the DOM.
2712 self.iframe.one( 'load', function() {
2716 deferred.resolveWith( self );
2718 setTimeout( function() {
2719 deferred.rejectWith( self, [ 'ready timeout' ] );
2720 }, self.sensitivity );
2724 self.targetWindow( self.iframe[0].contentWindow );
2726 self.targetWindow().document.open();
2727 self.targetWindow().document.write( response );
2728 self.targetWindow().document.close();
2732 login: function( deferred ) {
2736 reject = function() {
2737 deferred.rejectWith( self, [ 'logged out' ] );
2740 if ( this.triedLogin ) {
2744 // Check if we have an admin cookie.
2745 $.get( api.settings.url.ajax, {
2747 }).fail( reject ).done( function( response ) {
2750 if ( '1' !== response ) {
2754 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
2755 iframe.appendTo( self.container );
2756 iframe.load( function() {
2757 self.triedLogin = true;
2760 self.run( deferred );
2765 destroy: function() {
2766 api.Messenger.prototype.destroy.call( this );
2767 this.request.abort();
2770 this.iframe.remove();
2772 delete this.request;
2774 delete this.targetWindow;
2781 * Create a universally unique identifier.
2785 api.PreviewFrame.uuid = function() {
2786 return 'preview-' + uuid++;
2791 * Set the document title of the customizer.
2795 * @param {string} documentTitle
2797 api.setDocumentTitle = function ( documentTitle ) {
2799 tmpl = api.settings.documentTitleTmpl;
2800 title = tmpl.replace( '%s', documentTitle );
2801 document.title = title;
2802 api.trigger( 'title', title );
2807 * @augments wp.customize.Messenger
2808 * @augments wp.customize.Class
2809 * @mixes wp.customize.Events
2811 api.Previewer = api.Messenger.extend({
2816 * - container - a selector or jQuery element
2817 * - previewUrl - the URL of preview frame
2819 initialize: function( params, options ) {
2821 rscheme = /^https?/;
2823 $.extend( this, options || {} );
2825 active: $.Deferred()
2829 * Wrap this.refresh to prevent it from hammering the servers:
2831 * If refresh is called once and no other refresh requests are
2832 * loading, trigger the request immediately.
2834 * If refresh is called while another refresh request is loading,
2835 * debounce the refresh requests:
2836 * 1. Stop the loading request (as it is instantly outdated).
2837 * 2. Trigger the new request once refresh hasn't been called for
2838 * self.refreshBuffer milliseconds.
2840 this.refresh = (function( self ) {
2841 var refresh = self.refresh,
2842 callback = function() {
2844 refresh.call( self );
2849 if ( typeof timeout !== 'number' ) {
2850 if ( self.loading ) {
2857 clearTimeout( timeout );
2858 timeout = setTimeout( callback, self.refreshBuffer );
2862 this.container = api.ensure( params.container );
2863 this.allowedUrls = params.allowedUrls;
2864 this.signature = params.signature;
2866 params.url = window.location.href;
2868 api.Messenger.prototype.initialize.call( this, params );
2870 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
2871 var match = to.match( rscheme );
2872 return match ? match[0] : '';
2875 // Limit the URL to internal, front-end links.
2877 // If the frontend and the admin are served from the same domain, load the
2878 // preview over ssl if the Customizer is being loaded over ssl. This avoids
2879 // insecure content warnings. This is not attempted if the admin and frontend
2880 // are on different domains to avoid the case where the frontend doesn't have
2883 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
2886 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
2887 // Strip hashes and query strings before testing.
2888 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
2891 // Attempt to match the URL to the control frame's scheme
2892 // and check if it's allowed. If not, try the original URL.
2893 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
2894 $.each( self.allowedUrls, function( i, allowed ) {
2897 allowed = allowed.replace( /\/+$/, '' );
2898 path = url.replace( allowed, '' );
2900 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
2909 // If we found a matching result, return it. If not, bail.
2910 return result ? result : null;
2913 // Refresh the preview when the URL is changed (but not yet).
2914 this.previewUrl.bind( this.refresh );
2917 this.bind( 'scroll', function( distance ) {
2918 this.scroll = distance;
2921 // Update the URL when the iframe sends a URL message.
2922 this.bind( 'url', this.previewUrl );
2924 // Update the document title when the preview changes.
2925 this.bind( 'documentTitle', function ( title ) {
2926 api.setDocumentTitle( title );
2930 query: function() {},
2933 if ( this.loading ) {
2934 this.loading.destroy();
2935 delete this.loading;
2939 refresh: function() {
2942 // Display loading indicator
2943 this.send( 'loading-initiated' );
2947 this.loading = new api.PreviewFrame({
2949 previewUrl: this.previewUrl(),
2950 query: this.query() || {},
2951 container: this.container,
2952 signature: this.signature
2955 this.loading.done( function() {
2956 // 'this' is the loading frame
2957 this.bind( 'synced', function() {
2959 self.preview.destroy();
2960 self.preview = this;
2961 delete self.loading;
2963 self.targetWindow( this.targetWindow() );
2964 self.channel( this.channel() );
2966 self.deferred.active.resolve();
2967 self.send( 'active' );
2970 this.send( 'sync', {
2971 scroll: self.scroll,
2976 this.loading.fail( function( reason, location ) {
2977 self.send( 'loading-failed' );
2978 if ( 'redirect' === reason && location ) {
2979 self.previewUrl( location );
2982 if ( 'logged out' === reason ) {
2983 if ( self.preview ) {
2984 self.preview.destroy();
2985 delete self.preview;
2988 self.login().done( self.refresh );
2991 if ( 'cheatin' === reason ) {
2998 var previewer = this,
2999 deferred, messenger, iframe;
3004 deferred = $.Deferred();
3005 this._login = deferred.promise();
3007 messenger = new api.Messenger({
3009 url: api.settings.url.login
3012 iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
3014 messenger.targetWindow( iframe[0].contentWindow );
3016 messenger.bind( 'login', function () {
3017 var refreshNonces = previewer.refreshNonces();
3019 refreshNonces.always( function() {
3021 messenger.destroy();
3022 delete previewer._login;
3025 refreshNonces.done( function() {
3029 refreshNonces.fail( function() {
3030 previewer.cheatin();
3038 cheatin: function() {
3039 $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
3042 refreshNonces: function() {
3043 var request, deferred = $.Deferred();
3047 request = wp.ajax.post( 'customize_refresh_nonces', {
3049 theme: api.settings.theme.stylesheet
3052 request.done( function( response ) {
3053 api.trigger( 'nonce-refresh', response );
3057 request.fail( function() {
3065 api.controlConstructor = {
3066 color: api.ColorControl,
3067 media: api.MediaControl,
3068 upload: api.UploadControl,
3069 image: api.ImageControl,
3070 cropped_image: api.CroppedImageControl,
3071 site_icon: api.SiteIconControl,
3072 header: api.HeaderControl,
3073 background: api.BackgroundControl,
3074 theme: api.ThemeControl
3076 api.panelConstructor = {};
3077 api.sectionConstructor = {
3078 themes: api.ThemesSection
3082 api.settings = window._wpCustomizeSettings;
3083 api.l10n = window._wpCustomizeControlsL10n;
3085 // Check if we can run the Customizer.
3086 if ( ! api.settings ) {
3090 // Bail if any incompatibilities are found.
3091 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
3095 var parent, topFocus,
3096 body = $( document.body ),
3097 overlay = body.children( '.wp-full-overlay' ),
3098 title = $( '#customize-info .panel-title.site-title' ),
3099 closeBtn = $( '.customize-controls-close' ),
3100 saveBtn = $( '#save' );
3102 // Prevent the form from saving when enter is pressed on an input or select element.
3103 $('#customize-controls').on( 'keydown', function( e ) {
3104 var isEnter = ( 13 === e.which ),
3105 $el = $( e.target );
3107 if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
3112 // Expand/Collapse the main customizer customize info.
3113 $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
3114 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3117 event.preventDefault(); // Keep this AFTER the key filter above
3119 var section = $( this ).closest( '.accordion-section' ),
3120 content = section.find( '.customize-panel-description:first' );
3122 if ( section.hasClass( 'cannot-expand' ) ) {
3126 if ( section.hasClass( 'open' ) ) {
3127 section.toggleClass( 'open' );
3128 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
3129 $( this ).attr( 'aria-expanded', false );
3131 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
3132 section.toggleClass( 'open' );
3133 $( this ).attr( 'aria-expanded', true );
3137 // Initialize Previewer
3138 api.previewer = new api.Previewer({
3139 container: '#customize-preview',
3140 form: '#customize-controls',
3141 previewUrl: api.settings.url.preview,
3142 allowedUrls: api.settings.url.allowed,
3143 signature: 'WP_CUSTOMIZER_SIGNATURE'
3146 nonce: api.settings.nonce,
3149 var dirtyCustomized = {};
3150 api.each( function ( value, key ) {
3151 if ( value._dirty ) {
3152 dirtyCustomized[ key ] = value();
3158 theme: api.settings.theme.stylesheet,
3159 customized: JSON.stringify( dirtyCustomized ),
3160 nonce: this.nonce.preview
3166 processing = api.state( 'processing' ),
3167 submitWhenDoneProcessing,
3170 body.addClass( 'saving' );
3172 submit = function () {
3174 query = $.extend( self.query(), {
3175 nonce: self.nonce.save
3177 request = wp.ajax.post( 'customize_save', query );
3179 api.trigger( 'save', request );
3181 request.always( function () {
3182 body.removeClass( 'saving' );
3185 request.fail( function ( response ) {
3186 if ( '0' === response ) {
3187 response = 'not_logged_in';
3188 } else if ( '-1' === response ) {
3189 // Back-compat in case any other check_ajax_referer() call is dying
3190 response = 'invalid_nonce';
3193 if ( 'invalid_nonce' === response ) {
3195 } else if ( 'not_logged_in' === response ) {
3196 self.preview.iframe.hide();
3197 self.login().done( function() {
3199 self.preview.iframe.show();
3202 api.trigger( 'error', response );
3205 request.done( function( response ) {
3206 // Clear setting dirty states
3207 api.each( function ( value ) {
3208 value._dirty = false;
3211 api.trigger( 'saved', response );
3215 if ( 0 === processing() ) {
3218 submitWhenDoneProcessing = function () {
3219 if ( 0 === processing() ) {
3220 api.state.unbind( 'change', submitWhenDoneProcessing );
3224 api.state.bind( 'change', submitWhenDoneProcessing );
3230 // Refresh the nonces if the preview sends updated nonces over.
3231 api.previewer.bind( 'nonce', function( nonce ) {
3232 $.extend( this.nonce, nonce );
3235 // Refresh the nonces if login sends updated nonces over.
3236 api.bind( 'nonce-refresh', function( nonce ) {
3237 $.extend( api.settings.nonce, nonce );
3238 $.extend( api.previewer.nonce, nonce );
3242 $.each( api.settings.settings, function( id, data ) {
3243 api.create( id, id, data.value, {
3244 transport: data.transport,
3245 previewer: api.previewer,
3246 dirty: !! data.dirty
3251 $.each( api.settings.panels, function ( id, data ) {
3252 var constructor = api.panelConstructor[ data.type ] || api.Panel,
3255 panel = new constructor( id, {
3258 api.panel.add( id, panel );
3262 $.each( api.settings.sections, function ( id, data ) {
3263 var constructor = api.sectionConstructor[ data.type ] || api.Section,
3266 section = new constructor( id, {
3269 api.section.add( id, section );
3273 $.each( api.settings.controls, function( id, data ) {
3274 var constructor = api.controlConstructor[ data.type ] || api.Control,
3277 control = new constructor( id, {
3279 previewer: api.previewer
3281 api.control.add( id, control );
3284 // Focus the autofocused element
3285 _.each( [ 'panel', 'section', 'control' ], function ( type ) {
3286 var instance, id = api.settings.autofocus[ type ];
3287 if ( id && api[ type ]( id ) ) {
3288 instance = api[ type ]( id );
3289 // Wait until the element is embedded in the DOM
3290 instance.deferred.embedded.done( function () {
3291 // Wait until the preview has activated and so active panels, sections, controls have been set
3292 api.previewer.deferred.active.done( function () {
3300 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
3304 api.reflowPaneContents = _.bind( function () {
3306 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3308 if ( document.activeElement ) {
3309 activeElement = $( document.activeElement );
3312 // Sort the sections within each panel
3313 api.panel.each( function ( panel ) {
3314 var sections = panel.sections(),
3315 sectionContainers = _.pluck( sections, 'container' );
3316 rootNodes.push( panel );
3317 appendContainer = panel.container.find( 'ul:first' );
3318 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
3319 _( sections ).each( function ( section ) {
3320 appendContainer.append( section.container );
3326 // Sort the controls within each section
3327 api.section.each( function ( section ) {
3328 var controls = section.controls(),
3329 controlContainers = _.pluck( controls, 'container' );
3330 if ( ! section.panel() ) {
3331 rootNodes.push( section );
3333 appendContainer = section.container.find( 'ul:first' );
3334 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
3335 _( controls ).each( function ( control ) {
3336 appendContainer.append( control.container );
3342 // Sort the root panels and sections
3343 rootNodes.sort( api.utils.prioritySort );
3344 rootContainers = _.pluck( rootNodes, 'container' );
3345 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
3346 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
3347 _( rootNodes ).each( function ( rootNode ) {
3348 appendContainer.append( rootNode.container );
3353 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
3354 api.panel.each( function ( panel ) {
3355 var value = panel.active();
3356 panel.active.callbacks.fireWith( panel.active, [ value, value ] );
3358 api.section.each( function ( section ) {
3359 var value = section.active();
3360 section.active.callbacks.fireWith( section.active, [ value, value ] );
3363 // Restore focus if there was a reflow and there was an active (focused) element
3364 if ( wasReflowed && activeElement ) {
3365 activeElement.focus();
3367 api.trigger( 'pane-contents-reflowed' );
3369 api.bind( 'ready', api.reflowPaneContents );
3370 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
3371 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
3372 values.bind( 'add', api.reflowPaneContents );
3373 values.bind( 'change', api.reflowPaneContents );
3374 values.bind( 'remove', api.reflowPaneContents );
3377 // Check if preview url is valid and load the preview frame.
3378 if ( api.previewer.previewUrl() ) {
3379 api.previewer.refresh();
3381 api.previewer.previewUrl( api.settings.url.home );
3384 // Save and activated states
3386 var state = new api.Values(),
3387 saved = state.create( 'saved' ),
3388 activated = state.create( 'activated' ),
3389 processing = state.create( 'processing' );
3391 state.bind( 'change', function() {
3392 if ( ! activated() ) {
3393 saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
3394 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3396 } else if ( saved() ) {
3397 saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
3398 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
3401 saveBtn.val( api.l10n.save ).prop( 'disabled', false );
3402 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3406 // Set default states.
3408 activated( api.settings.theme.active );
3411 api.bind( 'change', function() {
3412 state('saved').set( false );
3415 api.bind( 'saved', function() {
3416 state('saved').set( true );
3417 state('activated').set( true );
3420 activated.bind( function( to ) {
3422 api.trigger( 'activated' );
3425 // Expose states to the API.
3430 saveBtn.click( function( event ) {
3431 api.previewer.save();
3432 event.preventDefault();
3433 }).keydown( function( event ) {
3434 if ( 9 === event.which ) // tab
3436 if ( 13 === event.which ) // enter
3437 api.previewer.save();
3438 event.preventDefault();
3441 closeBtn.keydown( function( event ) {
3442 if ( 9 === event.which ) // tab
3444 if ( 13 === event.which ) // enter
3446 event.preventDefault();
3449 $( '.collapse-sidebar' ).on( 'click', function() {
3450 if ( 'true' === $( this ).attr( 'aria-expanded' ) ) {
3451 $( this ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
3453 $( this ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
3456 overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
3459 $( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) {
3460 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3464 overlay.toggleClass( 'preview-only' );
3465 event.preventDefault();
3468 // Bind site title display to the corresponding field.
3469 if ( title.length ) {
3470 $( '#customize-control-blogname input' ).on( 'input', function() {
3471 title.text( this.value );
3475 // Create a potential postMessage connection with the parent frame.
3476 parent = new api.Messenger({
3477 url: api.settings.url.parent,
3481 // If we receive a 'back' event, we're inside an iframe.
3482 // Send any clicks to the 'Return' link to the parent page.
3483 parent.bind( 'back', function() {
3484 closeBtn.on( 'click.customize-controls-close', function( event ) {
3485 event.preventDefault();
3486 parent.send( 'close' );
3490 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
3491 $( window ).on( 'beforeunload', function () {
3492 if ( ! api.state( 'saved' )() ) {
3493 setTimeout( function() {
3494 overlay.removeClass( 'customize-loading' );
3496 return api.l10n.saveAlert;
3500 // Pass events through to the parent.
3501 $.each( [ 'saved', 'change' ], function ( i, event ) {
3502 api.bind( event, function() {
3503 parent.send( event );
3507 // When activated, let the loader handle redirecting the page.
3508 // If no loader exists, redirect the page ourselves (if a url exists).
3509 api.bind( 'activated', function() {
3510 if ( parent.targetWindow() )
3511 parent.send( 'activated', api.settings.url.activated );
3512 else if ( api.settings.url.activated )
3513 window.location = api.settings.url.activated;
3516 // Pass titles to the parent
3517 api.bind( 'title', function( newTitle ) {
3518 parent.send( 'title', newTitle );
3521 // Initialize the connection with the parent frame.
3522 parent.send( 'ready' );
3524 // Control visibility for default controls
3526 'background_image': {
3527 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
3528 callback: function( to ) { return !! to; }
3531 controls: [ 'page_on_front', 'page_for_posts' ],
3532 callback: function( to ) { return 'page' === to; }
3534 'header_textcolor': {
3535 controls: [ 'header_textcolor' ],
3536 callback: function( to ) { return 'blank' !== to; }
3538 }, function( settingId, o ) {
3539 api( settingId, function( setting ) {
3540 $.each( o.controls, function( i, controlId ) {
3541 api.control( controlId, function( control ) {
3542 var visibility = function( to ) {
3543 control.container.toggle( o.callback( to ) );
3546 visibility( setting.get() );
3547 setting.bind( visibility );
3553 // Juggle the two controls that use header_textcolor
3554 api.control( 'display_header_text', function( control ) {
3557 control.elements[0].unsync( api( 'header_textcolor' ) );
3559 control.element = new api.Element( control.container.find('input') );
3560 control.element.set( 'blank' !== control.setting() );
3562 control.element.bind( function( to ) {
3564 last = api( 'header_textcolor' ).get();
3566 control.setting.set( to ? last : 'blank' );
3569 control.setting.bind( function( to ) {
3570 control.element.set( 'blank' !== to );
3574 // Change previewed URL to the homepage when changing the page_on_front.
3575 api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
3576 var updatePreviewUrl = function() {
3577 if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
3578 api.previewer.previewUrl.set( api.settings.url.home );
3581 showOnFront.bind( updatePreviewUrl );
3582 pageOnFront.bind( updatePreviewUrl );
3585 // Change the previewed URL to the selected page when changing the page_for_posts.
3586 api( 'page_for_posts', function( setting ) {
3587 setting.bind(function( pageId ) {
3588 pageId = parseInt( pageId, 10 );
3590 api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
3595 api.trigger( 'ready' );
3597 // Make sure left column gets focus
3598 topFocus = closeBtn;
3600 setTimeout(function () {