home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-admin / js / customize-controls.js < prev    next >
Encoding:
JavaScript  |  2018-01-30  |  282.2 KB  |  9,298 lines

  1. /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console, confirm */
  2. (function( exports, $ ){
  3.     var Container, focus, normalizedTransitionendEventName, api = wp.customize;
  4.  
  5.     /**
  6.      * A notification that is displayed in a full-screen overlay.
  7.      *
  8.      * @since 4.9.0
  9.      * @class
  10.      * @augments wp.customize.Notification
  11.      */
  12.     api.OverlayNotification = api.Notification.extend({
  13.  
  14.         /**
  15.          * Whether the notification should show a loading spinner.
  16.          *
  17.          * @since 4.9.0
  18.          * @var {boolean}
  19.          */
  20.         loading: false,
  21.  
  22.         /**
  23.          * Initialize.
  24.          *
  25.          * @since 4.9.0
  26.          *
  27.          * @param {string} code - Code.
  28.          * @param {object} params - Params.
  29.          */
  30.         initialize: function( code, params ) {
  31.             var notification = this;
  32.             api.Notification.prototype.initialize.call( notification, code, params );
  33.             notification.containerClasses += ' notification-overlay';
  34.             if ( notification.loading ) {
  35.                 notification.containerClasses += ' notification-loading';
  36.             }
  37.         },
  38.  
  39.         /**
  40.          * Render notification.
  41.          *
  42.          * @since 4.9.0
  43.          *
  44.          * @return {jQuery} Notification container.
  45.          */
  46.         render: function() {
  47.             var li = api.Notification.prototype.render.call( this );
  48.             li.on( 'keydown', _.bind( this.handleEscape, this ) );
  49.             return li;
  50.         },
  51.  
  52.         /**
  53.          * Stop propagation on escape key presses, but also dismiss notification if it is dismissible.
  54.          *
  55.          * @since 4.9.0
  56.          *
  57.          * @param {jQuery.Event} event - Event.
  58.          * @returns {void}
  59.          */
  60.         handleEscape: function( event ) {
  61.             var notification = this;
  62.             if ( 27 === event.which ) {
  63.                 event.stopPropagation();
  64.                 if ( notification.dismissible && notification.parent ) {
  65.                     notification.parent.remove( notification.code );
  66.                 }
  67.             }
  68.         }
  69.     });
  70.  
  71.     /**
  72.      * A collection of observable notifications.
  73.      *
  74.      * @since 4.9.0
  75.      * @class
  76.      * @augments wp.customize.Values
  77.      */
  78.     api.Notifications = api.Values.extend({
  79.  
  80.         /**
  81.          * Whether the alternative style should be used.
  82.          *
  83.          * @since 4.9.0
  84.          * @type {boolean}
  85.          */
  86.         alt: false,
  87.  
  88.         /**
  89.          * The default constructor for items of the collection.
  90.          *
  91.          * @since 4.9.0
  92.          * @type {object}
  93.          */
  94.         defaultConstructor: api.Notification,
  95.  
  96.         /**
  97.          * Initialize notifications area.
  98.          *
  99.          * @since 4.9.0
  100.          * @constructor
  101.          * @param {object}  options - Options.
  102.          * @param {jQuery}  [options.container] - Container element for notifications. This can be injected later.
  103.          * @param {boolean} [options.alt] - Whether alternative style should be used when rendering notifications.
  104.          * @returns {void}
  105.          * @this {wp.customize.Notifications}
  106.          */
  107.         initialize: function( options ) {
  108.             var collection = this;
  109.  
  110.             api.Values.prototype.initialize.call( collection, options );
  111.  
  112.             _.bindAll( collection, 'constrainFocus' );
  113.  
  114.             // Keep track of the order in which the notifications were added for sorting purposes.
  115.             collection._addedIncrement = 0;
  116.             collection._addedOrder = {};
  117.  
  118.             // Trigger change event when notification is added or removed.
  119.             collection.bind( 'add', function( notification ) {
  120.                 collection.trigger( 'change', notification );
  121.             });
  122.             collection.bind( 'removed', function( notification ) {
  123.                 collection.trigger( 'change', notification );
  124.             });
  125.         },
  126.  
  127.         /**
  128.          * Get the number of notifications added.
  129.          *
  130.          * @since 4.9.0
  131.          * @return {number} Count of notifications.
  132.          */
  133.         count: function() {
  134.             return _.size( this._value );
  135.         },
  136.  
  137.         /**
  138.          * Add notification to the collection.
  139.          *
  140.          * @since 4.9.0
  141.          *
  142.          * @param {string|wp.customize.Notification} notification - Notification object to add. Alternatively code may be supplied, and in that case the second notificationObject argument must be supplied.
  143.          * @param {wp.customize.Notification} [notificationObject] - Notification to add when first argument is the code string.
  144.          * @returns {wp.customize.Notification} Added notification (or existing instance if it was already added).
  145.          */
  146.         add: function( notification, notificationObject ) {
  147.             var collection = this, code, instance;
  148.             if ( 'string' === typeof notification ) {
  149.                 code = notification;
  150.                 instance = notificationObject;
  151.             } else {
  152.                 code = notification.code;
  153.                 instance = notification;
  154.             }
  155.             if ( ! collection.has( code ) ) {
  156.                 collection._addedIncrement += 1;
  157.                 collection._addedOrder[ code ] = collection._addedIncrement;
  158.             }
  159.             return api.Values.prototype.add.call( collection, code, instance );
  160.         },
  161.  
  162.         /**
  163.          * Add notification to the collection.
  164.          *
  165.          * @since 4.9.0
  166.          * @param {string} code - Notification code to remove.
  167.          * @return {api.Notification} Added instance (or existing instance if it was already added).
  168.          */
  169.         remove: function( code ) {
  170.             var collection = this;
  171.             delete collection._addedOrder[ code ];
  172.             return api.Values.prototype.remove.call( this, code );
  173.         },
  174.  
  175.         /**
  176.          * Get list of notifications.
  177.          *
  178.          * Notifications may be sorted by type followed by added time.
  179.          *
  180.          * @since 4.9.0
  181.          * @param {object}  args - Args.
  182.          * @param {boolean} [args.sort=false] - Whether to return the notifications sorted.
  183.          * @return {Array.<wp.customize.Notification>} Notifications.
  184.          * @this {wp.customize.Notifications}
  185.          */
  186.         get: function( args ) {
  187.             var collection = this, notifications, errorTypePriorities, params;
  188.             notifications = _.values( collection._value );
  189.  
  190.             params = _.extend(
  191.                 { sort: false },
  192.                 args
  193.             );
  194.  
  195.             if ( params.sort ) {
  196.                 errorTypePriorities = { error: 4, warning: 3, success: 2, info: 1 };
  197.                 notifications.sort( function( a, b ) {
  198.                     var aPriority = 0, bPriority = 0;
  199.                     if ( ! _.isUndefined( errorTypePriorities[ a.type ] ) ) {
  200.                         aPriority = errorTypePriorities[ a.type ];
  201.                     }
  202.                     if ( ! _.isUndefined( errorTypePriorities[ b.type ] ) ) {
  203.                         bPriority = errorTypePriorities[ b.type ];
  204.                     }
  205.                     if ( aPriority !== bPriority ) {
  206.                         return bPriority - aPriority; // Show errors first.
  207.                     }
  208.                     return collection._addedOrder[ b.code ] - collection._addedOrder[ a.code ]; // Show newer notifications higher.
  209.                 });
  210.             }
  211.  
  212.             return notifications;
  213.         },
  214.  
  215.         /**
  216.          * Render notifications area.
  217.          *
  218.          * @since 4.9.0
  219.          * @returns {void}
  220.          * @this {wp.customize.Notifications}
  221.          */
  222.         render: function() {
  223.             var collection = this,
  224.                 notifications, hadOverlayNotification = false, hasOverlayNotification, overlayNotifications = [],
  225.                 previousNotificationsByCode = {},
  226.                 listElement, focusableElements;
  227.  
  228.             // Short-circuit if there are no container to render into.
  229.             if ( ! collection.container || ! collection.container.length ) {
  230.                 return;
  231.             }
  232.  
  233.             notifications = collection.get( { sort: true } );
  234.             collection.container.toggle( 0 !== notifications.length );
  235.  
  236.             // Short-circuit if there are no changes to the notifications.
  237.             if ( collection.container.is( collection.previousContainer ) && _.isEqual( notifications, collection.previousNotifications ) ) {
  238.                 return;
  239.             }
  240.  
  241.             // Make sure list is part of the container.
  242.             listElement = collection.container.children( 'ul' ).first();
  243.             if ( ! listElement.length ) {
  244.                 listElement = $( '<ul></ul>' );
  245.                 collection.container.append( listElement );
  246.             }
  247.  
  248.             // Remove all notifications prior to re-rendering.
  249.             listElement.find( '> [data-code]' ).remove();
  250.  
  251.             _.each( collection.previousNotifications, function( notification ) {
  252.                 previousNotificationsByCode[ notification.code ] = notification;
  253.             });
  254.  
  255.             // Add all notifications in the sorted order.
  256.             _.each( notifications, function( notification ) {
  257.                 var notificationContainer;
  258.                 if ( wp.a11y && ( ! previousNotificationsByCode[ notification.code ] || ! _.isEqual( notification.message, previousNotificationsByCode[ notification.code ].message ) ) ) {
  259.                     wp.a11y.speak( notification.message, 'assertive' );
  260.                 }
  261.                 notificationContainer = $( notification.render() );
  262.                 notification.container = notificationContainer;
  263.                 listElement.append( notificationContainer ); // @todo Consider slideDown() as enhancement.
  264.  
  265.                 if ( notification.extended( api.OverlayNotification ) ) {
  266.                     overlayNotifications.push( notification );
  267.                 }
  268.             });
  269.             hasOverlayNotification = Boolean( overlayNotifications.length );
  270.  
  271.             if ( collection.previousNotifications ) {
  272.                 hadOverlayNotification = Boolean( _.find( collection.previousNotifications, function( notification ) {
  273.                     return notification.extended( api.OverlayNotification );
  274.                 } ) );
  275.             }
  276.  
  277.             if ( hasOverlayNotification !== hadOverlayNotification ) {
  278.                 $( document.body ).toggleClass( 'customize-loading', hasOverlayNotification );
  279.                 collection.container.toggleClass( 'has-overlay-notifications', hasOverlayNotification );
  280.                 if ( hasOverlayNotification ) {
  281.                     collection.previousActiveElement = document.activeElement;
  282.                     $( document ).on( 'keydown', collection.constrainFocus );
  283.                 } else {
  284.                     $( document ).off( 'keydown', collection.constrainFocus );
  285.                 }
  286.             }
  287.  
  288.             if ( hasOverlayNotification ) {
  289.                 collection.focusContainer = overlayNotifications[ overlayNotifications.length - 1 ].container;
  290.                 collection.focusContainer.prop( 'tabIndex', -1 );
  291.                 focusableElements = collection.focusContainer.find( ':focusable' );
  292.                 if ( focusableElements.length ) {
  293.                     focusableElements.first().focus();
  294.                 } else {
  295.                     collection.focusContainer.focus();
  296.                 }
  297.             } else if ( collection.previousActiveElement ) {
  298.                 $( collection.previousActiveElement ).focus();
  299.                 collection.previousActiveElement = null;
  300.             }
  301.  
  302.             collection.previousNotifications = notifications;
  303.             collection.previousContainer = collection.container;
  304.             collection.trigger( 'rendered' );
  305.         },
  306.  
  307.         /**
  308.          * Constrain focus on focus container.
  309.          *
  310.          * @since 4.9.0
  311.          *
  312.          * @param {jQuery.Event} event - Event.
  313.          * @returns {void}
  314.          */
  315.         constrainFocus: function constrainFocus( event ) {
  316.             var collection = this, focusableElements;
  317.  
  318.             // Prevent keys from escaping.
  319.             event.stopPropagation();
  320.  
  321.             if ( 9 !== event.which ) { // Tab key.
  322.                 return;
  323.             }
  324.  
  325.             focusableElements = collection.focusContainer.find( ':focusable' );
  326.             if ( 0 === focusableElements.length ) {
  327.                 focusableElements = collection.focusContainer;
  328.             }
  329.  
  330.             if ( ! $.contains( collection.focusContainer[0], event.target ) || ! $.contains( collection.focusContainer[0], document.activeElement ) ) {
  331.                 event.preventDefault();
  332.                 focusableElements.first().focus();
  333.             } else if ( focusableElements.last().is( event.target ) && ! event.shiftKey ) {
  334.                 event.preventDefault();
  335.                 focusableElements.first().focus();
  336.             } else if ( focusableElements.first().is( event.target ) && event.shiftKey ) {
  337.                 event.preventDefault();
  338.                 focusableElements.last().focus();
  339.             }
  340.         }
  341.     });
  342.  
  343.     /**
  344.      * A Customizer Setting.
  345.      *
  346.      * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
  347.      * draft changes to in the Customizer.
  348.      *
  349.      * @see PHP class WP_Customize_Setting.
  350.      *
  351.      * @since 3.4.0
  352.      * @class
  353.      * @augments wp.customize.Value
  354.      * @augments wp.customize.Class
  355.      */
  356.     api.Setting = api.Value.extend({
  357.  
  358.         /**
  359.          * Default params.
  360.          *
  361.          * @since 4.9.0
  362.          * @var {object}
  363.          */
  364.         defaults: {
  365.             transport: 'refresh',
  366.             dirty: false
  367.         },
  368.  
  369.         /**
  370.          * Initialize.
  371.          *
  372.          * @since 3.4.0
  373.          *
  374.          * @param {string}  id                          - The setting ID.
  375.          * @param {*}       value                       - The initial value of the setting.
  376.          * @param {object}  [options={}]                - Options.
  377.          * @param {string}  [options.transport=refresh] - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
  378.          * @param {boolean} [options.dirty=false]       - Whether the setting should be considered initially dirty.
  379.          * @param {object}  [options.previewer]         - The Previewer instance to sync with. Defaults to wp.customize.previewer.
  380.          */
  381.         initialize: function( id, value, options ) {
  382.             var setting = this, params;
  383.             params = _.extend(
  384.                 { previewer: api.previewer },
  385.                 setting.defaults,
  386.                 options || {}
  387.             );
  388.  
  389.             api.Value.prototype.initialize.call( setting, value, params );
  390.  
  391.             setting.id = id;
  392.             setting._dirty = params.dirty; // The _dirty property is what the Customizer reads from.
  393.             setting.notifications = new api.Notifications();
  394.  
  395.             // Whenever the setting's value changes, refresh the preview.
  396.             setting.bind( setting.preview );
  397.         },
  398.  
  399.         /**
  400.          * Refresh the preview, respective of the setting's refresh policy.
  401.          *
  402.          * If the preview hasn't sent a keep-alive message and is likely
  403.          * disconnected by having navigated to a non-allowed URL, then the
  404.          * refresh transport will be forced when postMessage is the transport.
  405.          * Note that postMessage does not throw an error when the recipient window
  406.          * fails to match the origin window, so using try/catch around the
  407.          * previewer.send() call to then fallback to refresh will not work.
  408.          *
  409.          * @since 3.4.0
  410.          * @access public
  411.          *
  412.          * @returns {void}
  413.          */
  414.         preview: function() {
  415.             var setting = this, transport;
  416.             transport = setting.transport;
  417.  
  418.             if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) {
  419.                 transport = 'refresh';
  420.             }
  421.  
  422.             if ( 'postMessage' === transport ) {
  423.                 setting.previewer.send( 'setting', [ setting.id, setting() ] );
  424.             } else if ( 'refresh' === transport ) {
  425.                 setting.previewer.refresh();
  426.             }
  427.         },
  428.  
  429.         /**
  430.          * Find controls associated with this setting.
  431.          *
  432.          * @since 4.6.0
  433.          * @returns {wp.customize.Control[]} Controls associated with setting.
  434.          */
  435.         findControls: function() {
  436.             var setting = this, controls = [];
  437.             api.control.each( function( control ) {
  438.                 _.each( control.settings, function( controlSetting ) {
  439.                     if ( controlSetting.id === setting.id ) {
  440.                         controls.push( control );
  441.                     }
  442.                 } );
  443.             } );
  444.             return controls;
  445.         }
  446.     });
  447.  
  448.     /**
  449.      * Current change count.
  450.      *
  451.      * @since 4.7.0
  452.      * @type {number}
  453.      * @protected
  454.      */
  455.     api._latestRevision = 0;
  456.  
  457.     /**
  458.      * Last revision that was saved.
  459.      *
  460.      * @since 4.7.0
  461.      * @type {number}
  462.      * @protected
  463.      */
  464.     api._lastSavedRevision = 0;
  465.  
  466.     /**
  467.      * Latest revisions associated with the updated setting.
  468.      *
  469.      * @since 4.7.0
  470.      * @type {object}
  471.      * @protected
  472.      */
  473.     api._latestSettingRevisions = {};
  474.  
  475.     /*
  476.      * Keep track of the revision associated with each updated setting so that
  477.      * requestChangesetUpdate knows which dirty settings to include. Also, once
  478.      * ready is triggered and all initial settings have been added, increment
  479.      * revision for each newly-created initially-dirty setting so that it will
  480.      * also be included in changeset update requests.
  481.      */
  482.     api.bind( 'change', function incrementChangedSettingRevision( setting ) {
  483.         api._latestRevision += 1;
  484.         api._latestSettingRevisions[ setting.id ] = api._latestRevision;
  485.     } );
  486.     api.bind( 'ready', function() {
  487.         api.bind( 'add', function incrementCreatedSettingRevision( setting ) {
  488.             if ( setting._dirty ) {
  489.                 api._latestRevision += 1;
  490.                 api._latestSettingRevisions[ setting.id ] = api._latestRevision;
  491.             }
  492.         } );
  493.     } );
  494.  
  495.     /**
  496.      * Get the dirty setting values.
  497.      *
  498.      * @since 4.7.0
  499.      * @access public
  500.      *
  501.      * @param {object} [options] Options.
  502.      * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes).
  503.      * @returns {object} Dirty setting values.
  504.      */
  505.     api.dirtyValues = function dirtyValues( options ) {
  506.         var values = {};
  507.         api.each( function( setting ) {
  508.             var settingRevision;
  509.  
  510.             if ( ! setting._dirty ) {
  511.                 return;
  512.             }
  513.  
  514.             settingRevision = api._latestSettingRevisions[ setting.id ];
  515.  
  516.             // Skip including settings that have already been included in the changeset, if only requesting unsaved.
  517.             if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) {
  518.                 return;
  519.             }
  520.  
  521.             values[ setting.id ] = setting.get();
  522.         } );
  523.         return values;
  524.     };
  525.  
  526.     /**
  527.      * Request updates to the changeset.
  528.      *
  529.      * @since 4.7.0
  530.      * @access public
  531.      *
  532.      * @param {object}  [changes] - Mapping of setting IDs to setting params each normally including a value property, or mapping to null.
  533.      *                             If not provided, then the changes will still be obtained from unsaved dirty settings.
  534.      * @param {object}  [args] - Additional options for the save request.
  535.      * @param {boolean} [args.autosave=false] - Whether changes will be stored in autosave revision if the changeset has been promoted from an auto-draft.
  536.      * @param {boolean} [args.force=false] - Send request to update even when there are no changes to submit. This can be used to request the latest status of the changeset on the server.
  537.      * @param {string}  [args.title] - Title to update in the changeset. Optional.
  538.      * @param {string}  [args.date] - Date to update in the changeset. Optional.
  539.      * @returns {jQuery.Promise} Promise resolving with the response data.
  540.      */
  541.     api.requestChangesetUpdate = function requestChangesetUpdate( changes, args ) {
  542.         var deferred, request, submittedChanges = {}, data, submittedArgs;
  543.         deferred = new $.Deferred();
  544.  
  545.         // Prevent attempting changeset update while request is being made.
  546.         if ( 0 !== api.state( 'processing' ).get() ) {
  547.             deferred.reject( 'already_processing' );
  548.             return deferred.promise();
  549.         }
  550.  
  551.         submittedArgs = _.extend( {
  552.             title: null,
  553.             date: null,
  554.             autosave: false,
  555.             force: false
  556.         }, args );
  557.  
  558.         if ( changes ) {
  559.             _.extend( submittedChanges, changes );
  560.         }
  561.  
  562.         // Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes.
  563.         _.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) {
  564.             if ( ! changes || null !== changes[ settingId ] ) {
  565.                 submittedChanges[ settingId ] = _.extend(
  566.                     {},
  567.                     submittedChanges[ settingId ] || {},
  568.                     { value: dirtyValue }
  569.                 );
  570.             }
  571.         } );
  572.  
  573.         // Allow plugins to attach additional params to the settings.
  574.         api.trigger( 'changeset-save', submittedChanges, submittedArgs );
  575.  
  576.         // Short-circuit when there are no pending changes.
  577.         if ( ! submittedArgs.force && _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) {
  578.             deferred.resolve( {} );
  579.             return deferred.promise();
  580.         }
  581.  
  582.         // A status would cause a revision to be made, and for this wp.customize.previewer.save() should be used. Status is also disallowed for revisions regardless.
  583.         if ( submittedArgs.status ) {
  584.             return deferred.reject( { code: 'illegal_status_in_changeset_update' } ).promise();
  585.         }
  586.  
  587.         // Dates not beung allowed for revisions are is a technical limitation of post revisions.
  588.         if ( submittedArgs.date && submittedArgs.autosave ) {
  589.             return deferred.reject( { code: 'illegal_autosave_with_date_gmt' } ).promise();
  590.         }
  591.  
  592.         // Make sure that publishing a changeset waits for all changeset update requests to complete.
  593.         api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
  594.         deferred.always( function() {
  595.             api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
  596.         } );
  597.  
  598.         // Ensure that if any plugins add data to save requests by extending query() that they get included here.
  599.         data = api.previewer.query( { excludeCustomizedSaved: true } );
  600.         delete data.customized; // Being sent in customize_changeset_data instead.
  601.         _.extend( data, {
  602.             nonce: api.settings.nonce.save,
  603.             customize_theme: api.settings.theme.stylesheet,
  604.             customize_changeset_data: JSON.stringify( submittedChanges )
  605.         } );
  606.         if ( null !== submittedArgs.title ) {
  607.             data.customize_changeset_title = submittedArgs.title;
  608.         }
  609.         if ( null !== submittedArgs.date ) {
  610.             data.customize_changeset_date = submittedArgs.date;
  611.         }
  612.         if ( false !== submittedArgs.autosave ) {
  613.             data.customize_changeset_autosave = 'true';
  614.         }
  615.  
  616.         // Allow plugins to modify the params included with the save request.
  617.         api.trigger( 'save-request-params', data );
  618.  
  619.         request = wp.ajax.post( 'customize_save', data );
  620.  
  621.         request.done( function requestChangesetUpdateDone( data ) {
  622.             var savedChangesetValues = {};
  623.  
  624.             // Ensure that all settings updated subsequently will be included in the next changeset update request.
  625.             api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision );
  626.  
  627.             api.state( 'changesetStatus' ).set( data.changeset_status );
  628.  
  629.             if ( data.changeset_date ) {
  630.                 api.state( 'changesetDate' ).set( data.changeset_date );
  631.             }
  632.  
  633.             deferred.resolve( data );
  634.             api.trigger( 'changeset-saved', data );
  635.  
  636.             if ( data.setting_validities ) {
  637.                 _.each( data.setting_validities, function( validity, settingId ) {
  638.                     if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) {
  639.                         savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value;
  640.                     }
  641.                 } );
  642.             }
  643.  
  644.             api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) );
  645.         } );
  646.         request.fail( function requestChangesetUpdateFail( data ) {
  647.             deferred.reject( data );
  648.             api.trigger( 'changeset-error', data );
  649.         } );
  650.         request.always( function( data ) {
  651.             if ( data.setting_validities ) {
  652.                 api._handleSettingValidities( {
  653.                     settingValidities: data.setting_validities
  654.                 } );
  655.             }
  656.         } );
  657.  
  658.         return deferred.promise();
  659.     };
  660.  
  661.     /**
  662.      * Watch all changes to Value properties, and bubble changes to parent Values instance
  663.      *
  664.      * @since 4.1.0
  665.      *
  666.      * @param {wp.customize.Class} instance
  667.      * @param {Array}              properties  The names of the Value instances to watch.
  668.      */
  669.     api.utils.bubbleChildValueChanges = function ( instance, properties ) {
  670.         $.each( properties, function ( i, key ) {
  671.             instance[ key ].bind( function ( to, from ) {
  672.                 if ( instance.parent && to !== from ) {
  673.                     instance.parent.trigger( 'change', instance );
  674.                 }
  675.             } );
  676.         } );
  677.     };
  678.  
  679.     /**
  680.      * Expand a panel, section, or control and focus on the first focusable element.
  681.      *
  682.      * @since 4.1.0
  683.      *
  684.      * @param {Object}   [params]
  685.      * @param {Function} [params.completeCallback]
  686.      */
  687.     focus = function ( params ) {
  688.         var construct, completeCallback, focus, focusElement;
  689.         construct = this;
  690.         params = params || {};
  691.         focus = function () {
  692.             var focusContainer;
  693.             if ( ( construct.extended( api.Panel ) || construct.extended( api.Section ) ) && construct.expanded && construct.expanded() ) {
  694.                 focusContainer = construct.contentContainer;
  695.             } else {
  696.                 focusContainer = construct.container;
  697.             }
  698.  
  699.             focusElement = focusContainer.find( '.control-focus:first' );
  700.             if ( 0 === focusElement.length ) {
  701.                 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
  702.                 focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
  703.             }
  704.             focusElement.focus();
  705.         };
  706.         if ( params.completeCallback ) {
  707.             completeCallback = params.completeCallback;
  708.             params.completeCallback = function () {
  709.                 focus();
  710.                 completeCallback();
  711.             };
  712.         } else {
  713.             params.completeCallback = focus;
  714.         }
  715.  
  716.         api.state( 'paneVisible' ).set( true );
  717.         if ( construct.expand ) {
  718.             construct.expand( params );
  719.         } else {
  720.             params.completeCallback();
  721.         }
  722.     };
  723.  
  724.     /**
  725.      * Stable sort for Panels, Sections, and Controls.
  726.      *
  727.      * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
  728.      *
  729.      * @since 4.1.0
  730.      *
  731.      * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
  732.      * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
  733.      * @returns {Number}
  734.      */
  735.     api.utils.prioritySort = function ( a, b ) {
  736.         if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
  737.             return a.params.instanceNumber - b.params.instanceNumber;
  738.         } else {
  739.             return a.priority() - b.priority();
  740.         }
  741.     };
  742.  
  743.     /**
  744.      * Return whether the supplied Event object is for a keydown event but not the Enter key.
  745.      *
  746.      * @since 4.1.0
  747.      *
  748.      * @param {jQuery.Event} event
  749.      * @returns {boolean}
  750.      */
  751.     api.utils.isKeydownButNotEnterEvent = function ( event ) {
  752.         return ( 'keydown' === event.type && 13 !== event.which );
  753.     };
  754.  
  755.     /**
  756.      * Return whether the two lists of elements are the same and are in the same order.
  757.      *
  758.      * @since 4.1.0
  759.      *
  760.      * @param {Array|jQuery} listA
  761.      * @param {Array|jQuery} listB
  762.      * @returns {boolean}
  763.      */
  764.     api.utils.areElementListsEqual = function ( listA, listB ) {
  765.         var equal = (
  766.             listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
  767.             -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
  768.                 _.zip( listA, listB ), // pair up each element between the two lists
  769.                 function ( pair ) {
  770.                     return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
  771.                 }
  772.             ), false ) // check for presence of false in map's return value
  773.         );
  774.         return equal;
  775.     };
  776.  
  777.     /**
  778.      * Highlight the existence of a button.
  779.      *
  780.      * This function reminds the user of a button represented by the specified
  781.      * UI element, after an optional delay. If the user focuses the element
  782.      * before the delay passes, the reminder is canceled.
  783.      *
  784.      * @since 4.9.0
  785.      *
  786.      * @param {jQuery} button - The element to highlight.
  787.      * @param {object} [options] - Options.
  788.      * @param {number} [options.delay=0] - Delay in milliseconds.
  789.      * @param {jQuery} [options.focusTarget] - A target for user focus that defaults to the highlighted element.
  790.      *                                         If the user focuses the target before the delay passes, the reminder
  791.      *                                         is canceled. This option exists to accommodate compound buttons
  792.      *                                         containing auxiliary UI, such as the Publish button augmented with a
  793.      *                                         Settings button.
  794.      * @returns {Function} An idempotent function that cancels the reminder.
  795.      */
  796.     api.utils.highlightButton = function highlightButton( button, options ) {
  797.         var animationClass = 'button-see-me',
  798.             canceled = false,
  799.             params;
  800.  
  801.         params = _.extend(
  802.             {
  803.                 delay: 0,
  804.                 focusTarget: button
  805.             },
  806.             options
  807.         );
  808.  
  809.         function cancelReminder() {
  810.             canceled = true;
  811.         }
  812.  
  813.         params.focusTarget.on( 'focusin', cancelReminder );
  814.         setTimeout( function() {
  815.             params.focusTarget.off( 'focusin', cancelReminder );
  816.  
  817.             if ( ! canceled ) {
  818.                 button.addClass( animationClass );
  819.                 button.one( 'animationend', function() {
  820.                     /*
  821.                      * Remove animation class to avoid situations in Customizer where
  822.                      * DOM nodes are moved (re-inserted) and the animation repeats.
  823.                      */
  824.                     button.removeClass( animationClass );
  825.                 } );
  826.             }
  827.         }, params.delay );
  828.  
  829.         return cancelReminder;
  830.     };
  831.  
  832.     /**
  833.      * Get current timestamp adjusted for server clock time.
  834.      *
  835.      * Same functionality as the `current_time( 'mysql', false )` function in PHP.
  836.      *
  837.      * @since 4.9.0
  838.      *
  839.      * @returns {int} Current timestamp.
  840.      */
  841.     api.utils.getCurrentTimestamp = function getCurrentTimestamp() {
  842.         var currentDate, currentClientTimestamp, timestampDifferential;
  843.         currentClientTimestamp = _.now();
  844.         currentDate = new Date( api.settings.initialServerDate.replace( /-/g, '/' ) );
  845.         timestampDifferential = currentClientTimestamp - api.settings.initialClientTimestamp;
  846.         timestampDifferential += api.settings.initialClientTimestamp - api.settings.initialServerTimestamp;
  847.         currentDate.setTime( currentDate.getTime() + timestampDifferential );
  848.         return currentDate.getTime();
  849.     };
  850.  
  851.     /**
  852.      * Get remaining time of when the date is set.
  853.      *
  854.      * @since 4.9.0
  855.      *
  856.      * @param {string|int|Date} datetime - Date time or timestamp of the future date.
  857.      * @return {int} remainingTime - Remaining time in milliseconds.
  858.      */
  859.     api.utils.getRemainingTime = function getRemainingTime( datetime ) {
  860.         var millisecondsDivider = 1000, remainingTime, timestamp;
  861.         if ( datetime instanceof Date ) {
  862.             timestamp = datetime.getTime();
  863.         } else if ( 'string' === typeof datetime ) {
  864.             timestamp = ( new Date( datetime.replace( /-/g, '/' ) ) ).getTime();
  865.         } else {
  866.             timestamp = datetime;
  867.         }
  868.  
  869.         remainingTime = timestamp - api.utils.getCurrentTimestamp();
  870.         remainingTime = Math.ceil( remainingTime / millisecondsDivider );
  871.         return remainingTime;
  872.     };
  873.  
  874.     /**
  875.      * Return browser supported `transitionend` event name.
  876.      *
  877.      * @since 4.7.0
  878.      *
  879.      * @returns {string|null} Normalized `transitionend` event name or null if CSS transitions are not supported.
  880.      */
  881.     normalizedTransitionendEventName = (function () {
  882.         var el, transitions, prop;
  883.         el = document.createElement( 'div' );
  884.         transitions = {
  885.             'transition'      : 'transitionend',
  886.             'OTransition'     : 'oTransitionEnd',
  887.             'MozTransition'   : 'transitionend',
  888.             'WebkitTransition': 'webkitTransitionEnd'
  889.         };
  890.         prop = _.find( _.keys( transitions ), function( prop ) {
  891.             return ! _.isUndefined( el.style[ prop ] );
  892.         } );
  893.         if ( prop ) {
  894.             return transitions[ prop ];
  895.         } else {
  896.             return null;
  897.         }
  898.     })();
  899.  
  900.     /**
  901.      * Base class for Panel and Section.
  902.      *
  903.      * @since 4.1.0
  904.      *
  905.      * @class
  906.      * @augments wp.customize.Class
  907.      */
  908.     Container = api.Class.extend({
  909.         defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
  910.         defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
  911.         containerType: 'container',
  912.         defaults: {
  913.             title: '',
  914.             description: '',
  915.             priority: 100,
  916.             type: 'default',
  917.             content: null,
  918.             active: true,
  919.             instanceNumber: null
  920.         },
  921.  
  922.         /**
  923.          * @since 4.1.0
  924.          *
  925.          * @param {string}         id - The ID for the container.
  926.          * @param {object}         options - Object containing one property: params.
  927.          * @param {string}         options.title - Title shown when panel is collapsed and expanded.
  928.          * @param {string=}        [options.description] - Description shown at the top of the panel.
  929.          * @param {number=100}     [options.priority] - The sort priority for the panel.
  930.          * @param {string}         [options.templateId] - Template selector for container.
  931.          * @param {string=default} [options.type] - The type of the panel. See wp.customize.panelConstructor.
  932.          * @param {string=}        [options.content] - The markup to be used for the panel container. If empty, a JS template is used.
  933.          * @param {boolean=true}   [options.active] - Whether the panel is active or not.
  934.          * @param {object}         [options.params] - Deprecated wrapper for the above properties.
  935.          */
  936.         initialize: function ( id, options ) {
  937.             var container = this;
  938.             container.id = id;
  939.  
  940.             if ( ! Container.instanceCounter ) {
  941.                 Container.instanceCounter = 0;
  942.             }
  943.             Container.instanceCounter++;
  944.  
  945.             $.extend( container, {
  946.                 params: _.defaults(
  947.                     options.params || options, // Passing the params is deprecated.
  948.                     container.defaults
  949.                 )
  950.             } );
  951.             if ( ! container.params.instanceNumber ) {
  952.                 container.params.instanceNumber = Container.instanceCounter;
  953.             }
  954.             container.notifications = new api.Notifications();
  955.             container.templateSelector = container.params.templateId || 'customize-' + container.containerType + '-' + container.params.type;
  956.             container.container = $( container.params.content );
  957.             if ( 0 === container.container.length ) {
  958.                 container.container = $( container.getContainer() );
  959.             }
  960.             container.headContainer = container.container;
  961.             container.contentContainer = container.getContent();
  962.             container.container = container.container.add( container.contentContainer );
  963.  
  964.             container.deferred = {
  965.                 embedded: new $.Deferred()
  966.             };
  967.             container.priority = new api.Value();
  968.             container.active = new api.Value();
  969.             container.activeArgumentsQueue = [];
  970.             container.expanded = new api.Value();
  971.             container.expandedArgumentsQueue = [];
  972.  
  973.             container.active.bind( function ( active ) {
  974.                 var args = container.activeArgumentsQueue.shift();
  975.                 args = $.extend( {}, container.defaultActiveArguments, args );
  976.                 active = ( active && container.isContextuallyActive() );
  977.                 container.onChangeActive( active, args );
  978.             });
  979.             container.expanded.bind( function ( expanded ) {
  980.                 var args = container.expandedArgumentsQueue.shift();
  981.                 args = $.extend( {}, container.defaultExpandedArguments, args );
  982.                 container.onChangeExpanded( expanded, args );
  983.             });
  984.  
  985.             container.deferred.embedded.done( function () {
  986.                 container.setupNotifications();
  987.                 container.attachEvents();
  988.             });
  989.  
  990.             api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
  991.  
  992.             container.priority.set( container.params.priority );
  993.             container.active.set( container.params.active );
  994.             container.expanded.set( false );
  995.         },
  996.  
  997.         /**
  998.          * Get the element that will contain the notifications.
  999.          *
  1000.          * @since 4.9.0
  1001.          * @returns {jQuery} Notification container element.
  1002.          * @this {wp.customize.Control}
  1003.          */
  1004.         getNotificationsContainerElement: function() {
  1005.             var container = this;
  1006.             return container.contentContainer.find( '.customize-control-notifications-container:first' );
  1007.         },
  1008.  
  1009.         /**
  1010.          * Set up notifications.
  1011.          *
  1012.          * @since 4.9.0
  1013.          * @returns {void}
  1014.          */
  1015.         setupNotifications: function() {
  1016.             var container = this, renderNotifications;
  1017.             container.notifications.container = container.getNotificationsContainerElement();
  1018.  
  1019.             // Render notifications when they change and when the construct is expanded.
  1020.             renderNotifications = function() {
  1021.                 if ( container.expanded.get() ) {
  1022.                     container.notifications.render();
  1023.                 }
  1024.             };
  1025.             container.expanded.bind( renderNotifications );
  1026.             renderNotifications();
  1027.             container.notifications.bind( 'change', _.debounce( renderNotifications ) );
  1028.         },
  1029.  
  1030.         /**
  1031.          * @since 4.1.0
  1032.          *
  1033.          * @abstract
  1034.          */
  1035.         ready: function() {},
  1036.  
  1037.         /**
  1038.          * Get the child models associated with this parent, sorting them by their priority Value.
  1039.          *
  1040.          * @since 4.1.0
  1041.          *
  1042.          * @param {String} parentType
  1043.          * @param {String} childType
  1044.          * @returns {Array}
  1045.          */
  1046.         _children: function ( parentType, childType ) {
  1047.             var parent = this,
  1048.                 children = [];
  1049.             api[ childType ].each( function ( child ) {
  1050.                 if ( child[ parentType ].get() === parent.id ) {
  1051.                     children.push( child );
  1052.                 }
  1053.             } );
  1054.             children.sort( api.utils.prioritySort );
  1055.             return children;
  1056.         },
  1057.  
  1058.         /**
  1059.          * To override by subclass, to return whether the container has active children.
  1060.          *
  1061.          * @since 4.1.0
  1062.          *
  1063.          * @abstract
  1064.          */
  1065.         isContextuallyActive: function () {
  1066.             throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
  1067.         },
  1068.  
  1069.         /**
  1070.          * Active state change handler.
  1071.          *
  1072.          * Shows the container if it is active, hides it if not.
  1073.          *
  1074.          * To override by subclass, update the container's UI to reflect the provided active state.
  1075.          *
  1076.          * @since 4.1.0
  1077.          *
  1078.          * @param {boolean}  active - The active state to transiution to.
  1079.          * @param {Object}   [args] - Args.
  1080.          * @param {Object}   [args.duration] - The duration for the slideUp/slideDown animation.
  1081.          * @param {boolean}  [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
  1082.          * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
  1083.          */
  1084.         onChangeActive: function( active, args ) {
  1085.             var construct = this,
  1086.                 headContainer = construct.headContainer,
  1087.                 duration, expandedOtherPanel;
  1088.  
  1089.             if ( args.unchanged ) {
  1090.                 if ( args.completeCallback ) {
  1091.                     args.completeCallback();
  1092.                 }
  1093.                 return;
  1094.             }
  1095.  
  1096.             duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
  1097.  
  1098.             if ( construct.extended( api.Panel ) ) {
  1099.                 // If this is a panel is not currently expanded but another panel is expanded, do not animate.
  1100.                 api.panel.each(function ( panel ) {
  1101.                     if ( panel !== construct && panel.expanded() ) {
  1102.                         expandedOtherPanel = panel;
  1103.                         duration = 0;
  1104.                     }
  1105.                 });
  1106.  
  1107.                 // Collapse any expanded sections inside of this panel first before deactivating.
  1108.                 if ( ! active ) {
  1109.                     _.each( construct.sections(), function( section ) {
  1110.                         section.collapse( { duration: 0 } );
  1111.                     } );
  1112.                 }
  1113.             }
  1114.  
  1115.             if ( ! $.contains( document, headContainer.get( 0 ) ) ) {
  1116.                 // If the element is not in the DOM, then jQuery.fn.slideUp() does nothing. In this case, a hard toggle is required instead.
  1117.                 headContainer.toggle( active );
  1118.                 if ( args.completeCallback ) {
  1119.                     args.completeCallback();
  1120.                 }
  1121.             } else if ( active ) {
  1122.                 headContainer.slideDown( duration, args.completeCallback );
  1123.             } else {
  1124.                 if ( construct.expanded() ) {
  1125.                     construct.collapse({
  1126.                         duration: duration,
  1127.                         completeCallback: function() {
  1128.                             headContainer.slideUp( duration, args.completeCallback );
  1129.                         }
  1130.                     });
  1131.                 } else {
  1132.                     headContainer.slideUp( duration, args.completeCallback );
  1133.                 }
  1134.             }
  1135.         },
  1136.  
  1137.         /**
  1138.          * @since 4.1.0
  1139.          *
  1140.          * @params {Boolean} active
  1141.          * @param {Object}   [params]
  1142.          * @returns {Boolean} false if state already applied
  1143.          */
  1144.         _toggleActive: function ( active, params ) {
  1145.             var self = this;
  1146.             params = params || {};
  1147.             if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
  1148.                 params.unchanged = true;
  1149.                 self.onChangeActive( self.active.get(), params );
  1150.                 return false;
  1151.             } else {
  1152.                 params.unchanged = false;
  1153.                 this.activeArgumentsQueue.push( params );
  1154.                 this.active.set( active );
  1155.                 return true;
  1156.             }
  1157.         },
  1158.  
  1159.         /**
  1160.          * @param {Object} [params]
  1161.          * @returns {Boolean} false if already active
  1162.          */
  1163.         activate: function ( params ) {
  1164.             return this._toggleActive( true, params );
  1165.         },
  1166.  
  1167.         /**
  1168.          * @param {Object} [params]
  1169.          * @returns {Boolean} false if already inactive
  1170.          */
  1171.         deactivate: function ( params ) {
  1172.             return this._toggleActive( false, params );
  1173.         },
  1174.  
  1175.         /**
  1176.          * To override by subclass, update the container's UI to reflect the provided active state.
  1177.          * @abstract
  1178.          */
  1179.         onChangeExpanded: function () {
  1180.             throw new Error( 'Must override with subclass.' );
  1181.         },
  1182.  
  1183.         /**
  1184.          * Handle the toggle logic for expand/collapse.
  1185.          *
  1186.          * @param {Boolean}  expanded - The new state to apply.
  1187.          * @param {Object}   [params] - Object containing options for expand/collapse.
  1188.          * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
  1189.          * @returns {Boolean} false if state already applied or active state is false
  1190.          */
  1191.         _toggleExpanded: function( expanded, params ) {
  1192.             var instance = this, previousCompleteCallback;
  1193.             params = params || {};
  1194.             previousCompleteCallback = params.completeCallback;
  1195.  
  1196.             // Short-circuit expand() if the instance is not active.
  1197.             if ( expanded && ! instance.active() ) {
  1198.                 return false;
  1199.             }
  1200.  
  1201.             api.state( 'paneVisible' ).set( true );
  1202.             params.completeCallback = function() {
  1203.                 if ( previousCompleteCallback ) {
  1204.                     previousCompleteCallback.apply( instance, arguments );
  1205.                 }
  1206.                 if ( expanded ) {
  1207.                     instance.container.trigger( 'expanded' );
  1208.                 } else {
  1209.                     instance.container.trigger( 'collapsed' );
  1210.                 }
  1211.             };
  1212.             if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
  1213.                 params.unchanged = true;
  1214.                 instance.onChangeExpanded( instance.expanded.get(), params );
  1215.                 return false;
  1216.             } else {
  1217.                 params.unchanged = false;
  1218.                 instance.expandedArgumentsQueue.push( params );
  1219.                 instance.expanded.set( expanded );
  1220.                 return true;
  1221.             }
  1222.         },
  1223.  
  1224.         /**
  1225.          * @param {Object} [params]
  1226.          * @returns {Boolean} false if already expanded or if inactive.
  1227.          */
  1228.         expand: function ( params ) {
  1229.             return this._toggleExpanded( true, params );
  1230.         },
  1231.  
  1232.         /**
  1233.          * @param {Object} [params]
  1234.          * @returns {Boolean} false if already collapsed.
  1235.          */
  1236.         collapse: function ( params ) {
  1237.             return this._toggleExpanded( false, params );
  1238.         },
  1239.  
  1240.         /**
  1241.          * Animate container state change if transitions are supported by the browser.
  1242.          *
  1243.          * @since 4.7.0
  1244.          * @private
  1245.          *
  1246.          * @param {function} completeCallback Function to be called after transition is completed.
  1247.          * @returns {void}
  1248.          */
  1249.         _animateChangeExpanded: function( completeCallback ) {
  1250.             // Return if CSS transitions are not supported.
  1251.             if ( ! normalizedTransitionendEventName ) {
  1252.                 if ( completeCallback ) {
  1253.                     completeCallback();
  1254.                 }
  1255.                 return;
  1256.             }
  1257.  
  1258.             var construct = this,
  1259.                 content = construct.contentContainer,
  1260.                 overlay = content.closest( '.wp-full-overlay' ),
  1261.                 elements, transitionEndCallback, transitionParentPane;
  1262.  
  1263.             // Determine set of elements that are affected by the animation.
  1264.             elements = overlay.add( content );
  1265.  
  1266.             if ( ! construct.panel || '' === construct.panel() ) {
  1267.                 transitionParentPane = true;
  1268.             } else if ( api.panel( construct.panel() ).contentContainer.hasClass( 'skip-transition' ) ) {
  1269.                 transitionParentPane = true;
  1270.             } else {
  1271.                 transitionParentPane = false;
  1272.             }
  1273.             if ( transitionParentPane ) {
  1274.                 elements = elements.add( '#customize-info, .customize-pane-parent' );
  1275.             }
  1276.  
  1277.             // Handle `transitionEnd` event.
  1278.             transitionEndCallback = function( e ) {
  1279.                 if ( 2 !== e.eventPhase || ! $( e.target ).is( content ) ) {
  1280.                     return;
  1281.                 }
  1282.                 content.off( normalizedTransitionendEventName, transitionEndCallback );
  1283.                 elements.removeClass( 'busy' );
  1284.                 if ( completeCallback ) {
  1285.                     completeCallback();
  1286.                 }
  1287.             };
  1288.             content.on( normalizedTransitionendEventName, transitionEndCallback );
  1289.             elements.addClass( 'busy' );
  1290.  
  1291.             // Prevent screen flicker when pane has been scrolled before expanding.
  1292.             _.defer( function() {
  1293.                 var container = content.closest( '.wp-full-overlay-sidebar-content' ),
  1294.                     currentScrollTop = container.scrollTop(),
  1295.                     previousScrollTop = content.data( 'previous-scrollTop' ) || 0,
  1296.                     expanded = construct.expanded();
  1297.  
  1298.                 if ( expanded && 0 < currentScrollTop ) {
  1299.                     content.css( 'top', currentScrollTop + 'px' );
  1300.                     content.data( 'previous-scrollTop', currentScrollTop );
  1301.                 } else if ( ! expanded && 0 < currentScrollTop + previousScrollTop ) {
  1302.                     content.css( 'top', previousScrollTop - currentScrollTop + 'px' );
  1303.                     container.scrollTop( previousScrollTop );
  1304.                 }
  1305.             } );
  1306.         },
  1307.  
  1308.         /**
  1309.          * Bring the container into view and then expand this and bring it into view
  1310.          * @param {Object} [params]
  1311.          */
  1312.         focus: focus,
  1313.  
  1314.         /**
  1315.          * Return the container html, generated from its JS template, if it exists.
  1316.          *
  1317.          * @since 4.3.0
  1318.          */
  1319.         getContainer: function () {
  1320.             var template,
  1321.                 container = this;
  1322.  
  1323.             if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
  1324.                 template = wp.template( container.templateSelector );
  1325.             } else {
  1326.                 template = wp.template( 'customize-' + container.containerType + '-default' );
  1327.             }
  1328.             if ( template && container.container ) {
  1329.                 return $.trim( template( _.extend(
  1330.                     { id: container.id },
  1331.                     container.params
  1332.                 ) ) );
  1333.             }
  1334.  
  1335.             return '<li></li>';
  1336.         },
  1337.  
  1338.         /**
  1339.          * Find content element which is displayed when the section is expanded.
  1340.          *
  1341.          * After a construct is initialized, the return value will be available via the `contentContainer` property.
  1342.          * By default the element will be related it to the parent container with `aria-owns` and detached.
  1343.          * Custom panels and sections (such as the `NewMenuSection`) that do not have a sliding pane should
  1344.          * just return the content element without needing to add the `aria-owns` element or detach it from
  1345.          * the container. Such non-sliding pane custom sections also need to override the `onChangeExpanded`
  1346.          * method to handle animating the panel/section into and out of view.
  1347.          *
  1348.          * @since 4.7.0
  1349.          * @access public
  1350.          *
  1351.          * @returns {jQuery} Detached content element.
  1352.          */
  1353.         getContent: function() {
  1354.             var construct = this,
  1355.                 container = construct.container,
  1356.                 content = container.find( '.accordion-section-content, .control-panel-content' ).first(),
  1357.                 contentId = 'sub-' + container.attr( 'id' ),
  1358.                 ownedElements = contentId,
  1359.                 alreadyOwnedElements = container.attr( 'aria-owns' );
  1360.  
  1361.             if ( alreadyOwnedElements ) {
  1362.                 ownedElements = ownedElements + ' ' + alreadyOwnedElements;
  1363.             }
  1364.             container.attr( 'aria-owns', ownedElements );
  1365.  
  1366.             return content.detach().attr( {
  1367.                 'id': contentId,
  1368.                 'class': 'customize-pane-child ' + content.attr( 'class' ) + ' ' + container.attr( 'class' )
  1369.             } );
  1370.         }
  1371.     });
  1372.  
  1373.     /**
  1374.      * @since 4.1.0
  1375.      *
  1376.      * @class
  1377.      * @augments wp.customize.Class
  1378.      */
  1379.     api.Section = Container.extend({
  1380.         containerType: 'section',
  1381.         containerParent: '#customize-theme-controls',
  1382.         containerPaneParent: '.customize-pane-parent',
  1383.         defaults: {
  1384.             title: '',
  1385.             description: '',
  1386.             priority: 100,
  1387.             type: 'default',
  1388.             content: null,
  1389.             active: true,
  1390.             instanceNumber: null,
  1391.             panel: null,
  1392.             customizeAction: ''
  1393.         },
  1394.  
  1395.         /**
  1396.          * @since 4.1.0
  1397.          *
  1398.          * @param {string}         id - The ID for the section.
  1399.          * @param {object}         options - Options.
  1400.          * @param {string}         options.title - Title shown when section is collapsed and expanded.
  1401.          * @param {string=}        [options.description] - Description shown at the top of the section.
  1402.          * @param {number=100}     [options.priority] - The sort priority for the section.
  1403.          * @param {string=default} [options.type] - The type of the section. See wp.customize.sectionConstructor.
  1404.          * @param {string=}        [options.content] - The markup to be used for the section container. If empty, a JS template is used.
  1405.          * @param {boolean=true}   [options.active] - Whether the section is active or not.
  1406.          * @param {string}         options.panel - The ID for the panel this section is associated with.
  1407.          * @param {string=}        [options.customizeAction] - Additional context information shown before the section title when expanded.
  1408.          * @param {object}         [options.params] - Deprecated wrapper for the above properties.
  1409.          */
  1410.         initialize: function ( id, options ) {
  1411.             var section = this, params;
  1412.             params = options.params || options;
  1413.  
  1414.             // Look up the type if one was not supplied.
  1415.             if ( ! params.type ) {
  1416.                 _.find( api.sectionConstructor, function( Constructor, type ) {
  1417.                     if ( Constructor === section.constructor ) {
  1418.                         params.type = type;
  1419.                         return true;
  1420.                     }
  1421.                     return false;
  1422.                 } );
  1423.             }
  1424.  
  1425.             Container.prototype.initialize.call( section, id, params );
  1426.  
  1427.             section.id = id;
  1428.             section.panel = new api.Value();
  1429.             section.panel.bind( function ( id ) {
  1430.                 $( section.headContainer ).toggleClass( 'control-subsection', !! id );
  1431.             });
  1432.             section.panel.set( section.params.panel || '' );
  1433.             api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
  1434.  
  1435.             section.embed();
  1436.             section.deferred.embedded.done( function () {
  1437.                 section.ready();
  1438.             });
  1439.         },
  1440.  
  1441.         /**
  1442.          * Embed the container in the DOM when any parent panel is ready.
  1443.          *
  1444.          * @since 4.1.0
  1445.          */
  1446.         embed: function () {
  1447.             var inject,
  1448.                 section = this;
  1449.  
  1450.             section.containerParent = api.ensure( section.containerParent );
  1451.  
  1452.             // Watch for changes to the panel state.
  1453.             inject = function ( panelId ) {
  1454.                 var parentContainer;
  1455.                 if ( panelId ) {
  1456.                     // The panel has been supplied, so wait until the panel object is registered.
  1457.                     api.panel( panelId, function ( panel ) {
  1458.                         // The panel has been registered, wait for it to become ready/initialized.
  1459.                         panel.deferred.embedded.done( function () {
  1460.                             parentContainer = panel.contentContainer;
  1461.                             if ( ! section.headContainer.parent().is( parentContainer ) ) {
  1462.                                 parentContainer.append( section.headContainer );
  1463.                             }
  1464.                             if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
  1465.                                 section.containerParent.append( section.contentContainer );
  1466.                             }
  1467.                             section.deferred.embedded.resolve();
  1468.                         });
  1469.                     } );
  1470.                 } else {
  1471.                     // There is no panel, so embed the section in the root of the customizer
  1472.                     parentContainer = api.ensure( section.containerPaneParent );
  1473.                     if ( ! section.headContainer.parent().is( parentContainer ) ) {
  1474.                         parentContainer.append( section.headContainer );
  1475.                     }
  1476.                     if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
  1477.                         section.containerParent.append( section.contentContainer );
  1478.                     }
  1479.                     section.deferred.embedded.resolve();
  1480.                 }
  1481.             };
  1482.             section.panel.bind( inject );
  1483.             inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one.
  1484.         },
  1485.  
  1486.         /**
  1487.          * Add behaviors for the accordion section.
  1488.          *
  1489.          * @since 4.1.0
  1490.          */
  1491.         attachEvents: function () {
  1492.             var meta, content, section = this;
  1493.  
  1494.             if ( section.container.hasClass( 'cannot-expand' ) ) {
  1495.                 return;
  1496.             }
  1497.  
  1498.             // Expand/Collapse accordion sections on click.
  1499.             section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
  1500.                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1501.                     return;
  1502.                 }
  1503.                 event.preventDefault(); // Keep this AFTER the key filter above
  1504.  
  1505.                 if ( section.expanded() ) {
  1506.                     section.collapse();
  1507.                 } else {
  1508.                     section.expand();
  1509.                 }
  1510.             });
  1511.  
  1512.             // This is very similar to what is found for api.Panel.attachEvents().
  1513.             section.container.find( '.customize-section-title .customize-help-toggle' ).on( 'click', function() {
  1514.  
  1515.                 meta = section.container.find( '.section-meta' );
  1516.                 if ( meta.hasClass( 'cannot-expand' ) ) {
  1517.                     return;
  1518.                 }
  1519.                 content = meta.find( '.customize-section-description:first' );
  1520.                 content.toggleClass( 'open' );
  1521.                 content.slideToggle( section.defaultExpandedArguments.duration, function() {
  1522.                     content.trigger( 'toggled' );
  1523.                 } );
  1524.                 $( this ).attr( 'aria-expanded', function( i, attr ) {
  1525.                     return 'true' === attr ? 'false' : 'true';
  1526.                 });
  1527.             });
  1528.         },
  1529.  
  1530.         /**
  1531.          * Return whether this section has any active controls.
  1532.          *
  1533.          * @since 4.1.0
  1534.          *
  1535.          * @returns {Boolean}
  1536.          */
  1537.         isContextuallyActive: function () {
  1538.             var section = this,
  1539.                 controls = section.controls(),
  1540.                 activeCount = 0;
  1541.             _( controls ).each( function ( control ) {
  1542.                 if ( control.active() ) {
  1543.                     activeCount += 1;
  1544.                 }
  1545.             } );
  1546.             return ( activeCount !== 0 );
  1547.         },
  1548.  
  1549.         /**
  1550.          * Get the controls that are associated with this section, sorted by their priority Value.
  1551.          *
  1552.          * @since 4.1.0
  1553.          *
  1554.          * @returns {Array}
  1555.          */
  1556.         controls: function () {
  1557.             return this._children( 'section', 'control' );
  1558.         },
  1559.  
  1560.         /**
  1561.          * Update UI to reflect expanded state.
  1562.          *
  1563.          * @since 4.1.0
  1564.          *
  1565.          * @param {Boolean} expanded
  1566.          * @param {Object}  args
  1567.          */
  1568.         onChangeExpanded: function ( expanded, args ) {
  1569.             var section = this,
  1570.                 container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
  1571.                 content = section.contentContainer,
  1572.                 overlay = section.headContainer.closest( '.wp-full-overlay' ),
  1573.                 backBtn = content.find( '.customize-section-back' ),
  1574.                 sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
  1575.                 expand, panel;
  1576.  
  1577.             if ( expanded && ! content.hasClass( 'open' ) ) {
  1578.  
  1579.                 if ( args.unchanged ) {
  1580.                     expand = args.completeCallback;
  1581.                 } else {
  1582.                     expand = $.proxy( function() {
  1583.                         section._animateChangeExpanded( function() {
  1584.                             sectionTitle.attr( 'tabindex', '-1' );
  1585.                             backBtn.attr( 'tabindex', '0' );
  1586.  
  1587.                             backBtn.focus();
  1588.                             content.css( 'top', '' );
  1589.                             container.scrollTop( 0 );
  1590.  
  1591.                             if ( args.completeCallback ) {
  1592.                                 args.completeCallback();
  1593.                             }
  1594.                         } );
  1595.  
  1596.                         content.addClass( 'open' );
  1597.                         overlay.addClass( 'section-open' );
  1598.                         api.state( 'expandedSection' ).set( section );
  1599.                     }, this );
  1600.                 }
  1601.  
  1602.                 if ( ! args.allowMultiple ) {
  1603.                     api.section.each( function ( otherSection ) {
  1604.                         if ( otherSection !== section ) {
  1605.                             otherSection.collapse( { duration: args.duration } );
  1606.                         }
  1607.                     });
  1608.                 }
  1609.  
  1610.                 if ( section.panel() ) {
  1611.                     api.panel( section.panel() ).expand({
  1612.                         duration: args.duration,
  1613.                         completeCallback: expand
  1614.                     });
  1615.                 } else {
  1616.                     if ( ! args.allowMultiple ) {
  1617.                         api.panel.each( function( panel ) {
  1618.                             panel.collapse();
  1619.                         });
  1620.                     }
  1621.                     expand();
  1622.                 }
  1623.  
  1624.             } else if ( ! expanded && content.hasClass( 'open' ) ) {
  1625.                 if ( section.panel() ) {
  1626.                     panel = api.panel( section.panel() );
  1627.                     if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
  1628.                         panel.collapse();
  1629.                     }
  1630.                 }
  1631.                 section._animateChangeExpanded( function() {
  1632.                     backBtn.attr( 'tabindex', '-1' );
  1633.                     sectionTitle.attr( 'tabindex', '0' );
  1634.  
  1635.                     sectionTitle.focus();
  1636.                     content.css( 'top', '' );
  1637.  
  1638.                     if ( args.completeCallback ) {
  1639.                         args.completeCallback();
  1640.                     }
  1641.                 } );
  1642.  
  1643.                 content.removeClass( 'open' );
  1644.                 overlay.removeClass( 'section-open' );
  1645.                 if ( section === api.state( 'expandedSection' ).get() ) {
  1646.                     api.state( 'expandedSection' ).set( false );
  1647.                 }
  1648.  
  1649.             } else {
  1650.                 if ( args.completeCallback ) {
  1651.                     args.completeCallback();
  1652.                 }
  1653.             }
  1654.         }
  1655.     });
  1656.  
  1657.     /**
  1658.      * wp.customize.ThemesSection
  1659.      *
  1660.      * Custom section for themes that loads themes by category, and also
  1661.      * handles the theme-details view rendering and navigation.
  1662.      *
  1663.      * @constructor
  1664.      * @augments wp.customize.Section
  1665.      * @augments wp.customize.Container
  1666.      */
  1667.     api.ThemesSection = api.Section.extend({
  1668.         currentTheme: '',
  1669.         overlay: '',
  1670.         template: '',
  1671.         screenshotQueue: null,
  1672.         $window: null,
  1673.         $body: null,
  1674.         loaded: 0,
  1675.         loading: false,
  1676.         fullyLoaded: false,
  1677.         term: '',
  1678.         tags: '',
  1679.         nextTerm: '',
  1680.         nextTags: '',
  1681.         filtersHeight: 0,
  1682.         headerContainer: null,
  1683.         updateCountDebounced: null,
  1684.  
  1685.         /**
  1686.          * Initialize.
  1687.          *
  1688.          * @since 4.9.0
  1689.          *
  1690.          * @param {string} id - ID.
  1691.          * @param {object} options - Options.
  1692.          * @returns {void}
  1693.          */
  1694.         initialize: function( id, options ) {
  1695.             var section = this;
  1696.             section.headerContainer = $();
  1697.             section.$window = $( window );
  1698.             section.$body = $( document.body );
  1699.             api.Section.prototype.initialize.call( section, id, options );
  1700.             section.updateCountDebounced = _.debounce( section.updateCount, 500 );
  1701.         },
  1702.  
  1703.         /**
  1704.          * Embed the section in the DOM when the themes panel is ready.
  1705.          *
  1706.          * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel.
  1707.          *
  1708.          * @since 4.9.0
  1709.          */
  1710.         embed: function() {
  1711.             var inject,
  1712.                 section = this;
  1713.  
  1714.             // Watch for changes to the panel state
  1715.             inject = function( panelId ) {
  1716.                 var parentContainer;
  1717.                 api.panel( panelId, function( panel ) {
  1718.  
  1719.                     // The panel has been registered, wait for it to become ready/initialized
  1720.                     panel.deferred.embedded.done( function() {
  1721.                         parentContainer = panel.contentContainer;
  1722.                         if ( ! section.headContainer.parent().is( parentContainer ) ) {
  1723.                             parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer );
  1724.                         }
  1725.                         if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
  1726.                             section.containerParent.append( section.contentContainer );
  1727.                         }
  1728.                         section.deferred.embedded.resolve();
  1729.                     });
  1730.                 } );
  1731.             };
  1732.             section.panel.bind( inject );
  1733.             inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
  1734.         },
  1735.  
  1736.         /**
  1737.          * Set up.
  1738.          *
  1739.          * @since 4.2.0
  1740.          *
  1741.          * @returns {void}
  1742.          */
  1743.         ready: function() {
  1744.             var section = this;
  1745.             section.overlay = section.container.find( '.theme-overlay' );
  1746.             section.template = wp.template( 'customize-themes-details-view' );
  1747.  
  1748.             // Bind global keyboard events.
  1749.             section.container.on( 'keydown', function( event ) {
  1750.                 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
  1751.                     return;
  1752.                 }
  1753.  
  1754.                 // Pressing the right arrow key fires a theme:next event
  1755.                 if ( 39 === event.keyCode ) {
  1756.                     section.nextTheme();
  1757.                 }
  1758.  
  1759.                 // Pressing the left arrow key fires a theme:previous event
  1760.                 if ( 37 === event.keyCode ) {
  1761.                     section.previousTheme();
  1762.                 }
  1763.  
  1764.                 // Pressing the escape key fires a theme:collapse event
  1765.                 if ( 27 === event.keyCode ) {
  1766.                     if ( section.$body.hasClass( 'modal-open' ) ) {
  1767.  
  1768.                         // Escape from the details modal.
  1769.                         section.closeDetails();
  1770.                     } else {
  1771.  
  1772.                         // Escape from the inifinite scroll list.
  1773.                         section.headerContainer.find( '.customize-themes-section-title' ).focus();
  1774.                     }
  1775.                     event.stopPropagation(); // Prevent section from being collapsed.
  1776.                 }
  1777.             });
  1778.  
  1779.             section.renderScreenshots = _.throttle( section.renderScreenshots, 100 );
  1780.  
  1781.             _.bindAll( section, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' );
  1782.         },
  1783.  
  1784.         /**
  1785.          * Override Section.isContextuallyActive method.
  1786.          *
  1787.          * Ignore the active states' of the contained theme controls, and just
  1788.          * use the section's own active state instead. This prevents empty search
  1789.          * results for theme sections from causing the section to become inactive.
  1790.          *
  1791.          * @since 4.2.0
  1792.          *
  1793.          * @returns {Boolean}
  1794.          */
  1795.         isContextuallyActive: function () {
  1796.             return this.active();
  1797.         },
  1798.  
  1799.         /**
  1800.          * Attach events.
  1801.          *
  1802.          * @since 4.2.0
  1803.          *
  1804.          * @returns {void}
  1805.          */
  1806.         attachEvents: function () {
  1807.             var section = this, debounced;
  1808.  
  1809.             // Expand/Collapse accordion sections on click.
  1810.             section.container.find( '.customize-section-back' ).on( 'click keydown', function( event ) {
  1811.                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  1812.                     return;
  1813.                 }
  1814.                 event.preventDefault(); // Keep this AFTER the key filter above
  1815.                 section.collapse();
  1816.             });
  1817.  
  1818.             section.headerContainer = $( '#accordion-section-' + section.id );
  1819.  
  1820.             // Expand section/panel. Only collapse when opening another section.
  1821.             section.headerContainer.on( 'click', '.customize-themes-section-title', function() {
  1822.  
  1823.                 // Toggle accordion filters under section headers.
  1824.                 if ( section.headerContainer.find( '.filter-details' ).length ) {
  1825.                     section.headerContainer.find( '.customize-themes-section-title' )
  1826.                         .toggleClass( 'details-open' )
  1827.                         .attr( 'aria-expanded', function( i, attr ) {
  1828.                             return 'true' === attr ? 'false' : 'true';
  1829.                         });
  1830.                     section.headerContainer.find( '.filter-details' ).slideToggle( 180 );
  1831.                 }
  1832.  
  1833.                 // Open the section.
  1834.                 if ( ! section.expanded() ) {
  1835.                     section.expand();
  1836.                 }
  1837.             });
  1838.  
  1839.             // Preview installed themes.
  1840.             section.container.on( 'click', '.theme-actions .preview-theme', function() {
  1841.                 api.panel( 'themes' ).loadThemePreview( $( this ).data( 'slug' ) );
  1842.             });
  1843.  
  1844.             // Theme navigation in details view.
  1845.             section.container.on( 'click', '.left', function() {
  1846.                 section.previousTheme();
  1847.             });
  1848.  
  1849.             section.container.on( 'click', '.right', function() {
  1850.                 section.nextTheme();
  1851.             });
  1852.  
  1853.             section.container.on( 'click', '.theme-backdrop, .close', function() {
  1854.                 section.closeDetails();
  1855.             });
  1856.  
  1857.             if ( 'local' === section.params.filter_type ) {
  1858.  
  1859.                 // Filter-search all theme objects loaded in the section.
  1860.                 section.container.on( 'input', '.wp-filter-search-themes', function( event ) {
  1861.                     section.filterSearch( event.currentTarget.value );
  1862.                 });
  1863.  
  1864.             } else if ( 'remote' === section.params.filter_type ) {
  1865.  
  1866.                 // Event listeners for remote queries with user-entered terms.
  1867.                 // Search terms.
  1868.                 debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search.
  1869.                 section.contentContainer.on( 'input', '.wp-filter-search', function() {
  1870.                     if ( ! api.panel( 'themes' ).expanded() ) {
  1871.                         return;
  1872.                     }
  1873.                     debounced( section );
  1874.                     if ( ! section.expanded() ) {
  1875.                         section.expand();
  1876.                     }
  1877.                 });
  1878.  
  1879.                 // Feature filters.
  1880.                 section.contentContainer.on( 'click', '.filter-group input', function() {
  1881.                     section.filtersChecked();
  1882.                     section.checkTerm( section );
  1883.                 });
  1884.             }
  1885.  
  1886.             // Toggle feature filters.
  1887.             section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) {
  1888.                 var $themeContainer = $( '.customize-themes-full-container' ),
  1889.                     $filterToggle = $( e.currentTarget );
  1890.                 section.filtersHeight = $filterToggle.parent().next( '.filter-drawer' ).height();
  1891.  
  1892.                 if ( 0 < $themeContainer.scrollTop() ) {
  1893.                     $themeContainer.animate( { scrollTop: 0 }, 400 );
  1894.  
  1895.                     if ( $filterToggle.hasClass( 'open' ) ) {
  1896.                         return;
  1897.                     }
  1898.                 }
  1899.  
  1900.                 $filterToggle
  1901.                     .toggleClass( 'open' )
  1902.                     .attr( 'aria-expanded', function( i, attr ) {
  1903.                         return 'true' === attr ? 'false' : 'true';
  1904.                     })
  1905.                     .parent().next( '.filter-drawer' ).slideToggle( 180, 'linear' );
  1906.  
  1907.                 if ( $filterToggle.hasClass( 'open' ) ) {
  1908.                     var marginOffset = 1018 < window.innerWidth ? 50 : 76;
  1909.  
  1910.                     section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + marginOffset );
  1911.                 } else {
  1912.                     section.contentContainer.find( '.themes' ).css( 'margin-top', 0 );
  1913.                 }
  1914.             });
  1915.  
  1916.             // Setup section cross-linking.
  1917.             section.contentContainer.on( 'click', '.no-themes-local .search-dotorg-themes', function() {
  1918.                 api.section( 'wporg_themes' ).focus();
  1919.             });
  1920.  
  1921.             function updateSelectedState() {
  1922.                 var el = section.headerContainer.find( '.customize-themes-section-title' );
  1923.                 el.toggleClass( 'selected', section.expanded() );
  1924.                 el.attr( 'aria-expanded', section.expanded() ? 'true' : 'false' );
  1925.                 if ( ! section.expanded() ) {
  1926.                     el.removeClass( 'details-open' );
  1927.                 }
  1928.             }
  1929.             section.expanded.bind( updateSelectedState );
  1930.             updateSelectedState();
  1931.  
  1932.             // Move section controls to the themes area.
  1933.             api.bind( 'ready', function () {
  1934.                 section.contentContainer = section.container.find( '.customize-themes-section' );
  1935.                 section.contentContainer.appendTo( $( '.customize-themes-full-container' ) );
  1936.                 section.container.add( section.headerContainer );
  1937.             });
  1938.         },
  1939.  
  1940.         /**
  1941.          * Update UI to reflect expanded state
  1942.          *
  1943.          * @since 4.2.0
  1944.          *
  1945.          * @param {Boolean}  expanded
  1946.          * @param {Object}   args
  1947.          * @param {Boolean}  args.unchanged
  1948.          * @param {Function} args.completeCallback
  1949.          * @returns {void}
  1950.          */
  1951.         onChangeExpanded: function ( expanded, args ) {
  1952.  
  1953.             // Note: there is a second argument 'args' passed
  1954.             var section = this,
  1955.                 container = section.contentContainer.closest( '.customize-themes-full-container' );
  1956.  
  1957.             // Immediately call the complete callback if there were no changes
  1958.             if ( args.unchanged ) {
  1959.                 if ( args.completeCallback ) {
  1960.                     args.completeCallback();
  1961.                 }
  1962.                 return;
  1963.             }
  1964.  
  1965.             function expand() {
  1966.  
  1967.                 // Try to load controls if none are loaded yet.
  1968.                 if ( 0 === section.loaded ) {
  1969.                     section.loadThemes();
  1970.                 }
  1971.  
  1972.                 // Collapse any sibling sections/panels
  1973.                 api.section.each( function ( otherSection ) {
  1974.                     var searchTerm;
  1975.  
  1976.                     if ( otherSection !== section ) {
  1977.  
  1978.                         // Try to sync the current search term to the new section.
  1979.                         if ( 'themes' === otherSection.params.type ) {
  1980.                             searchTerm = otherSection.contentContainer.find( '.wp-filter-search' ).val();
  1981.                             section.contentContainer.find( '.wp-filter-search' ).val( searchTerm );
  1982.  
  1983.                             // Directly initialize an empty remote search to avoid a race condition.
  1984.                             if ( '' === searchTerm && '' !== section.term && 'local' !== section.params.filter_type ) {
  1985.                                 section.term = '';
  1986.                                 section.initializeNewQuery( section.term, section.tags );
  1987.                             } else {
  1988.                                 if ( 'remote' === section.params.filter_type ) {
  1989.                                     section.checkTerm( section );
  1990.                                 } else if ( 'local' === section.params.filter_type ) {
  1991.                                     section.filterSearch( searchTerm );
  1992.                                 }
  1993.                             }
  1994.                             otherSection.collapse( { duration: args.duration } );
  1995.                         }
  1996.                     }
  1997.                 });
  1998.  
  1999.                 section.contentContainer.addClass( 'current-section' );
  2000.                 container.scrollTop();
  2001.  
  2002.                 container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
  2003.                 container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
  2004.  
  2005.                 if ( args.completeCallback ) {
  2006.                     args.completeCallback();
  2007.                 }
  2008.                 section.updateCount(); // Show this section's count.
  2009.             }
  2010.  
  2011.             if ( expanded ) {
  2012.                 if ( section.panel() && api.panel.has( section.panel() ) ) {
  2013.                     api.panel( section.panel() ).expand({
  2014.                         duration: args.duration,
  2015.                         completeCallback: expand
  2016.                     });
  2017.                 } else {
  2018.                     expand();
  2019.                 }
  2020.             } else {
  2021.                 section.contentContainer.removeClass( 'current-section' );
  2022.  
  2023.                 // Always hide, even if they don't exist or are already hidden.
  2024.                 section.headerContainer.find( '.filter-details' ).slideUp( 180 );
  2025.  
  2026.                 container.off( 'scroll' );
  2027.  
  2028.                 if ( args.completeCallback ) {
  2029.                     args.completeCallback();
  2030.                 }
  2031.             }
  2032.         },
  2033.  
  2034.         /**
  2035.          * Return the section's content element without detaching from the parent.
  2036.          *
  2037.          * @since 4.9.0
  2038.          *
  2039.          * @returns {jQuery}
  2040.          */
  2041.         getContent: function() {
  2042.             return this.container.find( '.control-section-content' );
  2043.         },
  2044.  
  2045.         /**
  2046.          * Load theme data via Ajax and add themes to the section as controls.
  2047.          *
  2048.          * @since 4.9.0
  2049.          *
  2050.          * @returns {void}
  2051.          */
  2052.         loadThemes: function() {
  2053.             var section = this, params, page, request;
  2054.  
  2055.             if ( section.loading ) {
  2056.                 return; // We're already loading a batch of themes.
  2057.             }
  2058.  
  2059.             // Parameters for every API query. Additional params are set in PHP.
  2060.             page = Math.ceil( section.loaded / 100 ) + 1;
  2061.             params = {
  2062.                 'nonce': api.settings.nonce.switch_themes,
  2063.                 'wp_customize': 'on',
  2064.                 'theme_action': section.params.action,
  2065.                 'customized_theme': api.settings.theme.stylesheet,
  2066.                 'page': page
  2067.             };
  2068.  
  2069.             // Add fields for remote filtering.
  2070.             if ( 'remote' === section.params.filter_type ) {
  2071.                 params.search = section.term;
  2072.                 params.tags = section.tags;
  2073.             }
  2074.  
  2075.             // Load themes.
  2076.             section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' );
  2077.             section.loading = true;
  2078.             section.container.find( '.no-themes' ).hide();
  2079.             request = wp.ajax.post( 'customize_load_themes', params );
  2080.             request.done(function( data ) {
  2081.                 var themes = data.themes;
  2082.  
  2083.                 // Stop and try again if the term changed while loading.
  2084.                 if ( '' !== section.nextTerm || '' !== section.nextTags ) {
  2085.                     if ( section.nextTerm ) {
  2086.                         section.term = section.nextTerm;
  2087.                     }
  2088.                     if ( section.nextTags ) {
  2089.                         section.tags = section.nextTags;
  2090.                     }
  2091.                     section.nextTerm = '';
  2092.                     section.nextTags = '';
  2093.                     section.loading = false;
  2094.                     section.loadThemes();
  2095.                     return;
  2096.                 }
  2097.  
  2098.                 if ( 0 !== themes.length ) {
  2099.  
  2100.                     section.loadControls( themes, page );
  2101.  
  2102.                     if ( 1 === page ) {
  2103.  
  2104.                         // Pre-load the first 3 theme screenshots.
  2105.                         _.each( section.controls().slice( 0, 3 ), function( control ) {
  2106.                             var img, src = control.params.theme.screenshot[0];
  2107.                             if ( src ) {
  2108.                                 img = new Image();
  2109.                                 img.src = src;
  2110.                             }
  2111.                         });
  2112.                         if ( 'local' !== section.params.filter_type ) {
  2113.                             wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
  2114.                         }
  2115.                     }
  2116.  
  2117.                     _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
  2118.  
  2119.                     if ( 'local' === section.params.filter_type || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
  2120.                         section.fullyLoaded = true;
  2121.                     }
  2122.                 } else {
  2123.                     if ( 0 === section.loaded ) {
  2124.                         section.container.find( '.no-themes' ).show();
  2125.                         wp.a11y.speak( section.container.find( '.no-themes' ).text() );
  2126.                     } else {
  2127.                         section.fullyLoaded = true;
  2128.                     }
  2129.                 }
  2130.                 if ( 'local' === section.params.filter_type ) {
  2131.                     section.updateCount(); // Count of visible theme controls.
  2132.                 } else {
  2133.                     section.updateCount( data.info.results ); // Total number of results including pages not yet loaded.
  2134.                 }
  2135.                 section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
  2136.  
  2137.                 // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
  2138.                 section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
  2139.                 section.loading = false;
  2140.             });
  2141.             request.fail(function( data ) {
  2142.                 if ( 'undefined' === typeof data ) {
  2143.                     section.container.find( '.unexpected-error' ).show();
  2144.                     wp.a11y.speak( section.container.find( '.unexpected-error' ).text() );
  2145.                 } else if ( 'undefined' !== typeof console && console.error ) {
  2146.                     console.error( data );
  2147.                 }
  2148.  
  2149.                 // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
  2150.                 section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
  2151.                 section.loading = false;
  2152.             });
  2153.         },
  2154.  
  2155.         /**
  2156.          * Loads controls into the section from data received from loadThemes().
  2157.          *
  2158.          * @since 4.9.0
  2159.          * @param {Array} themes - Array of theme data to create controls with.
  2160.          * @param {integer} page - Page of results being loaded.
  2161.          * @returns {void}
  2162.          */
  2163.         loadControls: function( themes, page ) {
  2164.             var newThemeControls = [],
  2165.                 section = this;
  2166.  
  2167.             // Add controls for each theme.
  2168.             _.each( themes, function( theme ) {
  2169.                 var themeControl = new api.controlConstructor.theme( section.params.action + '_theme_' + theme.id, {
  2170.                     type: 'theme',
  2171.                     section: section.params.id,
  2172.                     theme: theme,
  2173.                     priority: section.loaded + 1
  2174.                 } );
  2175.  
  2176.                 api.control.add( themeControl );
  2177.                 newThemeControls.push( themeControl );
  2178.                 section.loaded = section.loaded + 1;
  2179.             });
  2180.  
  2181.             if ( 1 !== page ) {
  2182.                 Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
  2183.             }
  2184.         },
  2185.  
  2186.         /**
  2187.          * Determines whether more themes should be loaded, and loads them.
  2188.          *
  2189.          * @since 4.9.0
  2190.          * @returns {void}
  2191.          */
  2192.         loadMore: function() {
  2193.             var section = this, container, bottom, threshold;
  2194.             if ( ! section.fullyLoaded && ! section.loading ) {
  2195.                 container = section.container.closest( '.customize-themes-full-container' );
  2196.  
  2197.                 bottom = container.scrollTop() + container.height();
  2198.                 threshold = container.prop( 'scrollHeight' ) - 3000; // Use a fixed distance to the bottom of loaded results to avoid unnecessarily loading results sooner when using a percentage of scroll distance.
  2199.  
  2200.                 if ( bottom > threshold ) {
  2201.                     section.loadThemes();
  2202.                 }
  2203.             }
  2204.         },
  2205.  
  2206.         /**
  2207.          * Event handler for search input that filters visible controls.
  2208.          *
  2209.          * @since 4.9.0
  2210.          *
  2211.          * @param {string} term - The raw search input value.
  2212.          * @returns {void}
  2213.          */
  2214.         filterSearch: function( term ) {
  2215.             var count = 0,
  2216.                 visible = false,
  2217.                 section = this,
  2218.                 noFilter = ( api.section.has( 'wporg_themes' ) && 'remote' !== section.params.filter_type ) ? '.no-themes-local' : '.no-themes',
  2219.                 controls = section.controls(),
  2220.                 terms;
  2221.  
  2222.             if ( section.loading ) {
  2223.                 return;
  2224.             }
  2225.  
  2226.             // Standardize search term format and split into an array of individual words.
  2227.             terms = term.toLowerCase().trim().replace( /-/g, ' ' ).split( ' ' );
  2228.  
  2229.             _.each( controls, function( control ) {
  2230.                 visible = control.filter( terms ); // Shows/hides and sorts control based on the applicability of the search term.
  2231.                 if ( visible ) {
  2232.                     count = count + 1;
  2233.                 }
  2234.             });
  2235.  
  2236.             if ( 0 === count ) {
  2237.                 section.container.find( noFilter ).show();
  2238.                 wp.a11y.speak( section.container.find( noFilter ).text() );
  2239.             } else {
  2240.                 section.container.find( noFilter ).hide();
  2241.             }
  2242.  
  2243.             section.renderScreenshots();
  2244.             api.reflowPaneContents();
  2245.  
  2246.             // Update theme count.
  2247.             section.updateCountDebounced( count );
  2248.         },
  2249.  
  2250.         /**
  2251.          * Event handler for search input that determines if the terms have changed and loads new controls as needed.
  2252.          *
  2253.          * @since 4.9.0
  2254.          *
  2255.          * @param {wp.customize.ThemesSection} section - The current theme section, passed through the debouncer.
  2256.          * @returns {void}
  2257.          */
  2258.         checkTerm: function( section ) {
  2259.             var newTerm;
  2260.             if ( 'remote' === section.params.filter_type ) {
  2261.                 newTerm = section.contentContainer.find( '.wp-filter-search' ).val();
  2262.                 if ( section.term !== newTerm.trim() ) {
  2263.                     section.initializeNewQuery( newTerm, section.tags );
  2264.                 }
  2265.             }
  2266.         },
  2267.  
  2268.         /**
  2269.          * Check for filters checked in the feature filter list and initialize a new query.
  2270.          *
  2271.          * @since 4.9.0
  2272.          *
  2273.          * @returns {void}
  2274.          */
  2275.         filtersChecked: function() {
  2276.             var section = this,
  2277.                 items = section.container.find( '.filter-group' ).find( ':checkbox' ),
  2278.                 tags = [];
  2279.  
  2280.             _.each( items.filter( ':checked' ), function( item ) {
  2281.                 tags.push( $( item ).prop( 'value' ) );
  2282.             });
  2283.  
  2284.             // When no filters are checked, restore initial state. Update filter count.
  2285.             if ( 0 === tags.length ) {
  2286.                 tags = '';
  2287.                 section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).show();
  2288.                 section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).hide();
  2289.             } else {
  2290.                 section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length );
  2291.                 section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).hide();
  2292.                 section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).show();
  2293.             }
  2294.  
  2295.             // Check whether tags have changed, and either load or queue them.
  2296.             if ( ! _.isEqual( section.tags, tags ) ) {
  2297.                 if ( section.loading ) {
  2298.                     section.nextTags = tags;
  2299.                 } else {
  2300.                     if ( 'remote' === section.params.filter_type ) {
  2301.                         section.initializeNewQuery( section.term, tags );
  2302.                     } else if ( 'local' === section.params.filter_type ) {
  2303.                         section.filterSearch( tags.join( ' ' ) );
  2304.                     }
  2305.                 }
  2306.             }
  2307.         },
  2308.  
  2309.         /**
  2310.          * Reset the current query and load new results.
  2311.          *
  2312.          * @since 4.9.0
  2313.          *
  2314.          * @param {string} newTerm - New term.
  2315.          * @param {Array} newTags - New tags.
  2316.          * @returns {void}
  2317.          */
  2318.         initializeNewQuery: function( newTerm, newTags ) {
  2319.             var section = this;
  2320.  
  2321.             // Clear the controls in the section.
  2322.             _.each( section.controls(), function( control ) {
  2323.                 control.container.remove();
  2324.                 api.control.remove( control.id );
  2325.             });
  2326.             section.loaded = 0;
  2327.             section.fullyLoaded = false;
  2328.             section.screenshotQueue = null;
  2329.  
  2330.             // Run a new query, with loadThemes handling paging, etc.
  2331.             if ( ! section.loading ) {
  2332.                 section.term = newTerm;
  2333.                 section.tags = newTags;
  2334.                 section.loadThemes();
  2335.             } else {
  2336.                 section.nextTerm = newTerm; // This will reload from loadThemes() with the newest term once the current batch is loaded.
  2337.                 section.nextTags = newTags; // This will reload from loadThemes() with the newest tags once the current batch is loaded.
  2338.             }
  2339.             if ( ! section.expanded() ) {
  2340.                 section.expand(); // Expand the section if it isn't expanded.
  2341.             }
  2342.         },
  2343.  
  2344.         /**
  2345.          * Render control's screenshot if the control comes into view.
  2346.          *
  2347.          * @since 4.2.0
  2348.          *
  2349.          * @returns {void}
  2350.          */
  2351.         renderScreenshots: function() {
  2352.             var section = this;
  2353.  
  2354.             // Fill queue initially, or check for more if empty.
  2355.             if ( null === section.screenshotQueue || 0 === section.screenshotQueue.length ) {
  2356.  
  2357.                 // Add controls that haven't had their screenshots rendered.
  2358.                 section.screenshotQueue = _.filter( section.controls(), function( control ) {
  2359.                     return ! control.screenshotRendered;
  2360.                 });
  2361.             }
  2362.  
  2363.             // Are all screenshots rendered (for now)?
  2364.             if ( ! section.screenshotQueue.length ) {
  2365.                 return;
  2366.             }
  2367.  
  2368.             section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
  2369.                 var $imageWrapper = control.container.find( '.theme-screenshot' ),
  2370.                     $image = $imageWrapper.find( 'img' );
  2371.  
  2372.                 if ( ! $image.length ) {
  2373.                     return false;
  2374.                 }
  2375.  
  2376.                 if ( $image.is( ':hidden' ) ) {
  2377.                     return true;
  2378.                 }
  2379.  
  2380.                 // Based on unveil.js.
  2381.                 var wt = section.$window.scrollTop(),
  2382.                     wb = wt + section.$window.height(),
  2383.                     et = $image.offset().top,
  2384.                     ih = $imageWrapper.height(),
  2385.                     eb = et + ih,
  2386.                     threshold = ih * 3,
  2387.                     inView = eb >= wt - threshold && et <= wb + threshold;
  2388.  
  2389.                 if ( inView ) {
  2390.                     control.container.trigger( 'render-screenshot' );
  2391.                 }
  2392.  
  2393.                 // If the image is in view return false so it's cleared from the queue.
  2394.                 return ! inView;
  2395.             } );
  2396.         },
  2397.  
  2398.         /**
  2399.          * Get visible count.
  2400.          *
  2401.          * @since 4.9.0
  2402.          *
  2403.          * @returns {int} Visible count.
  2404.          */
  2405.         getVisibleCount: function() {
  2406.             return this.contentContainer.find( 'li.customize-control:visible' ).length;
  2407.         },
  2408.  
  2409.         /**
  2410.          * Update the number of themes in the section.
  2411.          *
  2412.          * @since 4.9.0
  2413.          *
  2414.          * @returns {void}
  2415.          */
  2416.         updateCount: function( count ) {
  2417.             var section = this, countEl, displayed;
  2418.  
  2419.             if ( ! count && 0 !== count ) {
  2420.                 count = section.getVisibleCount();
  2421.             }
  2422.  
  2423.             displayed = section.contentContainer.find( '.themes-displayed' );
  2424.             countEl = section.contentContainer.find( '.theme-count' );
  2425.  
  2426.             if ( 0 === count ) {
  2427.                 countEl.text( '0' );
  2428.             } else {
  2429.  
  2430.                 // Animate the count change for emphasis.
  2431.                 displayed.fadeOut( 180, function() {
  2432.                     countEl.text( count );
  2433.                     displayed.fadeIn( 180 );
  2434.                 } );
  2435.                 wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) );
  2436.             }
  2437.         },
  2438.  
  2439.         /**
  2440.          * Advance the modal to the next theme.
  2441.          *
  2442.          * @since 4.2.0
  2443.          *
  2444.          * @returns {void}
  2445.          */
  2446.         nextTheme: function () {
  2447.             var section = this;
  2448.             if ( section.getNextTheme() ) {
  2449.                 section.showDetails( section.getNextTheme(), function() {
  2450.                     section.overlay.find( '.right' ).focus();
  2451.                 } );
  2452.             }
  2453.         },
  2454.  
  2455.         /**
  2456.          * Get the next theme model.
  2457.          *
  2458.          * @since 4.2.0
  2459.          *
  2460.          * @returns {wp.customize.ThemeControl|boolean} Next theme.
  2461.          */
  2462.         getNextTheme: function () {
  2463.             var section = this, control, nextControl, sectionControls, i;
  2464.             control = api.control( section.params.action + '_theme_' + section.currentTheme );
  2465.             sectionControls = section.controls();
  2466.             i = _.indexOf( sectionControls, control );
  2467.             if ( -1 === i ) {
  2468.                 return false;
  2469.             }
  2470.  
  2471.             nextControl = sectionControls[ i + 1 ];
  2472.             if ( ! nextControl ) {
  2473.                 return false;
  2474.             }
  2475.             return nextControl.params.theme;
  2476.         },
  2477.  
  2478.         /**
  2479.          * Advance the modal to the previous theme.
  2480.          *
  2481.          * @since 4.2.0
  2482.          * @returns {void}
  2483.          */
  2484.         previousTheme: function () {
  2485.             var section = this;
  2486.             if ( section.getPreviousTheme() ) {
  2487.                 section.showDetails( section.getPreviousTheme(), function() {
  2488.                     section.overlay.find( '.left' ).focus();
  2489.                 } );
  2490.             }
  2491.         },
  2492.  
  2493.         /**
  2494.          * Get the previous theme model.
  2495.          *
  2496.          * @since 4.2.0
  2497.          * @returns {wp.customize.ThemeControl|boolean} Previous theme.
  2498.          */
  2499.         getPreviousTheme: function () {
  2500.             var section = this, control, nextControl, sectionControls, i;
  2501.             control = api.control( section.params.action + '_theme_' + section.currentTheme );
  2502.             sectionControls = section.controls();
  2503.             i = _.indexOf( sectionControls, control );
  2504.             if ( -1 === i ) {
  2505.                 return false;
  2506.             }
  2507.  
  2508.             nextControl = sectionControls[ i - 1 ];
  2509.             if ( ! nextControl ) {
  2510.                 return false;
  2511.             }
  2512.             return nextControl.params.theme;
  2513.         },
  2514.  
  2515.         /**
  2516.          * Disable buttons when we're viewing the first or last theme.
  2517.          *
  2518.          * @since 4.2.0
  2519.          *
  2520.          * @returns {void}
  2521.          */
  2522.         updateLimits: function () {
  2523.             if ( ! this.getNextTheme() ) {
  2524.                 this.overlay.find( '.right' ).addClass( 'disabled' );
  2525.             }
  2526.             if ( ! this.getPreviousTheme() ) {
  2527.                 this.overlay.find( '.left' ).addClass( 'disabled' );
  2528.             }
  2529.         },
  2530.  
  2531.         /**
  2532.          * Load theme preview.
  2533.          *
  2534.          * @since 4.7.0
  2535.          * @access public
  2536.          *
  2537.          * @deprecated
  2538.          * @param {string} themeId Theme ID.
  2539.          * @returns {jQuery.promise} Promise.
  2540.          */
  2541.         loadThemePreview: function( themeId ) {
  2542.             return api.ThemesPanel.prototype.loadThemePreview.call( this, themeId );
  2543.         },
  2544.  
  2545.         /**
  2546.          * Render & show the theme details for a given theme model.
  2547.          *
  2548.          * @since 4.2.0
  2549.          *
  2550.          * @param {object} theme - Theme.
  2551.          * @param {Function} [callback] - Callback once the details have been shown.
  2552.          * @returns {void}
  2553.          */
  2554.         showDetails: function ( theme, callback ) {
  2555.             var section = this, panel = api.panel( 'themes' );
  2556.             section.currentTheme = theme.id;
  2557.             section.overlay.html( section.template( theme ) )
  2558.                 .fadeIn( 'fast' )
  2559.                 .focus();
  2560.  
  2561.             function disableSwitchButtons() {
  2562.                 return ! panel.canSwitchTheme( theme.id );
  2563.             }
  2564.  
  2565.             // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
  2566.             function disableInstallButtons() {
  2567.                 return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
  2568.             }
  2569.  
  2570.             section.overlay.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
  2571.             section.overlay.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
  2572.  
  2573.             section.$body.addClass( 'modal-open' );
  2574.             section.containFocus( section.overlay );
  2575.             section.updateLimits();
  2576.             wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );
  2577.             if ( callback ) {
  2578.                 callback();
  2579.             }
  2580.         },
  2581.  
  2582.         /**
  2583.          * Close the theme details modal.
  2584.          *
  2585.          * @since 4.2.0
  2586.          *
  2587.          * @returns {void}
  2588.          */
  2589.         closeDetails: function () {
  2590.             var section = this;
  2591.             section.$body.removeClass( 'modal-open' );
  2592.             section.overlay.fadeOut( 'fast' );
  2593.             api.control( section.params.action + '_theme_' + section.currentTheme ).container.find( '.theme' ).focus();
  2594.         },
  2595.  
  2596.         /**
  2597.          * Keep tab focus within the theme details modal.
  2598.          *
  2599.          * @since 4.2.0
  2600.          *
  2601.          * @param {jQuery} el - Element to contain focus.
  2602.          * @returns {void}
  2603.          */
  2604.         containFocus: function( el ) {
  2605.             var tabbables;
  2606.  
  2607.             el.on( 'keydown', function( event ) {
  2608.  
  2609.                 // Return if it's not the tab key
  2610.                 // When navigating with prev/next focus is already handled
  2611.                 if ( 9 !== event.keyCode ) {
  2612.                     return;
  2613.                 }
  2614.  
  2615.                 // uses jQuery UI to get the tabbable elements
  2616.                 tabbables = $( ':tabbable', el );
  2617.  
  2618.                 // Keep focus within the overlay
  2619.                 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
  2620.                     tabbables.first().focus();
  2621.                     return false;
  2622.                 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
  2623.                     tabbables.last().focus();
  2624.                     return false;
  2625.                 }
  2626.             });
  2627.         }
  2628.     });
  2629.  
  2630.     /**
  2631.      * Class wp.customize.OuterSection.
  2632.      *
  2633.      * Creates section outside of the sidebar, there is no ui to trigger collapse/expand so
  2634.      * it would require custom handling.
  2635.      *
  2636.      * @since 4.9
  2637.      *
  2638.      * @constructor
  2639.      * @augments wp.customize.Section
  2640.      * @augments wp.customize.Container
  2641.      */
  2642.     api.OuterSection = api.Section.extend({
  2643.  
  2644.         /**
  2645.          * Initialize.
  2646.          *
  2647.          * @since 4.9.0
  2648.          *
  2649.          * @returns {void}
  2650.          */
  2651.         initialize: function() {
  2652.             var section = this;
  2653.             section.containerParent = '#customize-outer-theme-controls';
  2654.             section.containerPaneParent = '.customize-outer-pane-parent';
  2655.             api.Section.prototype.initialize.apply( section, arguments );
  2656.         },
  2657.  
  2658.         /**
  2659.          * Overrides api.Section.prototype.onChangeExpanded to prevent collapse/expand effect
  2660.          * on other sections and panels.
  2661.          *
  2662.          * @since 4.9.0
  2663.          *
  2664.          * @param {Boolean}  expanded - The expanded state to transition to.
  2665.          * @param {Object}   [args] - Args.
  2666.          * @param {boolean}  [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
  2667.          * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
  2668.          * @param {Object}   [args.duration] - The duration for the animation.
  2669.          */
  2670.         onChangeExpanded: function( expanded, args ) {
  2671.             var section = this,
  2672.                 container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
  2673.                 content = section.contentContainer,
  2674.                 backBtn = content.find( '.customize-section-back' ),
  2675.                 sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
  2676.                 body = $( document.body ),
  2677.                 expand, panel;
  2678.  
  2679.             body.toggleClass( 'outer-section-open', expanded );
  2680.             section.container.toggleClass( 'open', expanded );
  2681.             section.container.removeClass( 'busy' );
  2682.             api.section.each( function( _section ) {
  2683.                 if ( 'outer' === _section.params.type && _section.id !== section.id ) {
  2684.                     _section.container.removeClass( 'open' );
  2685.                 }
  2686.             } );
  2687.  
  2688.             if ( expanded && ! content.hasClass( 'open' ) ) {
  2689.  
  2690.                 if ( args.unchanged ) {
  2691.                     expand = args.completeCallback;
  2692.                 } else {
  2693.                     expand = $.proxy( function() {
  2694.                         section._animateChangeExpanded( function() {
  2695.                             sectionTitle.attr( 'tabindex', '-1' );
  2696.                             backBtn.attr( 'tabindex', '0' );
  2697.  
  2698.                             backBtn.focus();
  2699.                             content.css( 'top', '' );
  2700.                             container.scrollTop( 0 );
  2701.  
  2702.                             if ( args.completeCallback ) {
  2703.                                 args.completeCallback();
  2704.                             }
  2705.                         } );
  2706.  
  2707.                         content.addClass( 'open' );
  2708.                     }, this );
  2709.                 }
  2710.  
  2711.                 if ( section.panel() ) {
  2712.                     api.panel( section.panel() ).expand({
  2713.                         duration: args.duration,
  2714.                         completeCallback: expand
  2715.                     });
  2716.                 } else {
  2717.                     expand();
  2718.                 }
  2719.  
  2720.             } else if ( ! expanded && content.hasClass( 'open' ) ) {
  2721.                 if ( section.panel() ) {
  2722.                     panel = api.panel( section.panel() );
  2723.                     if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
  2724.                         panel.collapse();
  2725.                     }
  2726.                 }
  2727.                 section._animateChangeExpanded( function() {
  2728.                     backBtn.attr( 'tabindex', '-1' );
  2729.                     sectionTitle.attr( 'tabindex', '0' );
  2730.  
  2731.                     sectionTitle.focus();
  2732.                     content.css( 'top', '' );
  2733.  
  2734.                     if ( args.completeCallback ) {
  2735.                         args.completeCallback();
  2736.                     }
  2737.                 } );
  2738.  
  2739.                 content.removeClass( 'open' );
  2740.  
  2741.             } else {
  2742.                 if ( args.completeCallback ) {
  2743.                     args.completeCallback();
  2744.                 }
  2745.             }
  2746.         }
  2747.     });
  2748.  
  2749.     /**
  2750.      * @since 4.1.0
  2751.      *
  2752.      * @class
  2753.      * @augments wp.customize.Class
  2754.      */
  2755.     api.Panel = Container.extend({
  2756.         containerType: 'panel',
  2757.  
  2758.         /**
  2759.          * @since 4.1.0
  2760.          *
  2761.          * @param {string}         id - The ID for the panel.
  2762.          * @param {object}         options - Object containing one property: params.
  2763.          * @param {string}         options.title - Title shown when panel is collapsed and expanded.
  2764.          * @param {string=}        [options.description] - Description shown at the top of the panel.
  2765.          * @param {number=100}     [options.priority] - The sort priority for the panel.
  2766.          * @param {string=default} [options.type] - The type of the panel. See wp.customize.panelConstructor.
  2767.          * @param {string=}        [options.content] - The markup to be used for the panel container. If empty, a JS template is used.
  2768.          * @param {boolean=true}   [options.active] - Whether the panel is active or not.
  2769.          * @param {object}         [options.params] - Deprecated wrapper for the above properties.
  2770.          */
  2771.         initialize: function ( id, options ) {
  2772.             var panel = this, params;
  2773.             params = options.params || options;
  2774.  
  2775.             // Look up the type if one was not supplied.
  2776.             if ( ! params.type ) {
  2777.                 _.find( api.panelConstructor, function( Constructor, type ) {
  2778.                     if ( Constructor === panel.constructor ) {
  2779.                         params.type = type;
  2780.                         return true;
  2781.                     }
  2782.                     return false;
  2783.                 } );
  2784.             }
  2785.  
  2786.             Container.prototype.initialize.call( panel, id, params );
  2787.  
  2788.             panel.embed();
  2789.             panel.deferred.embedded.done( function () {
  2790.                 panel.ready();
  2791.             });
  2792.         },
  2793.  
  2794.         /**
  2795.          * Embed the container in the DOM when any parent panel is ready.
  2796.          *
  2797.          * @since 4.1.0
  2798.          */
  2799.         embed: function () {
  2800.             var panel = this,
  2801.                 container = $( '#customize-theme-controls' ),
  2802.                 parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
  2803.  
  2804.             if ( ! panel.headContainer.parent().is( parentContainer ) ) {
  2805.                 parentContainer.append( panel.headContainer );
  2806.             }
  2807.             if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
  2808.                 container.append( panel.contentContainer );
  2809.             }
  2810.             panel.renderContent();
  2811.  
  2812.             panel.deferred.embedded.resolve();
  2813.         },
  2814.  
  2815.         /**
  2816.          * @since 4.1.0
  2817.          */
  2818.         attachEvents: function () {
  2819.             var meta, panel = this;
  2820.  
  2821.             // Expand/Collapse accordion sections on click.
  2822.             panel.headContainer.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
  2823.                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2824.                     return;
  2825.                 }
  2826.                 event.preventDefault(); // Keep this AFTER the key filter above
  2827.  
  2828.                 if ( ! panel.expanded() ) {
  2829.                     panel.expand();
  2830.                 }
  2831.             });
  2832.  
  2833.             // Close panel.
  2834.             panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
  2835.                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  2836.                     return;
  2837.                 }
  2838.                 event.preventDefault(); // Keep this AFTER the key filter above
  2839.  
  2840.                 if ( panel.expanded() ) {
  2841.                     panel.collapse();
  2842.                 }
  2843.             });
  2844.  
  2845.             meta = panel.container.find( '.panel-meta:first' );
  2846.  
  2847.             meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
  2848.                 if ( meta.hasClass( 'cannot-expand' ) ) {
  2849.                     return;
  2850.                 }
  2851.  
  2852.                 var content = meta.find( '.customize-panel-description:first' );
  2853.                 if ( meta.hasClass( 'open' ) ) {
  2854.                     meta.toggleClass( 'open' );
  2855.                     content.slideUp( panel.defaultExpandedArguments.duration, function() {
  2856.                         content.trigger( 'toggled' );
  2857.                     } );
  2858.                     $( this ).attr( 'aria-expanded', false );
  2859.                 } else {
  2860.                     content.slideDown( panel.defaultExpandedArguments.duration, function() {
  2861.                         content.trigger( 'toggled' );
  2862.                     } );
  2863.                     meta.toggleClass( 'open' );
  2864.                     $( this ).attr( 'aria-expanded', true );
  2865.                 }
  2866.             });
  2867.  
  2868.         },
  2869.  
  2870.         /**
  2871.          * Get the sections that are associated with this panel, sorted by their priority Value.
  2872.          *
  2873.          * @since 4.1.0
  2874.          *
  2875.          * @returns {Array}
  2876.          */
  2877.         sections: function () {
  2878.             return this._children( 'panel', 'section' );
  2879.         },
  2880.  
  2881.         /**
  2882.          * Return whether this panel has any active sections.
  2883.          *
  2884.          * @since 4.1.0
  2885.          *
  2886.          * @returns {boolean} Whether contextually active.
  2887.          */
  2888.         isContextuallyActive: function () {
  2889.             var panel = this,
  2890.                 sections = panel.sections(),
  2891.                 activeCount = 0;
  2892.             _( sections ).each( function ( section ) {
  2893.                 if ( section.active() && section.isContextuallyActive() ) {
  2894.                     activeCount += 1;
  2895.                 }
  2896.             } );
  2897.             return ( activeCount !== 0 );
  2898.         },
  2899.  
  2900.         /**
  2901.          * Update UI to reflect expanded state.
  2902.          *
  2903.          * @since 4.1.0
  2904.          *
  2905.          * @param {Boolean}  expanded
  2906.          * @param {Object}   args
  2907.          * @param {Boolean}  args.unchanged
  2908.          * @param {Function} args.completeCallback
  2909.          * @returns {void}
  2910.          */
  2911.         onChangeExpanded: function ( expanded, args ) {
  2912.  
  2913.             // Immediately call the complete callback if there were no changes
  2914.             if ( args.unchanged ) {
  2915.                 if ( args.completeCallback ) {
  2916.                     args.completeCallback();
  2917.                 }
  2918.                 return;
  2919.             }
  2920.  
  2921.             // Note: there is a second argument 'args' passed
  2922.             var panel = this,
  2923.                 accordionSection = panel.contentContainer,
  2924.                 overlay = accordionSection.closest( '.wp-full-overlay' ),
  2925.                 container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
  2926.                 topPanel = panel.headContainer.find( '.accordion-section-title' ),
  2927.                 backBtn = accordionSection.find( '.customize-panel-back' ),
  2928.                 childSections = panel.sections(),
  2929.                 skipTransition;
  2930.  
  2931.             if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) {
  2932.                 // Collapse any sibling sections/panels
  2933.                 api.section.each( function ( section ) {
  2934.                     if ( panel.id !== section.panel() ) {
  2935.                         section.collapse( { duration: 0 } );
  2936.                     }
  2937.                 });
  2938.                 api.panel.each( function ( otherPanel ) {
  2939.                     if ( panel !== otherPanel ) {
  2940.                         otherPanel.collapse( { duration: 0 } );
  2941.                     }
  2942.                 });
  2943.  
  2944.                 if ( panel.params.autoExpandSoleSection && 1 === childSections.length && childSections[0].active.get() ) {
  2945.                     accordionSection.addClass( 'current-panel skip-transition' );
  2946.                     overlay.addClass( 'in-sub-panel' );
  2947.  
  2948.                     childSections[0].expand( {
  2949.                         completeCallback: args.completeCallback
  2950.                     } );
  2951.                 } else {
  2952.                     panel._animateChangeExpanded( function() {
  2953.                         topPanel.attr( 'tabindex', '-1' );
  2954.                         backBtn.attr( 'tabindex', '0' );
  2955.  
  2956.                         backBtn.focus();
  2957.                         accordionSection.css( 'top', '' );
  2958.                         container.scrollTop( 0 );
  2959.  
  2960.                         if ( args.completeCallback ) {
  2961.                             args.completeCallback();
  2962.                         }
  2963.                     } );
  2964.  
  2965.                     accordionSection.addClass( 'current-panel' );
  2966.                     overlay.addClass( 'in-sub-panel' );
  2967.                 }
  2968.  
  2969.                 api.state( 'expandedPanel' ).set( panel );
  2970.  
  2971.             } else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) {
  2972.                 skipTransition = accordionSection.hasClass( 'skip-transition' );
  2973.                 if ( ! skipTransition ) {
  2974.                     panel._animateChangeExpanded( function() {
  2975.                         topPanel.attr( 'tabindex', '0' );
  2976.                         backBtn.attr( 'tabindex', '-1' );
  2977.  
  2978.                         topPanel.focus();
  2979.                         accordionSection.css( 'top', '' );
  2980.  
  2981.                         if ( args.completeCallback ) {
  2982.                             args.completeCallback();
  2983.                         }
  2984.                     } );
  2985.                 } else {
  2986.                     accordionSection.removeClass( 'skip-transition' );
  2987.                 }
  2988.  
  2989.                 overlay.removeClass( 'in-sub-panel' );
  2990.                 accordionSection.removeClass( 'current-panel' );
  2991.                 if ( panel === api.state( 'expandedPanel' ).get() ) {
  2992.                     api.state( 'expandedPanel' ).set( false );
  2993.                 }
  2994.             }
  2995.         },
  2996.  
  2997.         /**
  2998.          * Render the panel from its JS template, if it exists.
  2999.          *
  3000.          * The panel's container must already exist in the DOM.
  3001.          *
  3002.          * @since 4.3.0
  3003.          */
  3004.         renderContent: function () {
  3005.             var template,
  3006.                 panel = this;
  3007.  
  3008.             // Add the content to the container.
  3009.             if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
  3010.                 template = wp.template( panel.templateSelector + '-content' );
  3011.             } else {
  3012.                 template = wp.template( 'customize-panel-default-content' );
  3013.             }
  3014.             if ( template && panel.headContainer ) {
  3015.                 panel.contentContainer.html( template( _.extend(
  3016.                     { id: panel.id },
  3017.                     panel.params
  3018.                 ) ) );
  3019.             }
  3020.         }
  3021.     });
  3022.  
  3023.     /**
  3024.      * Class wp.customize.ThemesPanel.
  3025.      *
  3026.      * Custom section for themes that displays without the customize preview.
  3027.      *
  3028.      * @constructor
  3029.      * @augments wp.customize.Panel
  3030.      * @augments wp.customize.Container
  3031.      */
  3032.     api.ThemesPanel = api.Panel.extend({
  3033.  
  3034.         /**
  3035.          * Initialize.
  3036.          *
  3037.          * @since 4.9.0
  3038.          *
  3039.          * @param {string} id - The ID for the panel.
  3040.          * @param {object} options - Options.
  3041.          * @returns {void}
  3042.          */
  3043.         initialize: function( id, options ) {
  3044.             var panel = this;
  3045.             panel.installingThemes = [];
  3046.             api.Panel.prototype.initialize.call( panel, id, options );
  3047.         },
  3048.  
  3049.         /**
  3050.          * Determine whether a given theme can be switched to, or in general.
  3051.          *
  3052.          * @since 4.9.0
  3053.          *
  3054.          * @param {string} [slug] - Theme slug.
  3055.          * @returns {boolean} Whether the theme can be switched to.
  3056.          */
  3057.         canSwitchTheme: function canSwitchTheme( slug ) {
  3058.             if ( slug && slug === api.settings.theme.stylesheet ) {
  3059.                 return true;
  3060.             }
  3061.             return 'publish' === api.state( 'selectedChangesetStatus' ).get() && ( '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get() );
  3062.         },
  3063.  
  3064.         /**
  3065.          * Attach events.
  3066.          *
  3067.          * @since 4.9.0
  3068.          * @returns {void}
  3069.          */
  3070.         attachEvents: function() {
  3071.             var panel = this;
  3072.  
  3073.             // Attach regular panel events.
  3074.             api.Panel.prototype.attachEvents.apply( panel );
  3075.  
  3076.             // Temporary since supplying SFTP credentials does not work yet. See #42184
  3077.             if ( api.settings.theme._canInstall && api.settings.theme._filesystemCredentialsNeeded ) {
  3078.                 panel.notifications.add( new api.Notification( 'theme_install_unavailable', {
  3079.                     message: api.l10n.themeInstallUnavailable,
  3080.                     type: 'info',
  3081.                     dismissible: true
  3082.                 } ) );
  3083.             }
  3084.  
  3085.             function toggleDisabledNotifications() {
  3086.                 if ( panel.canSwitchTheme() ) {
  3087.                     panel.notifications.remove( 'theme_switch_unavailable' );
  3088.                 } else {
  3089.                     panel.notifications.add( new api.Notification( 'theme_switch_unavailable', {
  3090.                         message: api.l10n.themePreviewUnavailable,
  3091.                         type: 'warning'
  3092.                     } ) );
  3093.                 }
  3094.             }
  3095.             toggleDisabledNotifications();
  3096.             api.state( 'selectedChangesetStatus' ).bind( toggleDisabledNotifications );
  3097.             api.state( 'changesetStatus' ).bind( toggleDisabledNotifications );
  3098.  
  3099.             // Collapse panel to customize the current theme.
  3100.             panel.contentContainer.on( 'click', '.customize-theme', function() {
  3101.                 panel.collapse();
  3102.             });
  3103.  
  3104.             // Toggle between filtering and browsing themes on mobile.
  3105.             panel.contentContainer.on( 'click', '.customize-themes-section-title, .customize-themes-mobile-back', function() {
  3106.                 $( '.wp-full-overlay' ).toggleClass( 'showing-themes' );
  3107.             });
  3108.  
  3109.             // Install (and maybe preview) a theme.
  3110.             panel.contentContainer.on( 'click', '.theme-install', function( event ) {
  3111.                 panel.installTheme( event );
  3112.             });
  3113.  
  3114.             // Update a theme. Theme cards have the class, the details modal has the id.
  3115.             panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) {
  3116.  
  3117.                 // #update-theme is a link.
  3118.                 event.preventDefault();
  3119.                 event.stopPropagation();
  3120.  
  3121.                 panel.updateTheme( event );
  3122.             });
  3123.  
  3124.             // Delete a theme.
  3125.             panel.contentContainer.on( 'click', '.delete-theme', function( event ) {
  3126.                 panel.deleteTheme( event );
  3127.             });
  3128.  
  3129.             _.bindAll( panel, 'installTheme', 'updateTheme' );
  3130.         },
  3131.  
  3132.         /**
  3133.          * Update UI to reflect expanded state
  3134.          *
  3135.          * @since 4.9.0
  3136.          *
  3137.          * @param {Boolean}  expanded - Expanded state.
  3138.          * @param {Object}   args - Args.
  3139.          * @param {Boolean}  args.unchanged - Whether or not the state changed.
  3140.          * @param {Function} args.completeCallback - Callback to execute when the animation completes.
  3141.          * @returns {void}
  3142.          */
  3143.         onChangeExpanded: function( expanded, args ) {
  3144.             var panel = this, overlay, sections, hasExpandedSection = false;
  3145.  
  3146.             // Expand/collapse the panel normally.
  3147.             api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] );
  3148.  
  3149.             // Immediately call the complete callback if there were no changes
  3150.             if ( args.unchanged ) {
  3151.                 if ( args.completeCallback ) {
  3152.                     args.completeCallback();
  3153.                 }
  3154.                 return;
  3155.             }
  3156.  
  3157.             overlay = panel.headContainer.closest( '.wp-full-overlay' );
  3158.  
  3159.             if ( expanded ) {
  3160.                 overlay
  3161.                     .addClass( 'in-themes-panel' )
  3162.                     .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' );
  3163.  
  3164.                 _.delay( function() {
  3165.                     overlay.addClass( 'themes-panel-expanded' );
  3166.                 }, 200 );
  3167.  
  3168.                 // Automatically open the first section (except on small screens), if one isn't already expanded.
  3169.                 if ( 600 < window.innerWidth ) {
  3170.                     sections = panel.sections();
  3171.                     _.each( sections, function( section ) {
  3172.                         if ( section.expanded() ) {
  3173.                             hasExpandedSection = true;
  3174.                         }
  3175.                     } );
  3176.                     if ( ! hasExpandedSection && sections.length > 0 ) {
  3177.                         sections[0].expand();
  3178.                     }
  3179.                 }
  3180.             } else {
  3181.                 overlay
  3182.                     .removeClass( 'in-themes-panel themes-panel-expanded' )
  3183.                     .find( '.customize-themes-full-container' ).removeClass( 'animate' );
  3184.             }
  3185.         },
  3186.  
  3187.         /**
  3188.          * Install a theme via wp.updates.
  3189.          *
  3190.          * @since 4.9.0
  3191.          *
  3192.          * @param {jQuery.Event} event - Event.
  3193.          * @returns {jQuery.promise} Promise.
  3194.          */
  3195.         installTheme: function( event ) {
  3196.             var panel = this, preview, onInstallSuccess, slug = $( event.target ).data( 'slug' ), deferred = $.Deferred(), request;
  3197.             preview = $( event.target ).hasClass( 'preview' );
  3198.  
  3199.             // Temporary since supplying SFTP credentials does not work yet. See #42184.
  3200.             if ( api.settings.theme._filesystemCredentialsNeeded ) {
  3201.                 deferred.reject({
  3202.                     errorCode: 'theme_install_unavailable'
  3203.                 });
  3204.                 return deferred.promise();
  3205.             }
  3206.  
  3207.             // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
  3208.             if ( ! panel.canSwitchTheme( slug ) ) {
  3209.                 deferred.reject({
  3210.                     errorCode: 'theme_switch_unavailable'
  3211.                 });
  3212.                 return deferred.promise();
  3213.             }
  3214.  
  3215.             // Theme is already being installed.
  3216.             if ( _.contains( panel.installingThemes, slug ) ) {
  3217.                 deferred.reject({
  3218.                     errorCode: 'theme_already_installing'
  3219.                 });
  3220.                 return deferred.promise();
  3221.             }
  3222.  
  3223.             wp.updates.maybeRequestFilesystemCredentials( event );
  3224.  
  3225.             onInstallSuccess = function( response ) {
  3226.                 var theme = false, themeControl;
  3227.                 if ( preview ) {
  3228.                     api.notifications.remove( 'theme_installing' );
  3229.  
  3230.                     panel.loadThemePreview( slug );
  3231.  
  3232.                 } else {
  3233.                     api.control.each( function( control ) {
  3234.                         if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
  3235.                             theme = control.params.theme; // Used below to add theme control.
  3236.                             control.rerenderAsInstalled( true );
  3237.                         }
  3238.                     });
  3239.  
  3240.                     // Don't add the same theme more than once.
  3241.                     if ( ! theme || api.control.has( 'installed_theme_' + theme.id ) ) {
  3242.                         deferred.resolve( response );
  3243.                         return;
  3244.                     }
  3245.  
  3246.                     // Add theme control to installed section.
  3247.                     theme.type = 'installed';
  3248.                     themeControl = new api.controlConstructor.theme( 'installed_theme_' + theme.id, {
  3249.                         type: 'theme',
  3250.                         section: 'installed_themes',
  3251.                         theme: theme,
  3252.                         priority: 0 // Add all newly-installed themes to the top.
  3253.                     } );
  3254.  
  3255.                     api.control.add( themeControl );
  3256.                     api.control( themeControl.id ).container.trigger( 'render-screenshot' );
  3257.  
  3258.                     // Close the details modal if it's open to the installed theme.
  3259.                     api.section.each( function( section ) {
  3260.                         if ( 'themes' === section.params.type ) {
  3261.                             if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
  3262.                                 section.closeDetails();
  3263.                             }
  3264.                         }
  3265.                     });
  3266.                 }
  3267.                 deferred.resolve( response );
  3268.             };
  3269.  
  3270.             panel.installingThemes.push( slug ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
  3271.             request = wp.updates.installTheme( {
  3272.                 slug: slug
  3273.             } );
  3274.  
  3275.             // Also preview the theme as the event is triggered on Install & Preview.
  3276.             if ( preview ) {
  3277.                 api.notifications.add( new api.OverlayNotification( 'theme_installing', {
  3278.                     message: api.l10n.themeDownloading,
  3279.                     type: 'info',
  3280.                     loading: true
  3281.                 } ) );
  3282.             }
  3283.  
  3284.             request.done( onInstallSuccess );
  3285.             request.fail( function() {
  3286.                 api.notifications.remove( 'theme_installing' );
  3287.             } );
  3288.  
  3289.             return deferred.promise();
  3290.         },
  3291.  
  3292.         /**
  3293.          * Load theme preview.
  3294.          *
  3295.          * @since 4.9.0
  3296.          *
  3297.          * @param {string} themeId Theme ID.
  3298.          * @returns {jQuery.promise} Promise.
  3299.          */
  3300.         loadThemePreview: function( themeId ) {
  3301.             var panel = this, deferred = $.Deferred(), onceProcessingComplete, urlParser, queryParams;
  3302.  
  3303.             // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
  3304.             if ( ! panel.canSwitchTheme( themeId ) ) {
  3305.                 deferred.reject({
  3306.                     errorCode: 'theme_switch_unavailable'
  3307.                 });
  3308.                 return deferred.promise();
  3309.             }
  3310.  
  3311.             urlParser = document.createElement( 'a' );
  3312.             urlParser.href = location.href;
  3313.             queryParams = _.extend(
  3314.                 api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
  3315.                 {
  3316.                     theme: themeId,
  3317.                     changeset_uuid: api.settings.changeset.uuid,
  3318.                     'return': api.settings.url['return']
  3319.                 }
  3320.             );
  3321.  
  3322.             // Include autosaved param to load autosave revision without prompting user to restore it.
  3323.             if ( ! api.state( 'saved' ).get() ) {
  3324.                 queryParams.customize_autosaved = 'on';
  3325.             }
  3326.  
  3327.             urlParser.search = $.param( queryParams );
  3328.  
  3329.             // Update loading message. Everything else is handled by reloading the page.
  3330.             api.notifications.add( new api.OverlayNotification( 'theme_previewing', {
  3331.                 message: api.l10n.themePreviewWait,
  3332.                 type: 'info',
  3333.                 loading: true
  3334.             } ) );
  3335.  
  3336.             onceProcessingComplete = function() {
  3337.                 var request;
  3338.                 if ( api.state( 'processing' ).get() > 0 ) {
  3339.                     return;
  3340.                 }
  3341.  
  3342.                 api.state( 'processing' ).unbind( onceProcessingComplete );
  3343.  
  3344.                 request = api.requestChangesetUpdate( {}, { autosave: true } );
  3345.                 request.done( function() {
  3346.                     deferred.resolve();
  3347.                     $( window ).off( 'beforeunload.customize-confirm' );
  3348.                     location.replace( urlParser.href );
  3349.                 } );
  3350.                 request.fail( function() {
  3351.  
  3352.                     // @todo Show notification regarding failure.
  3353.                     api.notifications.remove( 'theme_previewing' );
  3354.  
  3355.                     deferred.reject();
  3356.                 } );
  3357.             };
  3358.  
  3359.             if ( 0 === api.state( 'processing' ).get() ) {
  3360.                 onceProcessingComplete();
  3361.             } else {
  3362.                 api.state( 'processing' ).bind( onceProcessingComplete );
  3363.             }
  3364.  
  3365.             return deferred.promise();
  3366.         },
  3367.  
  3368.         /**
  3369.          * Update a theme via wp.updates.
  3370.          *
  3371.          * @since 4.9.0
  3372.          *
  3373.          * @param {jQuery.Event} event - Event.
  3374.          * @returns {void}
  3375.          */
  3376.         updateTheme: function( event ) {
  3377.             wp.updates.maybeRequestFilesystemCredentials( event );
  3378.  
  3379.             $( document ).one( 'wp-theme-update-success', function( e, response ) {
  3380.  
  3381.                 // Rerender the control to reflect the update.
  3382.                 api.control.each( function( control ) {
  3383.                     if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
  3384.                         control.params.theme.hasUpdate = false;
  3385.                         control.params.theme.version = response.newVersion;
  3386.                         setTimeout( function() {
  3387.                             control.rerenderAsInstalled( true );
  3388.                         }, 2000 );
  3389.                     }
  3390.                 });
  3391.             } );
  3392.  
  3393.             wp.updates.updateTheme( {
  3394.                 slug: $( event.target ).closest( '.notice' ).data( 'slug' )
  3395.             } );
  3396.         },
  3397.  
  3398.         /**
  3399.          * Delete a theme via wp.updates.
  3400.          *
  3401.          * @since 4.9.0
  3402.          *
  3403.          * @param {jQuery.Event} event - Event.
  3404.          * @returns {void}
  3405.          */
  3406.         deleteTheme: function( event ) {
  3407.             var theme, section;
  3408.             theme = $( event.target ).data( 'slug' );
  3409.             section = api.section( 'installed_themes' );
  3410.  
  3411.             event.preventDefault();
  3412.  
  3413.             // Temporary since supplying SFTP credentials does not work yet. See #42184.
  3414.             if ( api.settings.theme._filesystemCredentialsNeeded ) {
  3415.                 return;
  3416.             }
  3417.  
  3418.             // Confirmation dialog for deleting a theme.
  3419.             if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) {
  3420.                 return;
  3421.             }
  3422.  
  3423.             wp.updates.maybeRequestFilesystemCredentials( event );
  3424.  
  3425.             $( document ).one( 'wp-theme-delete-success', function() {
  3426.                 var control = api.control( 'installed_theme_' + theme );
  3427.  
  3428.                 // Remove theme control.
  3429.                 control.container.remove();
  3430.                 api.control.remove( control.id );
  3431.  
  3432.                 // Update installed count.
  3433.                 section.loaded = section.loaded - 1;
  3434.                 section.updateCount();
  3435.  
  3436.                 // Rerender any other theme controls as uninstalled.
  3437.                 api.control.each( function( control ) {
  3438.                     if ( 'theme' === control.params.type && control.params.theme.id === theme ) {
  3439.                         control.rerenderAsInstalled( false );
  3440.                     }
  3441.                 });
  3442.             } );
  3443.  
  3444.             wp.updates.deleteTheme( {
  3445.                 slug: theme
  3446.             } );
  3447.  
  3448.             // Close modal and focus the section.
  3449.             section.closeDetails();
  3450.             section.focus();
  3451.         }
  3452.     });
  3453.  
  3454.     /**
  3455.      * A Customizer Control.
  3456.      *
  3457.      * A control provides a UI element that allows a user to modify a Customizer Setting.
  3458.      *
  3459.      * @see PHP class WP_Customize_Control.
  3460.      *
  3461.      * @class
  3462.      * @augments wp.customize.Class
  3463.      */
  3464.     api.Control = api.Class.extend({
  3465.         defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
  3466.  
  3467.         /**
  3468.          * Default params.
  3469.          *
  3470.          * @since 4.9.0
  3471.          * @var {object}
  3472.          */
  3473.         defaults: {
  3474.             label: '',
  3475.             description: '',
  3476.             active: true,
  3477.             priority: 10
  3478.         },
  3479.  
  3480.         /**
  3481.          * Initialize.
  3482.          *
  3483.          * @param {string} id                       - Unique identifier for the control instance.
  3484.          * @param {object} options                  - Options hash for the control instance.
  3485.          * @param {object} options.type             - Type of control (e.g. text, radio, dropdown-pages, etc.)
  3486.          * @param {string} [options.content]        - The HTML content for the control or at least its container. This should normally be left blank and instead supplying a templateId.
  3487.          * @param {string} [options.templateId]     - Template ID for control's content.
  3488.          * @param {string} [options.priority=10]    - Order of priority to show the control within the section.
  3489.          * @param {string} [options.active=true]    - Whether the control is active.
  3490.          * @param {string} options.section          - The ID of the section the control belongs to.
  3491.          * @param {mixed}  [options.setting]        - The ID of the main setting or an instance of this setting.
  3492.          * @param {mixed}  options.settings         - An object with keys (e.g. default) that maps to setting IDs or Setting/Value objects, or an array of setting IDs or Setting/Value objects.
  3493.          * @param {mixed}  options.settings.default - The ID of the setting the control relates to.
  3494.          * @param {string} options.settings.data    - @todo Is this used?
  3495.          * @param {string} options.label            - Label.
  3496.          * @param {string} options.description      - Description.
  3497.          * @param {number} [options.instanceNumber] - Order in which this instance was created in relation to other instances.
  3498.          * @param {object} [options.params]         - Deprecated wrapper for the above properties.
  3499.          * @returns {void}
  3500.          */
  3501.         initialize: function( id, options ) {
  3502.             var control = this, deferredSettingIds = [], settings, gatherSettings;
  3503.  
  3504.             control.params = _.extend(
  3505.                 {},
  3506.                 control.defaults,
  3507.                 control.params || {}, // In case sub-class already defines.
  3508.                 options.params || options || {} // The options.params property is deprecated, but it is checked first for back-compat.
  3509.             );
  3510.  
  3511.             if ( ! api.Control.instanceCounter ) {
  3512.                 api.Control.instanceCounter = 0;
  3513.             }
  3514.             api.Control.instanceCounter++;
  3515.             if ( ! control.params.instanceNumber ) {
  3516.                 control.params.instanceNumber = api.Control.instanceCounter;
  3517.             }
  3518.  
  3519.             // Look up the type if one was not supplied.
  3520.             if ( ! control.params.type ) {
  3521.                 _.find( api.controlConstructor, function( Constructor, type ) {
  3522.                     if ( Constructor === control.constructor ) {
  3523.                         control.params.type = type;
  3524.                         return true;
  3525.                     }
  3526.                     return false;
  3527.                 } );
  3528.             }
  3529.  
  3530.             if ( ! control.params.content ) {
  3531.                 control.params.content = $( '<li></li>', {
  3532.                     id: 'customize-control-' + id.replace( /]/g, '' ).replace( /\[/g, '-' ),
  3533.                     'class': 'customize-control customize-control-' + control.params.type
  3534.                 } );
  3535.             }
  3536.  
  3537.             control.id = id;
  3538.             control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); // Deprecated, likely dead code from time before #28709.
  3539.             if ( control.params.content ) {
  3540.                 control.container = $( control.params.content );
  3541.             } else {
  3542.                 control.container = $( control.selector ); // Likely dead, per above. See #28709.
  3543.             }
  3544.  
  3545.             if ( control.params.templateId ) {
  3546.                 control.templateSelector = control.params.templateId;
  3547.             } else {
  3548.                 control.templateSelector = 'customize-control-' + control.params.type + '-content';
  3549.             }
  3550.  
  3551.             control.deferred = _.extend( control.deferred || {}, {
  3552.                 embedded: new $.Deferred()
  3553.             } );
  3554.             control.section = new api.Value();
  3555.             control.priority = new api.Value();
  3556.             control.active = new api.Value();
  3557.             control.activeArgumentsQueue = [];
  3558.             control.notifications = new api.Notifications({
  3559.                 alt: control.altNotice
  3560.             });
  3561.  
  3562.             control.elements = [];
  3563.  
  3564.             control.active.bind( function ( active ) {
  3565.                 var args = control.activeArgumentsQueue.shift();
  3566.                 args = $.extend( {}, control.defaultActiveArguments, args );
  3567.                 control.onChangeActive( active, args );
  3568.             } );
  3569.  
  3570.             control.section.set( control.params.section );
  3571.             control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
  3572.             control.active.set( control.params.active );
  3573.  
  3574.             api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
  3575.  
  3576.             control.settings = {};
  3577.  
  3578.             settings = {};
  3579.             if ( control.params.setting ) {
  3580.                 settings['default'] = control.params.setting;
  3581.             }
  3582.             _.extend( settings, control.params.settings );
  3583.  
  3584.             // Note: Settings can be an array or an object, with values being either setting IDs or Setting (or Value) objects.
  3585.             _.each( settings, function( value, key ) {
  3586.                 var setting;
  3587.                 if ( _.isObject( value ) && _.isFunction( value.extended ) && value.extended( api.Value ) ) {
  3588.                     control.settings[ key ] = value;
  3589.                 } else if ( _.isString( value ) ) {
  3590.                     setting = api( value );
  3591.                     if ( setting ) {
  3592.                         control.settings[ key ] = setting;
  3593.                     } else {
  3594.                         deferredSettingIds.push( value );
  3595.                     }
  3596.                 }
  3597.             } );
  3598.  
  3599.             gatherSettings = function() {
  3600.  
  3601.                 // Fill-in all resolved settings.
  3602.                 _.each( settings, function ( settingId, key ) {
  3603.                     if ( ! control.settings[ key ] && _.isString( settingId ) ) {
  3604.                         control.settings[ key ] = api( settingId );
  3605.                     }
  3606.                 } );
  3607.  
  3608.                 // Make sure settings passed as array gets associated with default.
  3609.                 if ( control.settings[0] && ! control.settings['default'] ) {
  3610.                     control.settings['default'] = control.settings[0];
  3611.                 }
  3612.  
  3613.                 // Identify the main setting.
  3614.                 control.setting = control.settings['default'] || null;
  3615.  
  3616.                 control.linkElements(); // Link initial elements present in server-rendered content.
  3617.                 control.embed();
  3618.             };
  3619.  
  3620.             if ( 0 === deferredSettingIds.length ) {
  3621.                 gatherSettings();
  3622.             } else {
  3623.                 api.apply( api, deferredSettingIds.concat( gatherSettings ) );
  3624.             }
  3625.  
  3626.             // After the control is embedded on the page, invoke the "ready" method.
  3627.             control.deferred.embedded.done( function () {
  3628.                 control.linkElements(); // Link any additional elements after template is rendered by renderContent().
  3629.                 control.setupNotifications();
  3630.                 control.ready();
  3631.             });
  3632.         },
  3633.  
  3634.         /**
  3635.          * Link elements between settings and inputs.
  3636.          *
  3637.          * @since 4.7.0
  3638.          * @access public
  3639.          *
  3640.          * @returns {void}
  3641.          */
  3642.         linkElements: function () {
  3643.             var control = this, nodes, radios, element;
  3644.  
  3645.             nodes = control.container.find( '[data-customize-setting-link], [data-customize-setting-key-link]' );
  3646.             radios = {};
  3647.  
  3648.             nodes.each( function () {
  3649.                 var node = $( this ), name, setting;
  3650.  
  3651.                 if ( node.data( 'customizeSettingLinked' ) ) {
  3652.                     return;
  3653.                 }
  3654.                 node.data( 'customizeSettingLinked', true ); // Prevent re-linking element.
  3655.  
  3656.                 if ( node.is( ':radio' ) ) {
  3657.                     name = node.prop( 'name' );
  3658.                     if ( radios[name] ) {
  3659.                         return;
  3660.                     }
  3661.  
  3662.                     radios[name] = true;
  3663.                     node = nodes.filter( '[name="' + name + '"]' );
  3664.                 }
  3665.  
  3666.                 // Let link by default refer to setting ID. If it doesn't exist, fallback to looking up by setting key.
  3667.                 if ( node.data( 'customizeSettingLink' ) ) {
  3668.                     setting = api( node.data( 'customizeSettingLink' ) );
  3669.                 } else if ( node.data( 'customizeSettingKeyLink' ) ) {
  3670.                     setting = control.settings[ node.data( 'customizeSettingKeyLink' ) ];
  3671.                 }
  3672.  
  3673.                 if ( setting ) {
  3674.                     element = new api.Element( node );
  3675.                     control.elements.push( element );
  3676.                     element.sync( setting );
  3677.                     element.set( setting() );
  3678.                 }
  3679.             } );
  3680.         },
  3681.  
  3682.         /**
  3683.          * Embed the control into the page.
  3684.          */
  3685.         embed: function () {
  3686.             var control = this,
  3687.                 inject;
  3688.  
  3689.             // Watch for changes to the section state
  3690.             inject = function ( sectionId ) {
  3691.                 var parentContainer;
  3692.                 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the front end.
  3693.                     return;
  3694.                 }
  3695.                 // Wait for the section to be registered
  3696.                 api.section( sectionId, function ( section ) {
  3697.                     // Wait for the section to be ready/initialized
  3698.                     section.deferred.embedded.done( function () {
  3699.                         parentContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
  3700.                         if ( ! control.container.parent().is( parentContainer ) ) {
  3701.                             parentContainer.append( control.container );
  3702.                             control.renderContent();
  3703.                         }
  3704.                         control.deferred.embedded.resolve();
  3705.                     });
  3706.                 });
  3707.             };
  3708.             control.section.bind( inject );
  3709.             inject( control.section.get() );
  3710.         },
  3711.  
  3712.         /**
  3713.          * Triggered when the control's markup has been injected into the DOM.
  3714.          *
  3715.          * @returns {void}
  3716.          */
  3717.         ready: function() {
  3718.             var control = this, newItem;
  3719.             if ( 'dropdown-pages' === control.params.type && control.params.allow_addition ) {
  3720.                 newItem = control.container.find( '.new-content-item' );
  3721.                 newItem.hide(); // Hide in JS to preserve flex display when showing.
  3722.                 control.container.on( 'click', '.add-new-toggle', function( e ) {
  3723.                     $( e.currentTarget ).slideUp( 180 );
  3724.                     newItem.slideDown( 180 );
  3725.                     newItem.find( '.create-item-input' ).focus();
  3726.                 });
  3727.                 control.container.on( 'click', '.add-content', function() {
  3728.                     control.addNewPage();
  3729.                 });
  3730.                 control.container.on( 'keydown', '.create-item-input', function( e ) {
  3731.                     if ( 13 === e.which ) { // Enter
  3732.                         control.addNewPage();
  3733.                     }
  3734.                 });
  3735.             }
  3736.         },
  3737.  
  3738.         /**
  3739.          * Get the element inside of a control's container that contains the validation error message.
  3740.          *
  3741.          * Control subclasses may override this to return the proper container to render notifications into.
  3742.          * Injects the notification container for existing controls that lack the necessary container,
  3743.          * including special handling for nav menu items and widgets.
  3744.          *
  3745.          * @since 4.6.0
  3746.          * @returns {jQuery} Setting validation message element.
  3747.          * @this {wp.customize.Control}
  3748.          */
  3749.         getNotificationsContainerElement: function() {
  3750.             var control = this, controlTitle, notificationsContainer;
  3751.  
  3752.             notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
  3753.             if ( notificationsContainer.length ) {
  3754.                 return notificationsContainer;
  3755.             }
  3756.  
  3757.             notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
  3758.  
  3759.             if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
  3760.                 control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
  3761.             } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
  3762.                 control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
  3763.             } else {
  3764.                 controlTitle = control.container.find( '.customize-control-title' );
  3765.                 if ( controlTitle.length ) {
  3766.                     controlTitle.after( notificationsContainer );
  3767.                 } else {
  3768.                     control.container.prepend( notificationsContainer );
  3769.                 }
  3770.             }
  3771.             return notificationsContainer;
  3772.         },
  3773.  
  3774.         /**
  3775.          * Set up notifications.
  3776.          *
  3777.          * @since 4.9.0
  3778.          * @returns {void}
  3779.          */
  3780.         setupNotifications: function() {
  3781.             var control = this, renderNotificationsIfVisible, onSectionAssigned;
  3782.  
  3783.             // Add setting notifications to the control notification.
  3784.             _.each( control.settings, function( setting ) {
  3785.                 if ( ! setting.notifications ) {
  3786.                     return;
  3787.                 }
  3788.                 setting.notifications.bind( 'add', function( settingNotification ) {
  3789.                     var params = _.extend(
  3790.                         {},
  3791.                         settingNotification,
  3792.                         {
  3793.                             setting: setting.id
  3794.                         }
  3795.                     );
  3796.                     control.notifications.add( new api.Notification( setting.id + ':' + settingNotification.code, params ) );
  3797.                 } );
  3798.                 setting.notifications.bind( 'remove', function( settingNotification ) {
  3799.                     control.notifications.remove( setting.id + ':' + settingNotification.code );
  3800.                 } );
  3801.             } );
  3802.  
  3803.             renderNotificationsIfVisible = function() {
  3804.                 var sectionId = control.section();
  3805.                 if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
  3806.                     control.notifications.render();
  3807.                 }
  3808.             };
  3809.  
  3810.             control.notifications.bind( 'rendered', function() {
  3811.                 var notifications = control.notifications.get();
  3812.                 control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
  3813.                 control.container.toggleClass( 'has-error', 0 !== _.where( notifications, { type: 'error' } ).length );
  3814.             } );
  3815.  
  3816.             onSectionAssigned = function( newSectionId, oldSectionId ) {
  3817.                 if ( oldSectionId && api.section.has( oldSectionId ) ) {
  3818.                     api.section( oldSectionId ).expanded.unbind( renderNotificationsIfVisible );
  3819.                 }
  3820.                 if ( newSectionId ) {
  3821.                     api.section( newSectionId, function( section ) {
  3822.                         section.expanded.bind( renderNotificationsIfVisible );
  3823.                         renderNotificationsIfVisible();
  3824.                     });
  3825.                 }
  3826.             };
  3827.  
  3828.             control.section.bind( onSectionAssigned );
  3829.             onSectionAssigned( control.section.get() );
  3830.             control.notifications.bind( 'change', _.debounce( renderNotificationsIfVisible ) );
  3831.         },
  3832.  
  3833.         /**
  3834.          * Render notifications.
  3835.          *
  3836.          * Renders the `control.notifications` into the control's container.
  3837.          * Control subclasses may override this method to do their own handling
  3838.          * of rendering notifications.
  3839.          *
  3840.          * @deprecated in favor of `control.notifications.render()`
  3841.          * @since 4.6.0
  3842.          * @this {wp.customize.Control}
  3843.          */
  3844.         renderNotifications: function() {
  3845.             var control = this, container, notifications, hasError = false;
  3846.  
  3847.             if ( 'undefined' !== typeof console && console.warn ) {
  3848.                 console.warn( '[DEPRECATED] wp.customize.Control.prototype.renderNotifications() is deprecated in favor of instantating a wp.customize.Notifications and calling its render() method.' );
  3849.             }
  3850.  
  3851.             container = control.getNotificationsContainerElement();
  3852.             if ( ! container || ! container.length ) {
  3853.                 return;
  3854.             }
  3855.             notifications = [];
  3856.             control.notifications.each( function( notification ) {
  3857.                 notifications.push( notification );
  3858.                 if ( 'error' === notification.type ) {
  3859.                     hasError = true;
  3860.                 }
  3861.             } );
  3862.  
  3863.             if ( 0 === notifications.length ) {
  3864.                 container.stop().slideUp( 'fast' );
  3865.             } else {
  3866.                 container.stop().slideDown( 'fast', null, function() {
  3867.                     $( this ).css( 'height', 'auto' );
  3868.                 } );
  3869.             }
  3870.  
  3871.             if ( ! control.notificationsTemplate ) {
  3872.                 control.notificationsTemplate = wp.template( 'customize-control-notifications' );
  3873.             }
  3874.  
  3875.             control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
  3876.             control.container.toggleClass( 'has-error', hasError );
  3877.             container.empty().append( $.trim(
  3878.                 control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } )
  3879.             ) );
  3880.         },
  3881.  
  3882.         /**
  3883.          * Normal controls do not expand, so just expand its parent
  3884.          *
  3885.          * @param {Object} [params]
  3886.          */
  3887.         expand: function ( params ) {
  3888.             api.section( this.section() ).expand( params );
  3889.         },
  3890.  
  3891.         /**
  3892.          * Bring the containing section and panel into view and then
  3893.          * this control into view, focusing on the first input.
  3894.          */
  3895.         focus: focus,
  3896.  
  3897.         /**
  3898.          * Update UI in response to a change in the control's active state.
  3899.          * This does not change the active state, it merely handles the behavior
  3900.          * for when it does change.
  3901.          *
  3902.          * @since 4.1.0
  3903.          *
  3904.          * @param {Boolean}  active
  3905.          * @param {Object}   args
  3906.          * @param {Number}   args.duration
  3907.          * @param {Function} args.completeCallback
  3908.          */
  3909.         onChangeActive: function ( active, args ) {
  3910.             if ( args.unchanged ) {
  3911.                 if ( args.completeCallback ) {
  3912.                     args.completeCallback();
  3913.                 }
  3914.                 return;
  3915.             }
  3916.  
  3917.             if ( ! $.contains( document, this.container[0] ) ) {
  3918.                 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
  3919.                 this.container.toggle( active );
  3920.                 if ( args.completeCallback ) {
  3921.                     args.completeCallback();
  3922.                 }
  3923.             } else if ( active ) {
  3924.                 this.container.slideDown( args.duration, args.completeCallback );
  3925.             } else {
  3926.                 this.container.slideUp( args.duration, args.completeCallback );
  3927.             }
  3928.         },
  3929.  
  3930.         /**
  3931.          * @deprecated 4.1.0 Use this.onChangeActive() instead.
  3932.          */
  3933.         toggle: function ( active ) {
  3934.             return this.onChangeActive( active, this.defaultActiveArguments );
  3935.         },
  3936.  
  3937.         /**
  3938.          * Shorthand way to enable the active state.
  3939.          *
  3940.          * @since 4.1.0
  3941.          *
  3942.          * @param {Object} [params]
  3943.          * @returns {Boolean} false if already active
  3944.          */
  3945.         activate: Container.prototype.activate,
  3946.  
  3947.         /**
  3948.          * Shorthand way to disable the active state.
  3949.          *
  3950.          * @since 4.1.0
  3951.          *
  3952.          * @param {Object} [params]
  3953.          * @returns {Boolean} false if already inactive
  3954.          */
  3955.         deactivate: Container.prototype.deactivate,
  3956.  
  3957.         /**
  3958.          * Re-use _toggleActive from Container class.
  3959.          *
  3960.          * @access private
  3961.          */
  3962.         _toggleActive: Container.prototype._toggleActive,
  3963.  
  3964.         // @todo This function appears to be dead code and can be removed.
  3965.         dropdownInit: function() {
  3966.             var control      = this,
  3967.                 statuses     = this.container.find('.dropdown-status'),
  3968.                 params       = this.params,
  3969.                 toggleFreeze = false,
  3970.                 update       = function( to ) {
  3971.                     if ( 'string' === typeof to && params.statuses && params.statuses[ to ] ) {
  3972.                         statuses.html( params.statuses[ to ] ).show();
  3973.                     } else {
  3974.                         statuses.hide();
  3975.                     }
  3976.                 };
  3977.  
  3978.             // Support the .dropdown class to open/close complex elements
  3979.             this.container.on( 'click keydown', '.dropdown', function( event ) {
  3980.                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  3981.                     return;
  3982.                 }
  3983.  
  3984.                 event.preventDefault();
  3985.  
  3986.                 if ( ! toggleFreeze ) {
  3987.                     control.container.toggleClass( 'open' );
  3988.                 }
  3989.  
  3990.                 if ( control.container.hasClass( 'open' ) ) {
  3991.                     control.container.parent().parent().find( 'li.library-selected' ).focus();
  3992.                 }
  3993.  
  3994.                 // Don't want to fire focus and click at same time
  3995.                 toggleFreeze = true;
  3996.                 setTimeout(function () {
  3997.                     toggleFreeze = false;
  3998.                 }, 400);
  3999.             });
  4000.  
  4001.             this.setting.bind( update );
  4002.             update( this.setting() );
  4003.         },
  4004.  
  4005.         /**
  4006.          * Render the control from its JS template, if it exists.
  4007.          *
  4008.          * The control's container must already exist in the DOM.
  4009.          *
  4010.          * @since 4.1.0
  4011.          */
  4012.         renderContent: function () {
  4013.             var control = this, template, standardTypes, templateId, sectionId;
  4014.  
  4015.             standardTypes = [
  4016.                 'button',
  4017.                 'checkbox',
  4018.                 'date',
  4019.                 'datetime-local',
  4020.                 'email',
  4021.                 'month',
  4022.                 'number',
  4023.                 'password',
  4024.                 'radio',
  4025.                 'range',
  4026.                 'search',
  4027.                 'select',
  4028.                 'tel',
  4029.                 'time',
  4030.                 'text',
  4031.                 'textarea',
  4032.                 'week',
  4033.                 'url'
  4034.             ];
  4035.  
  4036.             templateId = control.templateSelector;
  4037.  
  4038.             // Use default content template when a standard HTML type is used, there isn't a more specific template existing, and the control container is empty.
  4039.             if ( templateId === 'customize-control-' + control.params.type + '-content' &&
  4040.                 _.contains( standardTypes, control.params.type ) &&
  4041.                 ! document.getElementById( 'tmpl-' + templateId ) &&
  4042.                 0 === control.container.children().length )
  4043.             {
  4044.                 templateId = 'customize-control-default-content';
  4045.             }
  4046.  
  4047.             // Replace the container element's content with the control.
  4048.             if ( document.getElementById( 'tmpl-' + templateId ) ) {
  4049.                 template = wp.template( templateId );
  4050.                 if ( template && control.container ) {
  4051.                     control.container.html( template( control.params ) );
  4052.                 }
  4053.             }
  4054.  
  4055.             // Re-render notifications after content has been re-rendered.
  4056.             control.notifications.container = control.getNotificationsContainerElement();
  4057.             sectionId = control.section();
  4058.             if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
  4059.                 control.notifications.render();
  4060.             }
  4061.         },
  4062.  
  4063.         /**
  4064.          * Add a new page to a dropdown-pages control reusing menus code for this.
  4065.          *
  4066.          * @since 4.7.0
  4067.          * @access private
  4068.          * @returns {void}
  4069.          */
  4070.         addNewPage: function () {
  4071.             var control = this, promise, toggle, container, input, title, select;
  4072.  
  4073.             if ( 'dropdown-pages' !== control.params.type || ! control.params.allow_addition || ! api.Menus ) {
  4074.                 return;
  4075.             }
  4076.  
  4077.             toggle = control.container.find( '.add-new-toggle' );
  4078.             container = control.container.find( '.new-content-item' );
  4079.             input = control.container.find( '.create-item-input' );
  4080.             title = input.val();
  4081.             select = control.container.find( 'select' );
  4082.  
  4083.             if ( ! title ) {
  4084.                 input.addClass( 'invalid' );
  4085.                 return;
  4086.             }
  4087.  
  4088.             input.removeClass( 'invalid' );
  4089.             input.attr( 'disabled', 'disabled' );
  4090.  
  4091.             // The menus functions add the page, publish when appropriate, and also add the new page to the dropdown-pages controls.
  4092.             promise = api.Menus.insertAutoDraftPost( {
  4093.                 post_title: title,
  4094.                 post_type: 'page'
  4095.             } );
  4096.             promise.done( function( data ) {
  4097.                 var availableItem, $content, itemTemplate;
  4098.  
  4099.                 // Prepare the new page as an available menu item.
  4100.                 // See api.Menus.submitNew().
  4101.                 availableItem = new api.Menus.AvailableItemModel( {
  4102.                     'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
  4103.                     'title': title,
  4104.                     'type': 'post_type',
  4105.                     'type_label': api.Menus.data.l10n.page_label,
  4106.                     'object': 'page',
  4107.                     'object_id': data.post_id,
  4108.                     'url': data.url
  4109.                 } );
  4110.  
  4111.                 // Add the new item to the list of available menu items.
  4112.                 api.Menus.availableMenuItemsPanel.collection.add( availableItem );
  4113.                 $content = $( '#available-menu-items-post_type-page' ).find( '.available-menu-items-list' );
  4114.                 itemTemplate = wp.template( 'available-menu-item' );
  4115.                 $content.prepend( itemTemplate( availableItem.attributes ) );
  4116.  
  4117.                 // Focus the select control.
  4118.                 select.focus();
  4119.                 control.setting.set( String( data.post_id ) ); // Triggers a preview refresh and updates the setting.
  4120.  
  4121.                 // Reset the create page form.
  4122.                 container.slideUp( 180 );
  4123.                 toggle.slideDown( 180 );
  4124.             } );
  4125.             promise.always( function() {
  4126.                 input.val( '' ).removeAttr( 'disabled' );
  4127.             } );
  4128.         }
  4129.     });
  4130.  
  4131.     /**
  4132.      * A colorpicker control.
  4133.      *
  4134.      * @class
  4135.      * @augments wp.customize.Control
  4136.      * @augments wp.customize.Class
  4137.      */
  4138.     api.ColorControl = api.Control.extend({
  4139.         ready: function() {
  4140.             var control = this,
  4141.                 isHueSlider = this.params.mode === 'hue',
  4142.                 updating = false,
  4143.                 picker;
  4144.  
  4145.             if ( isHueSlider ) {
  4146.                 picker = this.container.find( '.color-picker-hue' );
  4147.                 picker.val( control.setting() ).wpColorPicker({
  4148.                     change: function( event, ui ) {
  4149.                         updating = true;
  4150.                         control.setting( ui.color.h() );
  4151.                         updating = false;
  4152.                     }
  4153.                 });
  4154.             } else {
  4155.                 picker = this.container.find( '.color-picker-hex' );
  4156.                 picker.val( control.setting() ).wpColorPicker({
  4157.                     change: function() {
  4158.                         updating = true;
  4159.                         control.setting.set( picker.wpColorPicker( 'color' ) );
  4160.                         updating = false;
  4161.                     },
  4162.                     clear: function() {
  4163.                         updating = true;
  4164.                         control.setting.set( '' );
  4165.                         updating = false;
  4166.                     }
  4167.                 });
  4168.             }
  4169.  
  4170.             control.setting.bind( function ( value ) {
  4171.                 // Bail if the update came from the control itself.
  4172.                 if ( updating ) {
  4173.                     return;
  4174.                 }
  4175.                 picker.val( value );
  4176.                 picker.wpColorPicker( 'color', value );
  4177.             } );
  4178.  
  4179.             // Collapse color picker when hitting Esc instead of collapsing the current section.
  4180.             control.container.on( 'keydown', function( event ) {
  4181.                 var pickerContainer;
  4182.                 if ( 27 !== event.which ) { // Esc.
  4183.                     return;
  4184.                 }
  4185.                 pickerContainer = control.container.find( '.wp-picker-container' );
  4186.                 if ( pickerContainer.hasClass( 'wp-picker-active' ) ) {
  4187.                     picker.wpColorPicker( 'close' );
  4188.                     control.container.find( '.wp-color-result' ).focus();
  4189.                     event.stopPropagation(); // Prevent section from being collapsed.
  4190.                 }
  4191.             } );
  4192.         }
  4193.     });
  4194.  
  4195.     /**
  4196.      * A control that implements the media modal.
  4197.      *
  4198.      * @class
  4199.      * @augments wp.customize.Control
  4200.      * @augments wp.customize.Class
  4201.      */
  4202.     api.MediaControl = api.Control.extend({
  4203.  
  4204.         /**
  4205.          * When the control's DOM structure is ready,
  4206.          * set up internal event bindings.
  4207.          */
  4208.         ready: function() {
  4209.             var control = this;
  4210.             // Shortcut so that we don't have to use _.bind every time we add a callback.
  4211.             _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
  4212.  
  4213.             // Bind events, with delegation to facilitate re-rendering.
  4214.             control.container.on( 'click keydown', '.upload-button', control.openFrame );
  4215.             control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
  4216.             control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
  4217.             control.container.on( 'click keydown', '.default-button', control.restoreDefault );
  4218.             control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
  4219.             control.container.on( 'click keydown', '.remove-button', control.removeFile );
  4220.             control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
  4221.  
  4222.             // Resize the player controls when it becomes visible (ie when section is expanded)
  4223.             api.section( control.section() ).container
  4224.                 .on( 'expanded', function() {
  4225.                     if ( control.player ) {
  4226.                         control.player.setControlsSize();
  4227.                     }
  4228.                 })
  4229.                 .on( 'collapsed', function() {
  4230.                     control.pausePlayer();
  4231.                 });
  4232.  
  4233.             /**
  4234.              * Set attachment data and render content.
  4235.              *
  4236.              * Note that BackgroundImage.prototype.ready applies this ready method
  4237.              * to itself. Since BackgroundImage is an UploadControl, the value
  4238.              * is the attachment URL instead of the attachment ID. In this case
  4239.              * we skip fetching the attachment data because we have no ID available,
  4240.              * and it is the responsibility of the UploadControl to set the control's
  4241.              * attachmentData before calling the renderContent method.
  4242.              *
  4243.              * @param {number|string} value Attachment
  4244.              */
  4245.             function setAttachmentDataAndRenderContent( value ) {
  4246.                 var hasAttachmentData = $.Deferred();
  4247.  
  4248.                 if ( control.extended( api.UploadControl ) ) {
  4249.                     hasAttachmentData.resolve();
  4250.                 } else {
  4251.                     value = parseInt( value, 10 );
  4252.                     if ( _.isNaN( value ) || value <= 0 ) {
  4253.                         delete control.params.attachment;
  4254.                         hasAttachmentData.resolve();
  4255.                     } else if ( control.params.attachment && control.params.attachment.id === value ) {
  4256.                         hasAttachmentData.resolve();
  4257.                     }
  4258.                 }
  4259.  
  4260.                 // Fetch the attachment data.
  4261.                 if ( 'pending' === hasAttachmentData.state() ) {
  4262.                     wp.media.attachment( value ).fetch().done( function() {
  4263.                         control.params.attachment = this.attributes;
  4264.                         hasAttachmentData.resolve();
  4265.  
  4266.                         // Send attachment information to the preview for possible use in `postMessage` transport.
  4267.                         wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
  4268.                     } );
  4269.                 }
  4270.  
  4271.                 hasAttachmentData.done( function() {
  4272.                     control.renderContent();
  4273.                 } );
  4274.             }
  4275.  
  4276.             // Ensure attachment data is initially set (for dynamically-instantiated controls).
  4277.             setAttachmentDataAndRenderContent( control.setting() );
  4278.  
  4279.             // Update the attachment data and re-render the control when the setting changes.
  4280.             control.setting.bind( setAttachmentDataAndRenderContent );
  4281.         },
  4282.  
  4283.         pausePlayer: function () {
  4284.             this.player && this.player.pause();
  4285.         },
  4286.  
  4287.         cleanupPlayer: function () {
  4288.             this.player && wp.media.mixin.removePlayer( this.player );
  4289.         },
  4290.  
  4291.         /**
  4292.          * Open the media modal.
  4293.          */
  4294.         openFrame: function( event ) {
  4295.             if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  4296.                 return;
  4297.             }
  4298.  
  4299.             event.preventDefault();
  4300.  
  4301.             if ( ! this.frame ) {
  4302.                 this.initFrame();
  4303.             }
  4304.  
  4305.             this.frame.open();
  4306.         },
  4307.  
  4308.         /**
  4309.          * Create a media modal select frame, and store it so the instance can be reused when needed.
  4310.          */
  4311.         initFrame: function() {
  4312.             this.frame = wp.media({
  4313.                 button: {
  4314.                     text: this.params.button_labels.frame_button
  4315.                 },
  4316.                 states: [
  4317.                     new wp.media.controller.Library({
  4318.                         title:     this.params.button_labels.frame_title,
  4319.                         library:   wp.media.query({ type: this.params.mime_type }),
  4320.                         multiple:  false,
  4321.                         date:      false
  4322.                     })
  4323.                 ]
  4324.             });
  4325.  
  4326.             // When a file is selected, run a callback.
  4327.             this.frame.on( 'select', this.select );
  4328.         },
  4329.  
  4330.         /**
  4331.          * Callback handler for when an attachment is selected in the media modal.
  4332.          * Gets the selected image information, and sets it within the control.
  4333.          */
  4334.         select: function() {
  4335.             // Get the attachment from the modal frame.
  4336.             var node,
  4337.                 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
  4338.                 mejsSettings = window._wpmejsSettings || {};
  4339.  
  4340.             this.params.attachment = attachment;
  4341.  
  4342.             // Set the Customizer setting; the callback takes care of rendering.
  4343.             this.setting( attachment.id );
  4344.             node = this.container.find( 'audio, video' ).get(0);
  4345.  
  4346.             // Initialize audio/video previews.
  4347.             if ( node ) {
  4348.                 this.player = new MediaElementPlayer( node, mejsSettings );
  4349.             } else {
  4350.                 this.cleanupPlayer();
  4351.             }
  4352.         },
  4353.  
  4354.         /**
  4355.          * Reset the setting to the default value.
  4356.          */
  4357.         restoreDefault: function( event ) {
  4358.             if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  4359.                 return;
  4360.             }
  4361.             event.preventDefault();
  4362.  
  4363.             this.params.attachment = this.params.defaultAttachment;
  4364.             this.setting( this.params.defaultAttachment.url );
  4365.         },
  4366.  
  4367.         /**
  4368.          * Called when the "Remove" link is clicked. Empties the setting.
  4369.          *
  4370.          * @param {object} event jQuery Event object
  4371.          */
  4372.         removeFile: function( event ) {
  4373.             if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  4374.                 return;
  4375.             }
  4376.             event.preventDefault();
  4377.  
  4378.             this.params.attachment = {};
  4379.             this.setting( '' );
  4380.             this.renderContent(); // Not bound to setting change when emptying.
  4381.         }
  4382.     });
  4383.  
  4384.     /**
  4385.      * An upload control, which utilizes the media modal.
  4386.      *
  4387.      * @class
  4388.      * @augments wp.customize.MediaControl
  4389.      * @augments wp.customize.Control
  4390.      * @augments wp.customize.Class
  4391.      */
  4392.     api.UploadControl = api.MediaControl.extend({
  4393.  
  4394.         /**
  4395.          * Callback handler for when an attachment is selected in the media modal.
  4396.          * Gets the selected image information, and sets it within the control.
  4397.          */
  4398.         select: function() {
  4399.             // Get the attachment from the modal frame.
  4400.             var node,
  4401.                 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
  4402.                 mejsSettings = window._wpmejsSettings || {};
  4403.  
  4404.             this.params.attachment = attachment;
  4405.  
  4406.             // Set the Customizer setting; the callback takes care of rendering.
  4407.             this.setting( attachment.url );
  4408.             node = this.container.find( 'audio, video' ).get(0);
  4409.  
  4410.             // Initialize audio/video previews.
  4411.             if ( node ) {
  4412.                 this.player = new MediaElementPlayer( node, mejsSettings );
  4413.             } else {
  4414.                 this.cleanupPlayer();
  4415.             }
  4416.         },
  4417.  
  4418.         // @deprecated
  4419.         success: function() {},
  4420.  
  4421.         // @deprecated
  4422.         removerVisibility: function() {}
  4423.     });
  4424.  
  4425.     /**
  4426.      * A control for uploading images.
  4427.      *
  4428.      * This control no longer needs to do anything more
  4429.      * than what the upload control does in JS.
  4430.      *
  4431.      * @class
  4432.      * @augments wp.customize.UploadControl
  4433.      * @augments wp.customize.MediaControl
  4434.      * @augments wp.customize.Control
  4435.      * @augments wp.customize.Class
  4436.      */
  4437.     api.ImageControl = api.UploadControl.extend({
  4438.         // @deprecated
  4439.         thumbnailSrc: function() {}
  4440.     });
  4441.  
  4442.     /**
  4443.      * A control for uploading background images.
  4444.      *
  4445.      * @class
  4446.      * @augments wp.customize.UploadControl
  4447.      * @augments wp.customize.MediaControl
  4448.      * @augments wp.customize.Control
  4449.      * @augments wp.customize.Class
  4450.      */
  4451.     api.BackgroundControl = api.UploadControl.extend({
  4452.  
  4453.         /**
  4454.          * When the control's DOM structure is ready,
  4455.          * set up internal event bindings.
  4456.          */
  4457.         ready: function() {
  4458.             api.UploadControl.prototype.ready.apply( this, arguments );
  4459.         },
  4460.  
  4461.         /**
  4462.          * Callback handler for when an attachment is selected in the media modal.
  4463.          * Does an additional AJAX request for setting the background context.
  4464.          */
  4465.         select: function() {
  4466.             api.UploadControl.prototype.select.apply( this, arguments );
  4467.  
  4468.             wp.ajax.post( 'custom-background-add', {
  4469.                 nonce: _wpCustomizeBackground.nonces.add,
  4470.                 wp_customize: 'on',
  4471.                 customize_theme: api.settings.theme.stylesheet,
  4472.                 attachment_id: this.params.attachment.id
  4473.             } );
  4474.         }
  4475.     });
  4476.  
  4477.     /**
  4478.      * A control for positioning a background image.
  4479.      *
  4480.      * @since 4.7.0
  4481.      *
  4482.      * @class
  4483.      * @augments wp.customize.Control
  4484.      * @augments wp.customize.Class
  4485.      */
  4486.     api.BackgroundPositionControl = api.Control.extend( {
  4487.  
  4488.         /**
  4489.          * Set up control UI once embedded in DOM and settings are created.
  4490.          *
  4491.          * @since 4.7.0
  4492.          * @access public
  4493.          */
  4494.         ready: function() {
  4495.             var control = this, updateRadios;
  4496.  
  4497.             control.container.on( 'change', 'input[name="background-position"]', function() {
  4498.                 var position = $( this ).val().split( ' ' );
  4499.                 control.settings.x( position[0] );
  4500.                 control.settings.y( position[1] );
  4501.             } );
  4502.  
  4503.             updateRadios = _.debounce( function() {
  4504.                 var x, y, radioInput, inputValue;
  4505.                 x = control.settings.x.get();
  4506.                 y = control.settings.y.get();
  4507.                 inputValue = String( x ) + ' ' + String( y );
  4508.                 radioInput = control.container.find( 'input[name="background-position"][value="' + inputValue + '"]' );
  4509.                 radioInput.click();
  4510.             } );
  4511.             control.settings.x.bind( updateRadios );
  4512.             control.settings.y.bind( updateRadios );
  4513.  
  4514.             updateRadios(); // Set initial UI.
  4515.         }
  4516.     } );
  4517.  
  4518.     /**
  4519.      * A control for selecting and cropping an image.
  4520.      *
  4521.      * @class
  4522.      * @augments wp.customize.MediaControl
  4523.      * @augments wp.customize.Control
  4524.      * @augments wp.customize.Class
  4525.      */
  4526.     api.CroppedImageControl = api.MediaControl.extend({
  4527.  
  4528.         /**
  4529.          * Open the media modal to the library state.
  4530.          */
  4531.         openFrame: function( event ) {
  4532.             if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  4533.                 return;
  4534.             }
  4535.  
  4536.             this.initFrame();
  4537.             this.frame.setState( 'library' ).open();
  4538.         },
  4539.  
  4540.         /**
  4541.          * Create a media modal select frame, and store it so the instance can be reused when needed.
  4542.          */
  4543.         initFrame: function() {
  4544.             var l10n = _wpMediaViewsL10n;
  4545.  
  4546.             this.frame = wp.media({
  4547.                 button: {
  4548.                     text: l10n.select,
  4549.                     close: false
  4550.                 },
  4551.                 states: [
  4552.                     new wp.media.controller.Library({
  4553.                         title: this.params.button_labels.frame_title,
  4554.                         library: wp.media.query({ type: 'image' }),
  4555.                         multiple: false,
  4556.                         date: false,
  4557.                         priority: 20,
  4558.                         suggestedWidth: this.params.width,
  4559.                         suggestedHeight: this.params.height
  4560.                     }),
  4561.                     new wp.media.controller.CustomizeImageCropper({
  4562.                         imgSelectOptions: this.calculateImageSelectOptions,
  4563.                         control: this
  4564.                     })
  4565.                 ]
  4566.             });
  4567.  
  4568.             this.frame.on( 'select', this.onSelect, this );
  4569.             this.frame.on( 'cropped', this.onCropped, this );
  4570.             this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
  4571.         },
  4572.  
  4573.         /**
  4574.          * After an image is selected in the media modal, switch to the cropper
  4575.          * state if the image isn't the right size.
  4576.          */
  4577.         onSelect: function() {
  4578.             var attachment = this.frame.state().get( 'selection' ).first().toJSON();
  4579.  
  4580.             if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
  4581.                 this.setImageFromAttachment( attachment );
  4582.                 this.frame.close();
  4583.             } else {
  4584.                 this.frame.setState( 'cropper' );
  4585.             }
  4586.         },
  4587.  
  4588.         /**
  4589.          * After the image has been cropped, apply the cropped image data to the setting.
  4590.          *
  4591.          * @param {object} croppedImage Cropped attachment data.
  4592.          */
  4593.         onCropped: function( croppedImage ) {
  4594.             this.setImageFromAttachment( croppedImage );
  4595.         },
  4596.  
  4597.         /**
  4598.          * Returns a set of options, computed from the attached image data and
  4599.          * control-specific data, to be fed to the imgAreaSelect plugin in
  4600.          * wp.media.view.Cropper.
  4601.          *
  4602.          * @param {wp.media.model.Attachment} attachment
  4603.          * @param {wp.media.controller.Cropper} controller
  4604.          * @returns {Object} Options
  4605.          */
  4606.         calculateImageSelectOptions: function( attachment, controller ) {
  4607.             var control    = controller.get( 'control' ),
  4608.                 flexWidth  = !! parseInt( control.params.flex_width, 10 ),
  4609.                 flexHeight = !! parseInt( control.params.flex_height, 10 ),
  4610.                 realWidth  = attachment.get( 'width' ),
  4611.                 realHeight = attachment.get( 'height' ),
  4612.                 xInit = parseInt( control.params.width, 10 ),
  4613.                 yInit = parseInt( control.params.height, 10 ),
  4614.                 ratio = xInit / yInit,
  4615.                 xImg  = xInit,
  4616.                 yImg  = yInit,
  4617.                 x1, y1, imgSelectOptions;
  4618.  
  4619.             controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
  4620.  
  4621.             if ( realWidth / realHeight > ratio ) {
  4622.                 yInit = realHeight;
  4623.                 xInit = yInit * ratio;
  4624.             } else {
  4625.                 xInit = realWidth;
  4626.                 yInit = xInit / ratio;
  4627.             }
  4628.  
  4629.             x1 = ( realWidth - xInit ) / 2;
  4630.             y1 = ( realHeight - yInit ) / 2;
  4631.  
  4632.             imgSelectOptions = {
  4633.                 handles: true,
  4634.                 keys: true,
  4635.                 instance: true,
  4636.                 persistent: true,
  4637.                 imageWidth: realWidth,
  4638.                 imageHeight: realHeight,
  4639.                 minWidth: xImg > xInit ? xInit : xImg,
  4640.                 minHeight: yImg > yInit ? yInit : yImg,
  4641.                 x1: x1,
  4642.                 y1: y1,
  4643.                 x2: xInit + x1,
  4644.                 y2: yInit + y1
  4645.             };
  4646.  
  4647.             if ( flexHeight === false && flexWidth === false ) {
  4648.                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
  4649.             }
  4650.  
  4651.             if ( true === flexHeight ) {
  4652.                 delete imgSelectOptions.minHeight;
  4653.                 imgSelectOptions.maxWidth = realWidth;
  4654.             }
  4655.  
  4656.             if ( true === flexWidth ) {
  4657.                 delete imgSelectOptions.minWidth;
  4658.                 imgSelectOptions.maxHeight = realHeight;
  4659.             }
  4660.  
  4661.             return imgSelectOptions;
  4662.         },
  4663.  
  4664.         /**
  4665.          * Return whether the image must be cropped, based on required dimensions.
  4666.          *
  4667.          * @param {bool} flexW
  4668.          * @param {bool} flexH
  4669.          * @param {int}  dstW
  4670.          * @param {int}  dstH
  4671.          * @param {int}  imgW
  4672.          * @param {int}  imgH
  4673.          * @return {bool}
  4674.          */
  4675.         mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
  4676.             if ( true === flexW && true === flexH ) {
  4677.                 return false;
  4678.             }
  4679.  
  4680.             if ( true === flexW && dstH === imgH ) {
  4681.                 return false;
  4682.             }
  4683.  
  4684.             if ( true === flexH && dstW === imgW ) {
  4685.                 return false;
  4686.             }
  4687.  
  4688.             if ( dstW === imgW && dstH === imgH ) {
  4689.                 return false;
  4690.             }
  4691.  
  4692.             if ( imgW <= dstW ) {
  4693.                 return false;
  4694.             }
  4695.  
  4696.             return true;
  4697.         },
  4698.  
  4699.         /**
  4700.          * If cropping was skipped, apply the image data directly to the setting.
  4701.          */
  4702.         onSkippedCrop: function() {
  4703.             var attachment = this.frame.state().get( 'selection' ).first().toJSON();
  4704.             this.setImageFromAttachment( attachment );
  4705.         },
  4706.  
  4707.         /**
  4708.          * Updates the setting and re-renders the control UI.
  4709.          *
  4710.          * @param {object} attachment
  4711.          */
  4712.         setImageFromAttachment: function( attachment ) {
  4713.             this.params.attachment = attachment;
  4714.  
  4715.             // Set the Customizer setting; the callback takes care of rendering.
  4716.             this.setting( attachment.id );
  4717.         }
  4718.     });
  4719.  
  4720.     /**
  4721.      * A control for selecting and cropping Site Icons.
  4722.      *
  4723.      * @class
  4724.      * @augments wp.customize.CroppedImageControl
  4725.      * @augments wp.customize.MediaControl
  4726.      * @augments wp.customize.Control
  4727.      * @augments wp.customize.Class
  4728.      */
  4729.     api.SiteIconControl = api.CroppedImageControl.extend({
  4730.  
  4731.         /**
  4732.          * Create a media modal select frame, and store it so the instance can be reused when needed.
  4733.          */
  4734.         initFrame: function() {
  4735.             var l10n = _wpMediaViewsL10n;
  4736.  
  4737.             this.frame = wp.media({
  4738.                 button: {
  4739.                     text: l10n.select,
  4740.                     close: false
  4741.                 },
  4742.                 states: [
  4743.                     new wp.media.controller.Library({
  4744.                         title: this.params.button_labels.frame_title,
  4745.                         library: wp.media.query({ type: 'image' }),
  4746.                         multiple: false,
  4747.                         date: false,
  4748.                         priority: 20,
  4749.                         suggestedWidth: this.params.width,
  4750.                         suggestedHeight: this.params.height
  4751.                     }),
  4752.                     new wp.media.controller.SiteIconCropper({
  4753.                         imgSelectOptions: this.calculateImageSelectOptions,
  4754.                         control: this
  4755.                     })
  4756.                 ]
  4757.             });
  4758.  
  4759.             this.frame.on( 'select', this.onSelect, this );
  4760.             this.frame.on( 'cropped', this.onCropped, this );
  4761.             this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
  4762.         },
  4763.  
  4764.         /**
  4765.          * After an image is selected in the media modal, switch to the cropper
  4766.          * state if the image isn't the right size.
  4767.          */
  4768.         onSelect: function() {
  4769.             var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
  4770.                 controller = this;
  4771.  
  4772.             if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
  4773.                 wp.ajax.post( 'crop-image', {
  4774.                     nonce: attachment.nonces.edit,
  4775.                     id: attachment.id,
  4776.                     context: 'site-icon',
  4777.                     cropDetails: {
  4778.                         x1: 0,
  4779.                         y1: 0,
  4780.                         width: this.params.width,
  4781.                         height: this.params.height,
  4782.                         dst_width: this.params.width,
  4783.                         dst_height: this.params.height
  4784.                     }
  4785.                 } ).done( function( croppedImage ) {
  4786.                     controller.setImageFromAttachment( croppedImage );
  4787.                     controller.frame.close();
  4788.                 } ).fail( function() {
  4789.                     controller.frame.trigger('content:error:crop');
  4790.                 } );
  4791.             } else {
  4792.                 this.frame.setState( 'cropper' );
  4793.             }
  4794.         },
  4795.  
  4796.         /**
  4797.          * Updates the setting and re-renders the control UI.
  4798.          *
  4799.          * @param {object} attachment
  4800.          */
  4801.         setImageFromAttachment: function( attachment ) {
  4802.             var sizes = [ 'site_icon-32', 'thumbnail', 'full' ], link,
  4803.                 icon;
  4804.  
  4805.             _.each( sizes, function( size ) {
  4806.                 if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
  4807.                     icon = attachment.sizes[ size ];
  4808.                 }
  4809.             } );
  4810.  
  4811.             this.params.attachment = attachment;
  4812.  
  4813.             // Set the Customizer setting; the callback takes care of rendering.
  4814.             this.setting( attachment.id );
  4815.  
  4816.             if ( ! icon ) {
  4817.                 return;
  4818.             }
  4819.  
  4820.             // Update the icon in-browser.
  4821.             link = $( 'link[rel="icon"][sizes="32x32"]' );
  4822.             link.attr( 'href', icon.url );
  4823.         },
  4824.  
  4825.         /**
  4826.          * Called when the "Remove" link is clicked. Empties the setting.
  4827.          *
  4828.          * @param {object} event jQuery Event object
  4829.          */
  4830.         removeFile: function( event ) {
  4831.             if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  4832.                 return;
  4833.             }
  4834.             event.preventDefault();
  4835.  
  4836.             this.params.attachment = {};
  4837.             this.setting( '' );
  4838.             this.renderContent(); // Not bound to setting change when emptying.
  4839.             $( 'link[rel="icon"][sizes="32x32"]' ).attr( 'href', '/favicon.ico' ); // Set to default.
  4840.         }
  4841.     });
  4842.  
  4843.     /**
  4844.      * @class
  4845.      * @augments wp.customize.Control
  4846.      * @augments wp.customize.Class
  4847.      */
  4848.     api.HeaderControl = api.Control.extend({
  4849.         ready: function() {
  4850.             this.btnRemove = $('#customize-control-header_image .actions .remove');
  4851.             this.btnNew    = $('#customize-control-header_image .actions .new');
  4852.  
  4853.             _.bindAll(this, 'openMedia', 'removeImage');
  4854.  
  4855.             this.btnNew.on( 'click', this.openMedia );
  4856.             this.btnRemove.on( 'click', this.removeImage );
  4857.  
  4858.             api.HeaderTool.currentHeader = this.getInitialHeaderImage();
  4859.  
  4860.             new api.HeaderTool.CurrentView({
  4861.                 model: api.HeaderTool.currentHeader,
  4862.                 el: '#customize-control-header_image .current .container'
  4863.             });
  4864.  
  4865.             new api.HeaderTool.ChoiceListView({
  4866.                 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
  4867.                 el: '#customize-control-header_image .choices .uploaded .list'
  4868.             });
  4869.  
  4870.             new api.HeaderTool.ChoiceListView({
  4871.                 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
  4872.                 el: '#customize-control-header_image .choices .default .list'
  4873.             });
  4874.  
  4875.             api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
  4876.                 api.HeaderTool.UploadsList,
  4877.                 api.HeaderTool.DefaultsList
  4878.             ]);
  4879.  
  4880.             // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
  4881.             wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
  4882.             wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet;
  4883.         },
  4884.  
  4885.         /**
  4886.          * Returns a new instance of api.HeaderTool.ImageModel based on the currently
  4887.          * saved header image (if any).
  4888.          *
  4889.          * @since 4.2.0
  4890.          *
  4891.          * @returns {Object} Options
  4892.          */
  4893.         getInitialHeaderImage: function() {
  4894.             if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
  4895.                 return new api.HeaderTool.ImageModel();
  4896.             }
  4897.  
  4898.             // Get the matching uploaded image object.
  4899.             var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
  4900.                 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
  4901.             } );
  4902.             // Fall back to raw current header image.
  4903.             if ( ! currentHeaderObject ) {
  4904.                 currentHeaderObject = {
  4905.                     url: api.get().header_image,
  4906.                     thumbnail_url: api.get().header_image,
  4907.                     attachment_id: api.get().header_image_data.attachment_id
  4908.                 };
  4909.             }
  4910.  
  4911.             return new api.HeaderTool.ImageModel({
  4912.                 header: currentHeaderObject,
  4913.                 choice: currentHeaderObject.url.split( '/' ).pop()
  4914.             });
  4915.         },
  4916.  
  4917.         /**
  4918.          * Returns a set of options, computed from the attached image data and
  4919.          * theme-specific data, to be fed to the imgAreaSelect plugin in
  4920.          * wp.media.view.Cropper.
  4921.          *
  4922.          * @param {wp.media.model.Attachment} attachment
  4923.          * @param {wp.media.controller.Cropper} controller
  4924.          * @returns {Object} Options
  4925.          */
  4926.         calculateImageSelectOptions: function(attachment, controller) {
  4927.             var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
  4928.                 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
  4929.                 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
  4930.                 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
  4931.                 ratio, xImg, yImg, realHeight, realWidth,
  4932.                 imgSelectOptions;
  4933.  
  4934.             realWidth = attachment.get('width');
  4935.             realHeight = attachment.get('height');
  4936.  
  4937.             this.headerImage = new api.HeaderTool.ImageModel();
  4938.             this.headerImage.set({
  4939.                 themeWidth: xInit,
  4940.                 themeHeight: yInit,
  4941.                 themeFlexWidth: flexWidth,
  4942.                 themeFlexHeight: flexHeight,
  4943.                 imageWidth: realWidth,
  4944.                 imageHeight: realHeight
  4945.             });
  4946.  
  4947.             controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
  4948.  
  4949.             ratio = xInit / yInit;
  4950.             xImg = realWidth;
  4951.             yImg = realHeight;
  4952.  
  4953.             if ( xImg / yImg > ratio ) {
  4954.                 yInit = yImg;
  4955.                 xInit = yInit * ratio;
  4956.             } else {
  4957.                 xInit = xImg;
  4958.                 yInit = xInit / ratio;
  4959.             }
  4960.  
  4961.             imgSelectOptions = {
  4962.                 handles: true,
  4963.                 keys: true,
  4964.                 instance: true,
  4965.                 persistent: true,
  4966.                 imageWidth: realWidth,
  4967.                 imageHeight: realHeight,
  4968.                 x1: 0,
  4969.                 y1: 0,
  4970.                 x2: xInit,
  4971.                 y2: yInit
  4972.             };
  4973.  
  4974.             if (flexHeight === false && flexWidth === false) {
  4975.                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
  4976.             }
  4977.             if (flexHeight === false ) {
  4978.                 imgSelectOptions.maxHeight = yInit;
  4979.             }
  4980.             if (flexWidth === false ) {
  4981.                 imgSelectOptions.maxWidth = xInit;
  4982.             }
  4983.  
  4984.             return imgSelectOptions;
  4985.         },
  4986.  
  4987.         /**
  4988.          * Sets up and opens the Media Manager in order to select an image.
  4989.          * Depending on both the size of the image and the properties of the
  4990.          * current theme, a cropping step after selection may be required or
  4991.          * skippable.
  4992.          *
  4993.          * @param {event} event
  4994.          */
  4995.         openMedia: function(event) {
  4996.             var l10n = _wpMediaViewsL10n;
  4997.  
  4998.             event.preventDefault();
  4999.  
  5000.             this.frame = wp.media({
  5001.                 button: {
  5002.                     text: l10n.selectAndCrop,
  5003.                     close: false
  5004.                 },
  5005.                 states: [
  5006.                     new wp.media.controller.Library({
  5007.                         title:     l10n.chooseImage,
  5008.                         library:   wp.media.query({ type: 'image' }),
  5009.                         multiple:  false,
  5010.                         date:      false,
  5011.                         priority:  20,
  5012.                         suggestedWidth: _wpCustomizeHeader.data.width,
  5013.                         suggestedHeight: _wpCustomizeHeader.data.height
  5014.                     }),
  5015.                     new wp.media.controller.Cropper({
  5016.                         imgSelectOptions: this.calculateImageSelectOptions
  5017.                     })
  5018.                 ]
  5019.             });
  5020.  
  5021.             this.frame.on('select', this.onSelect, this);
  5022.             this.frame.on('cropped', this.onCropped, this);
  5023.             this.frame.on('skippedcrop', this.onSkippedCrop, this);
  5024.  
  5025.             this.frame.open();
  5026.         },
  5027.  
  5028.         /**
  5029.          * After an image is selected in the media modal,
  5030.          * switch to the cropper state.
  5031.          */
  5032.         onSelect: function() {
  5033.             this.frame.setState('cropper');
  5034.         },
  5035.  
  5036.         /**
  5037.          * After the image has been cropped, apply the cropped image data to the setting.
  5038.          *
  5039.          * @param {object} croppedImage Cropped attachment data.
  5040.          */
  5041.         onCropped: function(croppedImage) {
  5042.             var url = croppedImage.url,
  5043.                 attachmentId = croppedImage.attachment_id,
  5044.                 w = croppedImage.width,
  5045.                 h = croppedImage.height;
  5046.             this.setImageFromURL(url, attachmentId, w, h);
  5047.         },
  5048.  
  5049.         /**
  5050.          * If cropping was skipped, apply the image data directly to the setting.
  5051.          *
  5052.          * @param {object} selection
  5053.          */
  5054.         onSkippedCrop: function(selection) {
  5055.             var url = selection.get('url'),
  5056.                 w = selection.get('width'),
  5057.                 h = selection.get('height');
  5058.             this.setImageFromURL(url, selection.id, w, h);
  5059.         },
  5060.  
  5061.         /**
  5062.          * Creates a new wp.customize.HeaderTool.ImageModel from provided
  5063.          * header image data and inserts it into the user-uploaded headers
  5064.          * collection.
  5065.          *
  5066.          * @param {String} url
  5067.          * @param {Number} attachmentId
  5068.          * @param {Number} width
  5069.          * @param {Number} height
  5070.          */
  5071.         setImageFromURL: function(url, attachmentId, width, height) {
  5072.             var choice, data = {};
  5073.  
  5074.             data.url = url;
  5075.             data.thumbnail_url = url;
  5076.             data.timestamp = _.now();
  5077.  
  5078.             if (attachmentId) {
  5079.                 data.attachment_id = attachmentId;
  5080.             }
  5081.  
  5082.             if (width) {
  5083.                 data.width = width;
  5084.             }
  5085.  
  5086.             if (height) {
  5087.                 data.height = height;
  5088.             }
  5089.  
  5090.             choice = new api.HeaderTool.ImageModel({
  5091.                 header: data,
  5092.                 choice: url.split('/').pop()
  5093.             });
  5094.             api.HeaderTool.UploadsList.add(choice);
  5095.             api.HeaderTool.currentHeader.set(choice.toJSON());
  5096.             choice.save();
  5097.             choice.importImage();
  5098.         },
  5099.  
  5100.         /**
  5101.          * Triggers the necessary events to deselect an image which was set as
  5102.          * the currently selected one.
  5103.          */
  5104.         removeImage: function() {
  5105.             api.HeaderTool.currentHeader.trigger('hide');
  5106.             api.HeaderTool.CombinedList.trigger('control:removeImage');
  5107.         }
  5108.  
  5109.     });
  5110.  
  5111.     /**
  5112.      * wp.customize.ThemeControl
  5113.      *
  5114.      * @constructor
  5115.      * @augments wp.customize.Control
  5116.      * @augments wp.customize.Class
  5117.      */
  5118.     api.ThemeControl = api.Control.extend({
  5119.  
  5120.         touchDrag: false,
  5121.         screenshotRendered: false,
  5122.  
  5123.         /**
  5124.          * @since 4.2.0
  5125.          */
  5126.         ready: function() {
  5127.             var control = this, panel = api.panel( 'themes' );
  5128.  
  5129.             function disableSwitchButtons() {
  5130.                 return ! panel.canSwitchTheme( control.params.theme.id );
  5131.             }
  5132.  
  5133.             // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
  5134.             function disableInstallButtons() {
  5135.                 return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
  5136.             }
  5137.             function updateButtons() {
  5138.                 control.container.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
  5139.                 control.container.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
  5140.             }
  5141.  
  5142.             api.state( 'selectedChangesetStatus' ).bind( updateButtons );
  5143.             api.state( 'changesetStatus' ).bind( updateButtons );
  5144.             updateButtons();
  5145.  
  5146.             control.container.on( 'touchmove', '.theme', function() {
  5147.                 control.touchDrag = true;
  5148.             });
  5149.  
  5150.             // Bind details view trigger.
  5151.             control.container.on( 'click keydown touchend', '.theme', function( event ) {
  5152.                 var section;
  5153.                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  5154.                     return;
  5155.                 }
  5156.  
  5157.                 // Bail if the user scrolled on a touch device.
  5158.                 if ( control.touchDrag === true ) {
  5159.                     return control.touchDrag = false;
  5160.                 }
  5161.  
  5162.                 // Prevent the modal from showing when the user clicks the action button.
  5163.                 if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) {
  5164.                     return;
  5165.                 }
  5166.  
  5167.                 event.preventDefault(); // Keep this AFTER the key filter above
  5168.                 section = api.section( control.section() );
  5169.                 section.showDetails( control.params.theme, function() {
  5170.  
  5171.                     // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
  5172.                     if ( api.settings.theme._filesystemCredentialsNeeded ) {
  5173.                         section.overlay.find( '.theme-actions .delete-theme' ).remove();
  5174.                     }
  5175.                 } );
  5176.             });
  5177.  
  5178.             control.container.on( 'render-screenshot', function() {
  5179.                 var $screenshot = $( this ).find( 'img' ),
  5180.                     source = $screenshot.data( 'src' );
  5181.  
  5182.                 if ( source ) {
  5183.                     $screenshot.attr( 'src', source );
  5184.                 }
  5185.                 control.screenshotRendered = true;
  5186.             });
  5187.         },
  5188.  
  5189.         /**
  5190.          * Show or hide the theme based on the presence of the term in the title, description, tags, and author.
  5191.          *
  5192.          * @since 4.2.0
  5193.          * @param {Array} terms - An array of terms to search for.
  5194.          * @returns {boolean} Whether a theme control was activated or not.
  5195.          */
  5196.         filter: function( terms ) {
  5197.             var control = this,
  5198.                 matchCount = 0,
  5199.                 haystack = control.params.theme.name + ' ' +
  5200.                     control.params.theme.description + ' ' +
  5201.                     control.params.theme.tags + ' ' +
  5202.                     control.params.theme.author + ' ';
  5203.             haystack = haystack.toLowerCase().replace( '-', ' ' );
  5204.  
  5205.             // Back-compat for behavior in WordPress 4.2.0 to 4.8.X.
  5206.             if ( ! _.isArray( terms ) ) {
  5207.                 terms = [ terms ];
  5208.             }
  5209.  
  5210.             // Always give exact name matches highest ranking.
  5211.             if ( control.params.theme.name.toLowerCase() === terms.join( ' ' ) ) {
  5212.                 matchCount = 100;
  5213.             } else {
  5214.  
  5215.                 // Search for and weight (by 10) complete term matches.
  5216.                 matchCount = matchCount + 10 * ( haystack.split( terms.join( ' ' ) ).length - 1 );
  5217.  
  5218.                 // Search for each term individually (as whole-word and partial match) and sum weighted match counts.
  5219.                 _.each( terms, function( term ) {
  5220.                     matchCount = matchCount + 2 * ( haystack.split( term + ' ' ).length - 1 ); // Whole-word, double-weighted.
  5221.                     matchCount = matchCount + haystack.split( term ).length - 1; // Partial word, to minimize empty intermediate searches while typing.
  5222.                 });
  5223.  
  5224.                 // Upper limit on match ranking.
  5225.                 if ( matchCount > 99 ) {
  5226.                     matchCount = 99;
  5227.                 }
  5228.             }
  5229.  
  5230.             if ( 0 !== matchCount ) {
  5231.                 control.activate();
  5232.                 control.params.priority = 101 - matchCount; // Sort results by match count.
  5233.                 return true;
  5234.             } else {
  5235.                 control.deactivate(); // Hide control
  5236.                 control.params.priority = 101;
  5237.                 return false;
  5238.             }
  5239.         },
  5240.  
  5241.         /**
  5242.          * Rerender the theme from its JS template with the installed type.
  5243.          *
  5244.          * @since 4.9.0
  5245.          *
  5246.          * @returns {void}
  5247.          */
  5248.         rerenderAsInstalled: function( installed ) {
  5249.             var control = this, section;
  5250.             if ( installed ) {
  5251.                 control.params.theme.type = 'installed';
  5252.             } else {
  5253.                 section = api.section( control.params.section );
  5254.                 control.params.theme.type = section.params.action;
  5255.             }
  5256.             control.renderContent(); // Replaces existing content.
  5257.             control.container.trigger( 'render-screenshot' );
  5258.         }
  5259.     });
  5260.  
  5261.     /**
  5262.      * Class wp.customize.CodeEditorControl
  5263.      *
  5264.      * @since 4.9.0
  5265.      *
  5266.      * @constructor
  5267.      * @augments wp.customize.Control
  5268.      * @augments wp.customize.Class
  5269.      */
  5270.     api.CodeEditorControl = api.Control.extend({
  5271.  
  5272.         /**
  5273.          * Initialize.
  5274.          *
  5275.          * @since 4.9.0
  5276.          * @param {string} id      - Unique identifier for the control instance.
  5277.          * @param {object} options - Options hash for the control instance.
  5278.          * @returns {void}
  5279.          */
  5280.         initialize: function( id, options ) {
  5281.             var control = this;
  5282.             control.deferred = _.extend( control.deferred || {}, {
  5283.                 codemirror: $.Deferred()
  5284.             } );
  5285.             api.Control.prototype.initialize.call( control, id, options );
  5286.  
  5287.             // Note that rendering is debounced so the props will be used when rendering happens after add event.
  5288.             control.notifications.bind( 'add', function( notification ) {
  5289.  
  5290.                 // Skip if control notification is not from setting csslint_error notification.
  5291.                 if ( notification.code !== control.setting.id + ':csslint_error' ) {
  5292.                     return;
  5293.                 }
  5294.  
  5295.                 // Customize the template and behavior of csslint_error notifications.
  5296.                 notification.templateId = 'customize-code-editor-lint-error-notification';
  5297.                 notification.render = (function( render ) {
  5298.                     return function() {
  5299.                         var li = render.call( this );
  5300.                         li.find( 'input[type=checkbox]' ).on( 'click', function() {
  5301.                             control.setting.notifications.remove( 'csslint_error' );
  5302.                         } );
  5303.                         return li;
  5304.                     };
  5305.                 })( notification.render );
  5306.             } );
  5307.         },
  5308.  
  5309.         /**
  5310.          * Initialize the editor when the containing section is ready and expanded.
  5311.          *
  5312.          * @since 4.9.0
  5313.          * @returns {void}
  5314.          */
  5315.         ready: function() {
  5316.             var control = this;
  5317.             if ( ! control.section() ) {
  5318.                 control.initEditor();
  5319.                 return;
  5320.             }
  5321.  
  5322.             // Wait to initialize editor until section is embedded and expanded.
  5323.             api.section( control.section(), function( section ) {
  5324.                 section.deferred.embedded.done( function() {
  5325.                     var onceExpanded;
  5326.                     if ( section.expanded() ) {
  5327.                         control.initEditor();
  5328.                     } else {
  5329.                         onceExpanded = function( isExpanded ) {
  5330.                             if ( isExpanded ) {
  5331.                                 control.initEditor();
  5332.                                 section.expanded.unbind( onceExpanded );
  5333.                             }
  5334.                         };
  5335.                         section.expanded.bind( onceExpanded );
  5336.                     }
  5337.                 } );
  5338.             } );
  5339.         },
  5340.  
  5341.         /**
  5342.          * Initialize editor.
  5343.          *
  5344.          * @since 4.9.0
  5345.          * @returns {void}
  5346.          */
  5347.         initEditor: function() {
  5348.             var control = this, element, editorSettings = false;
  5349.  
  5350.             // Obtain editorSettings for instantiation.
  5351.             if ( wp.codeEditor && ( _.isUndefined( control.params.editor_settings ) || false !== control.params.editor_settings ) ) {
  5352.  
  5353.                 // Obtain default editor settings.
  5354.                 editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {};
  5355.                 editorSettings.codemirror = _.extend(
  5356.                     {},
  5357.                     editorSettings.codemirror,
  5358.                     {
  5359.                         indentUnit: 2,
  5360.                         tabSize: 2
  5361.                     }
  5362.                 );
  5363.  
  5364.                 // Merge editor_settings param on top of defaults.
  5365.                 if ( _.isObject( control.params.editor_settings ) ) {
  5366.                     _.each( control.params.editor_settings, function( value, key ) {
  5367.                         if ( _.isObject( value ) ) {
  5368.                             editorSettings[ key ] = _.extend(
  5369.                                 {},
  5370.                                 editorSettings[ key ],
  5371.                                 value
  5372.                             );
  5373.                         }
  5374.                     } );
  5375.                 }
  5376.             }
  5377.  
  5378.             element = new api.Element( control.container.find( 'textarea' ) );
  5379.             control.elements.push( element );
  5380.             element.sync( control.setting );
  5381.             element.set( control.setting() );
  5382.  
  5383.             if ( editorSettings ) {
  5384.                 control.initSyntaxHighlightingEditor( editorSettings );
  5385.             } else {
  5386.                 control.initPlainTextareaEditor();
  5387.             }
  5388.         },
  5389.  
  5390.         /**
  5391.          * Make sure editor gets focused when control is focused.
  5392.          *
  5393.          * @since 4.9.0
  5394.          * @param {Object}   [params] - Focus params.
  5395.          * @param {Function} [params.completeCallback] - Function to call when expansion is complete.
  5396.          * @returns {void}
  5397.          */
  5398.         focus: function( params ) {
  5399.             var control = this, extendedParams = _.extend( {}, params ), originalCompleteCallback;
  5400.             originalCompleteCallback = extendedParams.completeCallback;
  5401.             extendedParams.completeCallback = function() {
  5402.                 if ( originalCompleteCallback ) {
  5403.                     originalCompleteCallback();
  5404.                 }
  5405.                 if ( control.editor ) {
  5406.                     control.editor.codemirror.focus();
  5407.                 }
  5408.             };
  5409.             api.Control.prototype.focus.call( control, extendedParams );
  5410.         },
  5411.  
  5412.         /**
  5413.          * Initialize syntax-highlighting editor.
  5414.          *
  5415.          * @since 4.9.0
  5416.          * @param {object} codeEditorSettings - Code editor settings.
  5417.          * @returns {void}
  5418.          */
  5419.         initSyntaxHighlightingEditor: function( codeEditorSettings ) {
  5420.             var control = this, $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false;
  5421.  
  5422.             settings = _.extend( {}, codeEditorSettings, {
  5423.                 onTabNext: _.bind( control.onTabNext, control ),
  5424.                 onTabPrevious: _.bind( control.onTabPrevious, control ),
  5425.                 onUpdateErrorNotice: _.bind( control.onUpdateErrorNotice, control )
  5426.             });
  5427.  
  5428.             control.editor = wp.codeEditor.initialize( $textarea, settings );
  5429.  
  5430.             // Improve the editor accessibility.
  5431.             $( control.editor.codemirror.display.lineDiv )
  5432.                 .attr({
  5433.                     role: 'textbox',
  5434.                     'aria-multiline': 'true',
  5435.                     'aria-label': control.params.label,
  5436.                     'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4'
  5437.                 });
  5438.  
  5439.             // Focus the editor when clicking on its label.
  5440.             control.container.find( 'label' ).on( 'click', function() {
  5441.                 control.editor.codemirror.focus();
  5442.             });
  5443.  
  5444.             /*
  5445.              * When the CodeMirror instance changes, mirror to the textarea,
  5446.              * where we have our "true" change event handler bound.
  5447.              */
  5448.             control.editor.codemirror.on( 'change', function( codemirror ) {
  5449.                 suspendEditorUpdate = true;
  5450.                 $textarea.val( codemirror.getValue() ).trigger( 'change' );
  5451.                 suspendEditorUpdate = false;
  5452.             });
  5453.  
  5454.             // Update CodeMirror when the setting is changed by another plugin.
  5455.             control.setting.bind( function( value ) {
  5456.                 if ( ! suspendEditorUpdate ) {
  5457.                     control.editor.codemirror.setValue( value );
  5458.                 }
  5459.             });
  5460.  
  5461.             // Prevent collapsing section when hitting Esc to tab out of editor.
  5462.             control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
  5463.                 var escKeyCode = 27;
  5464.                 if ( escKeyCode === event.keyCode ) {
  5465.                     event.stopPropagation();
  5466.                 }
  5467.             });
  5468.  
  5469.             control.deferred.codemirror.resolveWith( control, [ control.editor.codemirror ] );
  5470.         },
  5471.  
  5472.         /**
  5473.          * Handle tabbing to the field after the editor.
  5474.          *
  5475.          * @since 4.9.0
  5476.          * @returns {void}
  5477.          */
  5478.         onTabNext: function onTabNext() {
  5479.             var control = this, controls, controlIndex, section;
  5480.             section = api.section( control.section() );
  5481.             controls = section.controls();
  5482.             controlIndex = controls.indexOf( control );
  5483.             if ( controls.length === controlIndex + 1 ) {
  5484.                 $( '#customize-footer-actions .collapse-sidebar' ).focus();
  5485.             } else {
  5486.                 controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus();
  5487.             }
  5488.         },
  5489.  
  5490.         /**
  5491.          * Handle tabbing to the field before the editor.
  5492.          *
  5493.          * @since 4.9.0
  5494.          * @returns {void}
  5495.          */
  5496.         onTabPrevious: function onTabPrevious() {
  5497.             var control = this, controls, controlIndex, section;
  5498.             section = api.section( control.section() );
  5499.             controls = section.controls();
  5500.             controlIndex = controls.indexOf( control );
  5501.             if ( 0 === controlIndex ) {
  5502.                 section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus();
  5503.             } else {
  5504.                 controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus();
  5505.             }
  5506.         },
  5507.  
  5508.         /**
  5509.          * Update error notice.
  5510.          *
  5511.          * @since 4.9.0
  5512.          * @param {Array} errorAnnotations - Error annotations.
  5513.          * @returns {void}
  5514.          */
  5515.         onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
  5516.             var control = this, message;
  5517.             control.setting.notifications.remove( 'csslint_error' );
  5518.  
  5519.             if ( 0 !== errorAnnotations.length ) {
  5520.                 if ( 1 === errorAnnotations.length ) {
  5521.                     message = api.l10n.customCssError.singular.replace( '%d', '1' );
  5522.                 } else {
  5523.                     message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) );
  5524.                 }
  5525.                 control.setting.notifications.add( new api.Notification( 'csslint_error', {
  5526.                     message: message,
  5527.                     type: 'error'
  5528.                 } ) );
  5529.             }
  5530.         },
  5531.  
  5532.         /**
  5533.          * Initialize plain-textarea editor when syntax highlighting is disabled.
  5534.          *
  5535.          * @since 4.9.0
  5536.          * @returns {void}
  5537.          */
  5538.         initPlainTextareaEditor: function() {
  5539.             var control = this, $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
  5540.  
  5541.             $textarea.on( 'blur', function onBlur() {
  5542.                 $textarea.data( 'next-tab-blurs', false );
  5543.             } );
  5544.  
  5545.             $textarea.on( 'keydown', function onKeydown( event ) {
  5546.                 var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
  5547.  
  5548.                 if ( escKeyCode === event.keyCode ) {
  5549.                     if ( ! $textarea.data( 'next-tab-blurs' ) ) {
  5550.                         $textarea.data( 'next-tab-blurs', true );
  5551.                         event.stopPropagation(); // Prevent collapsing the section.
  5552.                     }
  5553.                     return;
  5554.                 }
  5555.  
  5556.                 // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
  5557.                 if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
  5558.                     return;
  5559.                 }
  5560.  
  5561.                 // Prevent capturing Tab characters if Esc was pressed.
  5562.                 if ( $textarea.data( 'next-tab-blurs' ) ) {
  5563.                     return;
  5564.                 }
  5565.  
  5566.                 selectionStart = textarea.selectionStart;
  5567.                 selectionEnd = textarea.selectionEnd;
  5568.                 value = textarea.value;
  5569.  
  5570.                 if ( selectionStart >= 0 ) {
  5571.                     textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
  5572.                     $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
  5573.                 }
  5574.  
  5575.                 event.stopPropagation();
  5576.                 event.preventDefault();
  5577.             });
  5578.  
  5579.             control.deferred.codemirror.rejectWith( control );
  5580.         }
  5581.     });
  5582.  
  5583.     /**
  5584.      * Class wp.customize.DateTimeControl.
  5585.      *
  5586.      * @since 4.9.0
  5587.      * @constructor
  5588.      * @augments wp.customize.Control
  5589.      * @augments wp.customize.Class
  5590.      */
  5591.     api.DateTimeControl = api.Control.extend({
  5592.  
  5593.         /**
  5594.          * Initialize behaviors.
  5595.          *
  5596.          * @since 4.9.0
  5597.          * @returns {void}
  5598.          */
  5599.         ready: function ready() {
  5600.             var control = this;
  5601.  
  5602.             control.inputElements = {};
  5603.             control.invalidDate = false;
  5604.  
  5605.             _.bindAll( control, 'populateSetting', 'updateDaysForMonth', 'populateDateInputs' );
  5606.  
  5607.             if ( ! control.setting ) {
  5608.                 throw new Error( 'Missing setting' );
  5609.             }
  5610.  
  5611.             control.container.find( '.date-input' ).each( function() {
  5612.                 var input = $( this ), component, element;
  5613.                 component = input.data( 'component' );
  5614.                 element = new api.Element( input );
  5615.                 control.inputElements[ component ] = element;
  5616.                 control.elements.push( element );
  5617.  
  5618.                 // Add invalid date error once user changes (and has blurred the input).
  5619.                 input.on( 'change', function() {
  5620.                     if ( control.invalidDate ) {
  5621.                         control.notifications.add( new api.Notification( 'invalid_date', {
  5622.                             message: api.l10n.invalidDate
  5623.                         } ) );
  5624.                     }
  5625.                 } );
  5626.  
  5627.                 // Remove the error immediately after validity change.
  5628.                 input.on( 'input', _.debounce( function() {
  5629.                     if ( ! control.invalidDate ) {
  5630.                         control.notifications.remove( 'invalid_date' );
  5631.                     }
  5632.                 } ) );
  5633.  
  5634.                 // Add zero-padding when blurring field.
  5635.                 input.on( 'blur', _.debounce( function() {
  5636.                     if ( ! control.invalidDate ) {
  5637.                         control.populateDateInputs();
  5638.                     }
  5639.                 } ) );
  5640.             } );
  5641.  
  5642.             control.inputElements.month.bind( control.updateDaysForMonth );
  5643.             control.inputElements.year.bind( control.updateDaysForMonth );
  5644.             control.populateDateInputs();
  5645.             control.setting.bind( control.populateDateInputs );
  5646.  
  5647.             // Start populating setting after inputs have been populated.
  5648.             _.each( control.inputElements, function( element ) {
  5649.                 element.bind( control.populateSetting );
  5650.             } );
  5651.         },
  5652.  
  5653.         /**
  5654.          * Parse datetime string.
  5655.          *
  5656.          * @since 4.9.0
  5657.          *
  5658.          * @param {string} datetime - Date/Time string. Accepts Y-m-d[ H:i[:s]] format.
  5659.          * @returns {object|null} Returns object containing date components or null if parse error.
  5660.          */
  5661.         parseDateTime: function parseDateTime( datetime ) {
  5662.             var control = this, matches, date, midDayHour = 12;
  5663.  
  5664.             if ( datetime ) {
  5665.                 matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d)(?: (\d\d):(\d\d)(?::(\d\d))?)?$/ );
  5666.             }
  5667.  
  5668.             if ( ! matches ) {
  5669.                 return null;
  5670.             }
  5671.  
  5672.             matches.shift();
  5673.  
  5674.             date = {
  5675.                 year: matches.shift(),
  5676.                 month: matches.shift(),
  5677.                 day: matches.shift(),
  5678.                 hour: matches.shift() || '00',
  5679.                 minute: matches.shift() || '00',
  5680.                 second: matches.shift() || '00'
  5681.             };
  5682.  
  5683.             if ( control.params.includeTime && control.params.twelveHourFormat ) {
  5684.                 date.hour = parseInt( date.hour, 10 );
  5685.                 date.meridian = date.hour >= midDayHour ? 'pm' : 'am';
  5686.                 date.hour = date.hour % midDayHour ? String( date.hour % midDayHour ) : String( midDayHour );
  5687.                 delete date.second; // @todo Why only if twelveHourFormat?
  5688.             }
  5689.  
  5690.             return date;
  5691.         },
  5692.  
  5693.         /**
  5694.          * Validates if input components have valid date and time.
  5695.          *
  5696.          * @since 4.9.0
  5697.          * @return {boolean} If date input fields has error.
  5698.          */
  5699.         validateInputs: function validateInputs() {
  5700.             var control = this, components, validityInput;
  5701.  
  5702.             control.invalidDate = false;
  5703.  
  5704.             components = [ 'year', 'day' ];
  5705.             if ( control.params.includeTime ) {
  5706.                 components.push( 'hour', 'minute' );
  5707.             }
  5708.  
  5709.             _.find( components, function( component ) {
  5710.                 var element, max, min, value;
  5711.  
  5712.                 element = control.inputElements[ component ];
  5713.                 validityInput = element.element.get( 0 );
  5714.                 max = parseInt( element.element.attr( 'max' ), 10 );
  5715.                 min = parseInt( element.element.attr( 'min' ), 10 );
  5716.                 value = parseInt( element(), 10 );
  5717.                 control.invalidDate = isNaN( value ) || value > max || value < min;
  5718.  
  5719.                 if ( ! control.invalidDate ) {
  5720.                     validityInput.setCustomValidity( '' );
  5721.                 }
  5722.  
  5723.                 return control.invalidDate;
  5724.             } );
  5725.  
  5726.             if ( control.inputElements.meridian && ! control.invalidDate ) {
  5727.                 validityInput = control.inputElements.meridian.element.get( 0 );
  5728.                 if ( 'am' !== control.inputElements.meridian.get() && 'pm' !== control.inputElements.meridian.get() ) {
  5729.                     control.invalidDate = true;
  5730.                 } else {
  5731.                     validityInput.setCustomValidity( '' );
  5732.                 }
  5733.             }
  5734.  
  5735.             if ( control.invalidDate ) {
  5736.                 validityInput.setCustomValidity( api.l10n.invalidValue );
  5737.             } else {
  5738.                 validityInput.setCustomValidity( '' );
  5739.             }
  5740.             if ( ! control.section() || api.section.has( control.section() ) && api.section( control.section() ).expanded() ) {
  5741.                 _.result( validityInput, 'reportValidity' );
  5742.             }
  5743.  
  5744.             return control.invalidDate;
  5745.         },
  5746.  
  5747.         /**
  5748.          * Updates number of days according to the month and year selected.
  5749.          *
  5750.          * @since 4.9.0
  5751.          * @return {void}
  5752.          */
  5753.         updateDaysForMonth: function updateDaysForMonth() {
  5754.             var control = this, daysInMonth, year, month, day;
  5755.  
  5756.             month = parseInt( control.inputElements.month(), 10 );
  5757.             year = parseInt( control.inputElements.year(), 10 );
  5758.             day = parseInt( control.inputElements.day(), 10 );
  5759.  
  5760.             if ( month && year ) {
  5761.                 daysInMonth = new Date( year, month, 0 ).getDate();
  5762.                 control.inputElements.day.element.attr( 'max', daysInMonth );
  5763.  
  5764.                 if ( day > daysInMonth ) {
  5765.                     control.inputElements.day( String( daysInMonth ) );
  5766.                 }
  5767.             }
  5768.         },
  5769.  
  5770.         /**
  5771.          * Populate setting value from the inputs.
  5772.          *
  5773.          * @since 4.9.0
  5774.          * @returns {boolean} If setting updated.
  5775.          */
  5776.         populateSetting: function populateSetting() {
  5777.             var control = this, date;
  5778.  
  5779.             if ( control.validateInputs() || ! control.params.allowPastDate && ! control.isFutureDate() ) {
  5780.                 return false;
  5781.             }
  5782.  
  5783.             date = control.convertInputDateToString();
  5784.             control.setting.set( date );
  5785.             return true;
  5786.         },
  5787.  
  5788.         /**
  5789.          * Converts input values to string in Y-m-d H:i:s format.
  5790.          *
  5791.          * @since 4.9.0
  5792.          * @return {string} Date string.
  5793.          */
  5794.         convertInputDateToString: function convertInputDateToString() {
  5795.             var control = this, date = '', dateFormat, hourInTwentyFourHourFormat,
  5796.                 getElementValue, pad;
  5797.  
  5798.             pad = function( number, padding ) {
  5799.                 var zeros;
  5800.                 if ( String( number ).length < padding ) {
  5801.                     zeros = padding - String( number ).length;
  5802.                     number = Math.pow( 10, zeros ).toString().substr( 1 ) + String( number );
  5803.                 }
  5804.                 return number;
  5805.             };
  5806.  
  5807.             getElementValue = function( component ) {
  5808.                 var value = parseInt( control.inputElements[ component ].get(), 10 );
  5809.  
  5810.                 if ( _.contains( [ 'month', 'day', 'hour', 'minute' ], component ) ) {
  5811.                     value = pad( value, 2 );
  5812.                 } else if ( 'year' === component ) {
  5813.                     value = pad( value, 4 );
  5814.                 }
  5815.                 return value;
  5816.             };
  5817.  
  5818.             dateFormat = [ 'year', '-', 'month', '-', 'day' ];
  5819.             if ( control.params.includeTime ) {
  5820.                 hourInTwentyFourHourFormat = control.inputElements.meridian ? control.convertHourToTwentyFourHourFormat( control.inputElements.hour(), control.inputElements.meridian() ) : control.inputElements.hour();
  5821.                 dateFormat = dateFormat.concat( [ ' ', pad( hourInTwentyFourHourFormat, 2 ), ':', 'minute', ':', '00' ] );
  5822.             }
  5823.  
  5824.             _.each( dateFormat, function( component ) {
  5825.                 date += control.inputElements[ component ] ? getElementValue( component ) : component;
  5826.             } );
  5827.  
  5828.             return date;
  5829.         },
  5830.  
  5831.         /**
  5832.          * Check if the date is in the future.
  5833.          *
  5834.          * @since 4.9.0
  5835.          * @returns {boolean} True if future date.
  5836.          */
  5837.         isFutureDate: function isFutureDate() {
  5838.             var control = this;
  5839.             return 0 < api.utils.getRemainingTime( control.convertInputDateToString() );
  5840.         },
  5841.  
  5842.         /**
  5843.          * Convert hour in twelve hour format to twenty four hour format.
  5844.          *
  5845.          * @since 4.9.0
  5846.          * @param {string} hourInTwelveHourFormat - Hour in twelve hour format.
  5847.          * @param {string} meridian - Either 'am' or 'pm'.
  5848.          * @returns {string} Hour in twenty four hour format.
  5849.          */
  5850.         convertHourToTwentyFourHourFormat: function convertHour( hourInTwelveHourFormat, meridian ) {
  5851.             var hourInTwentyFourHourFormat, hour, midDayHour = 12;
  5852.  
  5853.             hour = parseInt( hourInTwelveHourFormat, 10 );
  5854.             if ( isNaN( hour ) ) {
  5855.                 return '';
  5856.             }
  5857.  
  5858.             if ( 'pm' === meridian && hour < midDayHour ) {
  5859.                 hourInTwentyFourHourFormat = hour + midDayHour;
  5860.             } else if ( 'am' === meridian && midDayHour === hour ) {
  5861.                 hourInTwentyFourHourFormat = hour - midDayHour;
  5862.             } else {
  5863.                 hourInTwentyFourHourFormat = hour;
  5864.             }
  5865.  
  5866.             return String( hourInTwentyFourHourFormat );
  5867.         },
  5868.  
  5869.         /**
  5870.          * Populates date inputs in date fields.
  5871.          *
  5872.          * @since 4.9.0
  5873.          * @returns {boolean} Whether the inputs were populated.
  5874.          */
  5875.         populateDateInputs: function populateDateInputs() {
  5876.             var control = this, parsed;
  5877.  
  5878.             parsed = control.parseDateTime( control.setting.get() );
  5879.  
  5880.             if ( ! parsed ) {
  5881.                 return false;
  5882.             }
  5883.  
  5884.             _.each( control.inputElements, function( element, component ) {
  5885.                 var value = parsed[ component ]; // This will be zero-padded string.
  5886.  
  5887.                 // Set month and meridian regardless of focused state since they are dropdowns.
  5888.                 if ( 'month' === component || 'meridian' === component ) {
  5889.  
  5890.                     // Options in dropdowns are not zero-padded.
  5891.                     value = value.replace( /^0/, '' );
  5892.  
  5893.                     element.set( value );
  5894.                 } else {
  5895.  
  5896.                     value = parseInt( value, 10 );
  5897.                     if ( ! element.element.is( document.activeElement ) ) {
  5898.  
  5899.                         // Populate element with zero-padded value if not focused.
  5900.                         element.set( parsed[ component ] );
  5901.                     } else if ( value !== parseInt( element(), 10 ) ) {
  5902.  
  5903.                         // Forcibly update the value if its underlying value changed, regardless of zero-padding.
  5904.                         element.set( String( value ) );
  5905.                     }
  5906.                 }
  5907.             } );
  5908.  
  5909.             return true;
  5910.         },
  5911.  
  5912.         /**
  5913.          * Toggle future date notification for date control.
  5914.          *
  5915.          * @since 4.9.0
  5916.          * @param {boolean} notify Add or remove the notification.
  5917.          * @return {wp.customize.DateTimeControl}
  5918.          */
  5919.         toggleFutureDateNotification: function toggleFutureDateNotification( notify ) {
  5920.             var control = this, notificationCode, notification;
  5921.  
  5922.             notificationCode = 'not_future_date';
  5923.  
  5924.             if ( notify ) {
  5925.                 notification = new api.Notification( notificationCode, {
  5926.                     type: 'error',
  5927.                     message: api.l10n.futureDateError
  5928.                 } );
  5929.                 control.notifications.add( notification );
  5930.             } else {
  5931.                 control.notifications.remove( notificationCode );
  5932.             }
  5933.  
  5934.             return control;
  5935.         }
  5936.     });
  5937.  
  5938.     /**
  5939.      * Class PreviewLinkControl.
  5940.      *
  5941.      * @since 4.9.0
  5942.      * @constructor
  5943.      * @augments wp.customize.Control
  5944.      * @augments wp.customize.Class
  5945.      */
  5946.     api.PreviewLinkControl = api.Control.extend({
  5947.  
  5948.         defaults: _.extend( {}, api.Control.prototype.defaults, {
  5949.             templateId: 'customize-preview-link-control'
  5950.         } ),
  5951.  
  5952.         /**
  5953.          * Initialize behaviors.
  5954.          *
  5955.          * @since 4.9.0
  5956.          * @returns {void}
  5957.          */
  5958.         ready: function ready() {
  5959.             var control = this, element, component, node, url, input, button;
  5960.  
  5961.             _.bindAll( control, 'updatePreviewLink' );
  5962.  
  5963.             if ( ! control.setting ) {
  5964.                 control.setting = new api.Value();
  5965.             }
  5966.  
  5967.             control.previewElements = {};
  5968.  
  5969.             control.container.find( '.preview-control-element' ).each( function() {
  5970.                 node = $( this );
  5971.                 component = node.data( 'component' );
  5972.                 element = new api.Element( node );
  5973.                 control.previewElements[ component ] = element;
  5974.                 control.elements.push( element );
  5975.             } );
  5976.  
  5977.             url = control.previewElements.url;
  5978.             input = control.previewElements.input;
  5979.             button = control.previewElements.button;
  5980.  
  5981.             input.link( control.setting );
  5982.             url.link( control.setting );
  5983.  
  5984.             url.bind( function( value ) {
  5985.                 url.element.parent().attr( {
  5986.                     href: value,
  5987.                     target: api.settings.changeset.uuid
  5988.                 } );
  5989.             } );
  5990.  
  5991.             api.bind( 'ready', control.updatePreviewLink );
  5992.             api.state( 'saved' ).bind( control.updatePreviewLink );
  5993.             api.state( 'changesetStatus' ).bind( control.updatePreviewLink );
  5994.             api.state( 'activated' ).bind( control.updatePreviewLink );
  5995.             api.previewer.previewUrl.bind( control.updatePreviewLink );
  5996.  
  5997.             button.element.on( 'click', function( event ) {
  5998.                 event.preventDefault();
  5999.                 if ( control.setting() ) {
  6000.                     input.element.select();
  6001.                     document.execCommand( 'copy' );
  6002.                     button( button.element.data( 'copied-text' ) );
  6003.                 }
  6004.             } );
  6005.  
  6006.             url.element.parent().on( 'click', function( event ) {
  6007.                 if ( $( this ).hasClass( 'disabled' ) ) {
  6008.                     event.preventDefault();
  6009.                 }
  6010.             } );
  6011.  
  6012.             button.element.on( 'mouseenter', function() {
  6013.                 if ( control.setting() ) {
  6014.                     button( button.element.data( 'copy-text' ) );
  6015.                 }
  6016.             } );
  6017.         },
  6018.  
  6019.         /**
  6020.          * Updates Preview Link
  6021.          *
  6022.          * @since 4.9.0
  6023.          * @return {void}
  6024.          */
  6025.         updatePreviewLink: function updatePreviewLink() {
  6026.             var control = this, unsavedDirtyValues;
  6027.  
  6028.             unsavedDirtyValues = ! api.state( 'saved' ).get() || '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get();
  6029.  
  6030.             control.toggleSaveNotification( unsavedDirtyValues );
  6031.             control.previewElements.url.element.parent().toggleClass( 'disabled', unsavedDirtyValues );
  6032.             control.previewElements.button.element.prop( 'disabled', unsavedDirtyValues );
  6033.             control.setting.set( api.previewer.getFrontendPreviewUrl() );
  6034.         },
  6035.  
  6036.         /**
  6037.          * Toggles save notification.
  6038.          *
  6039.          * @since 4.9.0
  6040.          * @param {boolean} notify Add or remove notification.
  6041.          * @return {void}
  6042.          */
  6043.         toggleSaveNotification: function toggleSaveNotification( notify ) {
  6044.             var control = this, notificationCode, notification;
  6045.  
  6046.             notificationCode = 'changes_not_saved';
  6047.  
  6048.             if ( notify ) {
  6049.                 notification = new api.Notification( notificationCode, {
  6050.                     type: 'info',
  6051.                     message: api.l10n.saveBeforeShare
  6052.                 } );
  6053.                 control.notifications.add( notification );
  6054.             } else {
  6055.                 control.notifications.remove( notificationCode );
  6056.             }
  6057.         }
  6058.     });
  6059.  
  6060.     // Change objects contained within the main customize object to Settings.
  6061.     api.defaultConstructor = api.Setting;
  6062.  
  6063.     /**
  6064.      * Callback for resolved controls.
  6065.      *
  6066.      * @callback deferredControlsCallback
  6067.      * @param {wp.customize.Control[]} Resolved controls.
  6068.      */
  6069.  
  6070.     /**
  6071.      * Collection of all registered controls.
  6072.      *
  6073.      * @since 3.4.0
  6074.      *
  6075.      * @type {Function}
  6076.      * @param {...string} ids - One or more ids for controls to obtain.
  6077.      * @param {deferredControlsCallback} [callback] - Function called when all supplied controls exist.
  6078.      * @returns {wp.customize.Control|undefined|jQuery.promise} Control instance or undefined (if function called with one id param), or promise resolving to requested controls.
  6079.      *
  6080.      * @example <caption>Loop over all registered controls.</caption>
  6081.      * wp.customize.control.each( function( control ) { ... } );
  6082.      *
  6083.      * @example <caption>Getting `background_color` control instance.</caption>
  6084.      * control = wp.customize.control( 'background_color' );
  6085.      *
  6086.      * @example <caption>Check if control exists.</caption>
  6087.      * hasControl = wp.customize.control.has( 'background_color' );
  6088.      *
  6089.      * @example <caption>Deferred getting of `background_color` control until it exists, using callback.</caption>
  6090.      * wp.customize.control( 'background_color', function( control ) { ... } );
  6091.      *
  6092.      * @example <caption>Get title and tagline controls when they both exist, using promise (only available when multiple IDs are present).</caption>
  6093.      * promise = wp.customize.control( 'blogname', 'blogdescription' );
  6094.      * promise.done( function( titleControl, taglineControl ) { ... } );
  6095.      *
  6096.      * @example <caption>Get title and tagline controls when they both exist, using callback.</caption>
  6097.      * wp.customize.control( 'blogname', 'blogdescription', function( titleControl, taglineControl ) { ... } );
  6098.      *
  6099.      * @example <caption>Getting setting value for `background_color` control.</caption>
  6100.      * value = wp.customize.control( 'background_color ').setting.get();
  6101.      * value = wp.customize( 'background_color' ).get(); // Same as above, since setting ID and control ID are the same.
  6102.      *
  6103.      * @example <caption>Add new control for site title.</caption>
  6104.      * wp.customize.control.add( new wp.customize.Control( 'other_blogname', {
  6105.      *     setting: 'blogname',
  6106.      *     type: 'text',
  6107.      *     label: 'Site title',
  6108.      *     section: 'other_site_identify'
  6109.      * } ) );
  6110.      *
  6111.      * @example <caption>Remove control.</caption>
  6112.      * wp.customize.control.remove( 'other_blogname' );
  6113.      *
  6114.      * @example <caption>Listen for control being added.</caption>
  6115.      * wp.customize.control.bind( 'add', function( addedControl ) { ... } )
  6116.      *
  6117.      * @example <caption>Listen for control being removed.</caption>
  6118.      * wp.customize.control.bind( 'removed', function( removedControl ) { ... } )
  6119.      */
  6120.     api.control = new api.Values({ defaultConstructor: api.Control });
  6121.  
  6122.     /**
  6123.      * Callback for resolved sections.
  6124.      *
  6125.      * @callback deferredSectionsCallback
  6126.      * @param {wp.customize.Section[]} Resolved sections.
  6127.      */
  6128.  
  6129.     /**
  6130.      * Collection of all registered sections.
  6131.      *
  6132.      * @since 3.4.0
  6133.      *
  6134.      * @type {Function}
  6135.      * @param {...string} ids - One or more ids for sections to obtain.
  6136.      * @param {deferredSectionsCallback} [callback] - Function called when all supplied sections exist.
  6137.      * @returns {wp.customize.Section|undefined|jQuery.promise} Section instance or undefined (if function called with one id param), or promise resolving to requested sections.
  6138.      *
  6139.      * @example <caption>Loop over all registered sections.</caption>
  6140.      * wp.customize.section.each( function( section ) { ... } )
  6141.      *
  6142.      * @example <caption>Getting `title_tagline` section instance.</caption>
  6143.      * section = wp.customize.section( 'title_tagline' )
  6144.      *
  6145.      * @example <caption>Expand dynamically-created section when it exists.</caption>
  6146.      * wp.customize.section( 'dynamically_created', function( section ) {
  6147.      *     section.expand();
  6148.      * } );
  6149.      *
  6150.      * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
  6151.      */
  6152.     api.section = new api.Values({ defaultConstructor: api.Section });
  6153.  
  6154.     /**
  6155.      * Callback for resolved panels.
  6156.      *
  6157.      * @callback deferredPanelsCallback
  6158.      * @param {wp.customize.Panel[]} Resolved panels.
  6159.      */
  6160.  
  6161.     /**
  6162.      * Collection of all registered panels.
  6163.      *
  6164.      * @since 4.0.0
  6165.      *
  6166.      * @type {Function}
  6167.      * @param {...string} ids - One or more ids for panels to obtain.
  6168.      * @param {deferredPanelsCallback} [callback] - Function called when all supplied panels exist.
  6169.      * @returns {wp.customize.Panel|undefined|jQuery.promise} Panel instance or undefined (if function called with one id param), or promise resolving to requested panels.
  6170.      *
  6171.      * @example <caption>Loop over all registered panels.</caption>
  6172.      * wp.customize.panel.each( function( panel ) { ... } )
  6173.      *
  6174.      * @example <caption>Getting nav_menus panel instance.</caption>
  6175.      * panel = wp.customize.panel( 'nav_menus' );
  6176.      *
  6177.      * @example <caption>Expand dynamically-created panel when it exists.</caption>
  6178.      * wp.customize.panel( 'dynamically_created', function( panel ) {
  6179.      *     panel.expand();
  6180.      * } );
  6181.      *
  6182.      * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
  6183.      */
  6184.     api.panel = new api.Values({ defaultConstructor: api.Panel });
  6185.  
  6186.     /**
  6187.      * Callback for resolved notifications.
  6188.      *
  6189.      * @callback deferredNotificationsCallback
  6190.      * @param {wp.customize.Notification[]} Resolved notifications.
  6191.      */
  6192.  
  6193.     /**
  6194.      * Collection of all global notifications.
  6195.      *
  6196.      * @since 4.9.0
  6197.      *
  6198.      * @type {Function}
  6199.      * @param {...string} codes - One or more codes for notifications to obtain.
  6200.      * @param {deferredNotificationsCallback} [callback] - Function called when all supplied notifications exist.
  6201.      * @returns {wp.customize.Notification|undefined|jQuery.promise} notification instance or undefined (if function called with one code param), or promise resolving to requested notifications.
  6202.      *
  6203.      * @example <caption>Check if existing notification</caption>
  6204.      * exists = wp.customize.notifications.has( 'a_new_day_arrived' );
  6205.      *
  6206.      * @example <caption>Obtain existing notification</caption>
  6207.      * notification = wp.customize.notifications( 'a_new_day_arrived' );
  6208.      *
  6209.      * @example <caption>Obtain notification that may not exist yet.</caption>
  6210.      * wp.customize.notifications( 'a_new_day_arrived', function( notification ) { ... } );
  6211.      *
  6212.      * @example <caption>Add a warning notification.</caption>
  6213.      * wp.customize.notifications.add( new wp.customize.Notification( 'midnight_almost_here', {
  6214.      *     type: 'warning',
  6215.      *     message: 'Midnight has almost arrived!',
  6216.      *     dismissible: true
  6217.      * } ) );
  6218.      *
  6219.      * @example <caption>Remove a notification.</caption>
  6220.      * wp.customize.notifications.remove( 'a_new_day_arrived' );
  6221.      *
  6222.      * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
  6223.      */
  6224.     api.notifications = new api.Notifications();
  6225.  
  6226.     /**
  6227.      * An object that fetches a preview in the background of the document, which
  6228.      * allows for seamless replacement of an existing preview.
  6229.      *
  6230.      * @class
  6231.      * @augments wp.customize.Messenger
  6232.      * @augments wp.customize.Class
  6233.      * @mixes wp.customize.Events
  6234.      */
  6235.     api.PreviewFrame = api.Messenger.extend({
  6236.         sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity.
  6237.  
  6238.         /**
  6239.          * Initialize the PreviewFrame.
  6240.          *
  6241.          * @param {object} params.container
  6242.          * @param {object} params.previewUrl
  6243.          * @param {object} params.query
  6244.          * @param {object} options
  6245.          */
  6246.         initialize: function( params, options ) {
  6247.             var deferred = $.Deferred();
  6248.  
  6249.             /*
  6250.              * Make the instance of the PreviewFrame the promise object
  6251.              * so other objects can easily interact with it.
  6252.              */
  6253.             deferred.promise( this );
  6254.  
  6255.             this.container = params.container;
  6256.  
  6257.             $.extend( params, { channel: api.PreviewFrame.uuid() });
  6258.  
  6259.             api.Messenger.prototype.initialize.call( this, params, options );
  6260.  
  6261.             this.add( 'previewUrl', params.previewUrl );
  6262.  
  6263.             this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
  6264.  
  6265.             this.run( deferred );
  6266.         },
  6267.  
  6268.         /**
  6269.          * Run the preview request.
  6270.          *
  6271.          * @param {object} deferred jQuery Deferred object to be resolved with
  6272.          *                          the request.
  6273.          */
  6274.         run: function( deferred ) {
  6275.             var previewFrame = this,
  6276.                 loaded = false,
  6277.                 ready = false,
  6278.                 readyData = null,
  6279.                 hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized,
  6280.                 urlParser,
  6281.                 params,
  6282.                 form;
  6283.  
  6284.             if ( previewFrame._ready ) {
  6285.                 previewFrame.unbind( 'ready', previewFrame._ready );
  6286.             }
  6287.  
  6288.             previewFrame._ready = function( data ) {
  6289.                 ready = true;
  6290.                 readyData = data;
  6291.                 previewFrame.container.addClass( 'iframe-ready' );
  6292.                 if ( ! data ) {
  6293.                     return;
  6294.                 }
  6295.  
  6296.                 if ( loaded ) {
  6297.                     deferred.resolveWith( previewFrame, [ data ] );
  6298.                 }
  6299.             };
  6300.  
  6301.             previewFrame.bind( 'ready', previewFrame._ready );
  6302.  
  6303.             urlParser = document.createElement( 'a' );
  6304.             urlParser.href = previewFrame.previewUrl();
  6305.  
  6306.             params = _.extend(
  6307.                 api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
  6308.                 {
  6309.                     customize_changeset_uuid: previewFrame.query.customize_changeset_uuid,
  6310.                     customize_theme: previewFrame.query.customize_theme,
  6311.                     customize_messenger_channel: previewFrame.query.customize_messenger_channel
  6312.                 }
  6313.             );
  6314.             if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
  6315.                 params.customize_autosaved = 'on';
  6316.             }
  6317.  
  6318.             urlParser.search = $.param( params );
  6319.             previewFrame.iframe = $( '<iframe />', {
  6320.                 title: api.l10n.previewIframeTitle,
  6321.                 name: 'customize-' + previewFrame.channel()
  6322.             } );
  6323.             previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149.
  6324.  
  6325.             if ( ! hasPendingChangesetUpdate ) {
  6326.                 previewFrame.iframe.attr( 'src', urlParser.href );
  6327.             } else {
  6328.                 previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes.
  6329.             }
  6330.  
  6331.             previewFrame.iframe.appendTo( previewFrame.container );
  6332.             previewFrame.targetWindow( previewFrame.iframe[0].contentWindow );
  6333.  
  6334.             /*
  6335.              * Submit customized data in POST request to preview frame window since
  6336.              * there are setting value changes not yet written to changeset.
  6337.              */
  6338.             if ( hasPendingChangesetUpdate ) {
  6339.                 form = $( '<form>', {
  6340.                     action: urlParser.href,
  6341.                     target: previewFrame.iframe.attr( 'name' ),
  6342.                     method: 'post',
  6343.                     hidden: 'hidden'
  6344.                 } );
  6345.                 form.append( $( '<input>', {
  6346.                     type: 'hidden',
  6347.                     name: '_method',
  6348.                     value: 'GET'
  6349.                 } ) );
  6350.                 _.each( previewFrame.query, function( value, key ) {
  6351.                     form.append( $( '<input>', {
  6352.                         type: 'hidden',
  6353.                         name: key,
  6354.                         value: value
  6355.                     } ) );
  6356.                 } );
  6357.                 previewFrame.container.append( form );
  6358.                 form.submit();
  6359.                 form.remove(); // No need to keep the form around after submitted.
  6360.             }
  6361.  
  6362.             previewFrame.bind( 'iframe-loading-error', function( error ) {
  6363.                 previewFrame.iframe.remove();
  6364.  
  6365.                 // Check if the user is not logged in.
  6366.                 if ( 0 === error ) {
  6367.                     previewFrame.login( deferred );
  6368.                     return;
  6369.                 }
  6370.  
  6371.                 // Check for cheaters.
  6372.                 if ( -1 === error ) {
  6373.                     deferred.rejectWith( previewFrame, [ 'cheatin' ] );
  6374.                     return;
  6375.                 }
  6376.  
  6377.                 deferred.rejectWith( previewFrame, [ 'request failure' ] );
  6378.             } );
  6379.  
  6380.             previewFrame.iframe.one( 'load', function() {
  6381.                 loaded = true;
  6382.  
  6383.                 if ( ready ) {
  6384.                     deferred.resolveWith( previewFrame, [ readyData ] );
  6385.                 } else {
  6386.                     setTimeout( function() {
  6387.                         deferred.rejectWith( previewFrame, [ 'ready timeout' ] );
  6388.                     }, previewFrame.sensitivity );
  6389.                 }
  6390.             });
  6391.         },
  6392.  
  6393.         login: function( deferred ) {
  6394.             var self = this,
  6395.                 reject;
  6396.  
  6397.             reject = function() {
  6398.                 deferred.rejectWith( self, [ 'logged out' ] );
  6399.             };
  6400.  
  6401.             if ( this.triedLogin ) {
  6402.                 return reject();
  6403.             }
  6404.  
  6405.             // Check if we have an admin cookie.
  6406.             $.get( api.settings.url.ajax, {
  6407.                 action: 'logged-in'
  6408.             }).fail( reject ).done( function( response ) {
  6409.                 var iframe;
  6410.  
  6411.                 if ( '1' !== response ) {
  6412.                     reject();
  6413.                 }
  6414.  
  6415.                 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
  6416.                 iframe.appendTo( self.container );
  6417.                 iframe.on( 'load', function() {
  6418.                     self.triedLogin = true;
  6419.  
  6420.                     iframe.remove();
  6421.                     self.run( deferred );
  6422.                 });
  6423.             });
  6424.         },
  6425.  
  6426.         destroy: function() {
  6427.             api.Messenger.prototype.destroy.call( this );
  6428.  
  6429.             if ( this.iframe ) {
  6430.                 this.iframe.remove();
  6431.             }
  6432.  
  6433.             delete this.iframe;
  6434.             delete this.targetWindow;
  6435.         }
  6436.     });
  6437.  
  6438.     (function(){
  6439.         var id = 0;
  6440.         /**
  6441.          * Return an incremented ID for a preview messenger channel.
  6442.          *
  6443.          * This function is named "uuid" for historical reasons, but it is a
  6444.          * misnomer as it is not an actual UUID, and it is not universally unique.
  6445.          * This is not to be confused with `api.settings.changeset.uuid`.
  6446.          *
  6447.          * @return {string}
  6448.          */
  6449.         api.PreviewFrame.uuid = function() {
  6450.             return 'preview-' + String( id++ );
  6451.         };
  6452.     }());
  6453.  
  6454.     /**
  6455.      * Set the document title of the customizer.
  6456.      *
  6457.      * @since 4.1.0
  6458.      *
  6459.      * @param {string} documentTitle
  6460.      */
  6461.     api.setDocumentTitle = function ( documentTitle ) {
  6462.         var tmpl, title;
  6463.         tmpl = api.settings.documentTitleTmpl;
  6464.         title = tmpl.replace( '%s', documentTitle );
  6465.         document.title = title;
  6466.         api.trigger( 'title', title );
  6467.     };
  6468.  
  6469.     /**
  6470.      * @class
  6471.      * @augments wp.customize.Messenger
  6472.      * @augments wp.customize.Class
  6473.      * @mixes wp.customize.Events
  6474.      */
  6475.     api.Previewer = api.Messenger.extend({
  6476.         refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh.
  6477.  
  6478.         /**
  6479.          * @param {array}  params.allowedUrls
  6480.          * @param {string} params.container   A selector or jQuery element for the preview
  6481.          *                                    frame to be placed.
  6482.          * @param {string} params.form
  6483.          * @param {string} params.previewUrl  The URL to preview.
  6484.          * @param {object} options
  6485.          */
  6486.         initialize: function( params, options ) {
  6487.             var previewer = this,
  6488.                 urlParser = document.createElement( 'a' );
  6489.  
  6490.             $.extend( previewer, options || {} );
  6491.             previewer.deferred = {
  6492.                 active: $.Deferred()
  6493.             };
  6494.  
  6495.             // Debounce to prevent hammering server and then wait for any pending update requests.
  6496.             previewer.refresh = _.debounce(
  6497.                 ( function( originalRefresh ) {
  6498.                     return function() {
  6499.                         var isProcessingComplete, refreshOnceProcessingComplete;
  6500.                         isProcessingComplete = function() {
  6501.                             return 0 === api.state( 'processing' ).get();
  6502.                         };
  6503.                         if ( isProcessingComplete() ) {
  6504.                             originalRefresh.call( previewer );
  6505.                         } else {
  6506.                             refreshOnceProcessingComplete = function() {
  6507.                                 if ( isProcessingComplete() ) {
  6508.                                     originalRefresh.call( previewer );
  6509.                                     api.state( 'processing' ).unbind( refreshOnceProcessingComplete );
  6510.                                 }
  6511.                             };
  6512.                             api.state( 'processing' ).bind( refreshOnceProcessingComplete );
  6513.                         }
  6514.                     };
  6515.                 }( previewer.refresh ) ),
  6516.                 previewer.refreshBuffer
  6517.             );
  6518.  
  6519.             previewer.container   = api.ensure( params.container );
  6520.             previewer.allowedUrls = params.allowedUrls;
  6521.  
  6522.             params.url = window.location.href;
  6523.  
  6524.             api.Messenger.prototype.initialize.call( previewer, params );
  6525.  
  6526.             urlParser.href = previewer.origin();
  6527.             previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
  6528.  
  6529.             // Limit the URL to internal, front-end links.
  6530.             //
  6531.             // If the front end and the admin are served from the same domain, load the
  6532.             // preview over ssl if the Customizer is being loaded over ssl. This avoids
  6533.             // insecure content warnings. This is not attempted if the admin and front end
  6534.             // are on different domains to avoid the case where the front end doesn't have
  6535.             // ssl certs.
  6536.  
  6537.             previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
  6538.                 var result = null, urlParser, queryParams, parsedAllowedUrl, parsedCandidateUrls = [];
  6539.                 urlParser = document.createElement( 'a' );
  6540.                 urlParser.href = to;
  6541.  
  6542.                 // Abort if URL is for admin or (static) files in wp-includes or wp-content.
  6543.                 if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) {
  6544.                     return null;
  6545.                 }
  6546.  
  6547.                 // Remove state query params.
  6548.                 if ( urlParser.search.length > 1 ) {
  6549.                     queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  6550.                     delete queryParams.customize_changeset_uuid;
  6551.                     delete queryParams.customize_theme;
  6552.                     delete queryParams.customize_messenger_channel;
  6553.                     delete queryParams.customize_autosaved;
  6554.                     if ( _.isEmpty( queryParams ) ) {
  6555.                         urlParser.search = '';
  6556.                     } else {
  6557.                         urlParser.search = $.param( queryParams );
  6558.                     }
  6559.                 }
  6560.  
  6561.                 parsedCandidateUrls.push( urlParser );
  6562.  
  6563.                 // Prepend list with URL that matches the scheme/protocol of the iframe.
  6564.                 if ( previewer.scheme.get() + ':' !== urlParser.protocol ) {
  6565.                     urlParser = document.createElement( 'a' );
  6566.                     urlParser.href = parsedCandidateUrls[0].href;
  6567.                     urlParser.protocol = previewer.scheme.get() + ':';
  6568.                     parsedCandidateUrls.unshift( urlParser );
  6569.                 }
  6570.  
  6571.                 // Attempt to match the URL to the control frame's scheme and check if it's allowed. If not, try the original URL.
  6572.                 parsedAllowedUrl = document.createElement( 'a' );
  6573.                 _.find( parsedCandidateUrls, function( parsedCandidateUrl ) {
  6574.                     return ! _.isUndefined( _.find( previewer.allowedUrls, function( allowedUrl ) {
  6575.                         parsedAllowedUrl.href = allowedUrl;
  6576.                         if ( urlParser.protocol === parsedAllowedUrl.protocol && urlParser.host === parsedAllowedUrl.host && 0 === urlParser.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) ) ) {
  6577.                             result = parsedCandidateUrl.href;
  6578.                             return true;
  6579.                         }
  6580.                     } ) );
  6581.                 } );
  6582.  
  6583.                 return result;
  6584.             });
  6585.  
  6586.             previewer.bind( 'ready', previewer.ready );
  6587.  
  6588.             // Start listening for keep-alive messages when iframe first loads.
  6589.             previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) );
  6590.  
  6591.             previewer.bind( 'synced', function() {
  6592.                 previewer.send( 'active' );
  6593.             } );
  6594.  
  6595.             // Refresh the preview when the URL is changed (but not yet).
  6596.             previewer.previewUrl.bind( previewer.refresh );
  6597.  
  6598.             previewer.scroll = 0;
  6599.             previewer.bind( 'scroll', function( distance ) {
  6600.                 previewer.scroll = distance;
  6601.             });
  6602.  
  6603.             // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh.
  6604.             previewer.bind( 'url', function( url ) {
  6605.                 var onUrlChange, urlChanged = false;
  6606.                 previewer.scroll = 0;
  6607.                 onUrlChange = function() {
  6608.                     urlChanged = true;
  6609.                 };
  6610.                 previewer.previewUrl.bind( onUrlChange );
  6611.                 previewer.previewUrl.set( url );
  6612.                 previewer.previewUrl.unbind( onUrlChange );
  6613.                 if ( ! urlChanged ) {
  6614.                     previewer.refresh();
  6615.                 }
  6616.             } );
  6617.  
  6618.             // Update the document title when the preview changes.
  6619.             previewer.bind( 'documentTitle', function ( title ) {
  6620.                 api.setDocumentTitle( title );
  6621.             } );
  6622.         },
  6623.  
  6624.         /**
  6625.          * Handle the preview receiving the ready message.
  6626.          *
  6627.          * @since 4.7.0
  6628.          * @access public
  6629.          *
  6630.          * @param {object} data - Data from preview.
  6631.          * @param {string} data.currentUrl - Current URL.
  6632.          * @param {object} data.activePanels - Active panels.
  6633.          * @param {object} data.activeSections Active sections.
  6634.          * @param {object} data.activeControls Active controls.
  6635.          * @returns {void}
  6636.          */
  6637.         ready: function( data ) {
  6638.             var previewer = this, synced = {}, constructs;
  6639.  
  6640.             synced.settings = api.get();
  6641.             synced['settings-modified-while-loading'] = previewer.settingsModifiedWhileLoading;
  6642.             if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) {
  6643.                 synced.scroll = previewer.scroll;
  6644.             }
  6645.             synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get();
  6646.             previewer.send( 'sync', synced );
  6647.  
  6648.             // Set the previewUrl without causing the url to set the iframe.
  6649.             if ( data.currentUrl ) {
  6650.                 previewer.previewUrl.unbind( previewer.refresh );
  6651.                 previewer.previewUrl.set( data.currentUrl );
  6652.                 previewer.previewUrl.bind( previewer.refresh );
  6653.             }
  6654.  
  6655.             /*
  6656.              * Walk over all panels, sections, and controls and set their
  6657.              * respective active states to true if the preview explicitly
  6658.              * indicates as such.
  6659.              */
  6660.             constructs = {
  6661.                 panel: data.activePanels,
  6662.                 section: data.activeSections,
  6663.                 control: data.activeControls
  6664.             };
  6665.             _( constructs ).each( function ( activeConstructs, type ) {
  6666.                 api[ type ].each( function ( construct, id ) {
  6667.                     var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] );
  6668.  
  6669.                     /*
  6670.                      * If the construct was created statically in PHP (not dynamically in JS)
  6671.                      * then consider a missing (undefined) value in the activeConstructs to
  6672.                      * mean it should be deactivated (since it is gone). But if it is
  6673.                      * dynamically created then only toggle activation if the value is defined,
  6674.                      * as this means that the construct was also then correspondingly
  6675.                      * created statically in PHP and the active callback is available.
  6676.                      * Otherwise, dynamically-created constructs should normally have
  6677.                      * their active states toggled in JS rather than from PHP.
  6678.                      */
  6679.                     if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) {
  6680.                         if ( activeConstructs[ id ] ) {
  6681.                             construct.activate();
  6682.                         } else {
  6683.                             construct.deactivate();
  6684.                         }
  6685.                     }
  6686.                 } );
  6687.             } );
  6688.  
  6689.             if ( data.settingValidities ) {
  6690.                 api._handleSettingValidities( {
  6691.                     settingValidities: data.settingValidities,
  6692.                     focusInvalidControl: false
  6693.                 } );
  6694.             }
  6695.         },
  6696.  
  6697.         /**
  6698.          * Keep the preview alive by listening for ready and keep-alive messages.
  6699.          *
  6700.          * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL.
  6701.          *
  6702.          * @since 4.7.0
  6703.          * @access public
  6704.          *
  6705.          * @returns {void}
  6706.          */
  6707.         keepPreviewAlive: function keepPreviewAlive() {
  6708.             var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck;
  6709.  
  6710.             /**
  6711.              * Schedule a preview keep-alive check.
  6712.              *
  6713.              * Note that if a page load takes longer than keepAliveCheck milliseconds,
  6714.              * the keep-alive messages will still be getting sent from the previous
  6715.              * URL.
  6716.              */
  6717.             scheduleKeepAliveCheck = function() {
  6718.                 timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck );
  6719.             };
  6720.  
  6721.             /**
  6722.              * Set the previewerAlive state to true when receiving a message from the preview.
  6723.              */
  6724.             keepAliveTick = function() {
  6725.                 api.state( 'previewerAlive' ).set( true );
  6726.                 clearTimeout( timeoutId );
  6727.                 scheduleKeepAliveCheck();
  6728.             };
  6729.  
  6730.             /**
  6731.              * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message.
  6732.              *
  6733.              * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser
  6734.              * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage
  6735.              * transport to use refresh instead, causing the preview frame also to be replaced with the current
  6736.              * allowed preview URL.
  6737.              */
  6738.             handleMissingKeepAlive = function() {
  6739.                 api.state( 'previewerAlive' ).set( false );
  6740.             };
  6741.             scheduleKeepAliveCheck();
  6742.  
  6743.             previewer.bind( 'ready', keepAliveTick );
  6744.             previewer.bind( 'keep-alive', keepAliveTick );
  6745.         },
  6746.  
  6747.         /**
  6748.          * Query string data sent with each preview request.
  6749.          *
  6750.          * @abstract
  6751.          */
  6752.         query: function() {},
  6753.  
  6754.         abort: function() {
  6755.             if ( this.loading ) {
  6756.                 this.loading.destroy();
  6757.                 delete this.loading;
  6758.             }
  6759.         },
  6760.  
  6761.         /**
  6762.          * Refresh the preview seamlessly.
  6763.          *
  6764.          * @since 3.4.0
  6765.          * @access public
  6766.          * @returns {void}
  6767.          */
  6768.         refresh: function() {
  6769.             var previewer = this, onSettingChange;
  6770.  
  6771.             // Display loading indicator
  6772.             previewer.send( 'loading-initiated' );
  6773.  
  6774.             previewer.abort();
  6775.  
  6776.             previewer.loading = new api.PreviewFrame({
  6777.                 url:        previewer.url(),
  6778.                 previewUrl: previewer.previewUrl(),
  6779.                 query:      previewer.query( { excludeCustomizedSaved: true } ) || {},
  6780.                 container:  previewer.container
  6781.             });
  6782.  
  6783.             previewer.settingsModifiedWhileLoading = {};
  6784.             onSettingChange = function( setting ) {
  6785.                 previewer.settingsModifiedWhileLoading[ setting.id ] = true;
  6786.             };
  6787.             api.bind( 'change', onSettingChange );
  6788.             previewer.loading.always( function() {
  6789.                 api.unbind( 'change', onSettingChange );
  6790.             } );
  6791.  
  6792.             previewer.loading.done( function( readyData ) {
  6793.                 var loadingFrame = this, onceSynced;
  6794.  
  6795.                 previewer.preview = loadingFrame;
  6796.                 previewer.targetWindow( loadingFrame.targetWindow() );
  6797.                 previewer.channel( loadingFrame.channel() );
  6798.  
  6799.                 onceSynced = function() {
  6800.                     loadingFrame.unbind( 'synced', onceSynced );
  6801.                     if ( previewer._previousPreview ) {
  6802.                         previewer._previousPreview.destroy();
  6803.                     }
  6804.                     previewer._previousPreview = previewer.preview;
  6805.                     previewer.deferred.active.resolve();
  6806.                     delete previewer.loading;
  6807.                 };
  6808.                 loadingFrame.bind( 'synced', onceSynced );
  6809.  
  6810.                 // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh.
  6811.                 previewer.trigger( 'ready', readyData );
  6812.             });
  6813.  
  6814.             previewer.loading.fail( function( reason ) {
  6815.                 previewer.send( 'loading-failed' );
  6816.  
  6817.                 if ( 'logged out' === reason ) {
  6818.                     if ( previewer.preview ) {
  6819.                         previewer.preview.destroy();
  6820.                         delete previewer.preview;
  6821.                     }
  6822.  
  6823.                     previewer.login().done( previewer.refresh );
  6824.                 }
  6825.  
  6826.                 if ( 'cheatin' === reason ) {
  6827.                     previewer.cheatin();
  6828.                 }
  6829.             });
  6830.         },
  6831.  
  6832.         login: function() {
  6833.             var previewer = this,
  6834.                 deferred, messenger, iframe;
  6835.  
  6836.             if ( this._login ) {
  6837.                 return this._login;
  6838.             }
  6839.  
  6840.             deferred = $.Deferred();
  6841.             this._login = deferred.promise();
  6842.  
  6843.             messenger = new api.Messenger({
  6844.                 channel: 'login',
  6845.                 url:     api.settings.url.login
  6846.             });
  6847.  
  6848.             iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
  6849.  
  6850.             messenger.targetWindow( iframe[0].contentWindow );
  6851.  
  6852.             messenger.bind( 'login', function () {
  6853.                 var refreshNonces = previewer.refreshNonces();
  6854.  
  6855.                 refreshNonces.always( function() {
  6856.                     iframe.remove();
  6857.                     messenger.destroy();
  6858.                     delete previewer._login;
  6859.                 });
  6860.  
  6861.                 refreshNonces.done( function() {
  6862.                     deferred.resolve();
  6863.                 });
  6864.  
  6865.                 refreshNonces.fail( function() {
  6866.                     previewer.cheatin();
  6867.                     deferred.reject();
  6868.                 });
  6869.             });
  6870.  
  6871.             return this._login;
  6872.         },
  6873.  
  6874.         cheatin: function() {
  6875.             $( document.body ).empty().addClass( 'cheatin' ).append(
  6876.                 '<h1>' + api.l10n.cheatin + '</h1>' +
  6877.                 '<p>' + api.l10n.notAllowed + '</p>'
  6878.             );
  6879.         },
  6880.  
  6881.         refreshNonces: function() {
  6882.             var request, deferred = $.Deferred();
  6883.  
  6884.             deferred.promise();
  6885.  
  6886.             request = wp.ajax.post( 'customize_refresh_nonces', {
  6887.                 wp_customize: 'on',
  6888.                 customize_theme: api.settings.theme.stylesheet
  6889.             });
  6890.  
  6891.             request.done( function( response ) {
  6892.                 api.trigger( 'nonce-refresh', response );
  6893.                 deferred.resolve();
  6894.             });
  6895.  
  6896.             request.fail( function() {
  6897.                 deferred.reject();
  6898.             });
  6899.  
  6900.             return deferred;
  6901.         }
  6902.     });
  6903.  
  6904.     api.settingConstructor = {};
  6905.     api.controlConstructor = {
  6906.         color:               api.ColorControl,
  6907.         media:               api.MediaControl,
  6908.         upload:              api.UploadControl,
  6909.         image:               api.ImageControl,
  6910.         cropped_image:       api.CroppedImageControl,
  6911.         site_icon:           api.SiteIconControl,
  6912.         header:              api.HeaderControl,
  6913.         background:          api.BackgroundControl,
  6914.         background_position: api.BackgroundPositionControl,
  6915.         theme:               api.ThemeControl,
  6916.         date_time:           api.DateTimeControl,
  6917.         code_editor:         api.CodeEditorControl
  6918.     };
  6919.     api.panelConstructor = {
  6920.         themes: api.ThemesPanel
  6921.     };
  6922.     api.sectionConstructor = {
  6923.         themes: api.ThemesSection,
  6924.         outer: api.OuterSection
  6925.     };
  6926.  
  6927.     /**
  6928.      * Handle setting_validities in an error response for the customize-save request.
  6929.      *
  6930.      * Add notifications to the settings and focus on the first control that has an invalid setting.
  6931.      *
  6932.      * @since 4.6.0
  6933.      * @private
  6934.      *
  6935.      * @param {object}  args
  6936.      * @param {object}  args.settingValidities
  6937.      * @param {boolean} [args.focusInvalidControl=false]
  6938.      * @returns {void}
  6939.      */
  6940.     api._handleSettingValidities = function handleSettingValidities( args ) {
  6941.         var invalidSettingControls, invalidSettings = [], wasFocused = false;
  6942.  
  6943.         // Find the controls that correspond to each invalid setting.
  6944.         _.each( args.settingValidities, function( validity, settingId ) {
  6945.             var setting = api( settingId );
  6946.             if ( setting ) {
  6947.  
  6948.                 // Add notifications for invalidities.
  6949.                 if ( _.isObject( validity ) ) {
  6950.                     _.each( validity, function( params, code ) {
  6951.                         var notification, existingNotification, needsReplacement = false;
  6952.                         notification = new api.Notification( code, _.extend( { fromServer: true }, params ) );
  6953.  
  6954.                         // Remove existing notification if already exists for code but differs in parameters.
  6955.                         existingNotification = setting.notifications( notification.code );
  6956.                         if ( existingNotification ) {
  6957.                             needsReplacement = notification.type !== existingNotification.type || notification.message !== existingNotification.message || ! _.isEqual( notification.data, existingNotification.data );
  6958.                         }
  6959.                         if ( needsReplacement ) {
  6960.                             setting.notifications.remove( code );
  6961.                         }
  6962.  
  6963.                         if ( ! setting.notifications.has( notification.code ) ) {
  6964.                             setting.notifications.add( notification );
  6965.                         }
  6966.                         invalidSettings.push( setting.id );
  6967.                     } );
  6968.                 }
  6969.  
  6970.                 // Remove notification errors that are no longer valid.
  6971.                 setting.notifications.each( function( notification ) {
  6972.                     if ( notification.fromServer && 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
  6973.                         setting.notifications.remove( notification.code );
  6974.                     }
  6975.                 } );
  6976.             }
  6977.         } );
  6978.  
  6979.         if ( args.focusInvalidControl ) {
  6980.             invalidSettingControls = api.findControlsForSettings( invalidSettings );
  6981.  
  6982.             // Focus on the first control that is inside of an expanded section (one that is visible).
  6983.             _( _.values( invalidSettingControls ) ).find( function( controls ) {
  6984.                 return _( controls ).find( function( control ) {
  6985.                     var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
  6986.                     if ( isExpanded && control.expanded ) {
  6987.                         isExpanded = control.expanded();
  6988.                     }
  6989.                     if ( isExpanded ) {
  6990.                         control.focus();
  6991.                         wasFocused = true;
  6992.                     }
  6993.                     return wasFocused;
  6994.                 } );
  6995.             } );
  6996.  
  6997.             // Focus on the first invalid control.
  6998.             if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
  6999.                 _.values( invalidSettingControls )[0][0].focus();
  7000.             }
  7001.         }
  7002.     };
  7003.  
  7004.     /**
  7005.      * Find all controls associated with the given settings.
  7006.      *
  7007.      * @since 4.6.0
  7008.      * @param {string[]} settingIds Setting IDs.
  7009.      * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
  7010.      */
  7011.     api.findControlsForSettings = function findControlsForSettings( settingIds ) {
  7012.         var controls = {}, settingControls;
  7013.         _.each( _.unique( settingIds ), function( settingId ) {
  7014.             var setting = api( settingId );
  7015.             if ( setting ) {
  7016.                 settingControls = setting.findControls();
  7017.                 if ( settingControls && settingControls.length > 0 ) {
  7018.                     controls[ settingId ] = settingControls;
  7019.                 }
  7020.             }
  7021.         } );
  7022.         return controls;
  7023.     };
  7024.  
  7025.     /**
  7026.      * Sort panels, sections, controls by priorities. Hide empty sections and panels.
  7027.      *
  7028.      * @since 4.1.0
  7029.      */
  7030.     api.reflowPaneContents = _.bind( function () {
  7031.  
  7032.         var appendContainer, activeElement, rootHeadContainers, rootNodes = [], wasReflowed = false;
  7033.  
  7034.         if ( document.activeElement ) {
  7035.             activeElement = $( document.activeElement );
  7036.         }
  7037.  
  7038.         // Sort the sections within each panel
  7039.         api.panel.each( function ( panel ) {
  7040.             if ( 'themes' === panel.id ) {
  7041.                 return; // Don't reflow theme sections, as doing so moves them after the themes container.
  7042.             }
  7043.  
  7044.             var sections = panel.sections(),
  7045.                 sectionHeadContainers = _.pluck( sections, 'headContainer' );
  7046.             rootNodes.push( panel );
  7047.             appendContainer = ( panel.contentContainer.is( 'ul' ) ) ? panel.contentContainer : panel.contentContainer.find( 'ul:first' );
  7048.             if ( ! api.utils.areElementListsEqual( sectionHeadContainers, appendContainer.children( '[id]' ) ) ) {
  7049.                 _( sections ).each( function ( section ) {
  7050.                     appendContainer.append( section.headContainer );
  7051.                 } );
  7052.                 wasReflowed = true;
  7053.             }
  7054.         } );
  7055.  
  7056.         // Sort the controls within each section
  7057.         api.section.each( function ( section ) {
  7058.             var controls = section.controls(),
  7059.                 controlContainers = _.pluck( controls, 'container' );
  7060.             if ( ! section.panel() ) {
  7061.                 rootNodes.push( section );
  7062.             }
  7063.             appendContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
  7064.             if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
  7065.                 _( controls ).each( function ( control ) {
  7066.                     appendContainer.append( control.container );
  7067.                 } );
  7068.                 wasReflowed = true;
  7069.             }
  7070.         } );
  7071.  
  7072.         // Sort the root panels and sections
  7073.         rootNodes.sort( api.utils.prioritySort );
  7074.         rootHeadContainers = _.pluck( rootNodes, 'headContainer' );
  7075.         appendContainer = $( '#customize-theme-controls .customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
  7076.         if ( ! api.utils.areElementListsEqual( rootHeadContainers, appendContainer.children() ) ) {
  7077.             _( rootNodes ).each( function ( rootNode ) {
  7078.                 appendContainer.append( rootNode.headContainer );
  7079.             } );
  7080.             wasReflowed = true;
  7081.         }
  7082.  
  7083.         // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
  7084.         api.panel.each( function ( panel ) {
  7085.             var value = panel.active();
  7086.             panel.active.callbacks.fireWith( panel.active, [ value, value ] );
  7087.         } );
  7088.         api.section.each( function ( section ) {
  7089.             var value = section.active();
  7090.             section.active.callbacks.fireWith( section.active, [ value, value ] );
  7091.         } );
  7092.  
  7093.         // Restore focus if there was a reflow and there was an active (focused) element
  7094.         if ( wasReflowed && activeElement ) {
  7095.             activeElement.focus();
  7096.         }
  7097.         api.trigger( 'pane-contents-reflowed' );
  7098.     }, api );
  7099.  
  7100.     // Define state values.
  7101.     api.state = new api.Values();
  7102.     _.each( [
  7103.         'saved',
  7104.         'saving',
  7105.         'trashing',
  7106.         'activated',
  7107.         'processing',
  7108.         'paneVisible',
  7109.         'expandedPanel',
  7110.         'expandedSection',
  7111.         'changesetDate',
  7112.         'selectedChangesetDate',
  7113.         'changesetStatus',
  7114.         'selectedChangesetStatus',
  7115.         'remainingTimeToPublish',
  7116.         'previewerAlive',
  7117.         'editShortcutVisibility',
  7118.         'changesetLocked',
  7119.         'previewedDevice'
  7120.     ], function( name ) {
  7121.         api.state.create( name );
  7122.     });
  7123.  
  7124.     $( function() {
  7125.         api.settings = window._wpCustomizeSettings;
  7126.         api.l10n = window._wpCustomizeControlsL10n;
  7127.  
  7128.         // Check if we can run the Customizer.
  7129.         if ( ! api.settings ) {
  7130.             return;
  7131.         }
  7132.  
  7133.         // Bail if any incompatibilities are found.
  7134.         if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
  7135.             return;
  7136.         }
  7137.  
  7138.         if ( null === api.PreviewFrame.prototype.sensitivity ) {
  7139.             api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity;
  7140.         }
  7141.         if ( null === api.Previewer.prototype.refreshBuffer ) {
  7142.             api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh;
  7143.         }
  7144.  
  7145.         var parent,
  7146.             body = $( document.body ),
  7147.             overlay = body.children( '.wp-full-overlay' ),
  7148.             title = $( '#customize-info .panel-title.site-title' ),
  7149.             closeBtn = $( '.customize-controls-close' ),
  7150.             saveBtn = $( '#save' ),
  7151.             btnWrapper = $( '#customize-save-button-wrapper' ),
  7152.             publishSettingsBtn = $( '#publish-settings' ),
  7153.             footerActions = $( '#customize-footer-actions' );
  7154.  
  7155.         // Add publish settings section in JS instead of PHP since the Customizer depends on it to function.
  7156.         api.bind( 'ready', function() {
  7157.             api.section.add( new api.OuterSection( 'publish_settings', {
  7158.                 title: api.l10n.publishSettings,
  7159.                 priority: 0,
  7160.                 active: api.settings.theme.active
  7161.             } ) );
  7162.         } );
  7163.  
  7164.         // Set up publish settings section and its controls.
  7165.         api.section( 'publish_settings', function( section ) {
  7166.             var updateButtonsState, trashControl, updateSectionActive, isSectionActive, statusControl, dateControl, toggleDateControl, publishWhenTime, pollInterval, updateTimeArrivedPoller, cancelScheduleButtonReminder, timeArrivedPollingInterval = 1000;
  7167.  
  7168.             trashControl = new api.Control( 'trash_changeset', {
  7169.                 type: 'button',
  7170.                 section: section.id,
  7171.                 priority: 30,
  7172.                 input_attrs: {
  7173.                     'class': 'button-link button-link-delete',
  7174.                     value: api.l10n.discardChanges
  7175.                 }
  7176.             } );
  7177.             api.control.add( trashControl );
  7178.             trashControl.deferred.embedded.done( function() {
  7179.                 trashControl.container.find( '.button-link' ).on( 'click', function() {
  7180.                     if ( confirm( api.l10n.trashConfirm ) ) {
  7181.                         wp.customize.previewer.trash();
  7182.                     }
  7183.                 } );
  7184.             } );
  7185.  
  7186.             api.control.add( new api.PreviewLinkControl( 'changeset_preview_link', {
  7187.                 section: section.id,
  7188.                 priority: 100
  7189.             } ) );
  7190.  
  7191.             /**
  7192.              * Return whether the pubish settings section should be active.
  7193.              *
  7194.              * @return {boolean} Is section active.
  7195.              */
  7196.             isSectionActive = function() {
  7197.                 if ( ! api.state( 'activated' ).get() ) {
  7198.                     return false;
  7199.                 }
  7200.                 if ( api.state( 'trashing' ).get() || 'trash' === api.state( 'changesetStatus' ).get() ) {
  7201.                     return false;
  7202.                 }
  7203.                 if ( '' === api.state( 'changesetStatus' ).get() && api.state( 'saved' ).get() ) {
  7204.                     return false;
  7205.                 }
  7206.                 return true;
  7207.             };
  7208.  
  7209.             // Make sure publish settings are not available while the theme is not active and the customizer is in a published state.
  7210.             section.active.validate = isSectionActive;
  7211.             updateSectionActive = function() {
  7212.                 section.active.set( isSectionActive() );
  7213.             };
  7214.             api.state( 'activated' ).bind( updateSectionActive );
  7215.             api.state( 'trashing' ).bind( updateSectionActive );
  7216.             api.state( 'saved' ).bind( updateSectionActive );
  7217.             api.state( 'changesetStatus' ).bind( updateSectionActive );
  7218.             updateSectionActive();
  7219.  
  7220.             // Bind visibility of the publish settings button to whether the section is active.
  7221.             updateButtonsState = function() {
  7222.                 publishSettingsBtn.toggle( section.active.get() );
  7223.                 saveBtn.toggleClass( 'has-next-sibling', section.active.get() );
  7224.             };
  7225.             updateButtonsState();
  7226.             section.active.bind( updateButtonsState );
  7227.  
  7228.             function highlightScheduleButton() {
  7229.                 if ( ! cancelScheduleButtonReminder ) {
  7230.                     cancelScheduleButtonReminder = api.utils.highlightButton( btnWrapper, {
  7231.                         delay: 1000,
  7232.  
  7233.                         // Only abort the reminder when the save button is focused.
  7234.                         // If the user clicks the settings button to toggle the
  7235.                         // settings closed, we'll still remind them.
  7236.                         focusTarget: saveBtn
  7237.                     } );
  7238.                 }
  7239.             }
  7240.             function cancelHighlightScheduleButton() {
  7241.                 if ( cancelScheduleButtonReminder ) {
  7242.                     cancelScheduleButtonReminder();
  7243.                     cancelScheduleButtonReminder = null;
  7244.                 }
  7245.             }
  7246.             api.state( 'selectedChangesetStatus' ).bind( cancelHighlightScheduleButton );
  7247.  
  7248.             section.contentContainer.find( '.customize-action' ).text( api.l10n.updating );
  7249.             section.contentContainer.find( '.customize-section-back' ).removeAttr( 'tabindex' );
  7250.             publishSettingsBtn.prop( 'disabled', false );
  7251.  
  7252.             publishSettingsBtn.on( 'click', function( event ) {
  7253.                 event.preventDefault();
  7254.                 section.expanded.set( ! section.expanded.get() );
  7255.             } );
  7256.  
  7257.             section.expanded.bind( function( isExpanded ) {
  7258.                 var defaultChangesetStatus;
  7259.                 publishSettingsBtn.attr( 'aria-expanded', String( isExpanded ) );
  7260.                 publishSettingsBtn.toggleClass( 'active', isExpanded );
  7261.  
  7262.                 if ( isExpanded ) {
  7263.                     cancelHighlightScheduleButton();
  7264.                     return;
  7265.                 }
  7266.  
  7267.                 defaultChangesetStatus = api.state( 'changesetStatus' ).get();
  7268.                 if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
  7269.                     defaultChangesetStatus = 'publish';
  7270.                 }
  7271.  
  7272.                 if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
  7273.                     highlightScheduleButton();
  7274.                 } else if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
  7275.                     highlightScheduleButton();
  7276.                 }
  7277.             } );
  7278.  
  7279.             statusControl = new api.Control( 'changeset_status', {
  7280.                 priority: 10,
  7281.                 type: 'radio',
  7282.                 section: 'publish_settings',
  7283.                 setting: api.state( 'selectedChangesetStatus' ),
  7284.                 templateId: 'customize-selected-changeset-status-control',
  7285.                 label: api.l10n.action,
  7286.                 choices: api.settings.changeset.statusChoices
  7287.             } );
  7288.             api.control.add( statusControl );
  7289.  
  7290.             dateControl = new api.DateTimeControl( 'changeset_scheduled_date', {
  7291.                 priority: 20,
  7292.                 section: 'publish_settings',
  7293.                 setting: api.state( 'selectedChangesetDate' ),
  7294.                 minYear: ( new Date() ).getFullYear(),
  7295.                 allowPastDate: false,
  7296.                 includeTime: true,
  7297.                 twelveHourFormat: /a/i.test( api.settings.timeFormat ),
  7298.                 description: api.l10n.scheduleDescription
  7299.             } );
  7300.             dateControl.notifications.alt = true;
  7301.             api.control.add( dateControl );
  7302.  
  7303.             publishWhenTime = function() {
  7304.                 api.state( 'selectedChangesetStatus' ).set( 'publish' );
  7305.                 api.previewer.save();
  7306.             };
  7307.  
  7308.             // Start countdown for when the dateTime arrives, or clear interval when it is .
  7309.             updateTimeArrivedPoller = function() {
  7310.                 var shouldPoll = (
  7311.                     'future' === api.state( 'changesetStatus' ).get() &&
  7312.                     'future' === api.state( 'selectedChangesetStatus' ).get() &&
  7313.                     api.state( 'changesetDate' ).get() &&
  7314.                     api.state( 'selectedChangesetDate' ).get() === api.state( 'changesetDate' ).get() &&
  7315.                     api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ) >= 0
  7316.                 );
  7317.  
  7318.                 if ( shouldPoll && ! pollInterval ) {
  7319.                     pollInterval = setInterval( function() {
  7320.                         var remainingTime = api.utils.getRemainingTime( api.state( 'changesetDate' ).get() );
  7321.                         api.state( 'remainingTimeToPublish' ).set( remainingTime );
  7322.                         if ( remainingTime <= 0 ) {
  7323.                             clearInterval( pollInterval );
  7324.                             pollInterval = 0;
  7325.                             publishWhenTime();
  7326.                         }
  7327.                     }, timeArrivedPollingInterval );
  7328.                 } else if ( ! shouldPoll && pollInterval ) {
  7329.                     clearInterval( pollInterval );
  7330.                     pollInterval = 0;
  7331.                 }
  7332.             };
  7333.  
  7334.             api.state( 'changesetDate' ).bind( updateTimeArrivedPoller );
  7335.             api.state( 'selectedChangesetDate' ).bind( updateTimeArrivedPoller );
  7336.             api.state( 'changesetStatus' ).bind( updateTimeArrivedPoller );
  7337.             api.state( 'selectedChangesetStatus' ).bind( updateTimeArrivedPoller );
  7338.             updateTimeArrivedPoller();
  7339.  
  7340.             // Ensure dateControl only appears when selected status is future.
  7341.             dateControl.active.validate = function() {
  7342.                 return 'future' === api.state( 'selectedChangesetStatus' ).get();
  7343.             };
  7344.             toggleDateControl = function( value ) {
  7345.                 dateControl.active.set( 'future' === value );
  7346.             };
  7347.             toggleDateControl( api.state( 'selectedChangesetStatus' ).get() );
  7348.             api.state( 'selectedChangesetStatus' ).bind( toggleDateControl );
  7349.  
  7350.             // Show notification on date control when status is future but it isn't a future date.
  7351.             api.state( 'saving' ).bind( function( isSaving ) {
  7352.                 if ( isSaving && 'future' === api.state( 'selectedChangesetStatus' ).get() ) {
  7353.                     dateControl.toggleFutureDateNotification( ! dateControl.isFutureDate() );
  7354.                 }
  7355.             } );
  7356.         } );
  7357.  
  7358.         // Prevent the form from saving when enter is pressed on an input or select element.
  7359.         $('#customize-controls').on( 'keydown', function( e ) {
  7360.             var isEnter = ( 13 === e.which ),
  7361.                 $el = $( e.target );
  7362.  
  7363.             if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
  7364.                 e.preventDefault();
  7365.             }
  7366.         });
  7367.  
  7368.         // Expand/Collapse the main customizer customize info.
  7369.         $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
  7370.             var section = $( this ).closest( '.accordion-section' ),
  7371.                 content = section.find( '.customize-panel-description:first' );
  7372.  
  7373.             if ( section.hasClass( 'cannot-expand' ) ) {
  7374.                 return;
  7375.             }
  7376.  
  7377.             if ( section.hasClass( 'open' ) ) {
  7378.                 section.toggleClass( 'open' );
  7379.                 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration, function() {
  7380.                     content.trigger( 'toggled' );
  7381.                 } );
  7382.                 $( this ).attr( 'aria-expanded', false );
  7383.             } else {
  7384.                 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration, function() {
  7385.                     content.trigger( 'toggled' );
  7386.                 } );
  7387.                 section.toggleClass( 'open' );
  7388.                 $( this ).attr( 'aria-expanded', true );
  7389.             }
  7390.         });
  7391.  
  7392.         // Initialize Previewer
  7393.         api.previewer = new api.Previewer({
  7394.             container:   '#customize-preview',
  7395.             form:        '#customize-controls',
  7396.             previewUrl:  api.settings.url.preview,
  7397.             allowedUrls: api.settings.url.allowed
  7398.         }, {
  7399.  
  7400.             nonce: api.settings.nonce,
  7401.  
  7402.             /**
  7403.              * Build the query to send along with the Preview request.
  7404.              *
  7405.              * @since 3.4.0
  7406.              * @since 4.7.0 Added options param.
  7407.              * @access public
  7408.              *
  7409.              * @param {object}  [options] Options.
  7410.              * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset).
  7411.              * @return {object} Query vars.
  7412.              */
  7413.             query: function( options ) {
  7414.                 var queryVars = {
  7415.                     wp_customize: 'on',
  7416.                     customize_theme: api.settings.theme.stylesheet,
  7417.                     nonce: this.nonce.preview,
  7418.                     customize_changeset_uuid: api.settings.changeset.uuid
  7419.                 };
  7420.                 if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
  7421.                     queryVars.customize_autosaved = 'on';
  7422.                 }
  7423.  
  7424.                 /*
  7425.                  * Exclude customized data if requested especially for calls to requestChangesetUpdate.
  7426.                  * Changeset updates are differential and so it is a performance waste to send all of
  7427.                  * the dirty settings with each update.
  7428.                  */
  7429.                 queryVars.customized = JSON.stringify( api.dirtyValues( {
  7430.                     unsaved: options && options.excludeCustomizedSaved
  7431.                 } ) );
  7432.  
  7433.                 return queryVars;
  7434.             },
  7435.  
  7436.             /**
  7437.              * Save (and publish) the customizer changeset.
  7438.              *
  7439.              * Updates to the changeset are transactional. If any of the settings
  7440.              * are invalid then none of them will be written into the changeset.
  7441.              * A revision will be made for the changeset post if revisions support
  7442.              * has been added to the post type.
  7443.              *
  7444.              * @since 3.4.0
  7445.              * @since 4.7.0 Added args param and return value.
  7446.              *
  7447.              * @param {object} [args] Args.
  7448.              * @param {string} [args.status=publish] Status.
  7449.              * @param {string} [args.date] Date, in local time in MySQL format.
  7450.              * @param {string} [args.title] Title
  7451.              * @returns {jQuery.promise} Promise.
  7452.              */
  7453.             save: function( args ) {
  7454.                 var previewer = this,
  7455.                     deferred = $.Deferred(),
  7456.                     changesetStatus = api.state( 'selectedChangesetStatus' ).get(),
  7457.                     selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(),
  7458.                     processing = api.state( 'processing' ),
  7459.                     submitWhenDoneProcessing,
  7460.                     submit,
  7461.                     modifiedWhileSaving = {},
  7462.                     invalidSettings = [],
  7463.                     invalidControls = [],
  7464.                     invalidSettingLessControls = [];
  7465.  
  7466.                 if ( args && args.status ) {
  7467.                     changesetStatus = args.status;
  7468.                 }
  7469.  
  7470.                 if ( api.state( 'saving' ).get() ) {
  7471.                     deferred.reject( 'already_saving' );
  7472.                     deferred.promise();
  7473.                 }
  7474.  
  7475.                 api.state( 'saving' ).set( true );
  7476.  
  7477.                 function captureSettingModifiedDuringSave( setting ) {
  7478.                     modifiedWhileSaving[ setting.id ] = true;
  7479.                 }
  7480.  
  7481.                 submit = function () {
  7482.                     var request, query, settingInvalidities = {}, latestRevision = api._latestRevision, errorCode = 'client_side_error';
  7483.  
  7484.                     api.bind( 'change', captureSettingModifiedDuringSave );
  7485.                     api.notifications.remove( errorCode );
  7486.  
  7487.                     /*
  7488.                      * Block saving if there are any settings that are marked as
  7489.                      * invalid from the client (not from the server). Focus on
  7490.                      * the control.
  7491.                      */
  7492.                     api.each( function( setting ) {
  7493.                         setting.notifications.each( function( notification ) {
  7494.                             if ( 'error' === notification.type && ! notification.fromServer ) {
  7495.                                 invalidSettings.push( setting.id );
  7496.                                 if ( ! settingInvalidities[ setting.id ] ) {
  7497.                                     settingInvalidities[ setting.id ] = {};
  7498.                                 }
  7499.                                 settingInvalidities[ setting.id ][ notification.code ] = notification;
  7500.                             }
  7501.                         } );
  7502.                     } );
  7503.  
  7504.                     // Find all invalid setting less controls with notification type error.
  7505.                     api.control.each( function( control ) {
  7506.                         if ( ! control.setting || ! control.setting.id && control.active.get() ) {
  7507.                             control.notifications.each( function( notification ) {
  7508.                                 if ( 'error' === notification.type ) {
  7509.                                     invalidSettingLessControls.push( [ control ] );
  7510.                                 }
  7511.                             } );
  7512.                         }
  7513.                     } );
  7514.  
  7515.                     invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) );
  7516.                     if ( ! _.isEmpty( invalidControls ) ) {
  7517.  
  7518.                         invalidControls[0][0].focus();
  7519.                         api.unbind( 'change', captureSettingModifiedDuringSave );
  7520.  
  7521.                         if ( invalidSettings.length ) {
  7522.                             api.notifications.add( new api.Notification( errorCode, {
  7523.                                 message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ),
  7524.                                 type: 'error',
  7525.                                 dismissible: true,
  7526.                                 saveFailure: true
  7527.                             } ) );
  7528.                         }
  7529.  
  7530.                         deferred.rejectWith( previewer, [
  7531.                             { setting_invalidities: settingInvalidities }
  7532.                         ] );
  7533.                         api.state( 'saving' ).set( false );
  7534.                         return deferred.promise();
  7535.                     }
  7536.  
  7537.                     /*
  7538.                      * Note that excludeCustomizedSaved is intentionally false so that the entire
  7539.                      * set of customized data will be included if bypassed changeset update.
  7540.                      */
  7541.                     query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), {
  7542.                         nonce: previewer.nonce.save,
  7543.                         customize_changeset_status: changesetStatus
  7544.                     } );
  7545.  
  7546.                     if ( args && args.date ) {
  7547.                         query.customize_changeset_date = args.date;
  7548.                     } else if ( 'future' === changesetStatus && selectedChangesetDate ) {
  7549.                         query.customize_changeset_date = selectedChangesetDate;
  7550.                     }
  7551.  
  7552.                     if ( args && args.title ) {
  7553.                         query.customize_changeset_title = args.title;
  7554.                     }
  7555.  
  7556.                     // Allow plugins to modify the params included with the save request.
  7557.                     api.trigger( 'save-request-params', query );
  7558.  
  7559.                     /*
  7560.                      * Note that the dirty customized values will have already been set in the
  7561.                      * changeset and so technically query.customized could be deleted. However,
  7562.                      * it is remaining here to make sure that any settings that got updated
  7563.                      * quietly which may have not triggered an update request will also get
  7564.                      * included in the values that get saved to the changeset. This will ensure
  7565.                      * that values that get injected via the saved event will be included in
  7566.                      * the changeset. This also ensures that setting values that were invalid
  7567.                      * will get re-validated, perhaps in the case of settings that are invalid
  7568.                      * due to dependencies on other settings.
  7569.                      */
  7570.                     request = wp.ajax.post( 'customize_save', query );
  7571.                     api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
  7572.  
  7573.                     api.trigger( 'save', request );
  7574.  
  7575.                     request.always( function () {
  7576.                         api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
  7577.                         api.state( 'saving' ).set( false );
  7578.                         api.unbind( 'change', captureSettingModifiedDuringSave );
  7579.                     } );
  7580.  
  7581.                     // Remove notifications that were added due to save failures.
  7582.                     api.notifications.each( function( notification ) {
  7583.                         if ( notification.saveFailure ) {
  7584.                             api.notifications.remove( notification.code );
  7585.                         }
  7586.                     });
  7587.  
  7588.                     request.fail( function ( response ) {
  7589.                         var notification, notificationArgs;
  7590.                         notificationArgs = {
  7591.                             type: 'error',
  7592.                             dismissible: true,
  7593.                             fromServer: true,
  7594.                             saveFailure: true
  7595.                         };
  7596.  
  7597.                         if ( '0' === response ) {
  7598.                             response = 'not_logged_in';
  7599.                         } else if ( '-1' === response ) {
  7600.                             // Back-compat in case any other check_ajax_referer() call is dying
  7601.                             response = 'invalid_nonce';
  7602.                         }
  7603.  
  7604.                         if ( 'invalid_nonce' === response ) {
  7605.                             previewer.cheatin();
  7606.                         } else if ( 'not_logged_in' === response ) {
  7607.                             previewer.preview.iframe.hide();
  7608.                             previewer.login().done( function() {
  7609.                                 previewer.save();
  7610.                                 previewer.preview.iframe.show();
  7611.                             } );
  7612.                         } else if ( response.code ) {
  7613.                             if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) {
  7614.                                 api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus();
  7615.                             } else if ( 'changeset_locked' !== response.code ) {
  7616.                                 notification = new api.Notification( response.code, _.extend( notificationArgs, {
  7617.                                     message: response.message
  7618.                                 } ) );
  7619.                             }
  7620.                         } else {
  7621.                             notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
  7622.                                 message: api.l10n.unknownRequestFail
  7623.                             } ) );
  7624.                         }
  7625.  
  7626.                         if ( notification ) {
  7627.                             api.notifications.add( notification );
  7628.                         }
  7629.  
  7630.                         if ( response.setting_validities ) {
  7631.                             api._handleSettingValidities( {
  7632.                                 settingValidities: response.setting_validities,
  7633.                                 focusInvalidControl: true
  7634.                             } );
  7635.                         }
  7636.  
  7637.                         deferred.rejectWith( previewer, [ response ] );
  7638.                         api.trigger( 'error', response );
  7639.  
  7640.                         // Start a new changeset if the underlying changeset was published.
  7641.                         if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) {
  7642.                             api.settings.changeset.uuid = response.next_changeset_uuid;
  7643.                             api.state( 'changesetStatus' ).set( '' );
  7644.                             if ( api.settings.changeset.branching ) {
  7645.                                 parent.send( 'changeset-uuid', api.settings.changeset.uuid );
  7646.                             }
  7647.                             api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid );
  7648.                         }
  7649.                     } );
  7650.  
  7651.                     request.done( function( response ) {
  7652.  
  7653.                         previewer.send( 'saved', response );
  7654.  
  7655.                         api.state( 'changesetStatus' ).set( response.changeset_status );
  7656.                         if ( response.changeset_date ) {
  7657.                             api.state( 'changesetDate' ).set( response.changeset_date );
  7658.                         }
  7659.  
  7660.                         if ( 'publish' === response.changeset_status ) {
  7661.  
  7662.                             // Mark all published as clean if they haven't been modified during the request.
  7663.                             api.each( function( setting ) {
  7664.                                 /*
  7665.                                  * Note that the setting revision will be undefined in the case of setting
  7666.                                  * values that are marked as dirty when the customizer is loaded, such as
  7667.                                  * when applying starter content. All other dirty settings will have an
  7668.                                  * associated revision due to their modification triggering a change event.
  7669.                                  */
  7670.                                 if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) {
  7671.                                     setting._dirty = false;
  7672.                                 }
  7673.                             } );
  7674.  
  7675.                             api.state( 'changesetStatus' ).set( '' );
  7676.                             api.settings.changeset.uuid = response.next_changeset_uuid;
  7677.                             if ( api.settings.changeset.branching ) {
  7678.                                 parent.send( 'changeset-uuid', api.settings.changeset.uuid );
  7679.                             }
  7680.                         }
  7681.  
  7682.                         // Prevent subsequent requestChangesetUpdate() calls from including the settings that have been saved.
  7683.                         api._lastSavedRevision = Math.max( latestRevision, api._lastSavedRevision );
  7684.  
  7685.                         if ( response.setting_validities ) {
  7686.                             api._handleSettingValidities( {
  7687.                                 settingValidities: response.setting_validities,
  7688.                                 focusInvalidControl: true
  7689.                             } );
  7690.                         }
  7691.  
  7692.                         deferred.resolveWith( previewer, [ response ] );
  7693.                         api.trigger( 'saved', response );
  7694.  
  7695.                         // Restore the global dirty state if any settings were modified during save.
  7696.                         if ( ! _.isEmpty( modifiedWhileSaving ) ) {
  7697.                             api.state( 'saved' ).set( false );
  7698.                         }
  7699.                     } );
  7700.                 };
  7701.  
  7702.                 if ( 0 === processing() ) {
  7703.                     submit();
  7704.                 } else {
  7705.                     submitWhenDoneProcessing = function () {
  7706.                         if ( 0 === processing() ) {
  7707.                             api.state.unbind( 'change', submitWhenDoneProcessing );
  7708.                             submit();
  7709.                         }
  7710.                     };
  7711.                     api.state.bind( 'change', submitWhenDoneProcessing );
  7712.                 }
  7713.  
  7714.                 return deferred.promise();
  7715.             },
  7716.  
  7717.             /**
  7718.              * Trash the current changes.
  7719.              *
  7720.              * Revert the Customizer to it's previously-published state.
  7721.              *
  7722.              * @since 4.9.0
  7723.              *
  7724.              * @returns {jQuery.promise} Promise.
  7725.              */
  7726.             trash: function trash() {
  7727.                 var request, success, fail;
  7728.  
  7729.                 api.state( 'trashing' ).set( true );
  7730.                 api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
  7731.  
  7732.                 request = wp.ajax.post( 'customize_trash', {
  7733.                     customize_changeset_uuid: api.settings.changeset.uuid,
  7734.                     nonce: api.settings.nonce.trash
  7735.                 } );
  7736.                 api.notifications.add( new api.OverlayNotification( 'changeset_trashing', {
  7737.                     type: 'info',
  7738.                     message: api.l10n.revertingChanges,
  7739.                     loading: true
  7740.                 } ) );
  7741.  
  7742.                 success = function() {
  7743.                     var urlParser = document.createElement( 'a' ), queryParams;
  7744.  
  7745.                     api.state( 'changesetStatus' ).set( 'trash' );
  7746.                     api.each( function( setting ) {
  7747.                         setting._dirty = false;
  7748.                     } );
  7749.                     api.state( 'saved' ).set( true );
  7750.  
  7751.                     // Go back to Customizer without changeset.
  7752.                     urlParser.href = location.href;
  7753.                     queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  7754.                     delete queryParams.changeset_uuid;
  7755.                     queryParams['return'] = api.settings.url['return'];
  7756.                     urlParser.search = $.param( queryParams );
  7757.                     location.replace( urlParser.href );
  7758.                 };
  7759.  
  7760.                 fail = function( code, message ) {
  7761.                     var notificationCode = code || 'unknown_error';
  7762.                     api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
  7763.                     api.state( 'trashing' ).set( false );
  7764.                     api.notifications.remove( 'changeset_trashing' );
  7765.                     api.notifications.add( new api.Notification( notificationCode, {
  7766.                         message: message || api.l10n.unknownError,
  7767.                         dismissible: true,
  7768.                         type: 'error'
  7769.                     } ) );
  7770.                 };
  7771.  
  7772.                 request.done( function( response ) {
  7773.                     success( response.message );
  7774.                 } );
  7775.  
  7776.                 request.fail( function( response ) {
  7777.                     var code = response.code || 'trashing_failed';
  7778.                     if ( response.success || 'non_existent_changeset' === code || 'changeset_already_trashed' === code ) {
  7779.                         success( response.message );
  7780.                     } else {
  7781.                         fail( code, response.message );
  7782.                     }
  7783.                 } );
  7784.             },
  7785.  
  7786.             /**
  7787.              * Builds the front preview url with the current state of customizer.
  7788.              *
  7789.              * @since 4.9
  7790.              *
  7791.              * @return {string} Preview url.
  7792.              */
  7793.             getFrontendPreviewUrl: function() {
  7794.                 var previewer = this, params, urlParser;
  7795.                 urlParser = document.createElement( 'a' );
  7796.                 urlParser.href = previewer.previewUrl.get();
  7797.                 params = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  7798.  
  7799.                 if ( api.state( 'changesetStatus' ).get() && 'publish' !== api.state( 'changesetStatus' ).get() ) {
  7800.                     params.customize_changeset_uuid = api.settings.changeset.uuid;
  7801.                 }
  7802.                 if ( ! api.state( 'activated' ).get() ) {
  7803.                     params.customize_theme = api.settings.theme.stylesheet;
  7804.                 }
  7805.  
  7806.                 urlParser.search = $.param( params );
  7807.                 return urlParser.href;
  7808.             }
  7809.         });
  7810.  
  7811.         // Ensure preview nonce is included with every customized request, to allow post data to be read.
  7812.         $.ajaxPrefilter( function injectPreviewNonce( options ) {
  7813.             if ( ! /wp_customize=on/.test( options.data ) ) {
  7814.                 return;
  7815.             }
  7816.             options.data += '&' + $.param({
  7817.                 customize_preview_nonce: api.settings.nonce.preview
  7818.             });
  7819.         });
  7820.  
  7821.         // Refresh the nonces if the preview sends updated nonces over.
  7822.         api.previewer.bind( 'nonce', function( nonce ) {
  7823.             $.extend( this.nonce, nonce );
  7824.         });
  7825.  
  7826.         // Refresh the nonces if login sends updated nonces over.
  7827.         api.bind( 'nonce-refresh', function( nonce ) {
  7828.             $.extend( api.settings.nonce, nonce );
  7829.             $.extend( api.previewer.nonce, nonce );
  7830.             api.previewer.send( 'nonce-refresh', nonce );
  7831.         });
  7832.  
  7833.         // Create Settings
  7834.         $.each( api.settings.settings, function( id, data ) {
  7835.             var Constructor = api.settingConstructor[ data.type ] || api.Setting;
  7836.             api.add( new Constructor( id, data.value, {
  7837.                 transport: data.transport,
  7838.                 previewer: api.previewer,
  7839.                 dirty: !! data.dirty
  7840.             } ) );
  7841.         });
  7842.  
  7843.         // Create Panels
  7844.         $.each( api.settings.panels, function ( id, data ) {
  7845.             var Constructor = api.panelConstructor[ data.type ] || api.Panel, options;
  7846.             options = _.extend( { params: data }, data ); // Inclusion of params alias is for back-compat for custom panels that expect to augment this property.
  7847.             api.panel.add( new Constructor( id, options ) );
  7848.         });
  7849.  
  7850.         // Create Sections
  7851.         $.each( api.settings.sections, function ( id, data ) {
  7852.             var Constructor = api.sectionConstructor[ data.type ] || api.Section, options;
  7853.             options = _.extend( { params: data }, data ); // Inclusion of params alias is for back-compat for custom sections that expect to augment this property.
  7854.             api.section.add( new Constructor( id, options ) );
  7855.         });
  7856.  
  7857.         // Create Controls
  7858.         $.each( api.settings.controls, function( id, data ) {
  7859.             var Constructor = api.controlConstructor[ data.type ] || api.Control, options;
  7860.             options = _.extend( { params: data }, data ); // Inclusion of params alias is for back-compat for custom controls that expect to augment this property.
  7861.             api.control.add( new Constructor( id, options ) );
  7862.         });
  7863.  
  7864.         // Focus the autofocused element
  7865.         _.each( [ 'panel', 'section', 'control' ], function( type ) {
  7866.             var id = api.settings.autofocus[ type ];
  7867.             if ( ! id ) {
  7868.                 return;
  7869.             }
  7870.  
  7871.             /*
  7872.              * Defer focus until:
  7873.              * 1. The panel, section, or control exists (especially for dynamically-created ones).
  7874.              * 2. The instance is embedded in the document (and so is focusable).
  7875.              * 3. The preview has finished loading so that the active states have been set.
  7876.              */
  7877.             api[ type ]( id, function( instance ) {
  7878.                 instance.deferred.embedded.done( function() {
  7879.                     api.previewer.deferred.active.done( function() {
  7880.                         instance.focus();
  7881.                     });
  7882.                 });
  7883.             });
  7884.         });
  7885.  
  7886.         api.bind( 'ready', api.reflowPaneContents );
  7887.         $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
  7888.             var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents );
  7889.             values.bind( 'add', debouncedReflowPaneContents );
  7890.             values.bind( 'change', debouncedReflowPaneContents );
  7891.             values.bind( 'remove', debouncedReflowPaneContents );
  7892.         } );
  7893.  
  7894.         // Set up global notifications area.
  7895.         api.bind( 'ready', function setUpGlobalNotificationsArea() {
  7896.             var sidebar, containerHeight, containerInitialTop;
  7897.             api.notifications.container = $( '#customize-notifications-area' );
  7898.  
  7899.             api.notifications.bind( 'change', _.debounce( function() {
  7900.                 api.notifications.render();
  7901.             } ) );
  7902.  
  7903.             sidebar = $( '.wp-full-overlay-sidebar-content' );
  7904.             api.notifications.bind( 'rendered', function updateSidebarTop() {
  7905.                 sidebar.css( 'top', '' );
  7906.                 if ( 0 !== api.notifications.count() ) {
  7907.                     containerHeight = api.notifications.container.outerHeight() + 1;
  7908.                     containerInitialTop = parseInt( sidebar.css( 'top' ), 10 );
  7909.                     sidebar.css( 'top', containerInitialTop + containerHeight + 'px' );
  7910.                 }
  7911.                 api.notifications.trigger( 'sidebarTopUpdated' );
  7912.             });
  7913.  
  7914.             api.notifications.render();
  7915.         });
  7916.  
  7917.         // Save and activated states
  7918.         (function( state ) {
  7919.             var saved = state.instance( 'saved' ),
  7920.                 saving = state.instance( 'saving' ),
  7921.                 trashing = state.instance( 'trashing' ),
  7922.                 activated = state.instance( 'activated' ),
  7923.                 processing = state.instance( 'processing' ),
  7924.                 paneVisible = state.instance( 'paneVisible' ),
  7925.                 expandedPanel = state.instance( 'expandedPanel' ),
  7926.                 expandedSection = state.instance( 'expandedSection' ),
  7927.                 changesetStatus = state.instance( 'changesetStatus' ),
  7928.                 selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ),
  7929.                 changesetDate = state.instance( 'changesetDate' ),
  7930.                 selectedChangesetDate = state.instance( 'selectedChangesetDate' ),
  7931.                 previewerAlive = state.instance( 'previewerAlive' ),
  7932.                 editShortcutVisibility  = state.instance( 'editShortcutVisibility' ),
  7933.                 changesetLocked = state.instance( 'changesetLocked' ),
  7934.                 populateChangesetUuidParam, defaultSelectedChangesetStatus;
  7935.  
  7936.             state.bind( 'change', function() {
  7937.                 var canSave;
  7938.  
  7939.                 if ( ! activated() ) {
  7940.                     saveBtn.val( api.l10n.activate );
  7941.                     closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
  7942.  
  7943.                 } else if ( '' === changesetStatus.get() && saved() ) {
  7944.                     if ( api.settings.changeset.currentUserCanPublish ) {
  7945.                         saveBtn.val( api.l10n.published );
  7946.                     } else {
  7947.                         saveBtn.val( api.l10n.saved );
  7948.                     }
  7949.                     closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
  7950.  
  7951.                 } else {
  7952.                     if ( 'draft' === selectedChangesetStatus() ) {
  7953.                         if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
  7954.                             saveBtn.val( api.l10n.draftSaved );
  7955.                         } else {
  7956.                             saveBtn.val( api.l10n.saveDraft );
  7957.                         }
  7958.                     } else if ( 'future' === selectedChangesetStatus() ) {
  7959.                         if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
  7960.                             if ( changesetDate.get() !== selectedChangesetDate.get() ) {
  7961.                                 saveBtn.val( api.l10n.schedule );
  7962.                             } else {
  7963.                                 saveBtn.val( api.l10n.scheduled );
  7964.                             }
  7965.                         } else {
  7966.                             saveBtn.val( api.l10n.schedule );
  7967.                         }
  7968.                     } else if ( api.settings.changeset.currentUserCanPublish ) {
  7969.                         saveBtn.val( api.l10n.publish );
  7970.                     }
  7971.                     closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
  7972.                 }
  7973.  
  7974.                 /*
  7975.                  * Save (publish) button should be enabled if saving is not currently happening,
  7976.                  * and if the theme is not active or the changeset exists but is not published.
  7977.                  */
  7978.                 canSave = ! saving() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
  7979.  
  7980.                 saveBtn.prop( 'disabled', ! canSave );
  7981.             });
  7982.  
  7983.             selectedChangesetStatus.validate = function( status ) {
  7984.                 if ( '' === status || 'auto-draft' === status ) {
  7985.                     return null;
  7986.                 }
  7987.                 return status;
  7988.             };
  7989.  
  7990.             defaultSelectedChangesetStatus = api.settings.changeset.currentUserCanPublish ? 'publish' : 'draft';
  7991.  
  7992.             // Set default states.
  7993.             changesetStatus( api.settings.changeset.status );
  7994.             changesetLocked( Boolean( api.settings.changeset.lockUser ) );
  7995.             changesetDate( api.settings.changeset.publishDate );
  7996.             selectedChangesetDate( api.settings.changeset.publishDate );
  7997.             selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? defaultSelectedChangesetStatus : api.settings.changeset.status );
  7998.             selectedChangesetStatus.link( changesetStatus ); // Ensure that direct updates to status on server via wp.customizer.previewer.save() will update selection.
  7999.             saved( true );
  8000.             if ( '' === changesetStatus() ) { // Handle case for loading starter content.
  8001.                 api.each( function( setting ) {
  8002.                     if ( setting._dirty ) {
  8003.                         saved( false );
  8004.                     }
  8005.                 } );
  8006.             }
  8007.             saving( false );
  8008.             activated( api.settings.theme.active );
  8009.             processing( 0 );
  8010.             paneVisible( true );
  8011.             expandedPanel( false );
  8012.             expandedSection( false );
  8013.             previewerAlive( true );
  8014.             editShortcutVisibility( 'visible' );
  8015.  
  8016.             api.bind( 'change', function() {
  8017.                 if ( state( 'saved' ).get() ) {
  8018.                     state( 'saved' ).set( false );
  8019.                 }
  8020.             });
  8021.  
  8022.             // Populate changeset UUID param when state becomes dirty.
  8023.             if ( api.settings.changeset.branching ) {
  8024.                 saved.bind( function( isSaved ) {
  8025.                     if ( ! isSaved ) {
  8026.                         populateChangesetUuidParam( true );
  8027.                     }
  8028.                 });
  8029.             }
  8030.  
  8031.             saving.bind( function( isSaving ) {
  8032.                 body.toggleClass( 'saving', isSaving );
  8033.             } );
  8034.             trashing.bind( function( isTrashing ) {
  8035.                 body.toggleClass( 'trashing', isTrashing );
  8036.             } );
  8037.  
  8038.             api.bind( 'saved', function( response ) {
  8039.                 state('saved').set( true );
  8040.                 if ( 'publish' === response.changeset_status ) {
  8041.                     state( 'activated' ).set( true );
  8042.                 }
  8043.             });
  8044.  
  8045.             activated.bind( function( to ) {
  8046.                 if ( to ) {
  8047.                     api.trigger( 'activated' );
  8048.                 }
  8049.             });
  8050.  
  8051.             /**
  8052.              * Populate URL with UUID via `history.replaceState()`.
  8053.              *
  8054.              * @since 4.7.0
  8055.              * @access private
  8056.              *
  8057.              * @param {boolean} isIncluded Is UUID included.
  8058.              * @returns {void}
  8059.              */
  8060.             populateChangesetUuidParam = function( isIncluded ) {
  8061.                 var urlParser, queryParams;
  8062.  
  8063.                 // Abort on IE9 which doesn't support history management.
  8064.                 if ( ! history.replaceState ) {
  8065.                     return;
  8066.                 }
  8067.  
  8068.                 urlParser = document.createElement( 'a' );
  8069.                 urlParser.href = location.href;
  8070.                 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  8071.                 if ( isIncluded ) {
  8072.                     if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) {
  8073.                         return;
  8074.                     }
  8075.                     queryParams.changeset_uuid = api.settings.changeset.uuid;
  8076.                 } else {
  8077.                     if ( ! queryParams.changeset_uuid ) {
  8078.                         return;
  8079.                     }
  8080.                     delete queryParams.changeset_uuid;
  8081.                 }
  8082.                 urlParser.search = $.param( queryParams );
  8083.                 history.replaceState( {}, document.title, urlParser.href );
  8084.             };
  8085.  
  8086.             // Show changeset UUID in URL when in branching mode and there is a saved changeset.
  8087.             if ( api.settings.changeset.branching ) {
  8088.                 changesetStatus.bind( function( newStatus ) {
  8089.                     populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus && 'trash' !== newStatus );
  8090.                 } );
  8091.             }
  8092.         }( api.state ) );
  8093.  
  8094.         /**
  8095.          * Handles lock notice and take over request.
  8096.          *
  8097.          * @since 4.9.0
  8098.          */
  8099.         ( function checkAndDisplayLockNotice() {
  8100.  
  8101.             /**
  8102.              * A notification that is displayed in a full-screen overlay with information about the locked changeset.
  8103.              *
  8104.              * @since 4.9.0
  8105.              * @class
  8106.              * @augments wp.customize.Notification
  8107.              * @augments wp.customize.OverlayNotification
  8108.              */
  8109.             var LockedNotification = api.OverlayNotification.extend({
  8110.  
  8111.                 /**
  8112.                  * Template ID.
  8113.                  *
  8114.                  * @type {string}
  8115.                  */
  8116.                 templateId: 'customize-changeset-locked-notification',
  8117.  
  8118.                 /**
  8119.                  * Lock user.
  8120.                  *
  8121.                  * @type {object}
  8122.                  */
  8123.                 lockUser: null,
  8124.  
  8125.                 /**
  8126.                  * Initialize.
  8127.                  *
  8128.                  * @since 4.9.0
  8129.                  *
  8130.                  * @param {string} [code] - Code.
  8131.                  * @param {object} [params] - Params.
  8132.                  */
  8133.                 initialize: function( code, params ) {
  8134.                     var notification = this, _code, _params;
  8135.                     _code = code || 'changeset_locked';
  8136.                     _params = _.extend(
  8137.                         {
  8138.                             type: 'warning',
  8139.                             containerClasses: '',
  8140.                             lockUser: {}
  8141.                         },
  8142.                         params
  8143.                     );
  8144.                     _params.containerClasses += ' notification-changeset-locked';
  8145.                     api.OverlayNotification.prototype.initialize.call( notification, _code, _params );
  8146.                 },
  8147.  
  8148.                 /**
  8149.                  * Render notification.
  8150.                  *
  8151.                  * @since 4.9.0
  8152.                  *
  8153.                  * @return {jQuery} Notification container.
  8154.                  */
  8155.                 render: function() {
  8156.                     var notification = this, li, data, takeOverButton, request;
  8157.                     data = _.extend(
  8158.                         {
  8159.                             allowOverride: false,
  8160.                             returnUrl: api.settings.url['return'],
  8161.                             previewUrl: api.previewer.previewUrl.get(),
  8162.                             frontendPreviewUrl: api.previewer.getFrontendPreviewUrl()
  8163.                         },
  8164.                         this
  8165.                     );
  8166.  
  8167.                     li = api.OverlayNotification.prototype.render.call( data );
  8168.  
  8169.                     // Try to autosave the changeset now.
  8170.                     api.requestChangesetUpdate( {}, { autosave: true } ).fail( function( response ) {
  8171.                         if ( ! response.autosaved ) {
  8172.                             li.find( '.notice-error' ).prop( 'hidden', false ).text( response.message || api.l10n.unknownRequestFail );
  8173.                         }
  8174.                     } );
  8175.  
  8176.                     takeOverButton = li.find( '.customize-notice-take-over-button' );
  8177.                     takeOverButton.on( 'click', function( event ) {
  8178.                         event.preventDefault();
  8179.                         if ( request ) {
  8180.                             return;
  8181.                         }
  8182.  
  8183.                         takeOverButton.addClass( 'disabled' );
  8184.                         request = wp.ajax.post( 'customize_override_changeset_lock', {
  8185.                             wp_customize: 'on',
  8186.                             customize_theme: api.settings.theme.stylesheet,
  8187.                             customize_changeset_uuid: api.settings.changeset.uuid,
  8188.                             nonce: api.settings.nonce.override_lock
  8189.                         } );
  8190.  
  8191.                         request.done( function() {
  8192.                             api.notifications.remove( notification.code ); // Remove self.
  8193.                             api.state( 'changesetLocked' ).set( false );
  8194.                         } );
  8195.  
  8196.                         request.fail( function( response ) {
  8197.                             var message = response.message || api.l10n.unknownRequestFail;
  8198.                             li.find( '.notice-error' ).prop( 'hidden', false ).text( message );
  8199.  
  8200.                             request.always( function() {
  8201.                                 takeOverButton.removeClass( 'disabled' );
  8202.                             } );
  8203.                         } );
  8204.  
  8205.                         request.always( function() {
  8206.                             request = null;
  8207.                         } );
  8208.                     } );
  8209.  
  8210.                     return li;
  8211.                 }
  8212.             });
  8213.  
  8214.             /**
  8215.              * Start lock.
  8216.              *
  8217.              * @since 4.9.0
  8218.              *
  8219.              * @param {object} [args] - Args.
  8220.              * @param {object} [args.lockUser] - Lock user data.
  8221.              * @param {boolean} [args.allowOverride=false] - Whether override is allowed.
  8222.              * @returns {void}
  8223.              */
  8224.             function startLock( args ) {
  8225.                 if ( args && args.lockUser ) {
  8226.                     api.settings.changeset.lockUser = args.lockUser;
  8227.                 }
  8228.                 api.state( 'changesetLocked' ).set( true );
  8229.                 api.notifications.add( new LockedNotification( 'changeset_locked', {
  8230.                     lockUser: api.settings.changeset.lockUser,
  8231.                     allowOverride: Boolean( args && args.allowOverride )
  8232.                 } ) );
  8233.             }
  8234.  
  8235.             // Show initial notification.
  8236.             if ( api.settings.changeset.lockUser ) {
  8237.                 startLock( { allowOverride: true } );
  8238.             }
  8239.  
  8240.             // Check for lock when sending heartbeat requests.
  8241.             $( document ).on( 'heartbeat-send.update_lock_notice', function( event, data ) {
  8242.                 data.check_changeset_lock = true;
  8243.                 data.changeset_uuid = api.settings.changeset.uuid;
  8244.             } );
  8245.  
  8246.             // Handle heartbeat ticks.
  8247.             $( document ).on( 'heartbeat-tick.update_lock_notice', function( event, data ) {
  8248.                 var notification, code = 'changeset_locked';
  8249.                 if ( ! data.customize_changeset_lock_user ) {
  8250.                     return;
  8251.                 }
  8252.  
  8253.                 // Update notification when a different user takes over.
  8254.                 notification = api.notifications( code );
  8255.                 if ( notification && notification.lockUser.id !== api.settings.changeset.lockUser.id ) {
  8256.                     api.notifications.remove( code );
  8257.                 }
  8258.  
  8259.                 startLock( {
  8260.                     lockUser: data.customize_changeset_lock_user
  8261.                 } );
  8262.             } );
  8263.  
  8264.             // Handle locking in response to changeset save errors.
  8265.             api.bind( 'error', function( response ) {
  8266.                 if ( 'changeset_locked' === response.code && response.lock_user ) {
  8267.                     startLock( {
  8268.                         lockUser: response.lock_user
  8269.                     } );
  8270.                 }
  8271.             } );
  8272.         } )();
  8273.  
  8274.         // Set up initial notifications.
  8275.         (function() {
  8276.             var removedQueryParams = [], autosaveDismissed = false;
  8277.  
  8278.             /**
  8279.              * Obtain the URL to restore the autosave.
  8280.              *
  8281.              * @returns {string} Customizer URL.
  8282.              */
  8283.             function getAutosaveRestorationUrl() {
  8284.                 var urlParser, queryParams;
  8285.                 urlParser = document.createElement( 'a' );
  8286.                 urlParser.href = location.href;
  8287.                 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  8288.                 if ( api.settings.changeset.latestAutoDraftUuid ) {
  8289.                     queryParams.changeset_uuid = api.settings.changeset.latestAutoDraftUuid;
  8290.                 } else {
  8291.                     queryParams.customize_autosaved = 'on';
  8292.                 }
  8293.                 queryParams['return'] = api.settings.url['return'];
  8294.                 urlParser.search = $.param( queryParams );
  8295.                 return urlParser.href;
  8296.             }
  8297.  
  8298.             /**
  8299.              * Remove parameter from the URL.
  8300.              *
  8301.              * @param {Array} params - Parameter names to remove.
  8302.              * @returns {void}
  8303.              */
  8304.             function stripParamsFromLocation( params ) {
  8305.                 var urlParser = document.createElement( 'a' ), queryParams, strippedParams = 0;
  8306.                 urlParser.href = location.href;
  8307.                 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
  8308.                 _.each( params, function( param ) {
  8309.                     if ( 'undefined' !== typeof queryParams[ param ] ) {
  8310.                         strippedParams += 1;
  8311.                         delete queryParams[ param ];
  8312.                     }
  8313.                 } );
  8314.                 if ( 0 === strippedParams ) {
  8315.                     return;
  8316.                 }
  8317.  
  8318.                 urlParser.search = $.param( queryParams );
  8319.                 history.replaceState( {}, document.title, urlParser.href );
  8320.             }
  8321.  
  8322.             /**
  8323.              * Dismiss autosave.
  8324.              *
  8325.              * @returns {void}
  8326.              */
  8327.             function dismissAutosave() {
  8328.                 if ( autosaveDismissed ) {
  8329.                     return;
  8330.                 }
  8331.                 wp.ajax.post( 'customize_dismiss_autosave_or_lock', {
  8332.                     wp_customize: 'on',
  8333.                     customize_theme: api.settings.theme.stylesheet,
  8334.                     customize_changeset_uuid: api.settings.changeset.uuid,
  8335.                     nonce: api.settings.nonce.dismiss_autosave_or_lock,
  8336.                     dismiss_autosave: true
  8337.                 } );
  8338.                 autosaveDismissed = true;
  8339.             }
  8340.  
  8341.             /**
  8342.              * Add notification regarding the availability of an autosave to restore.
  8343.              *
  8344.              * @returns {void}
  8345.              */
  8346.             function addAutosaveRestoreNotification() {
  8347.                 var code = 'autosave_available', onStateChange;
  8348.  
  8349.                 // Since there is an autosave revision and the user hasn't loaded with autosaved, add notification to prompt to load autosaved version.
  8350.                 api.notifications.add( new api.Notification( code, {
  8351.                     message: api.l10n.autosaveNotice,
  8352.                     type: 'warning',
  8353.                     dismissible: true,
  8354.                     render: function() {
  8355.                         var li = api.Notification.prototype.render.call( this ), link;
  8356.  
  8357.                         // Handle clicking on restoration link.
  8358.                         link = li.find( 'a' );
  8359.                         link.prop( 'href', getAutosaveRestorationUrl() );
  8360.                         link.on( 'click', function( event ) {
  8361.                             event.preventDefault();
  8362.                             location.replace( getAutosaveRestorationUrl() );
  8363.                         } );
  8364.  
  8365.                         // Handle dismissal of notice.
  8366.                         li.find( '.notice-dismiss' ).on( 'click', dismissAutosave );
  8367.  
  8368.                         return li;
  8369.                     }
  8370.                 } ) );
  8371.  
  8372.                 // Remove the notification once the user starts making changes.
  8373.                 onStateChange = function() {
  8374.                     dismissAutosave();
  8375.                     api.notifications.remove( code );
  8376.                     api.unbind( 'change', onStateChange );
  8377.                     api.state( 'changesetStatus' ).unbind( onStateChange );
  8378.                 };
  8379.                 api.bind( 'change', onStateChange );
  8380.                 api.state( 'changesetStatus' ).bind( onStateChange );
  8381.             }
  8382.  
  8383.             if ( api.settings.changeset.autosaved ) {
  8384.                 api.state( 'saved' ).set( false );
  8385.                 removedQueryParams.push( 'customize_autosaved' );
  8386.             }
  8387.             if ( ! api.settings.changeset.branching && ( ! api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ) ) {
  8388.                 removedQueryParams.push( 'changeset_uuid' ); // Remove UUID when restoring autosave auto-draft.
  8389.             }
  8390.             if ( removedQueryParams.length > 0 ) {
  8391.                 stripParamsFromLocation( removedQueryParams );
  8392.             }
  8393.             if ( api.settings.changeset.latestAutoDraftUuid || api.settings.changeset.hasAutosaveRevision ) {
  8394.                 addAutosaveRestoreNotification();
  8395.             }
  8396.         })();
  8397.  
  8398.         // Check if preview url is valid and load the preview frame.
  8399.         if ( api.previewer.previewUrl() ) {
  8400.             api.previewer.refresh();
  8401.         } else {
  8402.             api.previewer.previewUrl( api.settings.url.home );
  8403.         }
  8404.  
  8405.         // Button bindings.
  8406.         saveBtn.click( function( event ) {
  8407.             api.previewer.save();
  8408.             event.preventDefault();
  8409.         }).keydown( function( event ) {
  8410.             if ( 9 === event.which ) { // Tab.
  8411.                 return;
  8412.             }
  8413.             if ( 13 === event.which ) { // Enter.
  8414.                 api.previewer.save();
  8415.             }
  8416.             event.preventDefault();
  8417.         });
  8418.  
  8419.         closeBtn.keydown( function( event ) {
  8420.             if ( 9 === event.which ) { // Tab.
  8421.                 return;
  8422.             }
  8423.             if ( 13 === event.which ) { // Enter.
  8424.                 this.click();
  8425.             }
  8426.             event.preventDefault();
  8427.         });
  8428.  
  8429.         $( '.collapse-sidebar' ).on( 'click', function() {
  8430.             api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
  8431.         });
  8432.  
  8433.         api.state( 'paneVisible' ).bind( function( paneVisible ) {
  8434.             overlay.toggleClass( 'preview-only', ! paneVisible );
  8435.             overlay.toggleClass( 'expanded', paneVisible );
  8436.             overlay.toggleClass( 'collapsed', ! paneVisible );
  8437.  
  8438.             if ( ! paneVisible ) {
  8439.                 $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
  8440.             } else {
  8441.                 $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
  8442.             }
  8443.         });
  8444.  
  8445.         // Keyboard shortcuts - esc to exit section/panel.
  8446.         body.on( 'keydown', function( event ) {
  8447.             var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
  8448.  
  8449.             if ( 27 !== event.which ) { // Esc.
  8450.                 return;
  8451.             }
  8452.  
  8453.             /*
  8454.              * Abort if the event target is not the body (the default) and not inside of #customize-controls.
  8455.              * This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else.
  8456.              */
  8457.             if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) {
  8458.                 return;
  8459.             }
  8460.  
  8461.             // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
  8462.             api.control.each( function( control ) {
  8463.                 if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
  8464.                     expandedControls.push( control );
  8465.                 }
  8466.             });
  8467.             api.section.each( function( section ) {
  8468.                 if ( section.expanded() ) {
  8469.                     expandedSections.push( section );
  8470.                 }
  8471.             });
  8472.             api.panel.each( function( panel ) {
  8473.                 if ( panel.expanded() ) {
  8474.                     expandedPanels.push( panel );
  8475.                 }
  8476.             });
  8477.  
  8478.             // Skip collapsing expanded controls if there are no expanded sections.
  8479.             if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
  8480.                 expandedControls.length = 0;
  8481.             }
  8482.  
  8483.             // Collapse the most granular expanded object.
  8484.             collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
  8485.             if ( collapsedObject ) {
  8486.                 if ( 'themes' === collapsedObject.params.type ) {
  8487.  
  8488.                     // Themes panel or section.
  8489.                     if ( body.hasClass( 'modal-open' ) ) {
  8490.                         collapsedObject.closeDetails();
  8491.                     } else if ( api.panel.has( 'themes' ) ) {
  8492.  
  8493.                         // If we're collapsing a section, collapse the panel also.
  8494.                         api.panel( 'themes' ).collapse();
  8495.                     }
  8496.                     return;
  8497.                 }
  8498.                 collapsedObject.collapse();
  8499.                 event.preventDefault();
  8500.             }
  8501.         });
  8502.  
  8503.         $( '.customize-controls-preview-toggle' ).on( 'click', function() {
  8504.             api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
  8505.         });
  8506.  
  8507.         /*
  8508.          * Sticky header feature.
  8509.          */
  8510.         (function initStickyHeaders() {
  8511.             var parentContainer = $( '.wp-full-overlay-sidebar-content' ),
  8512.                 changeContainer, updateHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader,
  8513.                 activeHeader, lastScrollTop;
  8514.  
  8515.             /**
  8516.              * Determine which panel or section is currently expanded.
  8517.              *
  8518.              * @since 4.7.0
  8519.              * @access private
  8520.              *
  8521.              * @param {wp.customize.Panel|wp.customize.Section} container Construct.
  8522.              * @returns {void}
  8523.              */
  8524.             changeContainer = function( container ) {
  8525.                 var newInstance = container,
  8526.                     expandedSection = api.state( 'expandedSection' ).get(),
  8527.                     expandedPanel = api.state( 'expandedPanel' ).get(),
  8528.                     headerElement;
  8529.  
  8530.                 if ( activeHeader && activeHeader.element ) {
  8531.                     // Release previously active header element.
  8532.                     releaseStickyHeader( activeHeader.element );
  8533.  
  8534.                     // Remove event listener in the previous panel or section.
  8535.                     activeHeader.element.find( '.description' ).off( 'toggled', updateHeaderHeight );
  8536.                 }
  8537.  
  8538.                 if ( ! newInstance ) {
  8539.                     if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) {
  8540.                         newInstance = expandedPanel;
  8541.                     } else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) {
  8542.                         newInstance = expandedSection;
  8543.                     } else {
  8544.                         activeHeader = false;
  8545.                         return;
  8546.                     }
  8547.                 }
  8548.  
  8549.                 headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first();
  8550.                 if ( headerElement.length ) {
  8551.                     activeHeader = {
  8552.                         instance: newInstance,
  8553.                         element:  headerElement,
  8554.                         parent:   headerElement.closest( '.customize-pane-child' ),
  8555.                         height:   headerElement.outerHeight()
  8556.                     };
  8557.  
  8558.                     // Update header height whenever help text is expanded or collapsed.
  8559.                     activeHeader.element.find( '.description' ).on( 'toggled', updateHeaderHeight );
  8560.  
  8561.                     if ( expandedSection ) {
  8562.                         resetStickyHeader( activeHeader.element, activeHeader.parent );
  8563.                     }
  8564.                 } else {
  8565.                     activeHeader = false;
  8566.                 }
  8567.             };
  8568.             api.state( 'expandedSection' ).bind( changeContainer );
  8569.             api.state( 'expandedPanel' ).bind( changeContainer );
  8570.  
  8571.             // Throttled scroll event handler.
  8572.             parentContainer.on( 'scroll', _.throttle( function() {
  8573.                 if ( ! activeHeader ) {
  8574.                     return;
  8575.                 }
  8576.  
  8577.                 var scrollTop = parentContainer.scrollTop(),
  8578.                     scrollDirection;
  8579.  
  8580.                 if ( ! lastScrollTop ) {
  8581.                     scrollDirection = 1;
  8582.                 } else {
  8583.                     if ( scrollTop === lastScrollTop ) {
  8584.                         scrollDirection = 0;
  8585.                     } else if ( scrollTop > lastScrollTop ) {
  8586.                         scrollDirection = 1;
  8587.                     } else {
  8588.                         scrollDirection = -1;
  8589.                     }
  8590.                 }
  8591.                 lastScrollTop = scrollTop;
  8592.                 if ( 0 !== scrollDirection ) {
  8593.                     positionStickyHeader( activeHeader, scrollTop, scrollDirection );
  8594.                 }
  8595.             }, 8 ) );
  8596.  
  8597.             // Update header position on sidebar layout change.
  8598.             api.notifications.bind( 'sidebarTopUpdated', function() {
  8599.                 if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) {
  8600.                     activeHeader.element.css( 'top', parentContainer.css( 'top' ) );
  8601.                 }
  8602.             });
  8603.  
  8604.             // Release header element if it is sticky.
  8605.             releaseStickyHeader = function( headerElement ) {
  8606.                 if ( ! headerElement.hasClass( 'is-sticky' ) ) {
  8607.                     return;
  8608.                 }
  8609.                 headerElement
  8610.                     .removeClass( 'is-sticky' )
  8611.                     .addClass( 'maybe-sticky is-in-view' )
  8612.                     .css( 'top', parentContainer.scrollTop() + 'px' );
  8613.             };
  8614.  
  8615.             // Reset position of the sticky header.
  8616.             resetStickyHeader = function( headerElement, headerParent ) {
  8617.                 if ( headerElement.hasClass( 'is-in-view' ) ) {
  8618.                     headerElement
  8619.                         .removeClass( 'maybe-sticky is-in-view' )
  8620.                         .css( {
  8621.                             width: '',
  8622.                             top:   ''
  8623.                         } );
  8624.                     headerParent.css( 'padding-top', '' );
  8625.                 }
  8626.             };
  8627.  
  8628.             /**
  8629.              * Update active header height.
  8630.              *
  8631.              * @since 4.7.0
  8632.              * @access private
  8633.              *
  8634.              * @returns {void}
  8635.              */
  8636.             updateHeaderHeight = function() {
  8637.                 activeHeader.height = activeHeader.element.outerHeight();
  8638.             };
  8639.  
  8640.             /**
  8641.              * Reposition header on throttled `scroll` event.
  8642.              *
  8643.              * @since 4.7.0
  8644.              * @access private
  8645.              *
  8646.              * @param {object} header - Header.
  8647.              * @param {number} scrollTop - Scroll top.
  8648.              * @param {number} scrollDirection - Scroll direction, negative number being up and positive being down.
  8649.              * @returns {void}
  8650.              */
  8651.             positionStickyHeader = function( header, scrollTop, scrollDirection ) {
  8652.                 var headerElement = header.element,
  8653.                     headerParent = header.parent,
  8654.                     headerHeight = header.height,
  8655.                     headerTop = parseInt( headerElement.css( 'top' ), 10 ),
  8656.                     maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
  8657.                     isSticky = headerElement.hasClass( 'is-sticky' ),
  8658.                     isInView = headerElement.hasClass( 'is-in-view' ),
  8659.                     isScrollingUp = ( -1 === scrollDirection );
  8660.  
  8661.                 // When scrolling down, gradually hide sticky header.
  8662.                 if ( ! isScrollingUp ) {
  8663.                     if ( isSticky ) {
  8664.                         headerTop = scrollTop;
  8665.                         headerElement
  8666.                             .removeClass( 'is-sticky' )
  8667.                             .css( {
  8668.                                 top:   headerTop + 'px',
  8669.                                 width: ''
  8670.                             } );
  8671.                     }
  8672.                     if ( isInView && scrollTop > headerTop + headerHeight ) {
  8673.                         headerElement.removeClass( 'is-in-view' );
  8674.                         headerParent.css( 'padding-top', '' );
  8675.                     }
  8676.                     return;
  8677.                 }
  8678.  
  8679.                 // Scrolling up.
  8680.                 if ( ! maybeSticky && scrollTop >= headerHeight ) {
  8681.                     maybeSticky = true;
  8682.                     headerElement.addClass( 'maybe-sticky' );
  8683.                 } else if ( 0 === scrollTop ) {
  8684.                     // Reset header in base position.
  8685.                     headerElement
  8686.                         .removeClass( 'maybe-sticky is-in-view is-sticky' )
  8687.                         .css( {
  8688.                             top:   '',
  8689.                             width: ''
  8690.                         } );
  8691.                     headerParent.css( 'padding-top', '' );
  8692.                     return;
  8693.                 }
  8694.  
  8695.                 if ( isInView && ! isSticky ) {
  8696.                     // Header is in the view but is not yet sticky.
  8697.                     if ( headerTop >= scrollTop ) {
  8698.                         // Header is fully visible.
  8699.                         headerElement
  8700.                             .addClass( 'is-sticky' )
  8701.                             .css( {
  8702.                                 top:   parentContainer.css( 'top' ),
  8703.                                 width: headerParent.outerWidth() + 'px'
  8704.                             } );
  8705.                     }
  8706.                 } else if ( maybeSticky && ! isInView ) {
  8707.                     // Header is out of the view.
  8708.                     headerElement
  8709.                         .addClass( 'is-in-view' )
  8710.                         .css( 'top', ( scrollTop - headerHeight ) + 'px' );
  8711.                     headerParent.css( 'padding-top', headerHeight + 'px' );
  8712.                 }
  8713.             };
  8714.         }());
  8715.  
  8716.         // Previewed device bindings. (The api.previewedDevice property is how this Value was first introduced, but since it has moved to api.state.)
  8717.         api.previewedDevice = api.state( 'previewedDevice' );
  8718.  
  8719.         // Set the default device.
  8720.         api.bind( 'ready', function() {
  8721.             _.find( api.settings.previewableDevices, function( value, key ) {
  8722.                 if ( true === value['default'] ) {
  8723.                     api.previewedDevice.set( key );
  8724.                     return true;
  8725.                 }
  8726.             } );
  8727.         } );
  8728.  
  8729.         // Set the toggled device.
  8730.         footerActions.find( '.devices button' ).on( 'click', function( event ) {
  8731.             api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
  8732.         });
  8733.  
  8734.         // Bind device changes.
  8735.         api.previewedDevice.bind( function( newDevice ) {
  8736.             var overlay = $( '.wp-full-overlay' ),
  8737.                 devices = '';
  8738.  
  8739.             footerActions.find( '.devices button' )
  8740.                 .removeClass( 'active' )
  8741.                 .attr( 'aria-pressed', false );
  8742.  
  8743.             footerActions.find( '.devices .preview-' + newDevice )
  8744.                 .addClass( 'active' )
  8745.                 .attr( 'aria-pressed', true );
  8746.  
  8747.             $.each( api.settings.previewableDevices, function( device ) {
  8748.                 devices += ' preview-' + device;
  8749.             } );
  8750.  
  8751.             overlay
  8752.                 .removeClass( devices )
  8753.                 .addClass( 'preview-' + newDevice );
  8754.         } );
  8755.  
  8756.         // Bind site title display to the corresponding field.
  8757.         if ( title.length ) {
  8758.             api( 'blogname', function( setting ) {
  8759.                 var updateTitle = function() {
  8760.                     title.text( $.trim( setting() ) || api.l10n.untitledBlogName );
  8761.                 };
  8762.                 setting.bind( updateTitle );
  8763.                 updateTitle();
  8764.             } );
  8765.         }
  8766.  
  8767.         /*
  8768.          * Create a postMessage connection with a parent frame,
  8769.          * in case the Customizer frame was opened with the Customize loader.
  8770.          *
  8771.          * @see wp.customize.Loader
  8772.          */
  8773.         parent = new api.Messenger({
  8774.             url: api.settings.url.parent,
  8775.             channel: 'loader'
  8776.         });
  8777.  
  8778.         // Handle exiting of Customizer.
  8779.         (function() {
  8780.             var isInsideIframe = false;
  8781.  
  8782.             function isCleanState() {
  8783.                 var defaultChangesetStatus;
  8784.  
  8785.                 /*
  8786.                  * Handle special case of previewing theme switch since some settings (for nav menus and widgets)
  8787.                  * are pre-dirty and non-active themes can only ever be auto-drafts.
  8788.                  */
  8789.                 if ( ! api.state( 'activated' ).get() ) {
  8790.                     return 0 === api._latestRevision;
  8791.                 }
  8792.  
  8793.                 // Dirty if the changeset status has been changed but not saved yet.
  8794.                 defaultChangesetStatus = api.state( 'changesetStatus' ).get();
  8795.                 if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
  8796.                     defaultChangesetStatus = 'publish';
  8797.                 }
  8798.                 if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
  8799.                     return false;
  8800.                 }
  8801.  
  8802.                 // Dirty if scheduled but the changeset date hasn't been saved yet.
  8803.                 if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
  8804.                     return false;
  8805.                 }
  8806.  
  8807.                 return api.state( 'saved' ).get() && 'auto-draft' !== api.state( 'changesetStatus' ).get();
  8808.             }
  8809.  
  8810.             /*
  8811.              * If we receive a 'back' event, we're inside an iframe.
  8812.              * Send any clicks to the 'Return' link to the parent page.
  8813.              */
  8814.             parent.bind( 'back', function() {
  8815.                 isInsideIframe = true;
  8816.             });
  8817.  
  8818.             function startPromptingBeforeUnload() {
  8819.                 api.unbind( 'change', startPromptingBeforeUnload );
  8820.                 api.state( 'selectedChangesetStatus' ).unbind( startPromptingBeforeUnload );
  8821.                 api.state( 'selectedChangesetDate' ).unbind( startPromptingBeforeUnload );
  8822.  
  8823.                 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
  8824.                 $( window ).on( 'beforeunload.customize-confirm', function() {
  8825.                     if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) {
  8826.                         setTimeout( function() {
  8827.                             overlay.removeClass( 'customize-loading' );
  8828.                         }, 1 );
  8829.                         return api.l10n.saveAlert;
  8830.                     }
  8831.                 });
  8832.             }
  8833.             api.bind( 'change', startPromptingBeforeUnload );
  8834.             api.state( 'selectedChangesetStatus' ).bind( startPromptingBeforeUnload );
  8835.             api.state( 'selectedChangesetDate' ).bind( startPromptingBeforeUnload );
  8836.  
  8837.             function requestClose() {
  8838.                 var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false;
  8839.  
  8840.                 if ( isCleanState() ) {
  8841.                     dismissLock = true;
  8842.                 } else if ( confirm( api.l10n.saveAlert ) ) {
  8843.  
  8844.                     dismissLock = true;
  8845.  
  8846.                     // Mark all settings as clean to prevent another call to requestChangesetUpdate.
  8847.                     api.each( function( setting ) {
  8848.                         setting._dirty = false;
  8849.                     });
  8850.                     $( document ).off( 'visibilitychange.wp-customize-changeset-update' );
  8851.                     $( window ).off( 'beforeunload.wp-customize-changeset-update' );
  8852.  
  8853.                     closeBtn.css( 'cursor', 'progress' );
  8854.                     if ( '' !== api.state( 'changesetStatus' ).get() ) {
  8855.                         dismissAutoSave = true;
  8856.                     }
  8857.                 } else {
  8858.                     clearedToClose.reject();
  8859.                 }
  8860.  
  8861.                 if ( dismissLock || dismissAutoSave ) {
  8862.                     wp.ajax.send( 'customize_dismiss_autosave_or_lock', {
  8863.                         timeout: 500, // Don't wait too long.
  8864.                         data: {
  8865.                             wp_customize: 'on',
  8866.                             customize_theme: api.settings.theme.stylesheet,
  8867.                             customize_changeset_uuid: api.settings.changeset.uuid,
  8868.                             nonce: api.settings.nonce.dismiss_autosave_or_lock,
  8869.                             dismiss_autosave: dismissAutoSave,
  8870.                             dismiss_lock: dismissLock
  8871.                         }
  8872.                     } ).always( function() {
  8873.                         clearedToClose.resolve();
  8874.                     } );
  8875.                 }
  8876.  
  8877.                 return clearedToClose.promise();
  8878.             }
  8879.  
  8880.             parent.bind( 'confirm-close', function() {
  8881.                 requestClose().done( function() {
  8882.                     parent.send( 'confirmed-close', true );
  8883.                 } ).fail( function() {
  8884.                     parent.send( 'confirmed-close', false );
  8885.                 } );
  8886.             } );
  8887.  
  8888.             closeBtn.on( 'click.customize-controls-close', function( event ) {
  8889.                 event.preventDefault();
  8890.                 if ( isInsideIframe ) {
  8891.                     parent.send( 'close' ); // See confirm-close logic above.
  8892.                 } else {
  8893.                     requestClose().done( function() {
  8894.                         $( window ).off( 'beforeunload.customize-confirm' );
  8895.                         window.location.href = closeBtn.prop( 'href' );
  8896.                     } );
  8897.                 }
  8898.             });
  8899.         })();
  8900.  
  8901.         // Pass events through to the parent.
  8902.         $.each( [ 'saved', 'change' ], function ( i, event ) {
  8903.             api.bind( event, function() {
  8904.                 parent.send( event );
  8905.             });
  8906.         } );
  8907.  
  8908.         // Pass titles to the parent
  8909.         api.bind( 'title', function( newTitle ) {
  8910.             parent.send( 'title', newTitle );
  8911.         });
  8912.  
  8913.         if ( api.settings.changeset.branching ) {
  8914.             parent.send( 'changeset-uuid', api.settings.changeset.uuid );
  8915.         }
  8916.  
  8917.         // Initialize the connection with the parent frame.
  8918.         parent.send( 'ready' );
  8919.  
  8920.         // Control visibility for default controls
  8921.         $.each({
  8922.             'background_image': {
  8923.                 controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ],
  8924.                 callback: function( to ) { return !! to; }
  8925.             },
  8926.             'show_on_front': {
  8927.                 controls: [ 'page_on_front', 'page_for_posts' ],
  8928.                 callback: function( to ) { return 'page' === to; }
  8929.             },
  8930.             'header_textcolor': {
  8931.                 controls: [ 'header_textcolor' ],
  8932.                 callback: function( to ) { return 'blank' !== to; }
  8933.             }
  8934.         }, function( settingId, o ) {
  8935.             api( settingId, function( setting ) {
  8936.                 $.each( o.controls, function( i, controlId ) {
  8937.                     api.control( controlId, function( control ) {
  8938.                         var visibility = function( to ) {
  8939.                             control.container.toggle( o.callback( to ) );
  8940.                         };
  8941.  
  8942.                         visibility( setting.get() );
  8943.                         setting.bind( visibility );
  8944.                     });
  8945.                 });
  8946.             });
  8947.         });
  8948.  
  8949.         api.control( 'background_preset', function( control ) {
  8950.             var visibility, defaultValues, values, toggleVisibility, updateSettings, preset;
  8951.  
  8952.             visibility = { // position, size, repeat, attachment
  8953.                 'default': [ false, false, false, false ],
  8954.                 'fill': [ true, false, false, false ],
  8955.                 'fit': [ true, false, true, false ],
  8956.                 'repeat': [ true, false, false, true ],
  8957.                 'custom': [ true, true, true, true ]
  8958.             };
  8959.  
  8960.             defaultValues = [
  8961.                 _wpCustomizeBackground.defaults['default-position-x'],
  8962.                 _wpCustomizeBackground.defaults['default-position-y'],
  8963.                 _wpCustomizeBackground.defaults['default-size'],
  8964.                 _wpCustomizeBackground.defaults['default-repeat'],
  8965.                 _wpCustomizeBackground.defaults['default-attachment']
  8966.             ];
  8967.  
  8968.             values = { // position_x, position_y, size, repeat, attachment
  8969.                 'default': defaultValues,
  8970.                 'fill': [ 'left', 'top', 'cover', 'no-repeat', 'fixed' ],
  8971.                 'fit': [ 'left', 'top', 'contain', 'no-repeat', 'fixed' ],
  8972.                 'repeat': [ 'left', 'top', 'auto', 'repeat', 'scroll' ]
  8973.             };
  8974.  
  8975.             // @todo These should actually toggle the active state, but without the preview overriding the state in data.activeControls.
  8976.             toggleVisibility = function( preset ) {
  8977.                 _.each( [ 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], function( controlId, i ) {
  8978.                     var control = api.control( controlId );
  8979.                     if ( control ) {
  8980.                         control.container.toggle( visibility[ preset ][ i ] );
  8981.                     }
  8982.                 } );
  8983.             };
  8984.  
  8985.             updateSettings = function( preset ) {
  8986.                 _.each( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ], function( settingId, i ) {
  8987.                     var setting = api( settingId );
  8988.                     if ( setting ) {
  8989.                         setting.set( values[ preset ][ i ] );
  8990.                     }
  8991.                 } );
  8992.             };
  8993.  
  8994.             preset = control.setting.get();
  8995.             toggleVisibility( preset );
  8996.  
  8997.             control.setting.bind( 'change', function( preset ) {
  8998.                 toggleVisibility( preset );
  8999.                 if ( 'custom' !== preset ) {
  9000.                     updateSettings( preset );
  9001.                 }
  9002.             } );
  9003.         } );
  9004.  
  9005.         api.control( 'background_repeat', function( control ) {
  9006.             control.elements[0].unsync( api( 'background_repeat' ) );
  9007.  
  9008.             control.element = new api.Element( control.container.find( 'input' ) );
  9009.             control.element.set( 'no-repeat' !== control.setting() );
  9010.  
  9011.             control.element.bind( function( to ) {
  9012.                 control.setting.set( to ? 'repeat' : 'no-repeat' );
  9013.             } );
  9014.  
  9015.             control.setting.bind( function( to ) {
  9016.                 control.element.set( 'no-repeat' !== to );
  9017.             } );
  9018.         } );
  9019.  
  9020.         api.control( 'background_attachment', function( control ) {
  9021.             control.elements[0].unsync( api( 'background_attachment' ) );
  9022.  
  9023.             control.element = new api.Element( control.container.find( 'input' ) );
  9024.             control.element.set( 'fixed' !== control.setting() );
  9025.  
  9026.             control.element.bind( function( to ) {
  9027.                 control.setting.set( to ? 'scroll' : 'fixed' );
  9028.             } );
  9029.  
  9030.             control.setting.bind( function( to ) {
  9031.                 control.element.set( 'fixed' !== to );
  9032.             } );
  9033.         } );
  9034.  
  9035.         // Juggle the two controls that use header_textcolor
  9036.         api.control( 'display_header_text', function( control ) {
  9037.             var last = '';
  9038.  
  9039.             control.elements[0].unsync( api( 'header_textcolor' ) );
  9040.  
  9041.             control.element = new api.Element( control.container.find('input') );
  9042.             control.element.set( 'blank' !== control.setting() );
  9043.  
  9044.             control.element.bind( function( to ) {
  9045.                 if ( ! to ) {
  9046.                     last = api( 'header_textcolor' ).get();
  9047.                 }
  9048.  
  9049.                 control.setting.set( to ? last : 'blank' );
  9050.             });
  9051.  
  9052.             control.setting.bind( function( to ) {
  9053.                 control.element.set( 'blank' !== to );
  9054.             });
  9055.         });
  9056.  
  9057.         // Add behaviors to the static front page controls.
  9058.         api( 'show_on_front', 'page_on_front', 'page_for_posts', function( showOnFront, pageOnFront, pageForPosts ) {
  9059.             var handleChange = function() {
  9060.                 var setting = this, pageOnFrontId, pageForPostsId, errorCode = 'show_on_front_page_collision';
  9061.                 pageOnFrontId = parseInt( pageOnFront(), 10 );
  9062.                 pageForPostsId = parseInt( pageForPosts(), 10 );
  9063.  
  9064.                 if ( 'page' === showOnFront() ) {
  9065.  
  9066.                     // Change previewed URL to the homepage when changing the page_on_front.
  9067.                     if ( setting === pageOnFront && pageOnFrontId > 0 ) {
  9068.                         api.previewer.previewUrl.set( api.settings.url.home );
  9069.                     }
  9070.  
  9071.                     // Change the previewed URL to the selected page when changing the page_for_posts.
  9072.                     if ( setting === pageForPosts && pageForPostsId > 0 ) {
  9073.                         api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageForPostsId );
  9074.                     }
  9075.                 }
  9076.  
  9077.                 // Toggle notification when the homepage and posts page are both set and the same.
  9078.                 if ( 'page' === showOnFront() && pageOnFrontId && pageForPostsId && pageOnFrontId === pageForPostsId ) {
  9079.                     showOnFront.notifications.add( new api.Notification( errorCode, {
  9080.                         type: 'error',
  9081.                         message: api.l10n.pageOnFrontError
  9082.                     } ) );
  9083.                 } else {
  9084.                     showOnFront.notifications.remove( errorCode );
  9085.                 }
  9086.             };
  9087.             showOnFront.bind( handleChange );
  9088.             pageOnFront.bind( handleChange );
  9089.             pageForPosts.bind( handleChange );
  9090.             handleChange.call( showOnFront, showOnFront() ); // Make sure initial notification is added after loading existing changeset.
  9091.  
  9092.             // Move notifications container to the bottom.
  9093.             api.control( 'show_on_front', function( showOnFrontControl ) {
  9094.                 showOnFrontControl.deferred.embedded.done( function() {
  9095.                     showOnFrontControl.container.append( showOnFrontControl.getNotificationsContainerElement() );
  9096.                 });
  9097.             });
  9098.         });
  9099.  
  9100.         // Add code editor for Custom CSS.
  9101.         (function() {
  9102.             var sectionReady = $.Deferred();
  9103.  
  9104.             api.section( 'custom_css', function( section ) {
  9105.                 section.deferred.embedded.done( function() {
  9106.                     if ( section.expanded() ) {
  9107.                         sectionReady.resolve( section );
  9108.                     } else {
  9109.                         section.expanded.bind( function( isExpanded ) {
  9110.                             if ( isExpanded ) {
  9111.                                 sectionReady.resolve( section );
  9112.                             }
  9113.                         } );
  9114.                     }
  9115.                 });
  9116.             });
  9117.  
  9118.             // Set up the section description behaviors.
  9119.             sectionReady.done( function setupSectionDescription( section ) {
  9120.                 var control = api.control( 'custom_css' );
  9121.  
  9122.                 // Hide redundant label for visual users.
  9123.                 control.container.find( '.customize-control-title:first' ).addClass( 'screen-reader-text' );
  9124.  
  9125.                 // Close the section description when clicking the close button.
  9126.                 section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() {
  9127.                     section.container.find( '.section-meta .customize-section-description:first' )
  9128.                         .removeClass( 'open' )
  9129.                         .slideUp();
  9130.  
  9131.                     section.container.find( '.customize-help-toggle' )
  9132.                         .attr( 'aria-expanded', 'false' )
  9133.                         .focus(); // Avoid focus loss.
  9134.                 });
  9135.  
  9136.                 // Reveal help text if setting is empty.
  9137.                 if ( control && ! control.setting.get() ) {
  9138.                     section.container.find( '.section-meta .customize-section-description:first' )
  9139.                         .addClass( 'open' )
  9140.                         .show()
  9141.                         .trigger( 'toggled' );
  9142.  
  9143.                     section.container.find( '.customize-help-toggle' ).attr( 'aria-expanded', 'true' );
  9144.                 }
  9145.             });
  9146.         })();
  9147.  
  9148.         // Toggle visibility of Header Video notice when active state change.
  9149.         api.control( 'header_video', function( headerVideoControl ) {
  9150.             headerVideoControl.deferred.embedded.done( function() {
  9151.                 var toggleNotice = function() {
  9152.                     var section = api.section( headerVideoControl.section() ), noticeCode = 'video_header_not_available';
  9153.                     if ( ! section ) {
  9154.                         return;
  9155.                     }
  9156.                     if ( headerVideoControl.active.get() ) {
  9157.                         section.notifications.remove( noticeCode );
  9158.                     } else {
  9159.                         section.notifications.add( new api.Notification( noticeCode, {
  9160.                             type: 'info',
  9161.                             message: api.l10n.videoHeaderNotice
  9162.                         } ) );
  9163.                     }
  9164.                 };
  9165.                 toggleNotice();
  9166.                 headerVideoControl.active.bind( toggleNotice );
  9167.             } );
  9168.         } );
  9169.  
  9170.         // Update the setting validities.
  9171.         api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
  9172.             api._handleSettingValidities( {
  9173.                 settingValidities: settingValidities,
  9174.                 focusInvalidControl: false
  9175.             } );
  9176.         } );
  9177.  
  9178.         // Focus on the control that is associated with the given setting.
  9179.         api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
  9180.             var matchedControls = [];
  9181.             api.control.each( function( control ) {
  9182.                 var settingIds = _.pluck( control.settings, 'id' );
  9183.                 if ( -1 !== _.indexOf( settingIds, settingId ) ) {
  9184.                     matchedControls.push( control );
  9185.                 }
  9186.             } );
  9187.  
  9188.             // Focus on the matched control with the lowest priority (appearing higher).
  9189.             if ( matchedControls.length ) {
  9190.                 matchedControls.sort( function( a, b ) {
  9191.                     return a.priority() - b.priority();
  9192.                 } );
  9193.                 matchedControls[0].focus();
  9194.             }
  9195.         } );
  9196.  
  9197.         // Refresh the preview when it requests.
  9198.         api.previewer.bind( 'refresh', function() {
  9199.             api.previewer.refresh();
  9200.         });
  9201.  
  9202.         // Update the edit shortcut visibility state.
  9203.         api.state( 'paneVisible' ).bind( function( isPaneVisible ) {
  9204.             var isMobileScreen;
  9205.             if ( window.matchMedia ) {
  9206.                 isMobileScreen = window.matchMedia( 'screen and ( max-width: 640px )' ).matches;
  9207.             } else {
  9208.                 isMobileScreen = $( window ).width() <= 640;
  9209.             }
  9210.             api.state( 'editShortcutVisibility' ).set( isPaneVisible || isMobileScreen ? 'visible' : 'hidden' );
  9211.         } );
  9212.         if ( window.matchMedia ) {
  9213.             window.matchMedia( 'screen and ( max-width: 640px )' ).addListener( function() {
  9214.                 var state = api.state( 'paneVisible' );
  9215.                 state.callbacks.fireWith( state, [ state.get(), state.get() ] );
  9216.             } );
  9217.         }
  9218.         api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) {
  9219.             api.state( 'editShortcutVisibility' ).set( visibility );
  9220.         } );
  9221.         api.state( 'editShortcutVisibility' ).bind( function( visibility ) {
  9222.             api.previewer.send( 'edit-shortcut-visibility', visibility );
  9223.         } );
  9224.  
  9225.         // Autosave changeset.
  9226.         function startAutosaving() {
  9227.             var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
  9228.  
  9229.             api.unbind( 'change', startAutosaving ); // Ensure startAutosaving only fires once.
  9230.  
  9231.             function onChangeSaved( isSaved ) {
  9232.                 if ( ! isSaved && ! api.settings.changeset.autosaved ) {
  9233.                     api.settings.changeset.autosaved = true; // Once a change is made then autosaving kicks in.
  9234.                     api.previewer.send( 'autosaving' );
  9235.                 }
  9236.             }
  9237.             api.state( 'saved' ).bind( onChangeSaved );
  9238.             onChangeSaved( api.state( 'saved' ).get() );
  9239.  
  9240.             /**
  9241.              * Request changeset update and then re-schedule the next changeset update time.
  9242.              *
  9243.              * @since 4.7.0
  9244.              * @private
  9245.              */
  9246.             updateChangesetWithReschedule = function() {
  9247.                 if ( ! updatePending ) {
  9248.                     updatePending = true;
  9249.                     api.requestChangesetUpdate( {}, { autosave: true } ).always( function() {
  9250.                         updatePending = false;
  9251.                     } );
  9252.                 }
  9253.                 scheduleChangesetUpdate();
  9254.             };
  9255.  
  9256.             /**
  9257.              * Schedule changeset update.
  9258.              *
  9259.              * @since 4.7.0
  9260.              * @private
  9261.              */
  9262.             scheduleChangesetUpdate = function() {
  9263.                 clearTimeout( timeoutId );
  9264.                 timeoutId = setTimeout( function() {
  9265.                     updateChangesetWithReschedule();
  9266.                 }, api.settings.timeouts.changesetAutoSave );
  9267.             };
  9268.  
  9269.             // Start auto-save interval for updating changeset.
  9270.             scheduleChangesetUpdate();
  9271.  
  9272.             // Save changeset when focus removed from window.
  9273.             $( document ).on( 'visibilitychange.wp-customize-changeset-update', function() {
  9274.                 if ( document.hidden ) {
  9275.                     updateChangesetWithReschedule();
  9276.                 }
  9277.             } );
  9278.  
  9279.             // Save changeset before unloading window.
  9280.             $( window ).on( 'beforeunload.wp-customize-changeset-update', function() {
  9281.                 updateChangesetWithReschedule();
  9282.             } );
  9283.         }
  9284.         api.bind( 'change', startAutosaving );
  9285.  
  9286.         // Make sure TinyMCE dialogs appear above Customizer UI.
  9287.         $( document ).one( 'wp-before-tinymce-init', function() {
  9288.             if ( ! window.tinymce.ui.FloatPanel.zIndex || window.tinymce.ui.FloatPanel.zIndex < 500001 ) {
  9289.                 window.tinymce.ui.FloatPanel.zIndex = 500001;
  9290.             }
  9291.         } );
  9292.  
  9293.         body.addClass( 'ready' );
  9294.         api.trigger( 'ready' );
  9295.     });
  9296.  
  9297. })( wp, jQuery );
  9298.