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.expanded && construct.expanded() ) {
72 focusContainer = construct.container.find( 'ul:first' );
74 focusContainer = construct.container;
77 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
78 focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first().focus();
80 if ( params.completeCallback ) {
81 completeCallback = params.completeCallback;
82 params.completeCallback = function () {
87 params.completeCallback = focus;
89 if ( construct.expand ) {
90 construct.expand( params );
92 params.completeCallback();
97 * Stable sort for Panels, Sections, and Controls.
99 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
103 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
104 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
107 api.utils.prioritySort = function ( a, b ) {
108 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
109 return a.params.instanceNumber - b.params.instanceNumber;
111 return a.priority() - b.priority();
116 * Return whether the supplied Event object is for a keydown event but not the Enter key.
120 * @param {jQuery.Event} event
123 api.utils.isKeydownButNotEnterEvent = function ( event ) {
124 return ( 'keydown' === event.type && 13 !== event.which );
128 * Return whether the two lists of elements are the same and are in the same order.
132 * @param {Array|jQuery} listA
133 * @param {Array|jQuery} listB
136 api.utils.areElementListsEqual = function ( listA, listB ) {
138 listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
139 -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
140 _.zip( listA, listB ), // pair up each element between the two lists
142 return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
144 ), false ) // check for presence of false in map's return value
150 * Base class for Panel and Section.
155 * @augments wp.customize.Class
157 Container = api.Class.extend({
158 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
159 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
160 containerType: 'container',
174 * @param {string} id - The ID for the container.
175 * @param {object} options - Object containing one property: params.
176 * @param {object} options.params - Object containing the following properties.
177 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
178 * @param {string=} [options.params.description] - Description shown at the top of the panel.
179 * @param {number=100} [options.params.priority] - The sort priority for the panel.
180 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
181 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
182 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
184 initialize: function ( id, options ) {
185 var container = this;
187 options = options || {};
189 options.params = _.defaults(
190 options.params || {},
194 $.extend( container, options );
195 container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
196 container.container = $( container.params.content );
197 if ( 0 === container.container.length ) {
198 container.container = $( container.getContainer() );
201 container.deferred = {
202 embedded: new $.Deferred()
204 container.priority = new api.Value();
205 container.active = new api.Value();
206 container.activeArgumentsQueue = [];
207 container.expanded = new api.Value();
208 container.expandedArgumentsQueue = [];
210 container.active.bind( function ( active ) {
211 var args = container.activeArgumentsQueue.shift();
212 args = $.extend( {}, container.defaultActiveArguments, args );
213 active = ( active && container.isContextuallyActive() );
214 container.onChangeActive( active, args );
216 container.expanded.bind( function ( expanded ) {
217 var args = container.expandedArgumentsQueue.shift();
218 args = $.extend( {}, container.defaultExpandedArguments, args );
219 container.onChangeExpanded( expanded, args );
222 container.deferred.embedded.done( function () {
223 container.attachEvents();
226 api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
228 container.priority.set( container.params.priority );
229 container.active.set( container.params.active );
230 container.expanded.set( false );
238 ready: function() {},
241 * Get the child models associated with this parent, sorting them by their priority Value.
245 * @param {String} parentType
246 * @param {String} childType
249 _children: function ( parentType, childType ) {
252 api[ childType ].each( function ( child ) {
253 if ( child[ parentType ].get() === parent.id ) {
254 children.push( child );
257 children.sort( api.utils.prioritySort );
262 * To override by subclass, to return whether the container has active children.
268 isContextuallyActive: function () {
269 throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
273 * Handle changes to the active state.
275 * This does not change the active state, it merely handles the behavior
276 * for when it does change.
278 * To override by subclass, update the container's UI to reflect the provided active state.
282 * @param {Boolean} active
283 * @param {Object} args
284 * @param {Object} args.duration
285 * @param {Object} args.completeCallback
287 onChangeActive: function ( active, args ) {
288 var duration, construct = this;
289 duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
290 if ( ! $.contains( document, construct.container[0] ) ) {
291 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
292 construct.container.toggle( active );
293 if ( args.completeCallback ) {
294 args.completeCallback();
296 } else if ( active ) {
297 construct.container.stop( true, true ).slideDown( duration, args.completeCallback );
299 if ( construct.expanded() ) {
302 completeCallback: function() {
303 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
307 construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
315 * @params {Boolean} active
316 * @param {Object} [params]
317 * @returns {Boolean} false if state already applied
319 _toggleActive: function ( active, params ) {
321 params = params || {};
322 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
323 params.unchanged = true;
324 self.onChangeActive( self.active.get(), params );
327 params.unchanged = false;
328 this.activeArgumentsQueue.push( params );
329 this.active.set( active );
335 * @param {Object} [params]
336 * @returns {Boolean} false if already active
338 activate: function ( params ) {
339 return this._toggleActive( true, params );
343 * @param {Object} [params]
344 * @returns {Boolean} false if already inactive
346 deactivate: function ( params ) {
347 return this._toggleActive( false, params );
351 * To override by subclass, update the container's UI to reflect the provided active state.
354 onChangeExpanded: function () {
355 throw new Error( 'Must override with subclass.' );
359 * @param {Boolean} expanded
360 * @param {Object} [params]
361 * @returns {Boolean} false if state already applied
363 _toggleExpanded: function ( expanded, params ) {
365 params = params || {};
366 var section = this, previousCompleteCallback = params.completeCallback;
367 params.completeCallback = function () {
368 if ( previousCompleteCallback ) {
369 previousCompleteCallback.apply( section, arguments );
372 section.container.trigger( 'expanded' );
374 section.container.trigger( 'collapsed' );
377 if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
378 params.unchanged = true;
379 self.onChangeExpanded( self.expanded.get(), params );
382 params.unchanged = false;
383 this.expandedArgumentsQueue.push( params );
384 this.expanded.set( expanded );
390 * @param {Object} [params]
391 * @returns {Boolean} false if already expanded
393 expand: function ( params ) {
394 return this._toggleExpanded( true, params );
398 * @param {Object} [params]
399 * @returns {Boolean} false if already collapsed
401 collapse: function ( params ) {
402 return this._toggleExpanded( false, params );
406 * Bring the container into view and then expand this and bring it into view
407 * @param {Object} [params]
412 * Return the container html, generated from its JS template, if it exists.
416 getContainer: function () {
420 if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
421 template = wp.template( container.templateSelector );
423 template = wp.template( 'customize-' + container.containerType + '-default' );
425 if ( template && container.container ) {
426 return $.trim( template( container.params ) );
437 * @augments wp.customize.Class
439 api.Section = Container.extend({
440 containerType: 'section',
448 instanceNumber: null,
456 * @param {string} id - The ID for the section.
457 * @param {object} options - Object containing one property: params.
458 * @param {object} options.params - Object containing the following properties.
459 * @param {string} options.params.title - Title shown when section is collapsed and expanded.
460 * @param {string=} [options.params.description] - Description shown at the top of the section.
461 * @param {number=100} [options.params.priority] - The sort priority for the section.
462 * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
463 * @param {string=} [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
464 * @param {boolean=true} [options.params.active] - Whether the section is active or not.
465 * @param {string} options.params.panel - The ID for the panel this section is associated with.
466 * @param {string=} [options.params.customizeAction] - Additional context information shown before the section title when expanded.
468 initialize: function ( id, options ) {
470 Container.prototype.initialize.call( section, id, options );
473 section.panel = new api.Value();
474 section.panel.bind( function ( id ) {
475 $( section.container ).toggleClass( 'control-subsection', !! id );
477 section.panel.set( section.params.panel || '' );
478 api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
481 section.deferred.embedded.done( function () {
487 * Embed the container in the DOM when any parent panel is ready.
492 var section = this, inject;
494 // Watch for changes to the panel state
495 inject = function ( panelId ) {
498 // The panel has been supplied, so wait until the panel object is registered
499 api.panel( panelId, function ( panel ) {
500 // The panel has been registered, wait for it to become ready/initialized
501 panel.deferred.embedded.done( function () {
502 parentContainer = panel.container.find( 'ul:first' );
503 if ( ! section.container.parent().is( parentContainer ) ) {
504 parentContainer.append( section.container );
506 section.deferred.embedded.resolve();
510 // There is no panel, so embed the section in the root of the customizer
511 parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
512 if ( ! section.container.parent().is( parentContainer ) ) {
513 parentContainer.append( section.container );
515 section.deferred.embedded.resolve();
518 section.panel.bind( inject );
519 inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
523 * Add behaviors for the accordion section.
527 attachEvents: function () {
530 // Expand/Collapse accordion sections on click.
531 section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
532 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
535 event.preventDefault(); // Keep this AFTER the key filter above
537 if ( section.expanded() ) {
546 * Return whether this section has any active controls.
552 isContextuallyActive: function () {
554 controls = section.controls(),
556 _( controls ).each( function ( control ) {
557 if ( control.active() ) {
561 return ( activeCount !== 0 );
565 * Get the controls that are associated with this section, sorted by their priority Value.
571 controls: function () {
572 return this._children( 'section', 'control' );
576 * Update UI to reflect expanded state.
580 * @param {Boolean} expanded
581 * @param {Object} args
583 onChangeExpanded: function ( expanded, args ) {
585 container = section.container.closest( '.wp-full-overlay-sidebar-content' ),
586 content = section.container.find( '.accordion-section-content' ),
587 overlay = section.container.closest( '.wp-full-overlay' ),
588 backBtn = section.container.find( '.customize-section-back' ),
589 sectionTitle = section.container.find( '.accordion-section-title' ).first(),
590 headerActionsHeight = $( '#customize-header-actions' ).height(),
591 resizeContentHeight, expand, position, scroll;
593 if ( expanded && ! section.container.hasClass( 'open' ) ) {
595 if ( args.unchanged ) {
596 expand = args.completeCallback;
598 container.scrollTop( 0 );
599 resizeContentHeight = function() {
600 var matchMedia, offset;
601 matchMedia = window.matchMedia || window.msMatchMedia;
602 offset = 90; // 45px for customize header actions + 45px for footer actions.
604 // No footer on small screens.
605 if ( matchMedia && matchMedia( '(max-width: 640px)' ).matches ) {
608 content.css( 'height', ( window.innerHeight - offset ) );
610 expand = function() {
611 section.container.addClass( 'open' );
612 overlay.addClass( 'section-open' );
613 position = content.offset().top;
614 scroll = container.scrollTop();
615 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
616 resizeContentHeight();
617 sectionTitle.attr( 'tabindex', '-1' );
618 backBtn.attr( 'tabindex', '0' );
620 if ( args.completeCallback ) {
621 args.completeCallback();
624 // Fix the height after browser resize.
625 $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
627 // Fix the top margin after reflow.
628 api.bind( 'pane-contents-reflowed', _.debounce( function() {
629 var offset = ( content.offset().top - headerActionsHeight );
631 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
637 if ( ! args.allowMultiple ) {
638 api.section.each( function ( otherSection ) {
639 if ( otherSection !== section ) {
640 otherSection.collapse( { duration: args.duration } );
645 if ( section.panel() ) {
646 api.panel( section.panel() ).expand({
647 duration: args.duration,
648 completeCallback: expand
654 } else if ( ! expanded && section.container.hasClass( 'open' ) ) {
655 section.container.removeClass( 'open' );
656 overlay.removeClass( 'section-open' );
657 content.css( 'margin-top', '' );
658 container.scrollTop( 0 );
659 backBtn.attr( 'tabindex', '-1' );
660 sectionTitle.attr( 'tabindex', '0' );
661 sectionTitle.focus();
662 if ( args.completeCallback ) {
663 args.completeCallback();
665 $( window ).off( 'resize.customizer-section' );
667 if ( args.completeCallback ) {
668 args.completeCallback();
675 * wp.customize.ThemesSection
677 * Custom section for themes that functions similarly to a backwards panel,
678 * and also handles the theme-details view rendering and navigation.
681 * @augments wp.customize.Section
682 * @augments wp.customize.Container
684 api.ThemesSection = api.Section.extend({
688 screenshotQueue: null,
689 $window: $( window ),
694 initialize: function () {
695 this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
696 return api.Section.prototype.initialize.apply( this, arguments );
704 section.overlay = section.container.find( '.theme-overlay' );
705 section.template = wp.template( 'customize-themes-details-view' );
707 // Bind global keyboard events.
708 $( 'body' ).on( 'keyup', function( event ) {
709 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
713 // Pressing the right arrow key fires a theme:next event
714 if ( 39 === event.keyCode ) {
718 // Pressing the left arrow key fires a theme:previous event
719 if ( 37 === event.keyCode ) {
720 section.previousTheme();
723 // Pressing the escape key fires a theme:collapse event
724 if ( 27 === event.keyCode ) {
725 section.closeDetails();
729 _.bindAll( this, 'renderScreenshots' );
733 * Override Section.isContextuallyActive method.
735 * Ignore the active states' of the contained theme controls, and just
736 * use the section's own active state instead. This ensures empty search
737 * results for themes to cause the section to become inactive.
743 isContextuallyActive: function () {
744 return this.active();
750 attachEvents: function () {
753 // Expand/Collapse section/panel.
754 section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
755 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
758 event.preventDefault(); // Keep this AFTER the key filter above
760 if ( section.expanded() ) {
767 // Theme navigation in details view.
768 section.container.on( 'click keydown', '.left', function( event ) {
769 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
773 event.preventDefault(); // Keep this AFTER the key filter above
775 section.previousTheme();
778 section.container.on( 'click keydown', '.right', function( event ) {
779 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
783 event.preventDefault(); // Keep this AFTER the key filter above
788 section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
789 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
793 event.preventDefault(); // Keep this AFTER the key filter above
795 section.closeDetails();
798 var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
799 section.container.on( 'input', '#themes-filter', function( event ) {
801 term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
802 controls = section.controls();
804 _.each( controls, function( control ) {
805 control.filter( term );
810 // Update theme count.
811 count = section.container.find( 'li.customize-control:visible' ).length;
812 section.container.find( '.theme-count' ).text( count );
815 // Pre-load the first 3 theme screenshots.
816 api.bind( 'ready', function () {
817 _.each( section.controls().slice( 0, 3 ), function ( control ) {
818 var img, src = control.params.theme.screenshot[0];
828 * Update UI to reflect expanded state
832 * @param {Boolean} expanded
833 * @param {Object} args
834 * @param {Boolean} args.unchanged
835 * @param {Callback} args.completeCallback
837 onChangeExpanded: function ( expanded, args ) {
839 // Immediately call the complete callback if there were no changes
840 if ( args.unchanged ) {
841 if ( args.completeCallback ) {
842 args.completeCallback();
847 // Note: there is a second argument 'args' passed
848 var position, scroll,
850 section = panel.container.closest( '.accordion-section' ),
851 overlay = section.closest( '.wp-full-overlay' ),
852 container = section.closest( '.wp-full-overlay-sidebar-content' ),
853 siblings = container.find( '.open' ),
854 customizeBtn = section.find( '.customize-theme' ),
855 changeBtn = section.find( '.change-theme' ),
856 content = section.find( '.control-panel-content' );
860 // Collapse any sibling sections/panels
861 api.section.each( function ( otherSection ) {
862 if ( otherSection !== panel ) {
863 otherSection.collapse( { duration: args.duration } );
866 api.panel.each( function ( otherPanel ) {
867 otherPanel.collapse( { duration: 0 } );
870 content.show( 0, function() {
871 position = content.offset().top;
872 scroll = container.scrollTop();
873 content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
874 section.addClass( 'current-panel' );
875 overlay.addClass( 'in-themes-panel' );
876 container.scrollTop( 0 );
877 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
878 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
879 if ( args.completeCallback ) {
880 args.completeCallback();
883 customizeBtn.focus();
885 siblings.removeClass( 'open' );
886 section.removeClass( 'current-panel' );
887 overlay.removeClass( 'in-themes-panel' );
888 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
889 content.delay( 180 ).hide( 0, function() {
890 content.css( 'margin-top', 'inherit' ); // Reset
891 if ( args.completeCallback ) {
892 args.completeCallback();
895 customizeBtn.attr( 'tabindex', '0' );
897 container.scrollTop( 0 );
902 * Render control's screenshot if the control comes into view.
906 renderScreenshots: function( ) {
909 // Fill queue initially.
910 if ( section.screenshotQueue === null ) {
911 section.screenshotQueue = section.controls();
914 // Are all screenshots rendered?
915 if ( ! section.screenshotQueue.length ) {
919 section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
920 var $imageWrapper = control.container.find( '.theme-screenshot' ),
921 $image = $imageWrapper.find( 'img' );
923 if ( ! $image.length ) {
927 if ( $image.is( ':hidden' ) ) {
931 // Based on unveil.js.
932 var wt = section.$window.scrollTop(),
933 wb = wt + section.$window.height(),
934 et = $image.offset().top,
935 ih = $imageWrapper.height(),
938 inView = eb >= wt - threshold && et <= wb + threshold;
941 control.container.trigger( 'render-screenshot' );
944 // If the image is in view return false so it's cleared from the queue.
950 * Advance the modal to the next theme.
954 nextTheme: function () {
956 if ( section.getNextTheme() ) {
957 section.showDetails( section.getNextTheme(), function() {
958 section.overlay.find( '.right' ).focus();
964 * Get the next theme model.
968 getNextTheme: function () {
970 control = api.control( 'theme_' + this.currentTheme );
971 next = control.container.next( 'li.customize-control-theme' );
972 if ( ! next.length ) {
975 next = next[0].id.replace( 'customize-control-', '' );
976 control = api.control( next );
978 return control.params.theme;
982 * Advance the modal to the previous theme.
986 previousTheme: function () {
988 if ( section.getPreviousTheme() ) {
989 section.showDetails( section.getPreviousTheme(), function() {
990 section.overlay.find( '.left' ).focus();
996 * Get the previous theme model.
1000 getPreviousTheme: function () {
1001 var control, previous;
1002 control = api.control( 'theme_' + this.currentTheme );
1003 previous = control.container.prev( 'li.customize-control-theme' );
1004 if ( ! previous.length ) {
1007 previous = previous[0].id.replace( 'customize-control-', '' );
1008 control = api.control( previous );
1010 return control.params.theme;
1014 * Disable buttons when we're viewing the first or last theme.
1018 updateLimits: function () {
1019 if ( ! this.getNextTheme() ) {
1020 this.overlay.find( '.right' ).addClass( 'disabled' );
1022 if ( ! this.getPreviousTheme() ) {
1023 this.overlay.find( '.left' ).addClass( 'disabled' );
1028 * Render & show the theme details for a given theme model.
1032 * @param {Object} theme
1034 showDetails: function ( theme, callback ) {
1036 callback = callback || function(){};
1037 section.currentTheme = theme.id;
1038 section.overlay.html( section.template( theme ) )
1041 $( 'body' ).addClass( 'modal-open' );
1042 section.containFocus( section.overlay );
1043 section.updateLimits();
1048 * Close the theme details modal.
1052 closeDetails: function () {
1053 $( 'body' ).removeClass( 'modal-open' );
1054 this.overlay.fadeOut( 'fast' );
1055 api.control( 'theme_' + this.currentTheme ).focus();
1059 * Keep tab focus within the theme details modal.
1063 containFocus: function( el ) {
1066 el.on( 'keydown', function( event ) {
1068 // Return if it's not the tab key
1069 // When navigating with prev/next focus is already handled
1070 if ( 9 !== event.keyCode ) {
1074 // uses jQuery UI to get the tabbable elements
1075 tabbables = $( ':tabbable', el );
1077 // Keep focus within the overlay
1078 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
1079 tabbables.first().focus();
1081 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
1082 tabbables.last().focus();
1093 * @augments wp.customize.Class
1095 api.Panel = Container.extend({
1096 containerType: 'panel',
1101 * @param {string} id - The ID for the panel.
1102 * @param {object} options - Object containing one property: params.
1103 * @param {object} options.params - Object containing the following properties.
1104 * @param {string} options.params.title - Title shown when panel is collapsed and expanded.
1105 * @param {string=} [options.params.description] - Description shown at the top of the panel.
1106 * @param {number=100} [options.params.priority] - The sort priority for the panel.
1107 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
1108 * @param {string=} [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
1109 * @param {boolean=true} [options.params.active] - Whether the panel is active or not.
1111 initialize: function ( id, options ) {
1113 Container.prototype.initialize.call( panel, id, options );
1115 panel.deferred.embedded.done( function () {
1121 * Embed the container in the DOM when any parent panel is ready.
1125 embed: function () {
1127 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
1129 if ( ! panel.container.parent().is( parentContainer ) ) {
1130 parentContainer.append( panel.container );
1131 panel.renderContent();
1133 panel.deferred.embedded.resolve();
1139 attachEvents: function () {
1140 var meta, panel = this;
1142 // Expand/Collapse accordion sections on click.
1143 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
1144 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1147 event.preventDefault(); // Keep this AFTER the key filter above
1149 if ( ! panel.expanded() ) {
1155 panel.container.find( '.customize-panel-back' ).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() ) {
1166 meta = panel.container.find( '.panel-meta:first' );
1168 meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
1169 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1172 event.preventDefault(); // Keep this AFTER the key filter above
1174 meta = panel.container.find( '.panel-meta' );
1175 if ( meta.hasClass( 'cannot-expand' ) ) {
1179 var content = meta.find( '.customize-panel-description:first' );
1180 if ( meta.hasClass( 'open' ) ) {
1181 meta.toggleClass( 'open' );
1182 content.slideUp( panel.defaultExpandedArguments.duration );
1183 $( this ).attr( 'aria-expanded', false );
1185 content.slideDown( panel.defaultExpandedArguments.duration );
1186 meta.toggleClass( 'open' );
1187 $( this ).attr( 'aria-expanded', true );
1194 * Get the sections that are associated with this panel, sorted by their priority Value.
1200 sections: function () {
1201 return this._children( 'panel', 'section' );
1205 * Return whether this panel has any active sections.
1209 * @returns {boolean}
1211 isContextuallyActive: function () {
1213 sections = panel.sections(),
1215 _( sections ).each( function ( section ) {
1216 if ( section.active() && section.isContextuallyActive() ) {
1220 return ( activeCount !== 0 );
1224 * Update UI to reflect expanded state
1228 * @param {Boolean} expanded
1229 * @param {Object} args
1230 * @param {Boolean} args.unchanged
1231 * @param {Callback} args.completeCallback
1233 onChangeExpanded: function ( expanded, args ) {
1235 // Immediately call the complete callback if there were no changes
1236 if ( args.unchanged ) {
1237 if ( args.completeCallback ) {
1238 args.completeCallback();
1243 // Note: there is a second argument 'args' passed
1244 var position, scroll,
1246 section = panel.container.closest( '.accordion-section' ), // This is actually the panel.
1247 overlay = section.closest( '.wp-full-overlay' ),
1248 container = section.closest( '.wp-full-overlay-sidebar-content' ),
1249 siblings = container.find( '.open' ),
1250 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
1251 backBtn = section.find( '.customize-panel-back' ),
1252 panelTitle = section.find( '.accordion-section-title' ).first(),
1253 content = section.find( '.control-panel-content' ),
1254 headerActionsHeight = $( '#customize-header-actions' ).height();
1258 // Collapse any sibling sections/panels
1259 api.section.each( function ( section ) {
1260 if ( ! section.panel() ) {
1261 section.collapse( { duration: 0 } );
1264 api.panel.each( function ( otherPanel ) {
1265 if ( panel !== otherPanel ) {
1266 otherPanel.collapse( { duration: 0 } );
1270 content.show( 0, function() {
1271 content.parent().show();
1272 position = content.offset().top;
1273 scroll = container.scrollTop();
1274 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
1275 section.addClass( 'current-panel' );
1276 overlay.addClass( 'in-sub-panel' );
1277 container.scrollTop( 0 );
1278 if ( args.completeCallback ) {
1279 args.completeCallback();
1282 topPanel.attr( 'tabindex', '-1' );
1283 backBtn.attr( 'tabindex', '0' );
1286 // Fix the top margin after reflow.
1287 api.bind( 'pane-contents-reflowed', _.debounce( function() {
1288 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
1291 siblings.removeClass( 'open' );
1292 section.removeClass( 'current-panel' );
1293 overlay.removeClass( 'in-sub-panel' );
1294 content.delay( 180 ).hide( 0, function() {
1295 content.css( 'margin-top', 'inherit' ); // Reset
1296 if ( args.completeCallback ) {
1297 args.completeCallback();
1300 topPanel.attr( 'tabindex', '0' );
1301 backBtn.attr( 'tabindex', '-1' );
1303 container.scrollTop( 0 );
1308 * Render the panel from its JS template, if it exists.
1310 * The panel's container must already exist in the DOM.
1314 renderContent: function () {
1318 // Add the content to the container.
1319 if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
1320 template = wp.template( panel.templateSelector + '-content' );
1322 template = wp.template( 'customize-panel-default-content' );
1324 if ( template && panel.container ) {
1325 panel.container.find( '.accordion-sub-container' ).html( template( panel.params ) );
1331 * A Customizer Control.
1333 * A control provides a UI element that allows a user to modify a Customizer Setting.
1335 * @see PHP class WP_Customize_Control.
1338 * @augments wp.customize.Class
1340 * @param {string} id Unique identifier for the control instance.
1341 * @param {object} options Options hash for the control instance.
1342 * @param {object} options.params
1343 * @param {object} options.params.type Type of control (e.g. text, radio, dropdown-pages, etc.)
1344 * @param {string} options.params.content The HTML content for the control.
1345 * @param {string} options.params.priority Order of priority to show the control within the section.
1346 * @param {string} options.params.active
1347 * @param {string} options.params.section
1348 * @param {string} options.params.label
1349 * @param {string} options.params.description
1350 * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
1352 api.Control = api.Class.extend({
1353 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
1355 initialize: function( id, options ) {
1357 nodes, radios, settings;
1359 control.params = {};
1360 $.extend( control, options || {} );
1362 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
1363 control.templateSelector = 'customize-control-' + control.params.type + '-content';
1364 control.container = control.params.content ? $( control.params.content ) : $( control.selector );
1366 control.deferred = {
1367 embedded: new $.Deferred()
1369 control.section = new api.Value();
1370 control.priority = new api.Value();
1371 control.active = new api.Value();
1372 control.activeArgumentsQueue = [];
1374 control.elements = [];
1376 nodes = control.container.find('[data-customize-setting-link]');
1379 nodes.each( function() {
1380 var node = $( this ),
1383 if ( node.is( ':radio' ) ) {
1384 name = node.prop( 'name' );
1385 if ( radios[ name ] ) {
1389 radios[ name ] = true;
1390 node = nodes.filter( '[name="' + name + '"]' );
1393 api( node.data( 'customizeSettingLink' ), function( setting ) {
1394 var element = new api.Element( node );
1395 control.elements.push( element );
1396 element.sync( setting );
1397 element.set( setting() );
1401 control.active.bind( function ( active ) {
1402 var args = control.activeArgumentsQueue.shift();
1403 args = $.extend( {}, control.defaultActiveArguments, args );
1404 control.onChangeActive( active, args );
1407 control.section.set( control.params.section );
1408 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
1409 control.active.set( control.params.active );
1411 api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
1413 // Associate this control with its settings when they are created
1414 settings = $.map( control.params.settings, function( value ) {
1417 api.apply( api, settings.concat( function () {
1420 control.settings = {};
1421 for ( key in control.params.settings ) {
1422 control.settings[ key ] = api( control.params.settings[ key ] );
1425 control.setting = control.settings['default'] || null;
1430 control.deferred.embedded.done( function () {
1436 * Embed the control into the page.
1438 embed: function () {
1442 // Watch for changes to the section state
1443 inject = function ( sectionId ) {
1444 var parentContainer;
1445 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the frontend
1448 // Wait for the section to be registered
1449 api.section( sectionId, function ( section ) {
1450 // Wait for the section to be ready/initialized
1451 section.deferred.embedded.done( function () {
1452 parentContainer = section.container.find( 'ul:first' );
1453 if ( ! control.container.parent().is( parentContainer ) ) {
1454 parentContainer.append( control.container );
1455 control.renderContent();
1457 control.deferred.embedded.resolve();
1461 control.section.bind( inject );
1462 inject( control.section.get() );
1466 * Triggered when the control's markup has been injected into the DOM.
1470 ready: function() {},
1473 * Normal controls do not expand, so just expand its parent
1475 * @param {Object} [params]
1477 expand: function ( params ) {
1478 api.section( this.section() ).expand( params );
1482 * Bring the containing section and panel into view and then
1483 * this control into view, focusing on the first input.
1488 * Update UI in response to a change in the control's active state.
1489 * This does not change the active state, it merely handles the behavior
1490 * for when it does change.
1494 * @param {Boolean} active
1495 * @param {Object} args
1496 * @param {Number} args.duration
1497 * @param {Callback} args.completeCallback
1499 onChangeActive: function ( active, args ) {
1500 if ( ! $.contains( document, this.container ) ) {
1501 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
1502 this.container.toggle( active );
1503 if ( args.completeCallback ) {
1504 args.completeCallback();
1506 } else if ( active ) {
1507 this.container.slideDown( args.duration, args.completeCallback );
1509 this.container.slideUp( args.duration, args.completeCallback );
1514 * @deprecated 4.1.0 Use this.onChangeActive() instead.
1516 toggle: function ( active ) {
1517 return this.onChangeActive( active, this.defaultActiveArguments );
1521 * Shorthand way to enable the active state.
1525 * @param {Object} [params]
1526 * @returns {Boolean} false if already active
1528 activate: Container.prototype.activate,
1531 * Shorthand way to disable the active state.
1535 * @param {Object} [params]
1536 * @returns {Boolean} false if already inactive
1538 deactivate: Container.prototype.deactivate,
1541 * Re-use _toggleActive from Container class.
1545 _toggleActive: Container.prototype._toggleActive,
1547 dropdownInit: function() {
1549 statuses = this.container.find('.dropdown-status'),
1550 params = this.params,
1551 toggleFreeze = false,
1552 update = function( to ) {
1553 if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
1554 statuses.html( params.statuses[ to ] ).show();
1559 // Support the .dropdown class to open/close complex elements
1560 this.container.on( 'click keydown', '.dropdown', function( event ) {
1561 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1565 event.preventDefault();
1568 control.container.toggleClass('open');
1570 if ( control.container.hasClass('open') )
1571 control.container.parent().parent().find('li.library-selected').focus();
1573 // Don't want to fire focus and click at same time
1574 toggleFreeze = true;
1575 setTimeout(function () {
1576 toggleFreeze = false;
1580 this.setting.bind( update );
1581 update( this.setting() );
1585 * Render the control from its JS template, if it exists.
1587 * The control's container must already exist in the DOM.
1591 renderContent: function () {
1595 // Replace the container element's content with the control.
1596 if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
1597 template = wp.template( control.templateSelector );
1598 if ( template && control.container ) {
1599 control.container.html( template( control.params ) );
1606 * A colorpicker control.
1609 * @augments wp.customize.Control
1610 * @augments wp.customize.Class
1612 api.ColorControl = api.Control.extend({
1615 picker = this.container.find('.color-picker-hex');
1617 picker.val( control.setting() ).wpColorPicker({
1618 change: function() {
1619 control.setting.set( picker.wpColorPicker('color') );
1622 control.setting.set( false );
1626 this.setting.bind( function ( value ) {
1627 picker.val( value );
1628 picker.wpColorPicker( 'color', value );
1634 * A control that implements the media modal.
1637 * @augments wp.customize.Control
1638 * @augments wp.customize.Class
1640 api.MediaControl = api.Control.extend({
1643 * When the control's DOM structure is ready,
1644 * set up internal event bindings.
1648 // Shortcut so that we don't have to use _.bind every time we add a callback.
1649 _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
1651 // Bind events, with delegation to facilitate re-rendering.
1652 control.container.on( 'click keydown', '.upload-button', control.openFrame );
1653 control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
1654 control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1655 control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1656 control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
1657 control.container.on( 'click keydown', '.remove-button', control.removeFile );
1658 control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
1660 // Resize the player controls when it becomes visible (ie when section is expanded)
1661 api.section( control.section() ).container
1662 .on( 'expanded', function() {
1663 if ( control.player ) {
1664 control.player.setControlsSize();
1667 .on( 'collapsed', function() {
1668 control.pausePlayer();
1671 // Re-render whenever the control's setting changes.
1672 control.setting.bind( function () { control.renderContent(); } );
1675 pausePlayer: function () {
1676 this.player && this.player.pause();
1679 cleanupPlayer: function () {
1680 this.player && wp.media.mixin.removePlayer( this.player );
1684 * Open the media modal.
1686 openFrame: function( event ) {
1687 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1691 event.preventDefault();
1693 if ( ! this.frame ) {
1701 * Create a media modal select frame, and store it so the instance can be reused when needed.
1703 initFrame: function() {
1704 this.frame = wp.media({
1706 text: this.params.button_labels.frame_button
1709 new wp.media.controller.Library({
1710 title: this.params.button_labels.frame_title,
1711 library: wp.media.query({ type: this.params.mime_type }),
1718 // When a file is selected, run a callback.
1719 this.frame.on( 'select', this.select );
1723 * Callback handler for when an attachment is selected in the media modal.
1724 * Gets the selected image information, and sets it within the control.
1726 select: function() {
1727 // Get the attachment from the modal frame.
1729 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1730 mejsSettings = window._wpmejsSettings || {};
1732 this.params.attachment = attachment;
1734 // Set the Customizer setting; the callback takes care of rendering.
1735 this.setting( attachment.id );
1736 node = this.container.find( 'audio, video' ).get(0);
1738 // Initialize audio/video previews.
1740 this.player = new MediaElementPlayer( node, mejsSettings );
1742 this.cleanupPlayer();
1747 * Reset the setting to the default value.
1749 restoreDefault: function( event ) {
1750 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1753 event.preventDefault();
1755 this.params.attachment = this.params.defaultAttachment;
1756 this.setting( this.params.defaultAttachment.url );
1760 * Called when the "Remove" link is clicked. Empties the setting.
1762 * @param {object} event jQuery Event object
1764 removeFile: function( event ) {
1765 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1768 event.preventDefault();
1770 this.params.attachment = {};
1772 this.renderContent(); // Not bound to setting change when emptying.
1777 * An upload control, which utilizes the media modal.
1780 * @augments wp.customize.MediaControl
1781 * @augments wp.customize.Control
1782 * @augments wp.customize.Class
1784 api.UploadControl = api.MediaControl.extend({
1787 * Callback handler for when an attachment is selected in the media modal.
1788 * Gets the selected image information, and sets it within the control.
1790 select: function() {
1791 // Get the attachment from the modal frame.
1793 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
1794 mejsSettings = window._wpmejsSettings || {};
1796 this.params.attachment = attachment;
1798 // Set the Customizer setting; the callback takes care of rendering.
1799 this.setting( attachment.url );
1800 node = this.container.find( 'audio, video' ).get(0);
1802 // Initialize audio/video previews.
1804 this.player = new MediaElementPlayer( node, mejsSettings );
1806 this.cleanupPlayer();
1811 success: function() {},
1814 removerVisibility: function() {}
1818 * A control for uploading images.
1820 * This control no longer needs to do anything more
1821 * than what the upload control does in JS.
1824 * @augments wp.customize.UploadControl
1825 * @augments wp.customize.MediaControl
1826 * @augments wp.customize.Control
1827 * @augments wp.customize.Class
1829 api.ImageControl = api.UploadControl.extend({
1831 thumbnailSrc: function() {}
1835 * A control for uploading background images.
1838 * @augments wp.customize.UploadControl
1839 * @augments wp.customize.MediaControl
1840 * @augments wp.customize.Control
1841 * @augments wp.customize.Class
1843 api.BackgroundControl = api.UploadControl.extend({
1846 * When the control's DOM structure is ready,
1847 * set up internal event bindings.
1850 api.UploadControl.prototype.ready.apply( this, arguments );
1854 * Callback handler for when an attachment is selected in the media modal.
1855 * Does an additional AJAX request for setting the background context.
1857 select: function() {
1858 api.UploadControl.prototype.select.apply( this, arguments );
1860 wp.ajax.post( 'custom-background-add', {
1861 nonce: _wpCustomizeBackground.nonces.add,
1863 theme: api.settings.theme.stylesheet,
1864 attachment_id: this.params.attachment.id
1870 * A control for selecting and cropping an image.
1873 * @augments wp.customize.MediaControl
1874 * @augments wp.customize.Control
1875 * @augments wp.customize.Class
1877 api.CroppedImageControl = api.MediaControl.extend({
1880 * Open the media modal to the library state.
1882 openFrame: function( event ) {
1883 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1888 this.frame.setState( 'library' ).open();
1892 * Create a media modal select frame, and store it so the instance can be reused when needed.
1894 initFrame: function() {
1895 var l10n = _wpMediaViewsL10n;
1897 this.frame = wp.media({
1903 new wp.media.controller.Library({
1904 title: this.params.button_labels.frame_title,
1905 library: wp.media.query({ type: 'image' }),
1909 suggestedWidth: this.params.width,
1910 suggestedHeight: this.params.height
1912 new wp.media.controller.CustomizeImageCropper({
1913 imgSelectOptions: this.calculateImageSelectOptions,
1919 this.frame.on( 'select', this.onSelect, this );
1920 this.frame.on( 'cropped', this.onCropped, this );
1921 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
1925 * After an image is selected in the media modal, switch to the cropper
1926 * state if the image isn't the right size.
1928 onSelect: function() {
1929 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
1931 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
1932 this.setImageFromAttachment( attachment );
1935 this.frame.setState( 'cropper' );
1940 * After the image has been cropped, apply the cropped image data to the setting.
1942 * @param {object} croppedImage Cropped attachment data.
1944 onCropped: function( croppedImage ) {
1945 this.setImageFromAttachment( croppedImage );
1949 * Returns a set of options, computed from the attached image data and
1950 * control-specific data, to be fed to the imgAreaSelect plugin in
1951 * wp.media.view.Cropper.
1953 * @param {wp.media.model.Attachment} attachment
1954 * @param {wp.media.controller.Cropper} controller
1955 * @returns {Object} Options
1957 calculateImageSelectOptions: function( attachment, controller ) {
1958 var control = controller.get( 'control' ),
1959 flexWidth = !! parseInt( control.params.flex_width, 10 ),
1960 flexHeight = !! parseInt( control.params.flex_height, 10 ),
1961 realWidth = attachment.get( 'width' ),
1962 realHeight = attachment.get( 'height' ),
1963 xInit = parseInt( control.params.width, 10 ),
1964 yInit = parseInt( control.params.height, 10 ),
1965 ratio = xInit / yInit,
1968 x1, y1, imgSelectOptions;
1970 controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
1972 if ( xImg / yImg > ratio ) {
1974 xInit = yInit * ratio;
1977 yInit = xInit / ratio;
1980 x1 = ( xImg - xInit ) / 2;
1981 y1 = ( yImg - yInit ) / 2;
1983 imgSelectOptions = {
1988 imageWidth: realWidth,
1989 imageHeight: realHeight,
1996 if ( flexHeight === false && flexWidth === false ) {
1997 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
1999 if ( flexHeight === false ) {
2000 imgSelectOptions.maxHeight = yInit;
2002 if ( flexWidth === false ) {
2003 imgSelectOptions.maxWidth = xInit;
2006 return imgSelectOptions;
2010 * Return whether the image must be cropped, based on required dimensions.
2012 * @param {bool} flexW
2013 * @param {bool} flexH
2020 mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
2021 if ( true === flexW && true === flexH ) {
2025 if ( true === flexW && dstH === imgH ) {
2029 if ( true === flexH && dstW === imgW ) {
2033 if ( dstW === imgW && dstH === imgH ) {
2037 if ( imgW <= dstW ) {
2045 * If cropping was skipped, apply the image data directly to the setting.
2047 onSkippedCrop: function() {
2048 var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2049 this.setImageFromAttachment( attachment );
2053 * Updates the setting and re-renders the control UI.
2055 * @param {object} attachment
2057 setImageFromAttachment: function( attachment ) {
2058 this.params.attachment = attachment;
2060 // Set the Customizer setting; the callback takes care of rendering.
2061 this.setting( attachment.id );
2066 * A control for selecting and cropping Site Icons.
2069 * @augments wp.customize.CroppedImageControl
2070 * @augments wp.customize.MediaControl
2071 * @augments wp.customize.Control
2072 * @augments wp.customize.Class
2074 api.SiteIconControl = api.CroppedImageControl.extend({
2077 * Create a media modal select frame, and store it so the instance can be reused when needed.
2079 initFrame: function() {
2080 var l10n = _wpMediaViewsL10n;
2082 this.frame = wp.media({
2088 new wp.media.controller.Library({
2089 title: this.params.button_labels.frame_title,
2090 library: wp.media.query({ type: 'image' }),
2094 suggestedWidth: this.params.width,
2095 suggestedHeight: this.params.height
2097 new wp.media.controller.SiteIconCropper({
2098 imgSelectOptions: this.calculateImageSelectOptions,
2104 this.frame.on( 'select', this.onSelect, this );
2105 this.frame.on( 'cropped', this.onCropped, this );
2106 this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2110 * After an image is selected in the media modal, switch to the cropper
2111 * state if the image isn't the right size.
2113 onSelect: function() {
2114 var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2117 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2118 wp.ajax.post( 'crop-image', {
2119 nonce: attachment.nonces.edit,
2121 context: 'site-icon',
2125 width: this.params.width,
2126 height: this.params.height,
2127 dst_width: this.params.width,
2128 dst_height: this.params.height
2130 } ).done( function( croppedImage ) {
2131 controller.setImageFromAttachment( croppedImage );
2132 controller.frame.close();
2133 } ).fail( function() {
2134 controller.trigger('content:error:crop');
2137 this.frame.setState( 'cropper' );
2142 * Updates the setting and re-renders the control UI.
2144 * @param {object} attachment
2146 setImageFromAttachment: function( attachment ) {
2147 var icon = typeof attachment.sizes['site_icon-32'] !== 'undefined' ? attachment.sizes['site_icon-32'] : attachment.sizes.thumbnail;
2149 this.params.attachment = attachment;
2151 // Set the Customizer setting; the callback takes care of rendering.
2152 this.setting( attachment.id );
2155 // Update the icon in-browser.
2156 $( 'link[sizes="32x32"]' ).attr( 'href', icon.url );
2160 * Called when the "Remove" link is clicked. Empties the setting.
2162 * @param {object} event jQuery Event object
2164 removeFile: function( event ) {
2165 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2168 event.preventDefault();
2170 this.params.attachment = {};
2172 this.renderContent(); // Not bound to setting change when emptying.
2173 $( 'link[rel="icon"]' ).attr( 'href', '' );
2179 * @augments wp.customize.Control
2180 * @augments wp.customize.Class
2182 api.HeaderControl = api.Control.extend({
2184 this.btnRemove = $('#customize-control-header_image .actions .remove');
2185 this.btnNew = $('#customize-control-header_image .actions .new');
2187 _.bindAll(this, 'openMedia', 'removeImage');
2189 this.btnNew.on( 'click', this.openMedia );
2190 this.btnRemove.on( 'click', this.removeImage );
2192 api.HeaderTool.currentHeader = this.getInitialHeaderImage();
2194 new api.HeaderTool.CurrentView({
2195 model: api.HeaderTool.currentHeader,
2196 el: '#customize-control-header_image .current .container'
2199 new api.HeaderTool.ChoiceListView({
2200 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
2201 el: '#customize-control-header_image .choices .uploaded .list'
2204 new api.HeaderTool.ChoiceListView({
2205 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
2206 el: '#customize-control-header_image .choices .default .list'
2209 api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
2210 api.HeaderTool.UploadsList,
2211 api.HeaderTool.DefaultsList
2216 * Returns a new instance of api.HeaderTool.ImageModel based on the currently
2217 * saved header image (if any).
2221 * @returns {Object} Options
2223 getInitialHeaderImage: function() {
2224 if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
2225 return new api.HeaderTool.ImageModel();
2228 // Get the matching uploaded image object.
2229 var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
2230 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
2232 // Fall back to raw current header image.
2233 if ( ! currentHeaderObject ) {
2234 currentHeaderObject = {
2235 url: api.get().header_image,
2236 thumbnail_url: api.get().header_image,
2237 attachment_id: api.get().header_image_data.attachment_id
2241 return new api.HeaderTool.ImageModel({
2242 header: currentHeaderObject,
2243 choice: currentHeaderObject.url.split( '/' ).pop()
2248 * Returns a set of options, computed from the attached image data and
2249 * theme-specific data, to be fed to the imgAreaSelect plugin in
2250 * wp.media.view.Cropper.
2252 * @param {wp.media.model.Attachment} attachment
2253 * @param {wp.media.controller.Cropper} controller
2254 * @returns {Object} Options
2256 calculateImageSelectOptions: function(attachment, controller) {
2257 var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
2258 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
2259 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
2260 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
2261 ratio, xImg, yImg, realHeight, realWidth,
2264 realWidth = attachment.get('width');
2265 realHeight = attachment.get('height');
2267 this.headerImage = new api.HeaderTool.ImageModel();
2268 this.headerImage.set({
2271 themeFlexWidth: flexWidth,
2272 themeFlexHeight: flexHeight,
2273 imageWidth: realWidth,
2274 imageHeight: realHeight
2277 controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
2279 ratio = xInit / yInit;
2283 if ( xImg / yImg > ratio ) {
2285 xInit = yInit * ratio;
2288 yInit = xInit / ratio;
2291 imgSelectOptions = {
2296 imageWidth: realWidth,
2297 imageHeight: realHeight,
2304 if (flexHeight === false && flexWidth === false) {
2305 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2307 if (flexHeight === false ) {
2308 imgSelectOptions.maxHeight = yInit;
2310 if (flexWidth === false ) {
2311 imgSelectOptions.maxWidth = xInit;
2314 return imgSelectOptions;
2318 * Sets up and opens the Media Manager in order to select an image.
2319 * Depending on both the size of the image and the properties of the
2320 * current theme, a cropping step after selection may be required or
2323 * @param {event} event
2325 openMedia: function(event) {
2326 var l10n = _wpMediaViewsL10n;
2328 event.preventDefault();
2330 this.frame = wp.media({
2332 text: l10n.selectAndCrop,
2336 new wp.media.controller.Library({
2337 title: l10n.chooseImage,
2338 library: wp.media.query({ type: 'image' }),
2342 suggestedWidth: _wpCustomizeHeader.data.width,
2343 suggestedHeight: _wpCustomizeHeader.data.height
2345 new wp.media.controller.Cropper({
2346 imgSelectOptions: this.calculateImageSelectOptions
2351 this.frame.on('select', this.onSelect, this);
2352 this.frame.on('cropped', this.onCropped, this);
2353 this.frame.on('skippedcrop', this.onSkippedCrop, this);
2359 * After an image is selected in the media modal,
2360 * switch to the cropper state.
2362 onSelect: function() {
2363 this.frame.setState('cropper');
2367 * After the image has been cropped, apply the cropped image data to the setting.
2369 * @param {object} croppedImage Cropped attachment data.
2371 onCropped: function(croppedImage) {
2372 var url = croppedImage.post_content,
2373 attachmentId = croppedImage.attachment_id,
2374 w = croppedImage.width,
2375 h = croppedImage.height;
2376 this.setImageFromURL(url, attachmentId, w, h);
2380 * If cropping was skipped, apply the image data directly to the setting.
2382 * @param {object} selection
2384 onSkippedCrop: function(selection) {
2385 var url = selection.get('url'),
2386 w = selection.get('width'),
2387 h = selection.get('height');
2388 this.setImageFromURL(url, selection.id, w, h);
2392 * Creates a new wp.customize.HeaderTool.ImageModel from provided
2393 * header image data and inserts it into the user-uploaded headers
2396 * @param {String} url
2397 * @param {Number} attachmentId
2398 * @param {Number} width
2399 * @param {Number} height
2401 setImageFromURL: function(url, attachmentId, width, height) {
2402 var choice, data = {};
2405 data.thumbnail_url = url;
2406 data.timestamp = _.now();
2409 data.attachment_id = attachmentId;
2417 data.height = height;
2420 choice = new api.HeaderTool.ImageModel({
2422 choice: url.split('/').pop()
2424 api.HeaderTool.UploadsList.add(choice);
2425 api.HeaderTool.currentHeader.set(choice.toJSON());
2427 choice.importImage();
2431 * Triggers the necessary events to deselect an image which was set as
2432 * the currently selected one.
2434 removeImage: function() {
2435 api.HeaderTool.currentHeader.trigger('hide');
2436 api.HeaderTool.CombinedList.trigger('control:removeImage');
2442 * wp.customize.ThemeControl
2445 * @augments wp.customize.Control
2446 * @augments wp.customize.Class
2448 api.ThemeControl = api.Control.extend({
2454 * Defer rendering the theme control until the section is displayed.
2458 renderContent: function () {
2460 renderContentArgs = arguments;
2462 api.section( control.section(), function( section ) {
2463 if ( section.expanded() ) {
2464 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2465 control.isRendered = true;
2467 section.expanded.bind( function( expanded ) {
2468 if ( expanded && ! control.isRendered ) {
2469 api.Control.prototype.renderContent.apply( control, renderContentArgs );
2470 control.isRendered = true;
2483 control.container.on( 'touchmove', '.theme', function() {
2484 control.touchDrag = true;
2487 // Bind details view trigger.
2488 control.container.on( 'click keydown touchend', '.theme', function( event ) {
2489 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2493 // Bail if the user scrolled on a touch device.
2494 if ( control.touchDrag === true ) {
2495 return control.touchDrag = false;
2498 // Prevent the modal from showing when the user clicks the action button.
2499 if ( $( event.target ).is( '.theme-actions .button' ) ) {
2503 var previewUrl = $( this ).data( 'previewUrl' );
2505 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
2507 window.parent.location = previewUrl;
2510 control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
2511 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2515 event.preventDefault(); // Keep this AFTER the key filter above
2517 api.section( control.section() ).showDetails( control.params.theme );
2520 control.container.on( 'render-screenshot', function() {
2521 var $screenshot = $( this ).find( 'img' ),
2522 source = $screenshot.data( 'src' );
2525 $screenshot.attr( 'src', source );
2531 * Show or hide the theme based on the presence of the term in the title, description, and author.
2535 filter: function( term ) {
2537 haystack = control.params.theme.name + ' ' +
2538 control.params.theme.description + ' ' +
2539 control.params.theme.tags + ' ' +
2540 control.params.theme.author;
2541 haystack = haystack.toLowerCase().replace( '-', ' ' );
2542 if ( -1 !== haystack.search( term ) ) {
2545 control.deactivate();
2550 // Change objects contained within the main customize object to Settings.
2551 api.defaultConstructor = api.Setting;
2553 // Create the collections for Controls, Sections and Panels.
2554 api.control = new api.Values({ defaultConstructor: api.Control });
2555 api.section = new api.Values({ defaultConstructor: api.Section });
2556 api.panel = new api.Values({ defaultConstructor: api.Panel });
2560 * @augments wp.customize.Messenger
2561 * @augments wp.customize.Class
2562 * @mixes wp.customize.Events
2564 api.PreviewFrame = api.Messenger.extend({
2567 initialize: function( params, options ) {
2568 var deferred = $.Deferred();
2570 // This is the promise object.
2571 deferred.promise( this );
2573 this.container = params.container;
2574 this.signature = params.signature;
2576 $.extend( params, { channel: api.PreviewFrame.uuid() });
2578 api.Messenger.prototype.initialize.call( this, params, options );
2580 this.add( 'previewUrl', params.previewUrl );
2582 this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
2584 this.run( deferred );
2587 run: function( deferred ) {
2592 if ( this._ready ) {
2593 this.unbind( 'ready', this._ready );
2596 this._ready = function() {
2600 deferred.resolveWith( self );
2604 this.bind( 'ready', this._ready );
2606 this.bind( 'ready', function ( data ) {
2608 this.container.addClass( 'iframe-ready' );
2615 * Walk over all panels, sections, and controls and set their
2616 * respective active states to true if the preview explicitly
2617 * indicates as such.
2620 panel: data.activePanels,
2621 section: data.activeSections,
2622 control: data.activeControls
2624 _( constructs ).each( function ( activeConstructs, type ) {
2625 api[ type ].each( function ( construct, id ) {
2626 var active = !! ( activeConstructs && activeConstructs[ id ] );
2628 construct.activate();
2630 construct.deactivate();
2636 this.request = $.ajax( this.previewUrl(), {
2640 withCredentials: true
2644 this.request.fail( function() {
2645 deferred.rejectWith( self, [ 'request failure' ] );
2648 this.request.done( function( response ) {
2649 var location = self.request.getResponseHeader('Location'),
2650 signature = self.signature,
2653 // Check if the location response header differs from the current URL.
2654 // If so, the request was redirected; try loading the requested page.
2655 if ( location && location !== self.previewUrl() ) {
2656 deferred.rejectWith( self, [ 'redirect', location ] );
2660 // Check if the user is not logged in.
2661 if ( '0' === response ) {
2662 self.login( deferred );
2666 // Check for cheaters.
2667 if ( '-1' === response ) {
2668 deferred.rejectWith( self, [ 'cheatin' ] );
2672 // Check for a signature in the request.
2673 index = response.lastIndexOf( signature );
2674 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
2675 deferred.rejectWith( self, [ 'unsigned' ] );
2679 // Strip the signature from the request.
2680 response = response.slice( 0, index ) + response.slice( index + signature.length );
2682 // Create the iframe and inject the html content.
2683 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
2685 // Bind load event after the iframe has been added to the page;
2686 // otherwise it will fire when injected into the DOM.
2687 self.iframe.one( 'load', function() {
2691 deferred.resolveWith( self );
2693 setTimeout( function() {
2694 deferred.rejectWith( self, [ 'ready timeout' ] );
2695 }, self.sensitivity );
2699 self.targetWindow( self.iframe[0].contentWindow );
2701 self.targetWindow().document.open();
2702 self.targetWindow().document.write( response );
2703 self.targetWindow().document.close();
2707 login: function( deferred ) {
2711 reject = function() {
2712 deferred.rejectWith( self, [ 'logged out' ] );
2715 if ( this.triedLogin ) {
2719 // Check if we have an admin cookie.
2720 $.get( api.settings.url.ajax, {
2722 }).fail( reject ).done( function( response ) {
2725 if ( '1' !== response ) {
2729 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
2730 iframe.appendTo( self.container );
2731 iframe.load( function() {
2732 self.triedLogin = true;
2735 self.run( deferred );
2740 destroy: function() {
2741 api.Messenger.prototype.destroy.call( this );
2742 this.request.abort();
2745 this.iframe.remove();
2747 delete this.request;
2749 delete this.targetWindow;
2756 * Create a universally unique identifier.
2760 api.PreviewFrame.uuid = function() {
2761 return 'preview-' + uuid++;
2766 * Set the document title of the customizer.
2770 * @param {string} documentTitle
2772 api.setDocumentTitle = function ( documentTitle ) {
2774 tmpl = api.settings.documentTitleTmpl;
2775 title = tmpl.replace( '%s', documentTitle );
2776 document.title = title;
2777 api.trigger( 'title', title );
2782 * @augments wp.customize.Messenger
2783 * @augments wp.customize.Class
2784 * @mixes wp.customize.Events
2786 api.Previewer = api.Messenger.extend({
2791 * - container - a selector or jQuery element
2792 * - previewUrl - the URL of preview frame
2794 initialize: function( params, options ) {
2796 rscheme = /^https?/;
2798 $.extend( this, options || {} );
2800 active: $.Deferred()
2804 * Wrap this.refresh to prevent it from hammering the servers:
2806 * If refresh is called once and no other refresh requests are
2807 * loading, trigger the request immediately.
2809 * If refresh is called while another refresh request is loading,
2810 * debounce the refresh requests:
2811 * 1. Stop the loading request (as it is instantly outdated).
2812 * 2. Trigger the new request once refresh hasn't been called for
2813 * self.refreshBuffer milliseconds.
2815 this.refresh = (function( self ) {
2816 var refresh = self.refresh,
2817 callback = function() {
2819 refresh.call( self );
2824 if ( typeof timeout !== 'number' ) {
2825 if ( self.loading ) {
2832 clearTimeout( timeout );
2833 timeout = setTimeout( callback, self.refreshBuffer );
2837 this.container = api.ensure( params.container );
2838 this.allowedUrls = params.allowedUrls;
2839 this.signature = params.signature;
2841 params.url = window.location.href;
2843 api.Messenger.prototype.initialize.call( this, params );
2845 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
2846 var match = to.match( rscheme );
2847 return match ? match[0] : '';
2850 // Limit the URL to internal, front-end links.
2852 // If the frontend and the admin are served from the same domain, load the
2853 // preview over ssl if the Customizer is being loaded over ssl. This avoids
2854 // insecure content warnings. This is not attempted if the admin and frontend
2855 // are on different domains to avoid the case where the frontend doesn't have
2858 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
2861 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
2862 // Strip hashes and query strings before testing.
2863 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
2866 // Attempt to match the URL to the control frame's scheme
2867 // and check if it's allowed. If not, try the original URL.
2868 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
2869 $.each( self.allowedUrls, function( i, allowed ) {
2872 allowed = allowed.replace( /\/+$/, '' );
2873 path = url.replace( allowed, '' );
2875 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
2884 // If we found a matching result, return it. If not, bail.
2885 return result ? result : null;
2888 // Refresh the preview when the URL is changed (but not yet).
2889 this.previewUrl.bind( this.refresh );
2892 this.bind( 'scroll', function( distance ) {
2893 this.scroll = distance;
2896 // Update the URL when the iframe sends a URL message.
2897 this.bind( 'url', this.previewUrl );
2899 // Update the document title when the preview changes.
2900 this.bind( 'documentTitle', function ( title ) {
2901 api.setDocumentTitle( title );
2905 query: function() {},
2908 if ( this.loading ) {
2909 this.loading.destroy();
2910 delete this.loading;
2914 refresh: function() {
2917 // Display loading indicator
2918 this.send( 'loading-initiated' );
2922 this.loading = new api.PreviewFrame({
2924 previewUrl: this.previewUrl(),
2925 query: this.query() || {},
2926 container: this.container,
2927 signature: this.signature
2930 this.loading.done( function() {
2931 // 'this' is the loading frame
2932 this.bind( 'synced', function() {
2934 self.preview.destroy();
2935 self.preview = this;
2936 delete self.loading;
2938 self.targetWindow( this.targetWindow() );
2939 self.channel( this.channel() );
2941 self.deferred.active.resolve();
2942 self.send( 'active' );
2945 this.send( 'sync', {
2946 scroll: self.scroll,
2951 this.loading.fail( function( reason, location ) {
2952 self.send( 'loading-failed' );
2953 if ( 'redirect' === reason && location ) {
2954 self.previewUrl( location );
2957 if ( 'logged out' === reason ) {
2958 if ( self.preview ) {
2959 self.preview.destroy();
2960 delete self.preview;
2963 self.login().done( self.refresh );
2966 if ( 'cheatin' === reason ) {
2973 var previewer = this,
2974 deferred, messenger, iframe;
2979 deferred = $.Deferred();
2980 this._login = deferred.promise();
2982 messenger = new api.Messenger({
2984 url: api.settings.url.login
2987 iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
2989 messenger.targetWindow( iframe[0].contentWindow );
2991 messenger.bind( 'login', function () {
2992 var refreshNonces = previewer.refreshNonces();
2994 refreshNonces.always( function() {
2996 messenger.destroy();
2997 delete previewer._login;
3000 refreshNonces.done( function() {
3004 refreshNonces.fail( function() {
3005 previewer.cheatin();
3013 cheatin: function() {
3014 $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
3017 refreshNonces: function() {
3018 var request, deferred = $.Deferred();
3022 request = wp.ajax.post( 'customize_refresh_nonces', {
3024 theme: api.settings.theme.stylesheet
3027 request.done( function( response ) {
3028 api.trigger( 'nonce-refresh', response );
3032 request.fail( function() {
3040 api.controlConstructor = {
3041 color: api.ColorControl,
3042 media: api.MediaControl,
3043 upload: api.UploadControl,
3044 image: api.ImageControl,
3045 cropped_image: api.CroppedImageControl,
3046 site_icon: api.SiteIconControl,
3047 header: api.HeaderControl,
3048 background: api.BackgroundControl,
3049 theme: api.ThemeControl
3051 api.panelConstructor = {};
3052 api.sectionConstructor = {
3053 themes: api.ThemesSection
3057 api.settings = window._wpCustomizeSettings;
3058 api.l10n = window._wpCustomizeControlsL10n;
3060 // Check if we can run the Customizer.
3061 if ( ! api.settings ) {
3065 // Bail if any incompatibilities are found.
3066 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
3070 var parent, topFocus,
3071 body = $( document.body ),
3072 overlay = body.children( '.wp-full-overlay' ),
3073 title = $( '#customize-info .panel-title.site-title' ),
3074 closeBtn = $( '.customize-controls-close' ),
3075 saveBtn = $( '#save' );
3077 // Prevent the form from saving when enter is pressed on an input or select element.
3078 $('#customize-controls').on( 'keydown', function( e ) {
3079 var isEnter = ( 13 === e.which ),
3080 $el = $( e.target );
3082 if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
3087 // Expand/Collapse the main customizer customize info.
3088 $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
3089 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3092 event.preventDefault(); // Keep this AFTER the key filter above
3094 var section = $( this ).closest( '.accordion-section' ),
3095 content = section.find( '.customize-panel-description:first' );
3097 if ( section.hasClass( 'cannot-expand' ) ) {
3101 if ( section.hasClass( 'open' ) ) {
3102 section.toggleClass( 'open' );
3103 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
3104 $( this ).attr( 'aria-expanded', false );
3106 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
3107 section.toggleClass( 'open' );
3108 $( this ).attr( 'aria-expanded', true );
3112 // Initialize Previewer
3113 api.previewer = new api.Previewer({
3114 container: '#customize-preview',
3115 form: '#customize-controls',
3116 previewUrl: api.settings.url.preview,
3117 allowedUrls: api.settings.url.allowed,
3118 signature: 'WP_CUSTOMIZER_SIGNATURE'
3121 nonce: api.settings.nonce,
3124 var dirtyCustomized = {};
3125 api.each( function ( value, key ) {
3126 if ( value._dirty ) {
3127 dirtyCustomized[ key ] = value();
3133 theme: api.settings.theme.stylesheet,
3134 customized: JSON.stringify( dirtyCustomized ),
3135 nonce: this.nonce.preview
3141 processing = api.state( 'processing' ),
3142 submitWhenDoneProcessing,
3145 body.addClass( 'saving' );
3147 submit = function () {
3149 query = $.extend( self.query(), {
3150 nonce: self.nonce.save
3152 request = wp.ajax.post( 'customize_save', query );
3154 api.trigger( 'save', request );
3156 request.always( function () {
3157 body.removeClass( 'saving' );
3160 request.fail( function ( response ) {
3161 if ( '0' === response ) {
3162 response = 'not_logged_in';
3163 } else if ( '-1' === response ) {
3164 // Back-compat in case any other check_ajax_referer() call is dying
3165 response = 'invalid_nonce';
3168 if ( 'invalid_nonce' === response ) {
3170 } else if ( 'not_logged_in' === response ) {
3171 self.preview.iframe.hide();
3172 self.login().done( function() {
3174 self.preview.iframe.show();
3177 api.trigger( 'error', response );
3180 request.done( function( response ) {
3181 // Clear setting dirty states
3182 api.each( function ( value ) {
3183 value._dirty = false;
3186 api.trigger( 'saved', response );
3190 if ( 0 === processing() ) {
3193 submitWhenDoneProcessing = function () {
3194 if ( 0 === processing() ) {
3195 api.state.unbind( 'change', submitWhenDoneProcessing );
3199 api.state.bind( 'change', submitWhenDoneProcessing );
3205 // Refresh the nonces if the preview sends updated nonces over.
3206 api.previewer.bind( 'nonce', function( nonce ) {
3207 $.extend( this.nonce, nonce );
3210 // Refresh the nonces if login sends updated nonces over.
3211 api.bind( 'nonce-refresh', function( nonce ) {
3212 $.extend( api.settings.nonce, nonce );
3213 $.extend( api.previewer.nonce, nonce );
3217 $.each( api.settings.settings, function( id, data ) {
3218 api.create( id, id, data.value, {
3219 transport: data.transport,
3220 previewer: api.previewer,
3221 dirty: !! data.dirty
3226 $.each( api.settings.panels, function ( id, data ) {
3227 var constructor = api.panelConstructor[ data.type ] || api.Panel,
3230 panel = new constructor( id, {
3233 api.panel.add( id, panel );
3237 $.each( api.settings.sections, function ( id, data ) {
3238 var constructor = api.sectionConstructor[ data.type ] || api.Section,
3241 section = new constructor( id, {
3244 api.section.add( id, section );
3248 $.each( api.settings.controls, function( id, data ) {
3249 var constructor = api.controlConstructor[ data.type ] || api.Control,
3252 control = new constructor( id, {
3254 previewer: api.previewer
3256 api.control.add( id, control );
3259 // Focus the autofocused element
3260 _.each( [ 'panel', 'section', 'control' ], function ( type ) {
3261 var instance, id = api.settings.autofocus[ type ];
3262 if ( id && api[ type ]( id ) ) {
3263 instance = api[ type ]( id );
3264 // Wait until the element is embedded in the DOM
3265 instance.deferred.embedded.done( function () {
3266 // Wait until the preview has activated and so active panels, sections, controls have been set
3267 api.previewer.deferred.active.done( function () {
3275 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
3279 api.reflowPaneContents = _.bind( function () {
3281 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3283 if ( document.activeElement ) {
3284 activeElement = $( document.activeElement );
3287 // Sort the sections within each panel
3288 api.panel.each( function ( panel ) {
3289 var sections = panel.sections(),
3290 sectionContainers = _.pluck( sections, 'container' );
3291 rootNodes.push( panel );
3292 appendContainer = panel.container.find( 'ul:first' );
3293 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
3294 _( sections ).each( function ( section ) {
3295 appendContainer.append( section.container );
3301 // Sort the controls within each section
3302 api.section.each( function ( section ) {
3303 var controls = section.controls(),
3304 controlContainers = _.pluck( controls, 'container' );
3305 if ( ! section.panel() ) {
3306 rootNodes.push( section );
3308 appendContainer = section.container.find( 'ul:first' );
3309 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
3310 _( controls ).each( function ( control ) {
3311 appendContainer.append( control.container );
3317 // Sort the root panels and sections
3318 rootNodes.sort( api.utils.prioritySort );
3319 rootContainers = _.pluck( rootNodes, 'container' );
3320 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
3321 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
3322 _( rootNodes ).each( function ( rootNode ) {
3323 appendContainer.append( rootNode.container );
3328 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
3329 api.panel.each( function ( panel ) {
3330 var value = panel.active();
3331 panel.active.callbacks.fireWith( panel.active, [ value, value ] );
3333 api.section.each( function ( section ) {
3334 var value = section.active();
3335 section.active.callbacks.fireWith( section.active, [ value, value ] );
3338 // Restore focus if there was a reflow and there was an active (focused) element
3339 if ( wasReflowed && activeElement ) {
3340 activeElement.focus();
3342 api.trigger( 'pane-contents-reflowed' );
3344 api.bind( 'ready', api.reflowPaneContents );
3345 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
3346 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
3347 values.bind( 'add', api.reflowPaneContents );
3348 values.bind( 'change', api.reflowPaneContents );
3349 values.bind( 'remove', api.reflowPaneContents );
3352 // Check if preview url is valid and load the preview frame.
3353 if ( api.previewer.previewUrl() ) {
3354 api.previewer.refresh();
3356 api.previewer.previewUrl( api.settings.url.home );
3359 // Save and activated states
3361 var state = new api.Values(),
3362 saved = state.create( 'saved' ),
3363 activated = state.create( 'activated' ),
3364 processing = state.create( 'processing' );
3366 state.bind( 'change', function() {
3367 if ( ! activated() ) {
3368 saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
3369 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3371 } else if ( saved() ) {
3372 saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
3373 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
3376 saveBtn.val( api.l10n.save ).prop( 'disabled', false );
3377 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3381 // Set default states.
3383 activated( api.settings.theme.active );
3386 api.bind( 'change', function() {
3387 state('saved').set( false );
3390 api.bind( 'saved', function() {
3391 state('saved').set( true );
3392 state('activated').set( true );
3395 activated.bind( function( to ) {
3397 api.trigger( 'activated' );
3400 // Expose states to the API.
3405 saveBtn.click( function( event ) {
3406 api.previewer.save();
3407 event.preventDefault();
3408 }).keydown( function( event ) {
3409 if ( 9 === event.which ) // tab
3411 if ( 13 === event.which ) // enter
3412 api.previewer.save();
3413 event.preventDefault();
3416 closeBtn.keydown( function( event ) {
3417 if ( 9 === event.which ) // tab
3419 if ( 13 === event.which ) // enter
3421 event.preventDefault();
3424 $( '.collapse-sidebar' ).on( 'click', function() {
3425 if ( 'true' === $( this ).attr( 'aria-expanded' ) ) {
3426 $( this ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
3428 $( this ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
3431 overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
3434 $( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) {
3435 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3439 overlay.toggleClass( 'preview-only' );
3440 event.preventDefault();
3443 // Bind site title display to the corresponding field.
3444 if ( title.length ) {
3445 $( '#customize-control-blogname input' ).on( 'input', function() {
3446 title.text( this.value );
3450 // Create a potential postMessage connection with the parent frame.
3451 parent = new api.Messenger({
3452 url: api.settings.url.parent,
3456 // If we receive a 'back' event, we're inside an iframe.
3457 // Send any clicks to the 'Return' link to the parent page.
3458 parent.bind( 'back', function() {
3459 closeBtn.on( 'click.customize-controls-close', function( event ) {
3460 event.preventDefault();
3461 parent.send( 'close' );
3465 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
3466 $( window ).on( 'beforeunload', function () {
3467 if ( ! api.state( 'saved' )() ) {
3468 setTimeout( function() {
3469 overlay.removeClass( 'customize-loading' );
3471 return api.l10n.saveAlert;
3475 // Pass events through to the parent.
3476 $.each( [ 'saved', 'change' ], function ( i, event ) {
3477 api.bind( event, function() {
3478 parent.send( event );
3482 // When activated, let the loader handle redirecting the page.
3483 // If no loader exists, redirect the page ourselves (if a url exists).
3484 api.bind( 'activated', function() {
3485 if ( parent.targetWindow() )
3486 parent.send( 'activated', api.settings.url.activated );
3487 else if ( api.settings.url.activated )
3488 window.location = api.settings.url.activated;
3491 // Pass titles to the parent
3492 api.bind( 'title', function( newTitle ) {
3493 parent.send( 'title', newTitle );
3496 // Initialize the connection with the parent frame.
3497 parent.send( 'ready' );
3499 // Control visibility for default controls
3501 'background_image': {
3502 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
3503 callback: function( to ) { return !! to; }
3506 controls: [ 'page_on_front', 'page_for_posts' ],
3507 callback: function( to ) { return 'page' === to; }
3509 'header_textcolor': {
3510 controls: [ 'header_textcolor' ],
3511 callback: function( to ) { return 'blank' !== to; }
3513 }, function( settingId, o ) {
3514 api( settingId, function( setting ) {
3515 $.each( o.controls, function( i, controlId ) {
3516 api.control( controlId, function( control ) {
3517 var visibility = function( to ) {
3518 control.container.toggle( o.callback( to ) );
3521 visibility( setting.get() );
3522 setting.bind( visibility );
3528 // Juggle the two controls that use header_textcolor
3529 api.control( 'display_header_text', function( control ) {
3532 control.elements[0].unsync( api( 'header_textcolor' ) );
3534 control.element = new api.Element( control.container.find('input') );
3535 control.element.set( 'blank' !== control.setting() );
3537 control.element.bind( function( to ) {
3539 last = api( 'header_textcolor' ).get();
3541 control.setting.set( to ? last : 'blank' );
3544 control.setting.bind( function( to ) {
3545 control.element.set( 'blank' !== to );
3549 // Change previewed URL to the homepage when changing the page_on_front.
3550 api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
3551 var updatePreviewUrl = function() {
3552 if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
3553 api.previewer.previewUrl.set( api.settings.url.home );
3556 showOnFront.bind( updatePreviewUrl );
3557 pageOnFront.bind( updatePreviewUrl );
3560 // Change the previewed URL to the selected page when changing the page_for_posts.
3561 api( 'page_for_posts', function( setting ) {
3562 setting.bind(function( pageId ) {
3563 pageId = parseInt( pageId, 10 );
3565 api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
3570 api.trigger( 'ready' );
3572 // Make sure left column gets focus
3573 topFocus = closeBtn;
3575 setTimeout(function () {