+ * @param options
+ * - previewer - The Previewer instance to sync with.
+ * - transport - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
+ */
+ api.Setting = api.Value.extend({
+ initialize: function( id, value, options ) {
+ api.Value.prototype.initialize.call( this, value, options );
+
+ this.id = id;
+ this.transport = this.transport || 'refresh';
+ this._dirty = options.dirty || false;
+
+ this.bind( this.preview );
+ },
+ preview: function() {
+ switch ( this.transport ) {
+ case 'refresh':
+ return this.previewer.refresh();
+ case 'postMessage':
+ return this.previewer.send( 'setting', [ this.id, this() ] );
+ }
+ }
+ });
+
+ /**
+ * Utility function namespace
+ */
+ api.utils = {};
+
+ /**
+ * Watch all changes to Value properties, and bubble changes to parent Values instance
+ *
+ * @since 4.1.0
+ *
+ * @param {wp.customize.Class} instance
+ * @param {Array} properties The names of the Value instances to watch.
+ */
+ api.utils.bubbleChildValueChanges = function ( instance, properties ) {
+ $.each( properties, function ( i, key ) {
+ instance[ key ].bind( function ( to, from ) {
+ if ( instance.parent && to !== from ) {
+ instance.parent.trigger( 'change', instance );
+ }
+ } );
+ } );
+ };
+
+ /**
+ * Expand a panel, section, or control and focus on the first focusable element.
+ *
+ * @since 4.1.0
+ *
+ * @param {Object} [params]
+ * @param {Callback} [params.completeCallback]
+ */
+ focus = function ( params ) {
+ var construct, completeCallback, focus;
+ construct = this;
+ params = params || {};
+ focus = function () {
+ var focusContainer;
+ if ( construct.extended( api.Panel ) && construct.expanded() ) {
+ focusContainer = construct.container.find( '.control-panel-content:first' );
+ } else {
+ focusContainer = construct.container;
+ }
+ focusContainer.find( ':focusable:first' ).focus();
+ focusContainer[0].scrollIntoView( true );
+ };
+ if ( params.completeCallback ) {
+ completeCallback = params.completeCallback;
+ params.completeCallback = function () {
+ focus();
+ completeCallback();
+ };
+ } else {
+ params.completeCallback = focus;
+ }
+ if ( construct.expand ) {
+ construct.expand( params );
+ } else {
+ params.completeCallback();
+ }
+ };
+
+ /**
+ * Stable sort for Panels, Sections, and Controls.
+ *
+ * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
+ *
+ * @since 4.1.0
+ *
+ * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
+ * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
+ * @returns {Number}
+ */
+ api.utils.prioritySort = function ( a, b ) {
+ if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
+ return a.params.instanceNumber - b.params.instanceNumber;
+ } else {
+ return a.priority() - b.priority();
+ }
+ };
+
+ /**
+ * Return whether the supplied Event object is for a keydown event but not the Enter key.
+ *
+ * @since 4.1.0
+ *
+ * @param {jQuery.Event} event
+ * @returns {boolean}
+ */
+ api.utils.isKeydownButNotEnterEvent = function ( event ) {
+ return ( 'keydown' === event.type && 13 !== event.which );
+ };
+
+ /**
+ * Return whether the two lists of elements are the same and are in the same order.
+ *
+ * @since 4.1.0
+ *
+ * @param {Array|jQuery} listA
+ * @param {Array|jQuery} listB
+ * @returns {boolean}
+ */
+ api.utils.areElementListsEqual = function ( listA, listB ) {
+ var equal = (
+ listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
+ -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
+ _.zip( listA, listB ), // pair up each element between the two lists
+ function ( pair ) {
+ return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
+ }
+ ), false ) // check for presence of false in map's return value
+ );
+ return equal;
+ };
+
+ /**
+ * Base class for Panel and Section.
+ *
+ * @since 4.1.0
+ *
+ * @class
+ * @augments wp.customize.Class
+ */
+ Container = api.Class.extend({
+ defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
+ defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
+
+ /**
+ * @since 4.1.0
+ *
+ * @param {String} id
+ * @param {Object} options
+ */
+ initialize: function ( id, options ) {
+ var container = this;
+ container.id = id;
+ container.params = {};
+ $.extend( container, options || {} );
+ container.container = $( container.params.content );
+
+ container.deferred = {
+ embedded: new $.Deferred()
+ };
+ container.priority = new api.Value();
+ container.active = new api.Value();
+ container.activeArgumentsQueue = [];
+ container.expanded = new api.Value();
+ container.expandedArgumentsQueue = [];
+
+ container.active.bind( function ( active ) {
+ var args = container.activeArgumentsQueue.shift();
+ args = $.extend( {}, container.defaultActiveArguments, args );
+ active = ( active && container.isContextuallyActive() );
+ container.onChangeActive( active, args );
+ });
+ container.expanded.bind( function ( expanded ) {
+ var args = container.expandedArgumentsQueue.shift();
+ args = $.extend( {}, container.defaultExpandedArguments, args );
+ container.onChangeExpanded( expanded, args );
+ });
+
+ container.attachEvents();
+
+ api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
+
+ container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority );
+ container.active.set( container.params.active );
+ container.expanded.set( false );
+ },
+
+ /**
+ * @since 4.1.0
+ *
+ * @abstract
+ */
+ ready: function() {},
+
+ /**
+ * Get the child models associated with this parent, sorting them by their priority Value.
+ *
+ * @since 4.1.0
+ *
+ * @param {String} parentType
+ * @param {String} childType
+ * @returns {Array}
+ */
+ _children: function ( parentType, childType ) {
+ var parent = this,
+ children = [];
+ api[ childType ].each( function ( child ) {
+ if ( child[ parentType ].get() === parent.id ) {
+ children.push( child );
+ }
+ } );
+ children.sort( api.utils.prioritySort );
+ return children;
+ },
+
+ /**
+ * To override by subclass, to return whether the container has active children.
+ *
+ * @since 4.1.0
+ *
+ * @abstract
+ */
+ isContextuallyActive: function () {
+ throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
+ },
+
+ /**
+ * Handle changes to the active state.
+ *
+ * This does not change the active state, it merely handles the behavior
+ * for when it does change.
+ *
+ * To override by subclass, update the container's UI to reflect the provided active state.
+ *
+ * @since 4.1.0
+ *
+ * @param {Boolean} active
+ * @param {Object} args
+ * @param {Object} args.duration
+ * @param {Object} args.completeCallback
+ */
+ onChangeActive: function ( active, args ) {
+ var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
+ if ( ! $.contains( document, this.container ) ) {
+ // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
+ this.container.toggle( active );
+ if ( args.completeCallback ) {
+ args.completeCallback();
+ }
+ } else if ( active ) {
+ this.container.stop( true, true ).slideDown( duration, args.completeCallback );
+ } else {
+ this.container.stop( true, true ).slideUp( duration, args.completeCallback );
+ }
+ },
+
+ /**
+ * @since 4.1.0
+ *
+ * @params {Boolean} active
+ * @param {Object} [params]
+ * @returns {Boolean} false if state already applied
+ */
+ _toggleActive: function ( active, params ) {
+ var self = this;
+ params = params || {};
+ if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
+ params.unchanged = true;
+ self.onChangeActive( self.active.get(), params );
+ return false;
+ } else {
+ params.unchanged = false;
+ this.activeArgumentsQueue.push( params );
+ this.active.set( active );
+ return true;
+ }
+ },
+
+ /**
+ * @param {Object} [params]
+ * @returns {Boolean} false if already active
+ */
+ activate: function ( params ) {
+ return this._toggleActive( true, params );
+ },
+
+ /**
+ * @param {Object} [params]
+ * @returns {Boolean} false if already inactive
+ */
+ deactivate: function ( params ) {
+ return this._toggleActive( false, params );
+ },
+
+ /**
+ * To override by subclass, update the container's UI to reflect the provided active state.
+ * @abstract
+ */
+ onChangeExpanded: function () {
+ throw new Error( 'Must override with subclass.' );
+ },
+
+ /**
+ * @param {Boolean} expanded
+ * @param {Object} [params]
+ * @returns {Boolean} false if state already applied
+ */
+ _toggleExpanded: function ( expanded, params ) {
+ var self = this;
+ params = params || {};
+ var section = this, previousCompleteCallback = params.completeCallback;
+ params.completeCallback = function () {
+ if ( previousCompleteCallback ) {
+ previousCompleteCallback.apply( section, arguments );
+ }
+ if ( expanded ) {
+ section.container.trigger( 'expanded' );
+ } else {
+ section.container.trigger( 'collapsed' );
+ }
+ };
+ if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
+ params.unchanged = true;
+ self.onChangeExpanded( self.expanded.get(), params );
+ return false;
+ } else {
+ params.unchanged = false;
+ this.expandedArgumentsQueue.push( params );
+ this.expanded.set( expanded );
+ return true;
+ }
+ },
+
+ /**
+ * @param {Object} [params]
+ * @returns {Boolean} false if already expanded
+ */
+ expand: function ( params ) {
+ return this._toggleExpanded( true, params );
+ },
+
+ /**
+ * @param {Object} [params]
+ * @returns {Boolean} false if already collapsed
+ */
+ collapse: function ( params ) {
+ return this._toggleExpanded( false, params );
+ },
+
+ /**
+ * Bring the container into view and then expand this and bring it into view
+ * @param {Object} [params]
+ */
+ focus: focus
+ });
+
+ /**
+ * @since 4.1.0
+ *
+ * @class
+ * @augments wp.customize.Class
+ */
+ api.Section = Container.extend({
+
+ /**
+ * @since 4.1.0
+ *
+ * @param {String} id
+ * @param {Array} options
+ */
+ initialize: function ( id, options ) {
+ var section = this;
+ Container.prototype.initialize.call( section, id, options );
+
+ section.id = id;
+ section.panel = new api.Value();
+ section.panel.bind( function ( id ) {
+ $( section.container ).toggleClass( 'control-subsection', !! id );
+ });
+ section.panel.set( section.params.panel || '' );
+ api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
+
+ section.embed();
+ section.deferred.embedded.done( function () {
+ section.ready();
+ });
+ },
+
+ /**
+ * Embed the container in the DOM when any parent panel is ready.
+ *
+ * @since 4.1.0
+ */
+ embed: function () {
+ var section = this, inject;
+
+ // Watch for changes to the panel state
+ inject = function ( panelId ) {
+ var parentContainer;
+ if ( panelId ) {
+ // The panel has been supplied, so wait until the panel object is registered
+ api.panel( panelId, function ( panel ) {
+ // The panel has been registered, wait for it to become ready/initialized
+ panel.deferred.embedded.done( function () {
+ parentContainer = panel.container.find( 'ul:first' );
+ if ( ! section.container.parent().is( parentContainer ) ) {
+ parentContainer.append( section.container );
+ }
+ section.deferred.embedded.resolve();
+ });
+ } );
+ } else {
+ // There is no panel, so embed the section in the root of the customizer
+ parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
+ if ( ! section.container.parent().is( parentContainer ) ) {
+ parentContainer.append( section.container );
+ }
+ section.deferred.embedded.resolve();
+ }
+ };
+ section.panel.bind( inject );
+ inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
+ },
+
+ /**
+ * Add behaviors for the accordion section.
+ *
+ * @since 4.1.0
+ */
+ attachEvents: function () {
+ var section = this;
+
+ // Expand/Collapse accordion sections on click.
+ section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
+ if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
+ return;
+ }
+ event.preventDefault(); // Keep this AFTER the key filter above
+
+ if ( section.expanded() ) {
+ section.collapse();
+ } else {
+ section.expand();
+ }
+ });
+ },
+
+ /**
+ * Return whether this section has any active controls.
+ *
+ * @since 4.1.0
+ *
+ * @returns {Boolean}
+ */
+ isContextuallyActive: function () {
+ var section = this,
+ controls = section.controls(),
+ activeCount = 0;
+ _( controls ).each( function ( control ) {
+ if ( control.active() ) {
+ activeCount += 1;
+ }
+ } );
+ return ( activeCount !== 0 );
+ },
+
+ /**
+ * Get the controls that are associated with this section, sorted by their priority Value.
+ *
+ * @since 4.1.0
+ *
+ * @returns {Array}
+ */
+ controls: function () {
+ return this._children( 'section', 'control' );
+ },
+
+ /**
+ * Update UI to reflect expanded state.
+ *
+ * @since 4.1.0
+ *
+ * @param {Boolean} expanded
+ * @param {Object} args
+ */
+ onChangeExpanded: function ( expanded, args ) {
+ var section = this,
+ content = section.container.find( '.accordion-section-content' ),
+ expand;
+
+ if ( expanded ) {
+
+ if ( args.unchanged ) {
+ expand = args.completeCallback;
+ } else {
+ expand = function () {
+ content.stop().slideDown( args.duration, args.completeCallback );
+ section.container.addClass( 'open' );
+ };
+ }
+
+ if ( ! args.allowMultiple ) {
+ api.section.each( function ( otherSection ) {
+ if ( otherSection !== section ) {
+ otherSection.collapse( { duration: args.duration } );
+ }
+ });
+ }
+
+ if ( section.panel() ) {
+ api.panel( section.panel() ).expand({
+ duration: args.duration,
+ completeCallback: expand
+ });
+ } else {
+ expand();
+ }
+
+ } else {
+ section.container.removeClass( 'open' );
+ content.slideUp( args.duration, args.completeCallback );
+ }
+ }
+ });
+
+ /**
+ * wp.customize.ThemesSection
+ *
+ * Custom section for themes that functions similarly to a backwards panel,
+ * and also handles the theme-details view rendering and navigation.
+ *
+ * @constructor
+ * @augments wp.customize.Section
+ * @augments wp.customize.Container
+ */
+ api.ThemesSection = api.Section.extend({
+ currentTheme: '',
+ overlay: '',
+ template: '',
+ screenshotQueue: null,
+ $window: $( window ),
+
+ /**
+ * @since 4.2.0
+ */
+ initialize: function () {
+ this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
+ return api.Section.prototype.initialize.apply( this, arguments );
+ },
+
+ /**
+ * @since 4.2.0
+ */
+ ready: function () {
+ var section = this;
+ section.overlay = section.container.find( '.theme-overlay' );
+ section.template = wp.template( 'customize-themes-details-view' );
+
+ // Bind global keyboard events.
+ $( 'body' ).on( 'keyup', function( event ) {
+ if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
+ return;
+ }
+
+ // Pressing the right arrow key fires a theme:next event
+ if ( 39 === event.keyCode ) {
+ section.nextTheme();
+ }
+
+ // Pressing the left arrow key fires a theme:previous event
+ if ( 37 === event.keyCode ) {
+ section.previousTheme();
+ }
+
+ // Pressing the escape key fires a theme:collapse event
+ if ( 27 === event.keyCode ) {
+ section.closeDetails();
+ }
+ });
+
+ _.bindAll( this, 'renderScreenshots' );
+ },
+
+ /**
+ * Override Section.isContextuallyActive method.
+ *
+ * Ignore the active states' of the contained theme controls, and just
+ * use the section's own active state instead. This ensures empty search
+ * results for themes to cause the section to become inactive.
+ *
+ * @since 4.2.0
+ *
+ * @returns {Boolean}
+ */
+ isContextuallyActive: function () {
+ return this.active();
+ },
+
+ /**
+ * @since 4.2.0
+ */
+ attachEvents: function () {
+ var section = this;
+
+ // Expand/Collapse section/panel.
+ section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
+ if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
+ return;
+ }
+ event.preventDefault(); // Keep this AFTER the key filter above
+
+ if ( section.expanded() ) {
+ section.collapse();
+ } else {
+ section.expand();
+ }
+ });
+
+ // Theme navigation in details view.
+ section.container.on( 'click keydown', '.left', function( event ) {
+ if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
+ return;
+ }
+
+ event.preventDefault(); // Keep this AFTER the key filter above
+
+ section.previousTheme();
+ });
+
+ section.container.on( 'click keydown', '.right', function( event ) {
+ if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
+ return;
+ }
+
+ event.preventDefault(); // Keep this AFTER the key filter above
+
+ section.nextTheme();
+ });
+
+ section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
+ if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
+ return;
+ }
+
+ event.preventDefault(); // Keep this AFTER the key filter above
+
+ section.closeDetails();
+ });
+
+ var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
+ section.container.on( 'input', '#themes-filter', function( event ) {
+ var count,
+ term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
+ controls = section.controls();
+
+ _.each( controls, function( control ) {
+ control.filter( term );
+ });
+
+ renderScreenshots();
+
+ // Update theme count.
+ count = section.container.find( 'li.customize-control:visible' ).length;
+ section.container.find( '.theme-count' ).text( count );
+ });
+
+ // Pre-load the first 3 theme screenshots.
+ api.bind( 'ready', function () {
+ _.each( section.controls().slice( 0, 3 ), function ( control ) {
+ var img, src = control.params.theme.screenshot[0];
+ if ( src ) {
+ img = new Image();
+ img.src = src;
+ }
+ });
+ });
+ },
+
+ /**
+ * Update UI to reflect expanded state
+ *
+ * @since 4.2.0
+ *
+ * @param {Boolean} expanded
+ * @param {Object} args
+ * @param {Boolean} args.unchanged
+ * @param {Callback} args.completeCallback
+ */
+ onChangeExpanded: function ( expanded, args ) {
+
+ // Immediately call the complete callback if there were no changes
+ if ( args.unchanged ) {
+ if ( args.completeCallback ) {
+ args.completeCallback();
+ }
+ return;
+ }
+
+ // Note: there is a second argument 'args' passed
+ var position, scroll,
+ panel = this,
+ section = panel.container.closest( '.accordion-section' ),
+ overlay = section.closest( '.wp-full-overlay' ),
+ container = section.closest( '.wp-full-overlay-sidebar-content' ),
+ siblings = container.find( '.open' ),
+ topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
+ customizeBtn = section.find( '.customize-theme' ),
+ changeBtn = section.find( '.change-theme' ),
+ content = section.find( '.control-panel-content' );
+
+ if ( expanded ) {
+
+ // Collapse any sibling sections/panels
+ api.section.each( function ( otherSection ) {
+ if ( otherSection !== panel ) {
+ otherSection.collapse( { duration: args.duration } );
+ }
+ });
+ api.panel.each( function ( otherPanel ) {
+ otherPanel.collapse( { duration: 0 } );
+ });
+
+ content.show( 0, function() {
+ position = content.offset().top;
+ scroll = container.scrollTop();
+ content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
+ section.addClass( 'current-panel' );
+ overlay.addClass( 'in-themes-panel' );
+ container.scrollTop( 0 );
+ _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
+ panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
+ if ( args.completeCallback ) {
+ args.completeCallback();
+ }
+ } );
+ topPanel.attr( 'tabindex', '-1' );
+ changeBtn.attr( 'tabindex', '-1' );
+ customizeBtn.focus();
+ } else {
+ siblings.removeClass( 'open' );
+ section.removeClass( 'current-panel' );
+ overlay.removeClass( 'in-themes-panel' );
+ panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
+ content.delay( 180 ).hide( 0, function() {
+ content.css( 'margin-top', 'inherit' ); // Reset
+ if ( args.completeCallback ) {
+ args.completeCallback();
+ }
+ } );
+ topPanel.attr( 'tabindex', '0' );
+ customizeBtn.attr( 'tabindex', '0' );
+ changeBtn.focus();
+ container.scrollTop( 0 );
+ }
+ },
+
+ /**
+ * Render control's screenshot if the control comes into view.
+ *
+ * @since 4.2.0
+ */
+ renderScreenshots: function( ) {
+ var section = this;
+
+ // Fill queue initially.
+ if ( section.screenshotQueue === null ) {
+ section.screenshotQueue = section.controls();
+ }
+
+ // Are all screenshots rendered?
+ if ( ! section.screenshotQueue.length ) {
+ return;
+ }
+
+ section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
+ var $imageWrapper = control.container.find( '.theme-screenshot' ),
+ $image = $imageWrapper.find( 'img' );
+
+ if ( ! $image.length ) {
+ return false;
+ }
+
+ if ( $image.is( ':hidden' ) ) {
+ return true;
+ }
+
+ // Based on unveil.js.
+ var wt = section.$window.scrollTop(),
+ wb = wt + section.$window.height(),
+ et = $image.offset().top,
+ ih = $imageWrapper.height(),
+ eb = et + ih,
+ threshold = ih * 3,
+ inView = eb >= wt - threshold && et <= wb + threshold;
+
+ if ( inView ) {
+ control.container.trigger( 'render-screenshot' );
+ }
+
+ // If the image is in view return false so it's cleared from the queue.
+ return ! inView;
+ } );
+ },
+
+ /**
+ * Advance the modal to the next theme.
+ *
+ * @since 4.2.0
+ */
+ nextTheme: function () {
+ var section = this;
+ if ( section.getNextTheme() ) {
+ section.showDetails( section.getNextTheme(), function() {
+ section.overlay.find( '.right' ).focus();
+ } );
+ }
+ },
+
+ /**
+ * Get the next theme model.
+ *
+ * @since 4.2.0
+ */
+ getNextTheme: function () {
+ var control, next;
+ control = api.control( 'theme_' + this.currentTheme );
+ next = control.container.next( 'li.customize-control-theme' );
+ if ( ! next.length ) {
+ return false;
+ }
+ next = next[0].id.replace( 'customize-control-', '' );
+ control = api.control( next );
+
+ return control.params.theme;
+ },
+
+ /**
+ * Advance the modal to the previous theme.
+ *
+ * @since 4.2.0
+ */
+ previousTheme: function () {
+ var section = this;
+ if ( section.getPreviousTheme() ) {
+ section.showDetails( section.getPreviousTheme(), function() {
+ section.overlay.find( '.left' ).focus();
+ } );
+ }
+ },
+
+ /**
+ * Get the previous theme model.
+ *
+ * @since 4.2.0
+ */
+ getPreviousTheme: function () {
+ var control, previous;
+ control = api.control( 'theme_' + this.currentTheme );
+ previous = control.container.prev( 'li.customize-control-theme' );
+ if ( ! previous.length ) {
+ return false;
+ }
+ previous = previous[0].id.replace( 'customize-control-', '' );
+ control = api.control( previous );
+
+ return control.params.theme;
+ },
+
+ /**
+ * Disable buttons when we're viewing the first or last theme.
+ *
+ * @since 4.2.0
+ */
+ updateLimits: function () {
+ if ( ! this.getNextTheme() ) {
+ this.overlay.find( '.right' ).addClass( 'disabled' );
+ }
+ if ( ! this.getPreviousTheme() ) {
+ this.overlay.find( '.left' ).addClass( 'disabled' );
+ }
+ },
+
+ /**
+ * Render & show the theme details for a given theme model.
+ *
+ * @since 4.2.0
+ *
+ * @param {Object} theme
+ */
+ showDetails: function ( theme, callback ) {
+ var section = this;
+ callback = callback || function(){};
+ section.currentTheme = theme.id;
+ section.overlay.html( section.template( theme ) )
+ .fadeIn( 'fast' )
+ .focus();
+ $( 'body' ).addClass( 'modal-open' );
+ section.containFocus( section.overlay );
+ section.updateLimits();
+ callback();
+ },
+
+ /**
+ * Close the theme details modal.
+ *
+ * @since 4.2.0
+ */
+ closeDetails: function () {
+ $( 'body' ).removeClass( 'modal-open' );
+ this.overlay.fadeOut( 'fast' );
+ api.control( 'theme_' + this.currentTheme ).focus();
+ },
+
+ /**
+ * Keep tab focus within the theme details modal.
+ *
+ * @since 4.2.0
+ */
+ containFocus: function( el ) {
+ var tabbables;
+
+ el.on( 'keydown', function( event ) {
+
+ // Return if it's not the tab key
+ // When navigating with prev/next focus is already handled
+ if ( 9 !== event.keyCode ) {
+ return;
+ }
+
+ // uses jQuery UI to get the tabbable elements
+ tabbables = $( ':tabbable', el );
+
+ // Keep focus within the overlay
+ if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
+ tabbables.first().focus();
+ return false;
+ } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
+ tabbables.last().focus();
+ return false;
+ }
+ });
+ }
+ });
+
+ /**
+ * @since 4.1.0
+ *
+ * @class
+ * @augments wp.customize.Class