[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> customize-controls.js (source)

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


Generated : Wed Sep 23 08:20:35 2026 Cross-referenced by PHPXref