[ 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' )
2601                      .addClass( 'disabled' )
2602                      .attr( 'aria-disabled', true );
2603              }
2604              if ( ! this.getPreviousTheme() ) {
2605                  this.overlay.find( '.left' )
2606                      .addClass( 'disabled' )
2607                      .attr( 'aria-disabled', true );
2608              }
2609          },
2610  
2611          /**
2612           * Load theme preview.
2613           *
2614           * @since 4.7.0
2615           * @access public
2616           *
2617           * @deprecated
2618           * @param {string} themeId Theme ID.
2619           * @return {JQuery.Promise<*>} Promise.
2620           */
2621          loadThemePreview: function( themeId ) {
2622              return api.ThemesPanel.prototype.loadThemePreview.call( this, themeId );
2623          },
2624  
2625          /**
2626           * Render & show the theme details for a given theme model.
2627           *
2628           * @since 4.2.0
2629           *
2630           * @param {Object}   theme      Theme.
2631           * @param {Function} [callback] Callback once the details have been shown.
2632           * @return {void}
2633           */
2634          showDetails: function ( theme, callback ) {
2635              var section = this, panel = api.panel( 'themes' );
2636              section.currentTheme = theme.id;
2637              section.overlay.html( section.template( theme ) )
2638                  .fadeIn( 'fast' )
2639                  .focus();
2640  
2641              /**
2642               * Disables the switch buttons if the theme cannot be switched to.
2643               *
2644               * @return {boolean} True if the switch buttons should be disabled, false otherwise.
2645               */
2646  			function disableSwitchButtons() {
2647                  return ! panel.canSwitchTheme( theme.id );
2648              }
2649  
2650              // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
2651              /**
2652               * Disables the install buttons if the theme cannot be installed or if filesystem credentials are needed.
2653               *
2654               * @return {boolean} True if the install buttons should be disabled, false otherwise.
2655               */
2656  			function disableInstallButtons() {
2657                  return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
2658              }
2659  
2660              section.overlay.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
2661              section.overlay.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
2662  
2663              section.$body.addClass( 'modal-open' );
2664              section.containFocus( section.overlay );
2665              section.updateLimits();
2666  
2667              section.announceThemeDebounced( theme.name );
2668              if ( callback ) {
2669                  callback();
2670              }
2671          },
2672  
2673          /**
2674           * Close the theme details modal.
2675           *
2676           * @since 4.2.0
2677           *
2678           * @return {void}
2679           */
2680          closeDetails: function () {
2681              var section = this;
2682              section.$body.removeClass( 'modal-open' );
2683              section.overlay.fadeOut( 'fast' );
2684              api.control( section.params.action + '_theme_' + section.currentTheme ).container.find( '.theme' ).focus();
2685              // Cancel any pending navigation announcement.
2686              section.announceThemeDebounced.cancel();
2687          },
2688  
2689          /**
2690           * Keep tab focus within the theme details modal.
2691           *
2692           * @since 4.2.0
2693           *
2694           * @param {JQuery} el Element to contain focus.
2695           * @return {void}
2696           */
2697          containFocus: function( el ) {
2698              var tabbables;
2699  
2700              el.on( 'keydown', function( event ) {
2701  
2702                  // Return if it's not the tab key
2703                  // When navigating with prev/next focus is already handled.
2704                  if ( 9 !== event.keyCode ) {
2705                      return;
2706                  }
2707  
2708                  // Uses jQuery UI to get the tabbable elements.
2709                  tabbables = $( ':tabbable', el );
2710  
2711                  // Keep focus within the overlay.
2712                  if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
2713                      tabbables.first().focus();
2714                      return false;
2715                  } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
2716                      tabbables.last().focus();
2717                      return false;
2718                  }
2719              });
2720          }
2721      });
2722  
2723      api.OuterSection = api.Section.extend(/** @lends wp.customize.OuterSection.prototype */{
2724  
2725          /**
2726           * Class wp.customize.OuterSection.
2727           *
2728           * Creates section outside of the sidebar, there is no ui to trigger collapse/expand so
2729           * it would require custom handling.
2730           *
2731           * @constructs wp.customize.OuterSection
2732           * @augments   wp.customize.Section
2733           *
2734           * @since 4.9.0
2735           *
2736           * @param {string} id      The ID for the section.
2737           * @param {Object} options Options, as accepted by wp.customize.Section.
2738           * @return {void}
2739           */
2740          initialize: function( id, options ) {
2741              var section = this;
2742              section.containerParent = '#customize-outer-theme-controls';
2743              section.containerPaneParent = '.customize-outer-pane-parent';
2744              api.Section.prototype.initialize.call( section, id, options );
2745          },
2746  
2747          /**
2748           * Overrides api.Section.prototype.onChangeExpanded to prevent collapse/expand effect
2749           * on other sections and panels.
2750           *
2751           * @since 4.9.0
2752           *
2753           * @param {boolean}  expanded                The expanded state to transition to.
2754           * @param {Object}   [args]                  Args.
2755           * @param {boolean}  [args.unchanged]        Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
2756           * @param {Function} [args.completeCallback] Function to call when the slideUp/slideDown has completed.
2757           * @param {Object}   [args.duration]         The duration for the animation.
2758           */
2759          onChangeExpanded: function( expanded, args ) {
2760              var section = this,
2761                  container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
2762                  content = section.contentContainer,
2763                  backBtn = content.find( '.customize-section-back' ),
2764                  sectionTitle = section.headContainer.find( '.accordion-section-title button, .accordion-section-title[tabindex]' ).first(),
2765                  body = $( document.body ),
2766                  expand, panel;
2767  
2768              body.toggleClass( 'outer-section-open', expanded );
2769              section.container.toggleClass( 'open', expanded );
2770              section.container.removeClass( 'busy' );
2771              api.section.each( function( _section ) {
2772                  if ( 'outer' === _section.params.type && _section.id !== section.id ) {
2773                      _section.container.removeClass( 'open' );
2774                  }
2775              } );
2776  
2777              if ( expanded && ! content.hasClass( 'open' ) ) {
2778  
2779                  if ( args.unchanged ) {
2780                      expand = args.completeCallback;
2781                  } else {
2782                      expand = function() {
2783                          section._animateChangeExpanded( function() {
2784                              backBtn.attr( 'tabindex', '0' );
2785                              backBtn.trigger( 'focus' );
2786                              content.css( 'top', '' );
2787                              container.scrollTop( 0 );
2788  
2789                              if ( args.completeCallback ) {
2790                                  args.completeCallback();
2791                              }
2792                          } );
2793  
2794                          content.addClass( 'open' );
2795                      }.bind( this );
2796                  }
2797  
2798                  if ( section.panel() ) {
2799                      api.panel( section.panel() ).expand({
2800                          duration: args.duration,
2801                          completeCallback: expand
2802                      });
2803                  } else {
2804                      expand();
2805                  }
2806  
2807              } else if ( ! expanded && content.hasClass( 'open' ) ) {
2808                  if ( section.panel() ) {
2809                      panel = api.panel( section.panel() );
2810                      if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
2811                          panel.collapse();
2812                      }
2813                  }
2814                  section._animateChangeExpanded( function() {
2815                      backBtn.attr( 'tabindex', '-1' );
2816                      sectionTitle.trigger( 'focus' );
2817                      content.css( 'top', '' );
2818  
2819                      if ( args.completeCallback ) {
2820                          args.completeCallback();
2821                      }
2822                  } );
2823  
2824                  content.removeClass( 'open' );
2825  
2826              } else {
2827                  if ( args.completeCallback ) {
2828                      args.completeCallback();
2829                  }
2830              }
2831          }
2832      });
2833  
2834      api.Panel = Container.extend(/** @lends wp.customize.Panel.prototype */{
2835          containerType: 'panel',
2836  
2837          /**
2838           * Initializes a panel.
2839           *
2840           * @constructs wp.customize.Panel
2841           * @augments   wp.customize~Container
2842           *
2843           * @since 4.1.0
2844           *
2845           * @param {string}  id                     The ID for the panel.
2846           * @param {Object}  options                Object containing one property: params.
2847           * @param {string}  options.title          Title shown when panel is collapsed and expanded.
2848           * @param {string}  [options.description]  Description shown at the top of the panel.
2849           * @param {number}  [options.priority=100] The sort priority for the panel.
2850           * @param {string}  [options.type=default] The type of the panel. See wp.customize.panelConstructor.
2851           * @param {string}  [options.content]      The markup to be used for the panel container. If empty, a JS template is used.
2852           * @param {boolean} [options.active=true]  Whether the panel is active or not.
2853           * @param {Object}  [options.params]       Deprecated wrapper for the above properties.
2854           */
2855          initialize: function ( id, options ) {
2856              var panel = this, params;
2857              params = options.params || options;
2858  
2859              // Look up the type if one was not supplied.
2860              if ( ! params.type ) {
2861                  _.find( api.panelConstructor, function( Constructor, type ) {
2862                      if ( Constructor === panel.constructor ) {
2863                          params.type = type;
2864                          return true;
2865                      }
2866                      return false;
2867                  } );
2868              }
2869  
2870              Container.prototype.initialize.call( panel, id, params );
2871  
2872              panel.embed();
2873              panel.deferred.embedded.done( function () {
2874                  panel.ready();
2875              });
2876          },
2877  
2878          /**
2879           * Embed the container in the DOM when any parent panel is ready.
2880           *
2881           * @since 4.1.0
2882           */
2883          embed: function () {
2884              var panel = this,
2885                  container = $( '#customize-theme-controls' ),
2886                  parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable.
2887  
2888              if ( ! panel.headContainer.parent().is( parentContainer ) ) {
2889                  parentContainer.append( panel.headContainer );
2890              }
2891              if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
2892                  container.append( panel.contentContainer );
2893              }
2894              panel.renderContent();
2895  
2896              panel.deferred.embedded.resolve();
2897          },
2898  
2899          /**
2900           * Attaches events to the panel.
2901           *
2902           * @since 4.1.0
2903           */
2904          attachEvents: function () {
2905              var meta, panel = this;
2906  
2907              // Expand/Collapse accordion sections on click.
2908              panel.headContainer.find( '.accordion-section-title button, .accordion-section-title[tabindex]' ).on( 'click keydown', function( event ) {
2909                  if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2910                      return;
2911                  }
2912                  event.preventDefault(); // Keep this AFTER the key filter above.
2913  
2914                  if ( ! panel.expanded() ) {
2915                      panel.expand();
2916                  }
2917              });
2918  
2919              // Close panel.
2920              panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
2921                  if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2922                      return;
2923                  }
2924                  event.preventDefault(); // Keep this AFTER the key filter above.
2925  
2926                  if ( panel.expanded() ) {
2927                      panel.collapse();
2928                  }
2929              });
2930  
2931              meta = panel.container.find( '.panel-meta:first' );
2932  
2933              meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
2934                  if ( meta.hasClass( 'cannot-expand' ) ) {
2935                      return;
2936                  }
2937  
2938                  var content = meta.find( '.customize-panel-description:first' );
2939                  if ( meta.hasClass( 'open' ) ) {
2940                      meta.toggleClass( 'open' );
2941                      content.slideUp( panel.defaultExpandedArguments.duration, function() {
2942                          content.trigger( 'toggled' );
2943                      } );
2944                      $( this ).attr( 'aria-expanded', false );
2945                  } else {
2946                      content.slideDown( panel.defaultExpandedArguments.duration, function() {
2947                          content.trigger( 'toggled' );
2948                      } );
2949                      meta.toggleClass( 'open' );
2950                      $( this ).attr( 'aria-expanded', true );
2951                  }
2952              });
2953  
2954          },
2955  
2956          /**
2957           * Get the sections that are associated with this panel, sorted by their priority Value.
2958           *
2959           * @since 4.1.0
2960           *
2961           * @return {wp.customize.Section[]} Array of sections.
2962           */
2963          sections: function () {
2964              return this._children( 'panel', 'section' );
2965          },
2966  
2967          /**
2968           * Return whether this panel has any active sections.
2969           *
2970           * @since 4.1.0
2971           *
2972           * @return {boolean} Whether contextually active.
2973           */
2974          isContextuallyActive: function () {
2975              var panel = this,
2976                  sections = panel.sections(),
2977                  activeCount = 0;
2978              _( sections ).each( function ( section ) {
2979                  if ( section.active() && section.isContextuallyActive() ) {
2980                      activeCount += 1;
2981                  }
2982              } );
2983              return ( activeCount !== 0 );
2984          },
2985  
2986          /**
2987           * Update UI to reflect expanded state.
2988           *
2989           * @since 4.1.0
2990           *
2991           * @param {boolean}  expanded              The expanded state to transition to.
2992           * @param {Object}   args                  Object containing options for expand/collapse.
2993           * @param {boolean}  args.unchanged        Whether the expanded state is unchanged.
2994           * @param {Function} args.completeCallback Callback to be executed once the expand/collapse action is complete.
2995           * @return {void}
2996           */
2997          onChangeExpanded: function ( expanded, args ) {
2998  
2999              // Immediately call the complete callback if there were no changes.
3000              if ( args.unchanged ) {
3001                  if ( args.completeCallback ) {
3002                      args.completeCallback();
3003                  }
3004                  return;
3005              }
3006  
3007              // Note: there is a second argument 'args' passed.
3008              var panel = this,
3009                  accordionSection = panel.contentContainer,
3010                  overlay = accordionSection.closest( '.wp-full-overlay' ),
3011                  container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
3012                  topPanel = panel.headContainer.find( '.accordion-section-title button, .accordion-section-title[tabindex]' ),
3013                  backBtn = accordionSection.find( '.customize-panel-back' ),
3014                  childSections = panel.sections(),
3015                  skipTransition;
3016  
3017              if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) {
3018                  // Collapse any sibling sections/panels.
3019                  api.section.each( function ( section ) {
3020                      if ( panel.id !== section.panel() ) {
3021                          section.collapse( { duration: 0 } );
3022                      }
3023                  });
3024                  api.panel.each( function ( otherPanel ) {
3025                      if ( panel !== otherPanel ) {
3026                          otherPanel.collapse( { duration: 0 } );
3027                      }
3028                  });
3029  
3030                  if ( panel.params.autoExpandSoleSection && 1 === childSections.length && childSections[0].active.get() ) {
3031                      accordionSection.addClass( 'current-panel skip-transition' );
3032                      overlay.addClass( 'in-sub-panel' );
3033  
3034                      childSections[0].expand( {
3035                          completeCallback: args.completeCallback
3036                      } );
3037                  } else {
3038                      panel._animateChangeExpanded( function() {
3039                          backBtn.attr( 'tabindex', '0' );
3040                          backBtn.trigger( 'focus' );
3041                          accordionSection.css( 'top', '' );
3042                          container.scrollTop( 0 );
3043  
3044                          if ( args.completeCallback ) {
3045                              args.completeCallback();
3046                          }
3047                      } );
3048  
3049                      accordionSection.addClass( 'current-panel' );
3050                      overlay.addClass( 'in-sub-panel' );
3051                  }
3052  
3053                  api.state( 'expandedPanel' ).set( panel );
3054  
3055              } else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) {
3056                  skipTransition = accordionSection.hasClass( 'skip-transition' );
3057                  if ( ! skipTransition ) {
3058                      panel._animateChangeExpanded( function() {
3059  
3060                          topPanel.focus();
3061                          accordionSection.css( 'top', '' );
3062  
3063                          if ( args.completeCallback ) {
3064                              args.completeCallback();
3065                          }
3066                      } );
3067                  } else {
3068                      accordionSection.removeClass( 'skip-transition' );
3069                  }
3070  
3071                  overlay.removeClass( 'in-sub-panel' );
3072                  accordionSection.removeClass( 'current-panel' );
3073                  if ( panel === api.state( 'expandedPanel' ).get() ) {
3074                      api.state( 'expandedPanel' ).set( false );
3075                  }
3076              }
3077          },
3078  
3079          /**
3080           * Render the panel from its JS template, if it exists.
3081           *
3082           * The panel's container must already exist in the DOM.
3083           *
3084           * @since 4.3.0
3085           */
3086          renderContent: function () {
3087              var template,
3088                  panel = this;
3089  
3090              // Add the content to the container.
3091              if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
3092                  template = wp.template( panel.templateSelector + '-content' );
3093              } else {
3094                  template = wp.template( 'customize-panel-default-content' );
3095              }
3096              if ( template && panel.headContainer ) {
3097                  panel.contentContainer.html( template( _.extend(
3098                      { id: panel.id },
3099                      panel.params
3100                  ) ) );
3101              }
3102          }
3103      });
3104  
3105      api.ThemesPanel = api.Panel.extend(/** @lends wp.customize.ThemesPanel.prototype */{
3106  
3107          /**
3108           * Class wp.customize.ThemesPanel.
3109           *
3110           * Custom section for themes that displays without the customize preview.
3111           *
3112           * @constructs wp.customize.ThemesPanel
3113           * @augments   wp.customize.Panel
3114           *
3115           * @since 4.9.0
3116           *
3117           * @param {string} id      The ID for the panel.
3118           * @param {Object} options Options.
3119           * @return {void}
3120           */
3121          initialize: function( id, options ) {
3122              var panel = this;
3123              panel.installingThemes = [];
3124              api.Panel.prototype.initialize.call( panel, id, options );
3125          },
3126  
3127          /**
3128           * Determine whether a given theme can be switched to, or in general.
3129           *
3130           * @since 4.9.0
3131           *
3132           * @param {string} [slug] Theme slug. When omitted, whether switching is possible at all.
3133           * @return {boolean} Whether the theme can be switched to.
3134           */
3135          canSwitchTheme: function canSwitchTheme( slug ) {
3136              if ( slug && slug === api.settings.theme.stylesheet ) {
3137                  return true;
3138              }
3139              return 'publish' === api.state( 'selectedChangesetStatus' ).get() && ( '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get() );
3140          },
3141  
3142          /**
3143           * Attach events.
3144           *
3145           * @since 4.9.0
3146           * @return {void}
3147           */
3148          attachEvents: function() {
3149              var panel = this;
3150  
3151              // Attach regular panel events.
3152              api.Panel.prototype.attachEvents.apply( panel );
3153  
3154              // Temporary since supplying SFTP credentials does not work yet. See #42184.
3155              if ( api.settings.theme._canInstall && api.settings.theme._filesystemCredentialsNeeded ) {
3156                  panel.notifications.add( new api.Notification( 'theme_install_unavailable', {
3157                      message: api.l10n.themeInstallUnavailable,
3158                      type: 'info',
3159                      dismissible: true
3160                  } ) );
3161              }
3162  
3163              /**
3164               * Toggles the theme switch unavailable notification based on whether switching is possible.
3165               */
3166  			function toggleDisabledNotifications() {
3167                  if ( panel.canSwitchTheme() ) {
3168                      panel.notifications.remove( 'theme_switch_unavailable' );
3169                  } else {
3170                      panel.notifications.add( new api.Notification( 'theme_switch_unavailable', {
3171                          message: api.l10n.themePreviewUnavailable,
3172                          type: 'warning'
3173                      } ) );
3174                  }
3175              }
3176              toggleDisabledNotifications();
3177              api.state( 'selectedChangesetStatus' ).bind( toggleDisabledNotifications );
3178              api.state( 'changesetStatus' ).bind( toggleDisabledNotifications );
3179  
3180              // Collapse panel to customize the current theme.
3181              panel.contentContainer.on( 'click', '.customize-theme', function() {
3182                  panel.collapse();
3183              });
3184  
3185              // Toggle between filtering and browsing themes on mobile.
3186              panel.contentContainer.on( 'click', '.customize-themes-section-title, .customize-themes-mobile-back', function() {
3187                  $( '.wp-full-overlay' ).toggleClass( 'showing-themes' );
3188              });
3189  
3190              // Install (and maybe preview) a theme.
3191              panel.contentContainer.on( 'click', '.theme-install', function( event ) {
3192                  panel.installTheme( event );
3193              });
3194  
3195              // Update a theme. Theme cards have the class, the details modal has the id.
3196              panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) {
3197  
3198                  // #update-theme is a link.
3199                  event.preventDefault();
3200                  event.stopPropagation();
3201  
3202                  panel.updateTheme( event );
3203              });
3204  
3205              // Delete a theme.
3206              panel.contentContainer.on( 'click', '.delete-theme', function( event ) {
3207                  panel.deleteTheme( event );
3208              });
3209  
3210              _.bindAll( panel, 'installTheme', 'updateTheme' );
3211          },
3212  
3213          /**
3214           * Update UI to reflect expanded state
3215           *
3216           * @since 4.9.0
3217           *
3218           * @param {boolean}  expanded              Expanded state.
3219           * @param {Object}   args                  Args.
3220           * @param {boolean}  args.unchanged        Whether or not the state changed.
3221           * @param {Function} args.completeCallback Callback to execute when the animation completes.
3222           * @return {void}
3223           */
3224          onChangeExpanded: function( expanded, args ) {
3225              var panel = this, overlay, sections, hasExpandedSection = false;
3226  
3227              // Expand/collapse the panel normally.
3228              api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] );
3229  
3230              // Immediately call the complete callback if there were no changes.
3231              if ( args.unchanged ) {
3232                  if ( args.completeCallback ) {
3233                      args.completeCallback();
3234                  }
3235                  return;
3236              }
3237  
3238              overlay = panel.headContainer.closest( '.wp-full-overlay' );
3239  
3240              if ( expanded ) {
3241                  overlay
3242                      .addClass( 'in-themes-panel' )
3243                      .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' );
3244  
3245                  _.delay( function() {
3246                      overlay.addClass( 'themes-panel-expanded' );
3247                  }, 200 );
3248  
3249                  // Automatically open the first section (except on small screens), if one isn't already expanded.
3250                  if ( 600 < window.innerWidth ) {
3251                      sections = panel.sections();
3252                      _.each( sections, function( section ) {
3253                          if ( section.expanded() ) {
3254                              hasExpandedSection = true;
3255                          }
3256                      } );
3257                      if ( ! hasExpandedSection && sections.length > 0 ) {
3258                          sections[0].expand();
3259                      }
3260                  }
3261              } else {
3262                  overlay
3263                      .removeClass( 'in-themes-panel themes-panel-expanded' )
3264                      .find( '.customize-themes-full-container' ).removeClass( 'animate' );
3265              }
3266          },
3267  
3268          /**
3269           * Install a theme via wp.updates.
3270           *
3271           * @since 4.9.0
3272           *
3273           * @param {JQuery.Event} event Event.
3274           * @return {JQuery.Promise<*>} Promise.
3275           */
3276          installTheme: function( event ) {
3277              var panel = this, preview, onInstallSuccess, slug = $( event.target ).data( 'slug' ), deferred = $.Deferred(), request;
3278              preview = $( event.target ).hasClass( 'preview' );
3279  
3280              // Temporary since supplying SFTP credentials does not work yet. See #42184.
3281              if ( api.settings.theme._filesystemCredentialsNeeded ) {
3282                  deferred.reject({
3283                      errorCode: 'theme_install_unavailable'
3284                  });
3285                  return deferred.promise();
3286              }
3287  
3288              // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
3289              if ( ! panel.canSwitchTheme( slug ) ) {
3290                  deferred.reject({
3291                      errorCode: 'theme_switch_unavailable'
3292                  });
3293                  return deferred.promise();
3294              }
3295  
3296              // Theme is already being installed.
3297              if ( _.contains( panel.installingThemes, slug ) ) {
3298                  deferred.reject({
3299                      errorCode: 'theme_already_installing'
3300                  });
3301                  return deferred.promise();
3302              }
3303  
3304              wp.updates.maybeRequestFilesystemCredentials( event );
3305  
3306              onInstallSuccess = function( response ) {
3307                  var theme = false, themeControl;
3308                  if ( preview ) {
3309                      api.notifications.remove( 'theme_installing' );
3310  
3311                      panel.loadThemePreview( slug );
3312  
3313                  } else {
3314                      api.control.each( function( control ) {
3315                          if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
3316                              theme = control.params.theme; // Used below to add theme control.
3317                              control.rerenderAsInstalled( true );
3318                          }
3319                      });
3320  
3321                      // Don't add the same theme more than once.
3322                      if ( ! theme || api.control.has( 'installed_theme_' + theme.id ) ) {
3323                          deferred.resolve( response );
3324                          return;
3325                      }
3326  
3327                      // Add theme control to installed section.
3328                      theme.type = 'installed';
3329                      themeControl = new api.controlConstructor.theme( 'installed_theme_' + theme.id, {
3330                          type: 'theme',
3331                          section: 'installed_themes',
3332                          theme: theme,
3333                          priority: 0 // Add all newly-installed themes to the top.
3334                      } );
3335  
3336                      api.control.add( themeControl );
3337                      api.control( themeControl.id ).container.trigger( 'render-screenshot' );
3338  
3339                      // Close the details modal if it's open to the installed theme.
3340                      api.section.each( function( section ) {
3341                          if ( 'themes' === section.params.type ) {
3342                              if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
3343                                  section.closeDetails();
3344                              }
3345                          }
3346                      });
3347                  }
3348                  deferred.resolve( response );
3349              };
3350  
3351              panel.installingThemes.push( slug ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
3352              request = wp.updates.installTheme( {
3353                  slug: slug
3354              } );
3355  
3356              // Also preview the theme as the event is triggered on Install & Preview.
3357              if ( preview ) {
3358                  api.notifications.add( new api.OverlayNotification( 'theme_installing', {
3359                      message: api.l10n.themeDownloading,
3360                      type: 'info',
3361                      loading: true
3362                  } ) );
3363              }
3364  
3365              request.done( onInstallSuccess );
3366              request.fail( function() {
3367                  api.notifications.remove( 'theme_installing' );
3368              } );
3369  
3370              return deferred.promise();
3371          },
3372  
3373          /**
3374           * Load theme preview.
3375           *
3376           * @since 4.9.0
3377           *
3378           * @param {string} themeId Theme ID.
3379           * @return {JQuery.Promise<*>} Promise.
3380           */
3381          loadThemePreview: function( themeId ) {
3382              var panel = this, deferred = $.Deferred(), onceProcessingComplete, urlParser, queryParams;
3383  
3384              // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
3385              if ( ! panel.canSwitchTheme( themeId ) ) {
3386                  deferred.reject({
3387                      errorCode: 'theme_switch_unavailable'
3388                  });
3389                  return deferred.promise();
3390              }
3391  
3392              urlParser = document.createElement( 'a' );
3393              urlParser.href = location.href;
3394              queryParams = _.extend(
3395                  api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
3396                  {
3397                      theme: themeId,
3398                      changeset_uuid: api.settings.changeset.uuid,
3399                      'return': api.settings.url['return']
3400                  }
3401              );
3402  
3403              // Include autosaved param to load autosave revision without prompting user to restore it.
3404              if ( ! api.state( 'saved' ).get() ) {
3405                  queryParams.customize_autosaved = 'on';
3406              }
3407  
3408              urlParser.search = $.param( queryParams );
3409  
3410              // Update loading message. Everything else is handled by reloading the page.
3411              api.notifications.add( new api.OverlayNotification( 'theme_previewing', {
3412                  message: api.l10n.themePreviewWait,
3413                  type: 'info',
3414                  loading: true
3415              } ) );
3416  
3417              onceProcessingComplete = function() {
3418                  var request;
3419                  if ( api.state( 'processing' ).get() > 0 ) {
3420                      return;
3421                  }
3422  
3423                  api.state( 'processing' ).unbind( onceProcessingComplete );
3424  
3425                  request = api.requestChangesetUpdate( {}, { autosave: true } );
3426                  request.done( function() {
3427                      deferred.resolve();
3428                      $( window ).off( 'beforeunload.customize-confirm' );
3429                      location.replace( urlParser.href );
3430                  } );
3431                  request.fail( function() {
3432  
3433                      // @todo Show notification regarding failure.
3434                      api.notifications.remove( 'theme_previewing' );
3435  
3436                      deferred.reject();
3437                  } );
3438              };
3439  
3440              if ( 0 === api.state( 'processing' ).get() ) {
3441                  onceProcessingComplete();
3442              } else {
3443                  api.state( 'processing' ).bind( onceProcessingComplete );
3444              }
3445  
3446              return deferred.promise();
3447          },
3448  
3449          /**
3450           * Update a theme via wp.updates.
3451           *
3452           * @since 4.9.0
3453           *
3454           * @param {JQuery.Event} event Event.
3455           * @return {void}
3456           */
3457          updateTheme: function( event ) {
3458              wp.updates.maybeRequestFilesystemCredentials( event );
3459  
3460              $( document ).one( 'wp-theme-update-success', function( e, response ) {
3461  
3462                  // Rerender the control to reflect the update.
3463                  api.control.each( function( control ) {
3464                      if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
3465                          control.params.theme.hasUpdate = false;
3466                          control.params.theme.version = response.newVersion;
3467                          setTimeout( function() {
3468                              control.rerenderAsInstalled( true );
3469                          }, 2000 );
3470                      }
3471                  });
3472              } );
3473  
3474              wp.updates.updateTheme( {
3475                  slug: $( event.target ).closest( '.notice' ).data( 'slug' )
3476              } );
3477          },
3478  
3479          /**
3480           * Delete a theme via wp.updates.
3481           *
3482           * @since 4.9.0
3483           *
3484           * @param {JQuery.Event} event Event.
3485           * @return {void}
3486           */
3487          deleteTheme: function( event ) {
3488              var theme, section;
3489              theme = $( event.target ).data( 'slug' );
3490              section = api.section( 'installed_themes' );
3491  
3492              event.preventDefault();
3493  
3494              // Temporary since supplying SFTP credentials does not work yet. See #42184.
3495              if ( api.settings.theme._filesystemCredentialsNeeded ) {
3496                  return;
3497              }
3498  
3499              // Confirmation dialog for deleting a theme.
3500              if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) {
3501                  return;
3502              }
3503  
3504              wp.updates.maybeRequestFilesystemCredentials( event );
3505  
3506              $( document ).one( 'wp-theme-delete-success', function() {
3507                  var control = api.control( 'installed_theme_' + theme );
3508  
3509                  // Remove theme control.
3510                  control.container.remove();
3511                  api.control.remove( control.id );
3512  
3513                  // Update installed count.
3514                  section.loaded = section.loaded - 1;
3515                  section.updateCount();
3516  
3517                  // Rerender any other theme controls as uninstalled.
3518                  api.control.each( function( control ) {
3519                      if ( 'theme' === control.params.type && control.params.theme.id === theme ) {
3520                          control.rerenderAsInstalled( false );
3521                      }
3522                  });
3523              } );
3524  
3525              wp.updates.deleteTheme( {
3526                  slug: theme
3527              } );
3528  
3529              // Close modal and focus the section.
3530              section.closeDetails();
3531              section.focus();
3532          }
3533      });
3534  
3535      api.Control = api.Class.extend(/** @lends wp.customize.Control.prototype */{
3536          defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
3537  
3538          /**
3539           * Default params.
3540           *
3541           * @since 4.9.0
3542           * @member {Object}
3543           */
3544          defaults: {
3545              label: '',
3546              description: '',
3547              active: true,
3548              priority: 10
3549          },
3550  
3551          /**
3552           * A Customizer Control.
3553           *
3554           * A control provides a UI element that allows a user to modify a Customizer Setting.
3555           *
3556           * @see PHP class WP_Customize_Control.
3557           *
3558           * @constructs wp.customize.Control
3559           * @augments   wp.customize.Class
3560           *
3561           * @borrows wp.customize~focus as this#focus
3562           * @borrows wp.customize~Container#activate as this#activate
3563           * @borrows wp.customize~Container#deactivate as this#deactivate
3564           * @borrows wp.customize~Container#_toggleActive as this#_toggleActive
3565           *
3566           * @param {string} id                       Unique identifier for the control instance.
3567           * @param {Object} options                  Options hash for the control instance.
3568           * @param {Object} options.type             Type of control (e.g. text, radio, dropdown-pages, etc.).
3569           * @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.
3570           * @param {string} [options.templateId]     Template ID for control's content.
3571           * @param {string} [options.priority=10]    Order of priority to show the control within the section.
3572           * @param {string} [options.active=true]    Whether the control is active.
3573           * @param {string} options.section          The ID of the section the control belongs to.
3574           * @param {*}      [options.setting]        The ID of the main setting or an instance of this setting.
3575           * @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.
3576           * @param {*}      options.settings.default The ID of the setting the control relates to.
3577           * @param {string} options.settings.data    @todo Is this used?
3578           * @param {string} options.label            Label.
3579           * @param {string} options.description      Description.
3580           * @param {number} [options.instanceNumber] Order in which this instance was created in relation to other instances.
3581           * @param {Object} [options.params]         Deprecated wrapper for the above properties.
3582           * @return {void}
3583           */
3584          initialize: function( id, options ) {
3585              var control = this, deferredSettingIds = [], settings, gatherSettings;
3586  
3587              control.params = _.extend(
3588                  {},
3589                  control.defaults,
3590                  control.params || {}, // In case subclass already defines.
3591                  options.params || options || {} // The options.params property is deprecated, but it is checked first for back-compat.
3592              );
3593  
3594              if ( ! api.Control.instanceCounter ) {
3595                  api.Control.instanceCounter = 0;
3596              }
3597              api.Control.instanceCounter++;
3598              if ( ! control.params.instanceNumber ) {
3599                  control.params.instanceNumber = api.Control.instanceCounter;
3600              }
3601  
3602              // Look up the type if one was not supplied.
3603              if ( ! control.params.type ) {
3604                  _.find( api.controlConstructor, function( Constructor, type ) {
3605                      if ( Constructor === control.constructor ) {
3606                          control.params.type = type;
3607                          return true;
3608                      }
3609                      return false;
3610                  } );
3611              }
3612  
3613              if ( ! control.params.content ) {
3614                  control.params.content = $( '<li></li>', {
3615                      id: 'customize-control-' + id.replace( /]/g, '' ).replace( /\[/g, '-' ),
3616                      'class': 'customize-control customize-control-' + control.params.type
3617                  } );
3618              }
3619  
3620              control.id = id;
3621              control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); // Deprecated, likely dead code from time before #28709.
3622              if ( control.params.content ) {
3623                  control.container = $( control.params.content );
3624              } else {
3625                  control.container = $( control.selector ); // Likely dead, per above. See #28709.
3626              }
3627  
3628              if ( control.params.templateId ) {
3629                  control.templateSelector = control.params.templateId;
3630              } else {
3631                  control.templateSelector = 'customize-control-' + control.params.type + '-content';
3632              }
3633  
3634              control.deferred = _.extend( control.deferred || {}, {
3635                  embedded: new $.Deferred()
3636              } );
3637              control.section = new api.Value();
3638              control.priority = new api.Value();
3639              control.active = new api.Value();
3640              control.activeArgumentsQueue = [];
3641              control.notifications = new api.Notifications({
3642                  alt: control.altNotice
3643              });
3644  
3645              control.elements = [];
3646  
3647              control.active.bind( function ( active ) {
3648                  var args = control.activeArgumentsQueue.shift();
3649                  args = $.extend( {}, control.defaultActiveArguments, args );
3650                  control.onChangeActive( active, args );
3651              } );
3652  
3653              control.section.set( control.params.section );
3654              control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
3655              control.active.set( control.params.active );
3656  
3657              api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
3658  
3659              control.settings = {};
3660  
3661              settings = {};
3662              if ( control.params.setting ) {
3663                  settings['default'] = control.params.setting;
3664              }
3665              _.extend( settings, control.params.settings );
3666  
3667              // Note: Settings can be an array or an object, with values being either setting IDs or Setting (or Value) objects.
3668              _.each( settings, function( value, key ) {
3669                  var setting;
3670                  if ( _.isObject( value ) && _.isFunction( value.extended ) && value.extended( api.Value ) ) {
3671                      control.settings[ key ] = value;
3672                  } else if ( _.isString( value ) ) {
3673                      setting = api( value );
3674                      if ( setting ) {
3675                          control.settings[ key ] = setting;
3676                      } else {
3677                          deferredSettingIds.push( value );
3678                      }
3679                  }
3680              } );
3681  
3682              gatherSettings = function() {
3683  
3684                  // Fill-in all resolved settings.
3685                  _.each( settings, function ( settingId, key ) {
3686                      if ( ! control.settings[ key ] && _.isString( settingId ) ) {
3687                          control.settings[ key ] = api( settingId );
3688                      }
3689                  } );
3690  
3691                  // Make sure settings passed as array gets associated with default.
3692                  if ( control.settings[0] && ! control.settings['default'] ) {
3693                      control.settings['default'] = control.settings[0];
3694                  }
3695  
3696                  // Identify the main setting.
3697                  control.setting = control.settings['default'] || null;
3698  
3699                  control.linkElements(); // Link initial elements present in server-rendered content.
3700                  control.embed();
3701              };
3702  
3703              if ( 0 === deferredSettingIds.length ) {
3704                  gatherSettings();
3705              } else {
3706                  api.apply( api, deferredSettingIds.concat( gatherSettings ) );
3707              }
3708  
3709              // After the control is embedded on the page, invoke the "ready" method.
3710              control.deferred.embedded.done( function () {
3711                  control.linkElements(); // Link any additional elements after template is rendered by renderContent().
3712                  control.setupNotifications();
3713                  control.ready();
3714              });
3715          },
3716  
3717          /**
3718           * Link elements between settings and inputs.
3719           *
3720           * @since 4.7.0
3721           * @access public
3722           *
3723           * @return {void}
3724           */
3725          linkElements: function () {
3726              var control = this, nodes, radios, element;
3727  
3728              nodes = control.container.find( '[data-customize-setting-link], [data-customize-setting-key-link]' );
3729              radios = {};
3730  
3731              nodes.each( function () {
3732                  var node = $( this ), name, setting;
3733  
3734                  if ( node.data( 'customizeSettingLinked' ) ) {
3735                      return;
3736                  }
3737                  node.data( 'customizeSettingLinked', true ); // Prevent re-linking element.
3738  
3739                  if ( node.is( ':radio' ) ) {
3740                      name = node.prop( 'name' );
3741                      if ( radios[name] ) {
3742                          return;
3743                      }
3744  
3745                      radios[name] = true;
3746                      node = nodes.filter( '[name="' + name + '"]' );
3747                  }
3748  
3749                  // Let link by default refer to setting ID. If it doesn't exist, fallback to looking up by setting key.
3750                  if ( node.data( 'customizeSettingLink' ) ) {
3751                      setting = api( node.data( 'customizeSettingLink' ) );
3752                  } else if ( node.data( 'customizeSettingKeyLink' ) ) {
3753                      setting = control.settings[ node.data( 'customizeSettingKeyLink' ) ];
3754                  }
3755  
3756                  if ( setting ) {
3757                      element = new api.Element( node );
3758                      control.elements.push( element );
3759                      element.sync( setting );
3760                      element.set( setting() );
3761                  }
3762              } );
3763          },
3764  
3765          /**
3766           * Embed the control into the page.
3767           */
3768          embed: function () {
3769              var control = this,
3770                  inject;
3771  
3772              // Watch for changes to the section state.
3773              inject = function ( sectionId ) {
3774                  var parentContainer;
3775                  if ( ! sectionId ) { // @todo Allow a control to be embedded without a section, for instance a control embedded in the front end.
3776                      return;
3777                  }
3778                  // Wait for the section to be registered.
3779                  api.section( sectionId, function ( section ) {
3780                      // Wait for the section to be ready/initialized.
3781                      section.deferred.embedded.done( function () {
3782                          parentContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
3783                          if ( ! control.container.parent().is( parentContainer ) ) {
3784                              parentContainer.append( control.container );
3785                          }
3786                          control.renderContent();
3787                          control.deferred.embedded.resolve();
3788                      });
3789                  });
3790              };
3791              control.section.bind( inject );
3792              inject( control.section.get() );
3793          },
3794  
3795          /**
3796           * Triggered when the control's markup has been injected into the DOM.
3797           *
3798           * @return {void}
3799           */
3800          ready: function() {
3801              var control = this, newItem;
3802              if ( 'dropdown-pages' === control.params.type && control.params.allow_addition ) {
3803                  newItem = control.container.find( '.new-content-item-wrapper' );
3804                  newItem.hide(); // Hide in JS to preserve flex display when showing.
3805                  control.container.on( 'click', '.add-new-toggle', function( e ) {
3806                      $( e.currentTarget ).slideUp( 180 );
3807                      newItem.slideDown( 180 );
3808                      newItem.find( '.create-item-input' ).focus();
3809                  });
3810                  control.container.on( 'click', '.add-content', function() {
3811                      control.addNewPage();
3812                  });
3813                  control.container.on( 'keydown', '.create-item-input', function( e ) {
3814                      if ( 13 === e.which ) { // Enter.
3815                          control.addNewPage();
3816                      }
3817                  });
3818              }
3819          },
3820  
3821          /**
3822           * Get the element inside of a control's container that contains the validation error message.
3823           *
3824           * Control subclasses may override this to return the proper container to render notifications into.
3825           * Injects the notification container for existing controls that lack the necessary container,
3826           * including special handling for nav menu items and widgets.
3827           *
3828           * @since 4.6.0
3829           * @return {JQuery} Setting validation message element.
3830           */
3831          getNotificationsContainerElement: function() {
3832              var control = this, controlTitle, notificationsContainer;
3833  
3834              notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
3835              if ( notificationsContainer.length ) {
3836                  return notificationsContainer;
3837              }
3838  
3839              notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
3840  
3841              if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
3842                  control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
3843              } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
3844                  control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
3845              } else {
3846                  controlTitle = control.container.find( '.customize-control-title' );
3847                  if ( controlTitle.length ) {
3848                      controlTitle.after( notificationsContainer );
3849                  } else {
3850                      control.container.prepend( notificationsContainer );
3851                  }
3852              }
3853              return notificationsContainer;
3854          },
3855  
3856          /**
3857           * Set up notifications.
3858           *
3859           * @since 4.9.0
3860           * @return {void}
3861           */
3862          setupNotifications: function() {
3863              var control = this, renderNotificationsIfVisible, onSectionAssigned;
3864  
3865              // Add setting notifications to the control notification.
3866              _.each( control.settings, function( setting ) {
3867                  if ( ! setting.notifications ) {
3868                      return;
3869                  }
3870                  setting.notifications.bind( 'add', function( settingNotification ) {
3871                      var params = _.extend(
3872                          {},
3873                          settingNotification,
3874                          {
3875                              setting: setting.id
3876                          }
3877                      );
3878                      control.notifications.add( new api.Notification( setting.id + ':' + settingNotification.code, params ) );
3879                  } );
3880                  setting.notifications.bind( 'remove', function( settingNotification ) {
3881                      control.notifications.remove( setting.id + ':' + settingNotification.code );
3882                  } );
3883              } );
3884  
3885              renderNotificationsIfVisible = function() {
3886                  var sectionId = control.section();
3887                  if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
3888                      control.notifications.render();
3889                  }
3890              };
3891  
3892              control.notifications.bind( 'rendered', function() {
3893                  var notifications = control.notifications.get();
3894                  control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
3895                  control.container.toggleClass( 'has-error', 0 !== _.where( notifications, { type: 'error' } ).length );
3896              } );
3897  
3898              onSectionAssigned = function( newSectionId, oldSectionId ) {
3899                  if ( oldSectionId && api.section.has( oldSectionId ) ) {
3900                      api.section( oldSectionId ).expanded.unbind( renderNotificationsIfVisible );
3901                  }
3902                  if ( newSectionId ) {
3903                      api.section( newSectionId, function( section ) {
3904                          section.expanded.bind( renderNotificationsIfVisible );
3905                          renderNotificationsIfVisible();
3906                      });
3907                  }
3908              };
3909  
3910              control.section.bind( onSectionAssigned );
3911              onSectionAssigned( control.section.get() );
3912              control.notifications.bind( 'change', _.debounce( renderNotificationsIfVisible ) );
3913          },
3914  
3915          /**
3916           * Render notifications.
3917           *
3918           * Renders the `control.notifications` into the control's container.
3919           * Control subclasses may override this method to do their own handling
3920           * of rendering notifications.
3921           *
3922           * @deprecated in favor of `control.notifications.render()`
3923           * @since 4.6.0
3924           * @this {wp.customize.Control}
3925           */
3926          renderNotifications: function() {
3927              var control = this, container, notifications, hasError = false;
3928  
3929              if ( 'undefined' !== typeof console && console.warn ) {
3930                  console.warn( '[DEPRECATED] wp.customize.Control.prototype.renderNotifications() is deprecated in favor of instantiating a wp.customize.Notifications and calling its render() method.' );
3931              }
3932  
3933              container = control.getNotificationsContainerElement();
3934              if ( ! container || ! container.length ) {
3935                  return;
3936              }
3937              notifications = [];
3938              control.notifications.each( function( notification ) {
3939                  notifications.push( notification );
3940                  if ( 'error' === notification.type ) {
3941                      hasError = true;
3942                  }
3943              } );
3944  
3945              if ( 0 === notifications.length ) {
3946                  container.stop().slideUp( 'fast' );
3947              } else {
3948                  container.stop().slideDown( 'fast', null, function() {
3949                      $( this ).css( 'height', 'auto' );
3950                  } );
3951              }
3952  
3953              if ( ! control.notificationsTemplate ) {
3954                  control.notificationsTemplate = wp.template( 'customize-control-notifications' );
3955              }
3956  
3957              control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
3958              control.container.toggleClass( 'has-error', hasError );
3959              container.empty().append(
3960                  control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } ).trim()
3961              );
3962          },
3963  
3964          /**
3965           * Normal controls do not expand, so just expand its parent
3966           *
3967           * @param {Object} [params] Parameters to pass to the section's expand method.
3968           */
3969          expand: function ( params ) {
3970              api.section( this.section() ).expand( params );
3971          },
3972  
3973          /*
3974           * Documented using @borrows in the constructor.
3975           */
3976          focus: focus,
3977  
3978          /**
3979           * Update UI in response to a change in the control's active state.
3980           * This does not change the active state, it merely handles the behavior
3981           * for when it does change.
3982           *
3983           * @since 4.1.0
3984           *
3985           * @param {boolean}  active                  The active state to transition to.
3986           * @param {Object}   [args]                  Object containing options for showing or hiding the control.
3987           * @param {number}   [args.duration]         The duration for the slideUp/slideDown animation.
3988           * @param {boolean}  [args.unchanged]        Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
3989           * @param {Function} [args.completeCallback] Function to call when the slideUp/slideDown has completed.
3990           */
3991          onChangeActive: function ( active, args ) {
3992              if ( args.unchanged ) {
3993                  if ( args.completeCallback ) {
3994                      args.completeCallback();
3995                  }
3996                  return;
3997              }
3998  
3999              if ( ! $.contains( document, this.container[0] ) ) {
4000                  // jQuery.fn.slideUp is not hiding an element if it is not in the DOM.
4001                  this.container.toggle( active );
4002                  if ( args.completeCallback ) {
4003                      args.completeCallback();
4004                  }
4005              } else if ( active ) {
4006                  this.container.slideDown( args.duration, args.completeCallback );
4007              } else {
4008                  this.container.slideUp( args.duration, args.completeCallback );
4009              }
4010          },
4011  
4012          /**
4013           * Toggle the control's active state.
4014           *
4015           * @param {boolean} active The active state to toggle.
4016           * @deprecated 4.1.0 Use this.onChangeActive() instead.
4017           * @return {void}
4018           */
4019          toggle: function ( active ) {
4020              return this.onChangeActive( active, this.defaultActiveArguments );
4021          },
4022  
4023          /*
4024           * Documented using @borrows in the constructor
4025           */
4026          activate: Container.prototype.activate,
4027  
4028          /*
4029           * Documented using @borrows in the constructor
4030           */
4031          deactivate: Container.prototype.deactivate,
4032  
4033          /*
4034           * Documented using @borrows in the constructor
4035           */
4036          _toggleActive: Container.prototype._toggleActive,
4037  
4038          // @todo This function appears to be dead code and can be removed.
4039          dropdownInit: function() {
4040              var control      = this,
4041                  statuses     = this.container.find('.dropdown-status'),
4042                  params       = this.params,
4043                  toggleFreeze = false,
4044                  update       = function( to ) {
4045                      if ( 'string' === typeof to && params.statuses && params.statuses[ to ] ) {
4046                          statuses.html( params.statuses[ to ] ).show();
4047                      } else {
4048                          statuses.hide();
4049                      }
4050                  };
4051  
4052              // Support the .dropdown class to open/close complex elements.
4053              this.container.on( 'click keydown', '.dropdown', function( event ) {
4054                  if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4055                      return;
4056                  }
4057  
4058                  event.preventDefault();
4059  
4060                  if ( ! toggleFreeze ) {
4061                      control.container.toggleClass( 'open' );
4062                  }
4063  
4064                  if ( control.container.hasClass( 'open' ) ) {
4065                      control.container.parent().parent().find( 'li.library-selected' ).focus();
4066                  }
4067  
4068                  // Don't want to fire focus and click at same time.
4069                  toggleFreeze = true;
4070                  setTimeout(function () {
4071                      toggleFreeze = false;
4072                  }, 400);
4073              });
4074  
4075              this.setting.bind( update );
4076              update( this.setting() );
4077          },
4078  
4079          /**
4080           * Render the control from its JS template, if it exists.
4081           *
4082           * The control's container must already exist in the DOM.
4083           *
4084           * @since 4.1.0
4085           */
4086          renderContent: function () {
4087              var control = this, template, standardTypes, templateId, sectionId;
4088  
4089              standardTypes = [
4090                  'button',
4091                  'checkbox',
4092                  'date',
4093                  'datetime-local',
4094                  'email',
4095                  'month',
4096                  'number',
4097                  'password',
4098                  'radio',
4099                  'range',
4100                  'search',
4101                  'select',
4102                  'tel',
4103                  'time',
4104                  'text',
4105                  'textarea',
4106                  'week',
4107                  'url'
4108              ];
4109  
4110              templateId = control.templateSelector;
4111  
4112              // Use default content template when a standard HTML type is used,
4113              // there isn't a more specific template existing, and the control container is empty.
4114              if ( templateId === 'customize-control-' + control.params.type + '-content' &&
4115                  _.contains( standardTypes, control.params.type ) &&
4116                  ! document.getElementById( 'tmpl-' + templateId ) &&
4117                  0 === control.container.children().length )
4118              {
4119                  templateId = 'customize-control-default-content';
4120              }
4121  
4122              // Replace the container element's content with the control.
4123              if ( document.getElementById( 'tmpl-' + templateId ) ) {
4124                  template = wp.template( templateId );
4125                  if ( template && control.container ) {
4126                      control.container.html( template( control.params ) );
4127                  }
4128              }
4129  
4130              // Re-render notifications after content has been re-rendered.
4131              control.notifications.container = control.getNotificationsContainerElement();
4132              sectionId = control.section();
4133              if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
4134                  control.notifications.render();
4135              }
4136          },
4137  
4138          /**
4139           * Add a new page to a dropdown-pages control reusing menus code for this.
4140           *
4141           * @since 4.7.0
4142           * @access private
4143           *
4144           * @return {void}
4145           */
4146          addNewPage: function () {
4147              var control = this, promise, toggle, container, input, inputError, title, select;
4148  
4149              if ( 'dropdown-pages' !== control.params.type || ! control.params.allow_addition || ! api.Menus ) {
4150                  return;
4151              }
4152  
4153              toggle = control.container.find( '.add-new-toggle' );
4154              container = control.container.find( '.new-content-item-wrapper' );
4155              input = control.container.find( '.create-item-input' );
4156              inputError = control.container.find('.create-item-error');
4157              title = input.val();
4158              select = control.container.find( 'select' );
4159  
4160              if ( ! title ) {
4161                  container.addClass( 'form-invalid' );
4162                  input.attr('aria-invalid', 'true');
4163                  input.attr('aria-describedby', inputError.attr('id'));
4164                  inputError.slideDown( 'fast' );
4165                  wp.a11y.speak( inputError.text() );
4166                  return;
4167              }
4168  
4169              container.removeClass( 'form-invalid' );
4170              input.attr('aria-invalid', 'false');
4171              input.removeAttr('aria-describedby');
4172              inputError.hide();
4173              input.attr( 'disabled', 'disabled' );
4174  
4175              // The menus functions add the page, publish when appropriate,
4176              // and also add the new page to the dropdown-pages controls.
4177              promise = api.Menus.insertAutoDraftPost( {
4178                  post_title: title,
4179                  post_type: 'page'
4180              } );
4181              promise.done( function( data ) {
4182                  var availableItem, $content, itemTemplate;
4183  
4184                  // Prepare the new page as an available menu item.
4185                  // See api.Menus.submitNew().
4186                  availableItem = new api.Menus.AvailableItemModel( {
4187                      'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
4188                      'title': title,
4189                      'type': 'post_type',
4190                      'type_label': api.Menus.data.l10n.page_label,
4191                      'object': 'page',
4192                      'object_id': data.post_id,
4193                      'url': data.url
4194                  } );
4195  
4196                  // Add the new item to the list of available menu items.
4197                  api.Menus.availableMenuItemsPanel.collection.add( availableItem );
4198                  $content = $( '#available-menu-items-post_type-page' ).find( '.available-menu-items-list' );
4199                  itemTemplate = wp.template( 'available-menu-item' );
4200                  $content.prepend( itemTemplate( availableItem.attributes ) );
4201  
4202                  // Focus the select control.
4203                  select.focus();
4204                  control.setting.set( String( data.post_id ) ); // Triggers a preview refresh and updates the setting.
4205  
4206                  // Reset the create page form.
4207                  container.slideUp( 180 );
4208                  toggle.slideDown( 180 );
4209              } );
4210              promise.always( function() {
4211                  input.val( '' ).removeAttr( 'disabled' );
4212              } );
4213          }
4214      });
4215  
4216      /**
4217       * A colorpicker control.
4218       *
4219       * @class    wp.customize.ColorControl
4220       * @augments wp.customize.Control
4221       */
4222      api.ColorControl = api.Control.extend(/** @lends wp.customize.ColorControl.prototype */{
4223          ready: function() {
4224              var control = this,
4225                  isHueSlider = this.params.mode === 'hue',
4226                  updating = false,
4227                  picker;
4228  
4229              if ( isHueSlider ) {
4230                  picker = this.container.find( '.color-picker-hue' );
4231                  picker.val( control.setting() ).wpColorPicker({
4232                      change: function( event, ui ) {
4233                          updating = true;
4234                          control.setting( ui.color.h() );
4235                          updating = false;
4236                      }
4237                  });
4238              } else {
4239                  picker = this.container.find( '.color-picker-hex' );
4240                  picker.val( control.setting() ).wpColorPicker({
4241                      change: function() {
4242                          updating = true;
4243                          control.setting.set( picker.wpColorPicker( 'color' ) );
4244                          updating = false;
4245                      },
4246                      clear: function() {
4247                          updating = true;
4248                          control.setting.set( '' );
4249                          updating = false;
4250                      }
4251                  });
4252              }
4253  
4254              control.setting.bind( function ( value ) {
4255                  // Bail if the update came from the control itself.
4256                  if ( updating ) {
4257                      return;
4258                  }
4259                  picker.val( value );
4260                  picker.wpColorPicker( 'color', value );
4261              } );
4262  
4263              // Collapse color picker when hitting Esc instead of collapsing the current section.
4264              control.container.on( 'keydown', function( event ) {
4265                  var pickerContainer;
4266                  if ( 27 !== event.which ) { // Esc.
4267                      return;
4268                  }
4269                  pickerContainer = control.container.find( '.wp-picker-container' );
4270                  if ( pickerContainer.hasClass( 'wp-picker-active' ) ) {
4271                      picker.wpColorPicker( 'close' );
4272                      control.container.find( '.wp-color-result' ).focus();
4273                      event.stopPropagation(); // Prevent section from being collapsed.
4274                  }
4275              } );
4276          }
4277      });
4278  
4279      /**
4280       * A control that implements the media modal.
4281       *
4282       * @class    wp.customize.MediaControl
4283       * @augments wp.customize.Control
4284       */
4285      api.MediaControl = api.Control.extend(/** @lends wp.customize.MediaControl.prototype */{
4286  
4287          /**
4288           * When the control's DOM structure is ready,
4289           * set up internal event bindings.
4290           */
4291          ready: function() {
4292              var control = this;
4293              // Shortcut so that we don't have to use _.bind every time we add a callback.
4294              _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
4295  
4296              // Bind events, with delegation to facilitate re-rendering.
4297              control.container.on( 'click keydown', '.upload-button', control.openFrame );
4298              control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
4299              control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
4300              control.container.on( 'click keydown', '.default-button', control.restoreDefault );
4301              control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
4302              control.container.on( 'click keydown', '.remove-button', control.removeFile );
4303              control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
4304  
4305              // Resize the player controls when it becomes visible (ie when section is expanded).
4306              api.section( control.section() ).container
4307                  .on( 'expanded', function() {
4308                      if ( control.player ) {
4309                          control.player.setControlsSize();
4310                      }
4311                  })
4312                  .on( 'collapsed', function() {
4313                      control.pausePlayer();
4314                  });
4315  
4316              /**
4317               * Set attachment data and render content.
4318               *
4319               * Note that BackgroundImage.prototype.ready applies this ready method
4320               * to itself. Since BackgroundImage is an UploadControl, the value
4321               * is the attachment URL instead of the attachment ID. In this case
4322               * we skip fetching the attachment data because we have no ID available,
4323               * and it is the responsibility of the UploadControl to set the control's
4324               * attachmentData before calling the renderContent method.
4325               *
4326               * @param {number|string} value Attachment.
4327               */
4328  			function setAttachmentDataAndRenderContent( value ) {
4329                  var hasAttachmentData = $.Deferred();
4330  
4331                  if ( control.extended( api.UploadControl ) ) {
4332                      hasAttachmentData.resolve();
4333                  } else {
4334                      value = parseInt( value, 10 );
4335                      if ( _.isNaN( value ) || value <= 0 ) {
4336                          delete control.params.attachment;
4337                          hasAttachmentData.resolve();
4338                      } else if ( control.params.attachment && control.params.attachment.id === value ) {
4339                          hasAttachmentData.resolve();
4340                      }
4341                  }
4342  
4343                  // Fetch the attachment data.
4344                  if ( 'pending' === hasAttachmentData.state() ) {
4345                      wp.media.attachment( value ).fetch().done( function() {
4346                          control.params.attachment = this.attributes;
4347                          hasAttachmentData.resolve();
4348  
4349                          // Send attachment information to the preview for possible use in `postMessage` transport.
4350                          wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
4351                      } );
4352                  }
4353  
4354                  hasAttachmentData.done( function() {
4355                      control.renderContent();
4356                  } );
4357              }
4358  
4359              // Ensure attachment data is initially set (for dynamically-instantiated controls).
4360              setAttachmentDataAndRenderContent( control.setting() );
4361  
4362              // Update the attachment data and re-render the control when the setting changes.
4363              control.setting.bind( setAttachmentDataAndRenderContent );
4364          },
4365  
4366          pausePlayer: function () {
4367              this.player && this.player.pause();
4368          },
4369  
4370          cleanupPlayer: function () {
4371              this.player && wp.media.mixin.removePlayer( this.player );
4372          },
4373  
4374          /**
4375           * Open the media modal.
4376           *
4377           * @param {JQuery.Event} event Event.
4378           */
4379          openFrame: function( event ) {
4380              if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4381                  return;
4382              }
4383  
4384              event.preventDefault();
4385  
4386              if ( ! this.frame ) {
4387                  this.initFrame();
4388              }
4389  
4390              this.frame.open();
4391          },
4392  
4393          /**
4394           * Create a media modal select frame, and store it so the instance can be reused when needed.
4395           */
4396          initFrame: function() {
4397              this.frame = wp.media({
4398                  button: {
4399                      text: this.params.button_labels.frame_button
4400                  },
4401                  states: [
4402                      new wp.media.controller.Library({
4403                          title:     this.params.button_labels.frame_title,
4404                          library:   wp.media.query({ type: this.params.mime_type }),
4405                          multiple:  false,
4406                          date:      false
4407                      })
4408                  ]
4409              });
4410  
4411              // When a file is selected, run a callback.
4412              this.frame.on( 'select', this.select );
4413          },
4414  
4415          /**
4416           * Callback handler for when an attachment is selected in the media modal.
4417           * Gets the selected image information, and sets it within the control.
4418           */
4419          select: function() {
4420              // Get the attachment from the modal frame.
4421              var node,
4422                  attachment = this.frame.state().get( 'selection' ).first().toJSON(),
4423                  mejsSettings = window._wpmejsSettings || {};
4424  
4425              this.params.attachment = attachment;
4426  
4427              // Set the Customizer setting; the callback takes care of rendering.
4428              this.setting( attachment.id );
4429              node = this.container.find( 'audio, video' ).get(0);
4430  
4431              // Initialize audio/video previews.
4432              if ( node ) {
4433                  this.player = new MediaElementPlayer( node, mejsSettings );
4434              } else {
4435                  this.cleanupPlayer();
4436              }
4437          },
4438  
4439          /**
4440           * Reset the setting to the default value.
4441           *
4442           * @param {JQuery.Event} event Event.
4443           */
4444          restoreDefault: function( event ) {
4445              if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4446                  return;
4447              }
4448              event.preventDefault();
4449  
4450              this.params.attachment = this.params.defaultAttachment;
4451              this.setting( this.params.defaultAttachment.url );
4452          },
4453  
4454          /**
4455           * Called when the "Remove" link is clicked. Empties the setting.
4456           *
4457           * @param {JQuery.Event} event Event.
4458           */
4459          removeFile: function( event ) {
4460              if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4461                  return;
4462              }
4463              event.preventDefault();
4464  
4465              this.params.attachment = {};
4466              this.setting( '' );
4467              this.renderContent(); // Not bound to setting change when emptying.
4468          }
4469      });
4470  
4471      /**
4472       * An upload control, which utilizes the media modal.
4473       *
4474       * @class    wp.customize.UploadControl
4475       * @augments wp.customize.MediaControl
4476       */
4477      api.UploadControl = api.MediaControl.extend(/** @lends wp.customize.UploadControl.prototype */{
4478  
4479          /**
4480           * Callback handler for when an attachment is selected in the media modal.
4481           * Gets the selected image information, and sets it within the control.
4482           */
4483          select: function() {
4484              // Get the attachment from the modal frame.
4485              var node,
4486                  attachment = this.frame.state().get( 'selection' ).first().toJSON(),
4487                  mejsSettings = window._wpmejsSettings || {};
4488  
4489              this.params.attachment = attachment;
4490  
4491              // Set the Customizer setting; the callback takes care of rendering.
4492              this.setting( attachment.url );
4493              node = this.container.find( 'audio, video' ).get(0);
4494  
4495              // Initialize audio/video previews.
4496              if ( node ) {
4497                  this.player = new MediaElementPlayer( node, mejsSettings );
4498              } else {
4499                  this.cleanupPlayer();
4500              }
4501          },
4502  
4503          // @deprecated
4504          success: function() {},
4505  
4506          // @deprecated
4507          removerVisibility: function() {}
4508      });
4509  
4510      /**
4511       * A control for uploading images.
4512       *
4513       * This control no longer needs to do anything more
4514       * than what the upload control does in JS.
4515       *
4516       * @class    wp.customize.ImageControl
4517       * @augments wp.customize.UploadControl
4518       */
4519      api.ImageControl = api.UploadControl.extend(/** @lends wp.customize.ImageControl.prototype */{
4520          // @deprecated
4521          thumbnailSrc: function() {}
4522      });
4523  
4524      /**
4525       * A control for uploading background images.
4526       *
4527       * @class    wp.customize.BackgroundControl
4528       * @augments wp.customize.UploadControl
4529       */
4530      api.BackgroundControl = api.UploadControl.extend(/** @lends wp.customize.BackgroundControl.prototype */{
4531  
4532          /**
4533           * When the control's DOM structure is ready,
4534           * set up internal event bindings.
4535           */
4536          ready: function() {
4537              api.UploadControl.prototype.ready.call( this );
4538          },
4539  
4540          /**
4541           * Callback handler for when an attachment is selected in the media modal.
4542           * Does an additional Ajax request for setting the background context.
4543           */
4544          select: function() {
4545              api.UploadControl.prototype.select.call( this );
4546  
4547              wp.ajax.post( 'custom-background-add', {
4548                  nonce: _wpCustomizeBackground.nonces.add,
4549                  wp_customize: 'on',
4550                  customize_theme: api.settings.theme.stylesheet,
4551                  attachment_id: this.params.attachment.id
4552              } );
4553          }
4554      });
4555  
4556      /**
4557       * A control for positioning a background image.
4558       *
4559       * @since 4.7.0
4560       *
4561       * @class    wp.customize.BackgroundPositionControl
4562       * @augments wp.customize.Control
4563       */
4564      api.BackgroundPositionControl = api.Control.extend(/** @lends wp.customize.BackgroundPositionControl.prototype */{
4565  
4566          /**
4567           * Set up control UI once embedded in DOM and settings are created.
4568           *
4569           * @since 4.7.0
4570           * @access public
4571           */
4572          ready: function() {
4573              var control = this, updateRadios;
4574  
4575              control.container.on( 'change', 'input[name="background-position"]', function() {
4576                  var position = $( this ).val().split( ' ' );
4577                  control.settings.x( position[0] );
4578                  control.settings.y( position[1] );
4579              } );
4580  
4581              updateRadios = _.debounce( function() {
4582                  var x, y, radioInput, inputValue;
4583                  x = control.settings.x.get();
4584                  y = control.settings.y.get();
4585                  inputValue = String( x ) + ' ' + String( y );
4586                  radioInput = control.container.find( 'input[name="background-position"][value="' + inputValue + '"]' );
4587                  radioInput.trigger( 'click' );
4588              } );
4589              control.settings.x.bind( updateRadios );
4590              control.settings.y.bind( updateRadios );
4591  
4592              updateRadios(); // Set initial UI.
4593          }
4594      } );
4595  
4596      /**
4597       * A control for selecting and cropping an image.
4598       *
4599       * @class    wp.customize.CroppedImageControl
4600       * @augments wp.customize.MediaControl
4601       */
4602      api.CroppedImageControl = api.MediaControl.extend(/** @lends wp.customize.CroppedImageControl.prototype */{
4603  
4604          /**
4605           * Open the media modal to the library state.
4606           *
4607           * @param {JQuery.Event} event Event.
4608           */
4609          openFrame: function( event ) {
4610              if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4611                  return;
4612              }
4613  
4614              this.initFrame();
4615              this.frame.setState( 'library' ).open();
4616          },
4617  
4618          /**
4619           * Create a media modal select frame, and store it so the instance can be reused when needed.
4620           */
4621          initFrame: function() {
4622              var l10n = _wpMediaViewsL10n;
4623  
4624              this.frame = wp.media({
4625                  button: {
4626                      text: l10n.select,
4627                      close: false
4628                  },
4629                  states: [
4630                      new wp.media.controller.Library({
4631                          title: this.params.button_labels.frame_title,
4632                          library: wp.media.query({ type: 'image' }),
4633                          multiple: false,
4634                          date: false,
4635                          priority: 20,
4636                          suggestedWidth: this.params.width,
4637                          suggestedHeight: this.params.height
4638                      }),
4639                      new wp.media.controller.CustomizeImageCropper({
4640                          imgSelectOptions: this.calculateImageSelectOptions,
4641                          control: this
4642                      })
4643                  ]
4644              });
4645  
4646              this.frame.on( 'select', this.onSelect, this );
4647              this.frame.on( 'cropped', this.onCropped, this );
4648              this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
4649          },
4650  
4651          /**
4652           * After an image is selected in the media modal, switch to the cropper
4653           * state if the image isn't the right size.
4654           */
4655          onSelect: function() {
4656              var attachment = this.frame.state().get( 'selection' ).first().toJSON();
4657  
4658              if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
4659                  this.setImageFromAttachment( attachment );
4660                  this.frame.close();
4661              } else {
4662                  this.frame.setState( 'cropper' );
4663              }
4664          },
4665  
4666          /**
4667           * After the image has been cropped, apply the cropped image data to the setting.
4668           *
4669           * @param {Object} croppedImage Cropped attachment data.
4670           */
4671          onCropped: function( croppedImage ) {
4672              this.setImageFromAttachment( croppedImage );
4673          },
4674  
4675          /**
4676           * Returns a set of options, computed from the attached image data and
4677           * control-specific data, to be fed to the imgAreaSelect plugin in
4678           * wp.media.view.Cropper.
4679           *
4680           * @param {wp.media.model.Attachment}   attachment The attachment to be cropped.
4681           * @param {wp.media.controller.Cropper} controller The cropper controller.
4682           * @return {Object} Options for the imgAreaSelect plugin.
4683           */
4684          calculateImageSelectOptions: function( attachment, controller ) {
4685              var control       = controller.get( 'control' ),
4686                  flexWidth     = !! parseInt( control.params.flex_width, 10 ),
4687                  flexHeight    = !! parseInt( control.params.flex_height, 10 ),
4688                  realWidth     = attachment.get( 'width' ),
4689                  realHeight    = attachment.get( 'height' ),
4690                  xInit         = parseInt( control.params.width, 10 ),
4691                  yInit         = parseInt( control.params.height, 10 ),
4692                  requiredRatio = xInit / yInit,
4693                  realRatio     = realWidth / realHeight,
4694                  xImg          = xInit,
4695                  yImg          = yInit,
4696                  x1, y1, imgSelectOptions;
4697  
4698              controller.set( 'hasRequiredAspectRatio', control.hasRequiredAspectRatio( requiredRatio, realRatio ) );
4699              controller.set( 'suggestedCropSize', { width: realWidth, height: realHeight, x1: 0, y1: 0, x2: xInit, y2: yInit } );
4700              controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
4701  
4702              if ( realRatio > requiredRatio ) {
4703                  yInit = realHeight;
4704                  xInit = yInit * requiredRatio;
4705              } else {
4706                  xInit = realWidth;
4707                  yInit = xInit / requiredRatio;
4708              }
4709  
4710              x1 = ( realWidth - xInit ) / 2;
4711              y1 = ( realHeight - yInit ) / 2;
4712  
4713              imgSelectOptions = {
4714                  handles: true,
4715                  keys: true,
4716                  instance: true,
4717                  persistent: true,
4718                  imageWidth: realWidth,
4719                  imageHeight: realHeight,
4720                  minWidth: xImg > xInit ? xInit : xImg,
4721                  minHeight: yImg > yInit ? yInit : yImg,
4722                  x1: x1,
4723                  y1: y1,
4724                  x2: xInit + x1,
4725                  y2: yInit + y1
4726              };
4727  
4728              if ( flexHeight === false && flexWidth === false ) {
4729                  imgSelectOptions.aspectRatio = xInit + ':' + yInit;
4730              }
4731  
4732              if ( true === flexHeight ) {
4733                  delete imgSelectOptions.minHeight;
4734                  imgSelectOptions.maxWidth = realWidth;
4735              }
4736  
4737              if ( true === flexWidth ) {
4738                  delete imgSelectOptions.minWidth;
4739                  imgSelectOptions.maxHeight = realHeight;
4740              }
4741  
4742              return imgSelectOptions;
4743          },
4744  
4745          /**
4746           * Return whether the image must be cropped, based on required dimensions.
4747           *
4748           * @param {boolean} flexW Width is flexible.
4749           * @param {boolean} flexH Height is flexible.
4750           * @param {number}  dstW  Required width.
4751           * @param {number}  dstH  Required height.
4752           * @param {number}  imgW  Provided image's width.
4753           * @param {number}  imgH  Provided image's height.
4754           * @return {boolean} Whether cropping is required.
4755           */
4756          mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
4757              if ( true === flexW && true === flexH ) {
4758                  return false;
4759              }
4760  
4761              if ( true === flexW && dstH === imgH ) {
4762                  return false;
4763              }
4764  
4765              if ( true === flexH && dstW === imgW ) {
4766                  return false;
4767              }
4768  
4769              if ( dstW === imgW && dstH === imgH ) {
4770                  return false;
4771              }
4772  
4773              if ( imgW <= dstW ) {
4774                  return false;
4775              }
4776  
4777              return true;
4778          },
4779  
4780          /**
4781           * Check if the image's aspect ratio essentially matches the required aspect ratio.
4782           *
4783           * Floating point precision is low, so this allows a small tolerance. This
4784           * tolerance allows for images over 100,000 px on either side to still trigger
4785           * the cropping flow.
4786           *
4787           * @param {number} requiredRatio Required image ratio.
4788           * @param {number} realRatio     Provided image ratio.
4789           * @return {boolean} Whether the image has the required aspect ratio.
4790           */
4791          hasRequiredAspectRatio: function ( requiredRatio, realRatio ) {
4792              if ( Math.abs( requiredRatio - realRatio ) < 0.000001 ) {
4793                  return true;
4794              }
4795  
4796              return false;
4797          },
4798  
4799          /**
4800           * If cropping was skipped, apply the image data directly to the setting.
4801           */
4802          onSkippedCrop: function() {
4803              var attachment = this.frame.state().get( 'selection' ).first().toJSON();
4804              this.setImageFromAttachment( attachment );
4805          },
4806  
4807          /**
4808           * Updates the setting and re-renders the control UI.
4809           *
4810           * @param {Object} attachment Attachment object for the selected image.
4811           */
4812          setImageFromAttachment: function( attachment ) {
4813              var control = this;
4814              this.params.attachment = attachment;
4815  
4816              // Set the Customizer setting; the callback takes care of rendering.
4817              this.setting( attachment.id );
4818  
4819              // Set focus to the first relevant button after the icon.
4820              _.defer( function() {
4821                  var firstButton = control.container.find( '.actions .button' ).first();
4822                  if ( firstButton.length ) {
4823                      firstButton.focus();
4824                  }
4825              } );
4826          }
4827      });
4828  
4829      /**
4830       * A control for selecting and cropping Site Icons.
4831       *
4832       * @class    wp.customize.SiteIconControl
4833       * @augments wp.customize.CroppedImageControl
4834       */
4835      api.SiteIconControl = api.CroppedImageControl.extend(/** @lends wp.customize.SiteIconControl.prototype */{
4836  
4837          /**
4838           * Create a media modal select frame, and store it so the instance can be reused when needed.
4839           */
4840          initFrame: function() {
4841              var l10n = _wpMediaViewsL10n;
4842  
4843              this.frame = wp.media({
4844                  button: {
4845                      text: l10n.select,
4846                      close: false
4847                  },
4848                  states: [
4849                      new wp.media.controller.Library({
4850                          title: this.params.button_labels.frame_title,
4851                          library: wp.media.query({ type: 'image' }),
4852                          multiple: false,
4853                          date: false,
4854                          priority: 20,
4855                          suggestedWidth: this.params.width,
4856                          suggestedHeight: this.params.height
4857                      }),
4858                      new wp.media.controller.SiteIconCropper({
4859                          imgSelectOptions: this.calculateImageSelectOptions,
4860                          control: this
4861                      })
4862                  ]
4863              });
4864  
4865              this.frame.on( 'select', this.onSelect, this );
4866              this.frame.on( 'cropped', this.onCropped, this );
4867              this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
4868          },
4869  
4870          /**
4871           * After an image is selected in the media modal, switch to the cropper
4872           * state if the image isn't the right size.
4873           */
4874          onSelect: function() {
4875              var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
4876                  controller = this;
4877  
4878              if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
4879                  wp.ajax.post( 'crop-image', {
4880                      nonce: attachment.nonces.edit,
4881                      id: attachment.id,
4882                      context: 'site-icon',
4883                      cropDetails: {
4884                          x1: 0,
4885                          y1: 0,
4886                          width: this.params.width,
4887                          height: this.params.height,
4888                          dst_width: this.params.width,
4889                          dst_height: this.params.height
4890                      }
4891                  } ).done( function( croppedImage ) {
4892                      controller.setImageFromAttachment( croppedImage );
4893                      controller.frame.close();
4894                  } ).fail( function() {
4895                      controller.frame.trigger('content:error:crop');
4896                  } );
4897              } else {
4898                  this.frame.setState( 'cropper' );
4899              }
4900          },
4901  
4902          /**
4903           * Updates the setting and re-renders the control UI.
4904           *
4905           * @param {Object} attachment Attachment object for the selected image.
4906           */
4907          setImageFromAttachment: function( attachment ) {
4908              var control = this,
4909                  sizes = [ 'site_icon-32', 'thumbnail', 'full' ], link,
4910                  icon;
4911  
4912              _.each( sizes, function( size ) {
4913                  if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
4914                      icon = attachment.sizes[ size ];
4915                  }
4916              } );
4917  
4918              this.params.attachment = attachment;
4919  
4920              // Set the Customizer setting; the callback takes care of rendering.
4921              this.setting( attachment.id );
4922  
4923              if ( ! icon ) {
4924                  return;
4925              }
4926  
4927              // Update the icon in-browser.
4928              link = $( 'link[rel="icon"][sizes="32x32"]' );
4929              link.attr( 'href', icon.url );
4930  
4931              // Set focus to the first relevant button after the icon.
4932              _.defer( function() {
4933                  var firstButton = control.container.find( '.actions .button' ).first();
4934                  if ( firstButton.length ) {
4935                      firstButton.focus();
4936                  }
4937              } );
4938          },
4939  
4940          /**
4941           * Called when the "Remove" link is clicked. Empties the setting.
4942           *
4943           * @param {JQuery.Event} event Event.
4944           */
4945          removeFile: function( event ) {
4946              if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4947                  return;
4948              }
4949              event.preventDefault();
4950  
4951              this.params.attachment = {};
4952              this.setting( '' );
4953              this.renderContent(); // Not bound to setting change when emptying.
4954              $( 'link[rel="icon"][sizes="32x32"]' ).attr( 'href', '/favicon.ico' ); // Set to default.
4955          }
4956      });
4957  
4958      /**
4959       * @class    wp.customize.HeaderControl
4960       * @augments wp.customize.Control
4961       */
4962      api.HeaderControl = api.Control.extend(/** @lends wp.customize.HeaderControl.prototype */{
4963          ready: function() {
4964              this.btnRemove = $('#customize-control-header_image .actions .remove');
4965              this.btnNew    = $('#customize-control-header_image .actions .new');
4966  
4967              _.bindAll(this, 'openMedia', 'removeImage');
4968  
4969              this.btnNew.on( 'click', this.openMedia );
4970              this.btnRemove.on( 'click', this.removeImage );
4971  
4972              api.HeaderTool.currentHeader = this.getInitialHeaderImage();
4973  
4974              new api.HeaderTool.CurrentView({
4975                  model: api.HeaderTool.currentHeader,
4976                  el: '#customize-control-header_image .current .container'
4977              });
4978  
4979              new api.HeaderTool.ChoiceListView({
4980                  collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
4981                  el: '#customize-control-header_image .choices .uploaded .list'
4982              });
4983  
4984              new api.HeaderTool.ChoiceListView({
4985                  collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
4986                  el: '#customize-control-header_image .choices .default .list'
4987              });
4988  
4989              api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
4990                  api.HeaderTool.UploadsList,
4991                  api.HeaderTool.DefaultsList
4992              ]);
4993  
4994              // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
4995              wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
4996              wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet;
4997          },
4998  
4999          /**
5000           * Returns a new instance of api.HeaderTool.ImageModel based on the currently
5001           * saved header image (if any).
5002           *
5003           * @since 4.2.0
5004           *
5005           * @return {wp.customize.HeaderTool.ImageModel} Model for the currently saved header image, or an empty model when none is set.
5006           */
5007          getInitialHeaderImage: function() {
5008              if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
5009                  return new api.HeaderTool.ImageModel();
5010              }
5011  
5012              // Get the matching uploaded image object.
5013              var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
5014                  return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
5015              } );
5016              // Fall back to raw current header image.
5017              if ( ! currentHeaderObject ) {
5018                  currentHeaderObject = {
5019                      url: api.get().header_image,
5020                      thumbnail_url: api.get().header_image,
5021                      attachment_id: api.get().header_image_data.attachment_id
5022                  };
5023              }
5024  
5025              return new api.HeaderTool.ImageModel({
5026                  header: currentHeaderObject,
5027                  choice: currentHeaderObject.url.split( '/' ).pop()
5028              });
5029          },
5030  
5031          /**
5032           * Returns a set of options, computed from the attached image data and
5033           * theme-specific data, to be fed to the imgAreaSelect plugin in
5034           * wp.media.view.Cropper.
5035           *
5036           * @param {wp.media.model.Attachment}   attachment The attachment to be cropped.
5037           * @param {wp.media.controller.Cropper} controller The cropper controller.
5038           * @return {Object} Options for the imgAreaSelect plugin.
5039           */
5040          calculateImageSelectOptions: function(attachment, controller) {
5041              var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
5042                  yInit = parseInt(_wpCustomizeHeader.data.height, 10),
5043                  flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
5044                  flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
5045                  ratio, xImg, yImg, realHeight, realWidth,
5046                  imgSelectOptions;
5047  
5048              realWidth = attachment.get('width');
5049              realHeight = attachment.get('height');
5050  
5051              this.headerImage = new api.HeaderTool.ImageModel();
5052              this.headerImage.set({
5053                  themeWidth: xInit,
5054                  themeHeight: yInit,
5055                  themeFlexWidth: flexWidth,
5056                  themeFlexHeight: flexHeight,
5057                  imageWidth: realWidth,
5058                  imageHeight: realHeight
5059              });
5060  
5061              controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
5062  
5063              ratio = xInit / yInit;
5064              xImg = realWidth;
5065              yImg = realHeight;
5066  
5067              if ( xImg / yImg > ratio ) {
5068                  yInit = yImg;
5069                  xInit = yInit * ratio;
5070              } else {
5071                  xInit = xImg;
5072                  yInit = xInit / ratio;
5073              }
5074  
5075              imgSelectOptions = {
5076                  handles: true,
5077                  keys: true,
5078                  instance: true,
5079                  persistent: true,
5080                  imageWidth: realWidth,
5081                  imageHeight: realHeight,
5082                  x1: 0,
5083                  y1: 0,
5084                  x2: xInit,
5085                  y2: yInit
5086              };
5087  
5088              if (flexHeight === false && flexWidth === false) {
5089                  imgSelectOptions.aspectRatio = xInit + ':' + yInit;
5090              }
5091              if (flexHeight === false ) {
5092                  imgSelectOptions.maxHeight = yInit;
5093              }
5094              if (flexWidth === false ) {
5095                  imgSelectOptions.maxWidth = xInit;
5096              }
5097  
5098              return imgSelectOptions;
5099          },
5100  
5101          /**
5102           * Sets up and opens the Media Manager in order to select an image.
5103           * Depending on both the size of the image and the properties of the
5104           * current theme, a cropping step after selection may be required or
5105           * skippable.
5106           *
5107           * @param {JQuery.Event} event Event.
5108           */
5109          openMedia: function(event) {
5110              var l10n = _wpMediaViewsL10n;
5111  
5112              event.preventDefault();
5113  
5114              this.frame = wp.media({
5115                  button: {
5116                      text: l10n.selectAndCrop,
5117                      close: false
5118                  },
5119                  states: [
5120                      new wp.media.controller.Library({
5121                          title:     l10n.chooseImage,
5122                          library:   wp.media.query({ type: 'image' }),
5123                          multiple:  false,
5124                          date:      false,
5125                          priority:  20,
5126                          suggestedWidth: _wpCustomizeHeader.data.width,
5127                          suggestedHeight: _wpCustomizeHeader.data.height
5128                      }),
5129                      new wp.media.controller.Cropper({
5130                          imgSelectOptions: this.calculateImageSelectOptions
5131                      })
5132                  ]
5133              });
5134  
5135              this.frame.on('select', this.onSelect, this);
5136              this.frame.on('cropped', this.onCropped, this);
5137              this.frame.on('skippedcrop', this.onSkippedCrop, this);
5138  
5139              this.frame.open();
5140          },
5141  
5142          /**
5143           * After an image is selected in the media modal,
5144           * switch to the cropper state.
5145           */
5146          onSelect: function() {
5147              this.frame.setState('cropper');
5148          },
5149  
5150          /**
5151           * After the image has been cropped, apply the cropped image data to the setting.
5152           *
5153           * @param {Object} croppedImage Cropped attachment data.
5154           */
5155          onCropped: function(croppedImage) {
5156              var url = croppedImage.url,
5157                  attachmentId = croppedImage.attachment_id,
5158                  w = croppedImage.width,
5159                  h = croppedImage.height;
5160              this.setImageFromURL(url, attachmentId, w, h);
5161          },
5162  
5163          /**
5164           * If cropping was skipped, apply the image data directly to the setting.
5165           *
5166           * @param {Object} selection The image selected in the media modal.
5167           */
5168          onSkippedCrop: function(selection) {
5169              var url = selection.get('url'),
5170                  w = selection.get('width'),
5171                  h = selection.get('height');
5172              this.setImageFromURL(url, selection.id, w, h);
5173          },
5174  
5175          /**
5176           * Creates a new wp.customize.HeaderTool.ImageModel from provided
5177           * header image data and inserts it into the user-uploaded headers
5178           * collection.
5179           *
5180           * @param {string} url            URL of the header image.
5181           * @param {number} [attachmentId] ID of the image attachment, when the image has one.
5182           * @param {number} [width]        Width of the image in pixels.
5183           * @param {number} [height]       Height of the image in pixels.
5184           */
5185          setImageFromURL: function(url, attachmentId, width, height) {
5186              var choice, data = {};
5187  
5188              data.url = url;
5189              data.thumbnail_url = url;
5190              data.timestamp = _.now();
5191  
5192              if (attachmentId) {
5193                  data.attachment_id = attachmentId;
5194              }
5195  
5196              if (width) {
5197                  data.width = width;
5198              }
5199  
5200              if (height) {
5201                  data.height = height;
5202              }
5203  
5204              choice = new api.HeaderTool.ImageModel({
5205                  header: data,
5206                  choice: url.split('/').pop()
5207              });
5208              api.HeaderTool.UploadsList.add(choice);
5209              api.HeaderTool.currentHeader.set(choice.toJSON());
5210              choice.save();
5211              choice.importImage();
5212          },
5213  
5214          /**
5215           * Triggers the necessary events to deselect an image which was set as
5216           * the currently selected one.
5217           */
5218          removeImage: function() {
5219              api.HeaderTool.currentHeader.trigger('hide');
5220              api.HeaderTool.CombinedList.trigger('control:removeImage');
5221          }
5222  
5223      });
5224  
5225      /**
5226       * wp.customize.ThemeControl
5227       *
5228       * @class    wp.customize.ThemeControl
5229       * @augments wp.customize.Control
5230       */
5231      api.ThemeControl = api.Control.extend(/** @lends wp.customize.ThemeControl.prototype */{
5232  
5233          touchDrag: false,
5234          screenshotRendered: false,
5235  
5236          /**
5237           * Handles the control's ready state.
5238           *
5239           * @since 4.2.0
5240           */
5241          ready: function() {
5242              var control = this, panel = api.panel( 'themes' );
5243  
5244              /**
5245               * Disables the preview buttons if the theme cannot be switched to.
5246               *
5247               * @return {boolean} True if the preview buttons should be disabled, false otherwise.
5248               */
5249  			function disableSwitchButtons() {
5250                  return ! panel.canSwitchTheme( control.params.theme.id );
5251              }
5252  
5253              // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
5254              /**
5255               * Disables the install buttons if the theme cannot be installed or if filesystem credentials are needed.
5256               *
5257               * @return {boolean} True if the install buttons should be disabled, false otherwise.
5258               */
5259  			function disableInstallButtons() {
5260                  return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
5261              }
5262  
5263              /**
5264               * Updates the state of the preview and install buttons based on whether the theme can be switched to or installed.
5265               */
5266  			function updateButtons() {
5267                  control.container.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
5268                  control.container.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
5269              }
5270  
5271              api.state( 'selectedChangesetStatus' ).bind( updateButtons );
5272              api.state( 'changesetStatus' ).bind( updateButtons );
5273              updateButtons();
5274  
5275              control.container.on( 'touchmove', '.theme', function() {
5276                  control.touchDrag = true;
5277              });
5278  
5279              // Bind details view trigger.
5280              control.container.on( 'click keydown touchend', '.theme', function( event ) {
5281                  var section;
5282                  if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
5283                      return;
5284                  }
5285  
5286                  // Bail if the user scrolled on a touch device.
5287                  if ( control.touchDrag === true ) {
5288                      return control.touchDrag = false;
5289                  }
5290  
5291                  // Prevent the modal from showing when the user clicks the action button.
5292                  if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) {
5293                      return;
5294                  }
5295  
5296                  event.preventDefault(); // Keep this AFTER the key filter above.
5297                  section = api.section( control.section() );
5298                  section.showDetails( control.params.theme, function() {
5299  
5300                      // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
5301                      if ( api.settings.theme._filesystemCredentialsNeeded ) {
5302                          section.overlay.find( '.theme-actions .delete-theme' ).remove();
5303                      }
5304                  } );
5305              });
5306  
5307              control.container.on( 'render-screenshot', function() {
5308                  var $screenshot = $( this ).find( 'img' ),
5309                      source = $screenshot.data( 'src' );
5310  
5311                  if ( source ) {
5312                      $screenshot.attr( 'src', source );
5313                  }
5314                  control.screenshotRendered = true;
5315              });
5316          },
5317  
5318          /**
5319           * Show or hide the theme based on the presence of the term in the title, description, tags, and author.
5320           *
5321           * @since 4.2.0
5322           * @param {string[]} terms An array of terms to search for.
5323           * @return {boolean} Whether a theme control was activated or not.
5324           */
5325          filter: function( terms ) {
5326              var control = this,
5327                  matchCount = 0,
5328                  haystack = control.params.theme.name + ' ' +
5329                      control.params.theme.description + ' ' +
5330                      control.params.theme.tags + ' ' +
5331                      control.params.theme.author + ' ';
5332              haystack = haystack.toLowerCase().replace( '-', ' ' );
5333  
5334              // Back-compat for behavior in WordPress 4.2.0 to 4.8.X.
5335              if ( ! _.isArray( terms ) ) {
5336                  terms = [ terms ];
5337              }
5338  
5339              // Always give exact name matches highest ranking.
5340              if ( control.params.theme.name.toLowerCase() === terms.join( ' ' ) ) {
5341                  matchCount = 100;
5342              } else {
5343  
5344                  // Search for and weight (by 10) complete term matches.
5345                  matchCount = matchCount + 10 * ( haystack.split( terms.join( ' ' ) ).length - 1 );
5346  
5347                  // Search for each term individually (as whole-word and partial match) and sum weighted match counts.
5348                  _.each( terms, function( term ) {
5349                      matchCount = matchCount + 2 * ( haystack.split( term + ' ' ).length - 1 ); // Whole-word, double-weighted.
5350                      matchCount = matchCount + haystack.split( term ).length - 1; // Partial word, to minimize empty intermediate searches while typing.
5351                  });
5352  
5353                  // Upper limit on match ranking.
5354                  if ( matchCount > 99 ) {
5355                      matchCount = 99;
5356                  }
5357              }
5358  
5359              if ( 0 !== matchCount ) {
5360                  control.activate();
5361                  control.params.priority = 101 - matchCount; // Sort results by match count.
5362                  return true;
5363              } else {
5364                  control.deactivate(); // Hide control.
5365                  control.params.priority = 101;
5366                  return false;
5367              }
5368          },
5369  
5370          /**
5371           * Rerender the theme from its JS template with the installed type.
5372           *
5373           * @param {boolean} installed Whether the theme is installed.
5374           * @since 4.9.0
5375           *
5376           * @return {void}
5377           */
5378          rerenderAsInstalled: function( installed ) {
5379              var control = this, section;
5380              if ( installed ) {
5381                  control.params.theme.type = 'installed';
5382              } else {
5383                  section = api.section( control.params.section );
5384                  control.params.theme.type = section.params.action;
5385              }
5386              control.renderContent(); // Replaces existing content.
5387              control.container.trigger( 'render-screenshot' );
5388          }
5389      });
5390  
5391      /**
5392       * Class wp.customize.CodeEditorControl
5393       *
5394       * @since 4.9.0
5395       *
5396       * @class    wp.customize.CodeEditorControl
5397       * @augments wp.customize.Control
5398       */
5399      api.CodeEditorControl = api.Control.extend(/** @lends wp.customize.CodeEditorControl.prototype */{
5400  
5401          /**
5402           * Initialize.
5403           *
5404           * @since 4.9.0
5405           * @param {string} id      Unique identifier for the control instance.
5406           * @param {Object} options Options hash for the control instance.
5407           * @return {void}
5408           */
5409          initialize: function( id, options ) {
5410              var control = this;
5411              control.deferred = _.extend( control.deferred || {}, {
5412                  codemirror: $.Deferred()
5413              } );
5414              api.Control.prototype.initialize.call( control, id, options );
5415  
5416              // Note that rendering is debounced so the props will be used when rendering happens after add event.
5417              control.notifications.bind( 'add', function( notification ) {
5418  
5419                  // Skip if control notification is not from setting csslint_error notification.
5420                  if ( notification.code !== control.setting.id + ':csslint_error' ) {
5421                      return;
5422                  }
5423  
5424                  // Customize the template and behavior of csslint_error notifications.
5425                  notification.templateId = 'customize-code-editor-lint-error-notification';
5426                  notification.render = (function( render ) {
5427                      return function() {
5428                          var li = render.call( this );
5429                          li.find( 'input[type=checkbox]' ).on( 'click', function() {
5430                              control.setting.notifications.remove( 'csslint_error' );
5431                          } );
5432                          return li;
5433                      };
5434                  })( notification.render );
5435              } );
5436          },
5437  
5438          /**
5439           * Initialize the editor when the containing section is ready and expanded.
5440           *
5441           * @since 4.9.0
5442           * @return {void}
5443           */
5444          ready: function() {
5445              var control = this;
5446              if ( ! control.section() ) {
5447                  control.initEditor();
5448                  return;
5449              }
5450  
5451              // Wait to initialize editor until section is embedded and expanded.
5452              api.section( control.section(), function( section ) {
5453                  section.deferred.embedded.done( function() {
5454                      var onceExpanded;
5455                      if ( section.expanded() ) {
5456                          control.initEditor();
5457                      } else {
5458                          onceExpanded = function( isExpanded ) {
5459                              if ( isExpanded ) {
5460                                  control.initEditor();
5461                                  section.expanded.unbind( onceExpanded );
5462                              }
5463                          };
5464                          section.expanded.bind( onceExpanded );
5465                      }
5466                  } );
5467              } );
5468          },
5469  
5470          /**
5471           * Initialize editor.
5472           *
5473           * @since 4.9.0
5474           * @return {void}
5475           */
5476          initEditor: function() {
5477              var control = this, element, editorSettings = false;
5478  
5479              // Obtain editorSettings for instantiation.
5480              if ( wp.codeEditor && ( _.isUndefined( control.params.editor_settings ) || false !== control.params.editor_settings ) ) {
5481  
5482                  // Obtain default editor settings.
5483                  editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {};
5484                  editorSettings.codemirror = _.extend(
5485                      {},
5486                      editorSettings.codemirror,
5487                      {
5488                          indentUnit: 2,
5489                          tabSize: 2
5490                      }
5491                  );
5492  
5493                  // Merge editor_settings param on top of defaults.
5494                  if ( _.isObject( control.params.editor_settings ) ) {
5495                      _.each( control.params.editor_settings, function( value, key ) {
5496                          if ( _.isObject( value ) ) {
5497                              editorSettings[ key ] = _.extend(
5498                                  {},
5499                                  editorSettings[ key ],
5500                                  value
5501                              );
5502                          }
5503                      } );
5504                  }
5505              }
5506  
5507              element = new api.Element( control.container.find( 'textarea' ) );
5508              control.elements.push( element );
5509              element.sync( control.setting );
5510              element.set( control.setting() );
5511  
5512              if ( editorSettings ) {
5513                  control.initSyntaxHighlightingEditor( editorSettings );
5514              } else {
5515                  control.initPlainTextareaEditor();
5516              }
5517          },
5518  
5519          /**
5520           * Make sure editor gets focused when control is focused.
5521           *
5522           * @since 4.9.0
5523           * @param {Object}   [params]                  Focus params.
5524           * @param {Function} [params.completeCallback] Function to call when expansion is complete.
5525           * @return {void}
5526           */
5527          focus: function( params ) {
5528              var control = this, extendedParams = _.extend( {}, params ), originalCompleteCallback;
5529              originalCompleteCallback = extendedParams.completeCallback;
5530              extendedParams.completeCallback = function() {
5531                  if ( originalCompleteCallback ) {
5532                      originalCompleteCallback();
5533                  }
5534                  if ( control.editor ) {
5535                      control.editor.codemirror.focus();
5536                  }
5537              };
5538              api.Control.prototype.focus.call( control, extendedParams );
5539          },
5540  
5541          /**
5542           * Initialize syntax-highlighting editor.
5543           *
5544           * @since 4.9.0
5545           * @param {Object} codeEditorSettings Code editor settings.
5546           * @return {void}
5547           */
5548          initSyntaxHighlightingEditor: function( codeEditorSettings ) {
5549              var control = this, $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false;
5550  
5551              settings = _.extend( {}, codeEditorSettings, {
5552                  onTabNext: _.bind( control.onTabNext, control ),
5553                  onTabPrevious: _.bind( control.onTabPrevious, control ),
5554                  onUpdateErrorNotice: _.bind( control.onUpdateErrorNotice, control )
5555              });
5556  
5557              control.editor = wp.codeEditor.initialize( $textarea, settings );
5558  
5559              // Improve the editor accessibility.
5560              $( control.editor.codemirror.display.lineDiv )
5561                  .attr({
5562                      role: 'textbox',
5563                      'aria-multiline': 'true',
5564                      'aria-label': control.params.label,
5565                      'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4'
5566                  });
5567  
5568              // Focus the editor when clicking on its label.
5569              control.container.find( 'label' ).on( 'click', function() {
5570                  control.editor.codemirror.focus();
5571              });
5572  
5573              /*
5574               * When the CodeMirror instance changes, mirror to the textarea,
5575               * where we have our "true" change event handler bound.
5576               */
5577              control.editor.codemirror.on( 'change', function( codemirror ) {
5578                  suspendEditorUpdate = true;
5579                  $textarea.val( codemirror.getValue() ).trigger( 'change' );
5580                  suspendEditorUpdate = false;
5581              });
5582  
5583              // Update CodeMirror when the setting is changed by another plugin.
5584              control.setting.bind( function( value ) {
5585                  if ( ! suspendEditorUpdate ) {
5586                      control.editor.codemirror.setValue( value );
5587                  }
5588              });
5589  
5590              // Prevent collapsing section when hitting Esc to tab out of editor.
5591              control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
5592                  var escKeyCode = 27;
5593                  if ( escKeyCode === event.keyCode ) {
5594                      event.stopPropagation();
5595                  }
5596              });
5597  
5598              control.deferred.codemirror.resolveWith( control, [ control.editor.codemirror ] );
5599          },
5600  
5601          /**
5602           * Handle tabbing to the field after the editor.
5603           *
5604           * @since 4.9.0
5605           * @return {void}
5606           */
5607          onTabNext: function onTabNext() {
5608              var control = this, controls, controlIndex, section;
5609              section = api.section( control.section() );
5610              controls = section.controls();
5611              controlIndex = controls.indexOf( control );
5612              if ( controls.length === controlIndex + 1 ) {
5613                  $( '#customize-footer-actions .collapse-sidebar' ).trigger( 'focus' );
5614              } else {
5615                  controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus();
5616              }
5617          },
5618  
5619          /**
5620           * Handle tabbing to the field before the editor.
5621           *
5622           * @since 4.9.0
5623           * @return {void}
5624           */
5625          onTabPrevious: function onTabPrevious() {
5626              var control = this, controls, controlIndex, section;
5627              section = api.section( control.section() );
5628              controls = section.controls();
5629              controlIndex = controls.indexOf( control );
5630              if ( 0 === controlIndex ) {
5631                  section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus();
5632              } else {
5633                  controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus();
5634              }
5635          },
5636  
5637          /**
5638           * Update error notice.
5639           *
5640           * @since 4.9.0
5641           * @param {Object[]} errorAnnotations Error annotations.
5642           * @return {void}
5643           */
5644          onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
5645              var control = this, message;
5646              control.setting.notifications.remove( 'csslint_error' );
5647  
5648              if ( 0 !== errorAnnotations.length ) {
5649                  if ( 1 === errorAnnotations.length ) {
5650                      message = api.l10n.customCssError.singular.replace( '%d', '1' );
5651                  } else {
5652                      message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) );
5653                  }
5654                  control.setting.notifications.add( new api.Notification( 'csslint_error', {
5655                      message: message,
5656                      type: 'error'
5657                  } ) );
5658              }
5659          },
5660  
5661          /**
5662           * Initialize plain-textarea editor when syntax highlighting is disabled.
5663           *
5664           * @since 4.9.0
5665           * @return {void}
5666           */
5667          initPlainTextareaEditor: function() {
5668              var control = this, $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
5669  
5670              $textarea.on( 'blur', function onBlur() {
5671                  $textarea.data( 'next-tab-blurs', false );
5672              } );
5673  
5674              $textarea.on( 'keydown', function onKeydown( event ) {
5675                  var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
5676  
5677                  if ( escKeyCode === event.keyCode ) {
5678                      if ( ! $textarea.data( 'next-tab-blurs' ) ) {
5679                          $textarea.data( 'next-tab-blurs', true );
5680                          event.stopPropagation(); // Prevent collapsing the section.
5681                      }
5682                      return;
5683                  }
5684  
5685                  // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
5686                  if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
5687                      return;
5688                  }
5689  
5690                  // Prevent capturing Tab characters if Esc was pressed.
5691                  if ( $textarea.data( 'next-tab-blurs' ) ) {
5692                      return;
5693                  }
5694  
5695                  selectionStart = textarea.selectionStart;
5696                  selectionEnd = textarea.selectionEnd;
5697                  value = textarea.value;
5698  
5699                  if ( selectionStart >= 0 ) {
5700                      textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
5701                      $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
5702                  }
5703  
5704                  event.stopPropagation();
5705                  event.preventDefault();
5706              });
5707  
5708              control.deferred.codemirror.rejectWith( control );
5709          }
5710      });
5711  
5712      /**
5713       * Class wp.customize.DateTimeControl.
5714       *
5715       * @since 4.9.0
5716       * @class    wp.customize.DateTimeControl
5717       * @augments wp.customize.Control
5718       */
5719      api.DateTimeControl = api.Control.extend(/** @lends wp.customize.DateTimeControl.prototype */{
5720  
5721          /**
5722           * Initialize behaviors.
5723           *
5724           * @since 4.9.0
5725           * @return {void}
5726           */
5727          ready: function ready() {
5728              var control = this;
5729  
5730              control.inputElements = {};
5731              control.invalidDate = false;
5732  
5733              _.bindAll( control, 'populateSetting', 'updateDaysForMonth', 'populateDateInputs' );
5734  
5735              if ( ! control.setting ) {
5736                  throw new Error( 'Missing setting' );
5737              }
5738  
5739              control.container.find( '.date-input' ).each( function() {
5740                  var input = $( this ), component, element;
5741                  component = input.data( 'component' );
5742                  element = new api.Element( input );
5743                  control.inputElements[ component ] = element;
5744                  control.elements.push( element );
5745  
5746                  // Add invalid date error once user changes (and has blurred the input).
5747                  input.on( 'change', function() {
5748                      if ( control.invalidDate ) {
5749                          control.notifications.add( new api.Notification( 'invalid_date', {
5750                              message: api.l10n.invalidDate
5751                          } ) );
5752                      }
5753                  } );
5754  
5755                  // Remove the error immediately after validity change.
5756                  input.on( 'input', _.debounce( function() {
5757                      if ( ! control.invalidDate ) {
5758                          control.notifications.remove( 'invalid_date' );
5759                      }
5760                  } ) );
5761  
5762                  // Add zero-padding when blurring field.
5763                  input.on( 'blur', _.debounce( function() {
5764                      if ( ! control.invalidDate ) {
5765                          control.populateDateInputs();
5766                      }
5767                  } ) );
5768              } );
5769  
5770              control.inputElements.month.bind( control.updateDaysForMonth );
5771              control.inputElements.year.bind( control.updateDaysForMonth );
5772              control.populateDateInputs();
5773              control.setting.bind( control.populateDateInputs );
5774  
5775              // Start populating setting after inputs have been populated.
5776              _.each( control.inputElements, function( element ) {
5777                  element.bind( control.populateSetting );
5778              } );
5779          },
5780  
5781          /**
5782           * Parse datetime string.
5783           *
5784           * @since 4.9.0
5785           *
5786           * @param {string} datetime Date/Time string. Accepts Y-m-d[ H:i[:s]] format.
5787           * @return {Object|null} Returns object containing date components or null if parse error.
5788           */
5789          parseDateTime: function parseDateTime( datetime ) {
5790              var control = this, matches, date, midDayHour = 12;
5791  
5792              if ( datetime ) {
5793                  matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d)(?: (\d\d):(\d\d)(?::(\d\d))?)?$/ );
5794              }
5795  
5796              if ( ! matches ) {
5797                  return null;
5798              }
5799  
5800              matches.shift();
5801  
5802              date = {
5803                  year: matches.shift(),
5804                  month: matches.shift(),
5805                  day: matches.shift(),
5806                  hour: matches.shift() || '00',
5807                  minute: matches.shift() || '00',
5808                  second: matches.shift() || '00'
5809              };
5810  
5811              if ( control.params.includeTime && control.params.twelveHourFormat ) {
5812                  date.hour = parseInt( date.hour, 10 );
5813                  date.meridian = date.hour >= midDayHour ? 'pm' : 'am';
5814                  date.hour = date.hour % midDayHour ? String( date.hour % midDayHour ) : String( midDayHour );
5815                  delete date.second; // @todo Why only if twelveHourFormat?
5816              }
5817  
5818              return date;
5819          },
5820  
5821          /**
5822           * Validates if input components have valid date and time.
5823           *
5824           * @since 4.9.0
5825           * @return {boolean} If date input fields has error.
5826           */
5827          validateInputs: function validateInputs() {
5828              var control = this, components, validityInput;
5829  
5830              control.invalidDate = false;
5831  
5832              components = [ 'year', 'day' ];
5833              if ( control.params.includeTime ) {
5834                  components.push( 'hour', 'minute' );
5835              }
5836  
5837              _.find( components, function( component ) {
5838                  var element, max, min, value;
5839  
5840                  element = control.inputElements[ component ];
5841                  validityInput = element.element.get( 0 );
5842                  max = parseInt( element.element.attr( 'max' ), 10 );
5843                  min = parseInt( element.element.attr( 'min' ), 10 );
5844                  value = parseInt( element(), 10 );
5845                  control.invalidDate = isNaN( value ) || value > max || value < min;
5846  
5847                  if ( ! control.invalidDate ) {
5848                      validityInput.setCustomValidity( '' );
5849                  }
5850  
5851                  return control.invalidDate;
5852              } );
5853  
5854              if ( control.inputElements.meridian && ! control.invalidDate ) {
5855                  validityInput = control.inputElements.meridian.element.get( 0 );
5856                  if ( 'am' !== control.inputElements.meridian.get() && 'pm' !== control.inputElements.meridian.get() ) {
5857                      control.invalidDate = true;
5858                  } else {
5859                      validityInput.setCustomValidity( '' );
5860                  }
5861              }
5862  
5863              if ( control.invalidDate ) {
5864                  validityInput.setCustomValidity( api.l10n.invalidValue );
5865              } else {
5866                  validityInput.setCustomValidity( '' );
5867              }
5868              if ( ! control.section() || api.section.has( control.section() ) && api.section( control.section() ).expanded() ) {
5869                  _.result( validityInput, 'reportValidity' );
5870              }
5871  
5872              return control.invalidDate;
5873          },
5874  
5875          /**
5876           * Updates number of days according to the month and year selected.
5877           *
5878           * @since 4.9.0
5879           * @return {void}
5880           */
5881          updateDaysForMonth: function updateDaysForMonth() {
5882              var control = this, daysInMonth, year, month, day;
5883  
5884              month = parseInt( control.inputElements.month(), 10 );
5885              year = parseInt( control.inputElements.year(), 10 );
5886              day = parseInt( control.inputElements.day(), 10 );
5887  
5888              if ( month && year ) {
5889                  daysInMonth = new Date( year, month, 0 ).getDate();
5890                  control.inputElements.day.element.attr( 'max', daysInMonth );
5891  
5892                  if ( day > daysInMonth ) {
5893                      control.inputElements.day( String( daysInMonth ) );
5894                  }
5895              }
5896          },
5897  
5898          /**
5899           * Populate setting value from the inputs.
5900           *
5901           * @since 4.9.0
5902           * @return {boolean} If setting updated.
5903           */
5904          populateSetting: function populateSetting() {
5905              var control = this, date;
5906  
5907              if ( control.validateInputs() || ! control.params.allowPastDate && ! control.isFutureDate() ) {
5908                  return false;
5909              }
5910  
5911              date = control.convertInputDateToString();
5912              control.setting.set( date );
5913              return true;
5914          },
5915  
5916          /**
5917           * Converts input values to string in Y-m-d H:i:s format.
5918           *
5919           * @since 4.9.0
5920           * @return {string} Date string.
5921           */
5922          convertInputDateToString: function convertInputDateToString() {
5923              var control = this, date = '', dateFormat, hourInTwentyFourHourFormat,
5924                  getElementValue, pad;
5925  
5926              pad = function( number, padding ) {
5927                  var zeros;
5928                  if ( String( number ).length < padding ) {
5929                      zeros = padding - String( number ).length;
5930                      number = Math.pow( 10, zeros ).toString().substr( 1 ) + String( number );
5931                  }
5932                  return number;
5933              };
5934  
5935              getElementValue = function( component ) {
5936                  var value = parseInt( control.inputElements[ component ].get(), 10 );
5937  
5938                  if ( _.contains( [ 'month', 'day', 'hour', 'minute' ], component ) ) {
5939                      value = pad( value, 2 );
5940                  } else if ( 'year' === component ) {
5941                      value = pad( value, 4 );
5942                  }
5943                  return value;
5944              };
5945  
5946              dateFormat = [ 'year', '-', 'month', '-', 'day' ];
5947              if ( control.params.includeTime ) {
5948                  hourInTwentyFourHourFormat = control.inputElements.meridian ? control.convertHourToTwentyFourHourFormat( control.inputElements.hour(), control.inputElements.meridian() ) : control.inputElements.hour();
5949                  dateFormat = dateFormat.concat( [ ' ', pad( hourInTwentyFourHourFormat, 2 ), ':', 'minute', ':', '00' ] );
5950              }
5951  
5952              _.each( dateFormat, function( component ) {
5953                  date += control.inputElements[ component ] ? getElementValue( component ) : component;
5954              } );
5955  
5956              return date;
5957          },
5958  
5959          /**
5960           * Check if the date is in the future.
5961           *
5962           * @since 4.9.0
5963           * @return {boolean} True if future date.
5964           */
5965          isFutureDate: function isFutureDate() {
5966              var control = this;
5967              return 0 < api.utils.getRemainingTime( control.convertInputDateToString() );
5968          },
5969  
5970          /**
5971           * Convert hour in twelve hour format to twenty four hour format.
5972           *
5973           * @since 4.9.0
5974           * @param {string} hourInTwelveHourFormat Hour in twelve hour format.
5975           * @param {string} meridian               Either 'am' or 'pm'.
5976           * @return {string} Hour in twenty four hour format.
5977           */
5978          convertHourToTwentyFourHourFormat: function convertHour( hourInTwelveHourFormat, meridian ) {
5979              var hourInTwentyFourHourFormat, hour, midDayHour = 12;
5980  
5981              hour = parseInt( hourInTwelveHourFormat, 10 );
5982              if ( isNaN( hour ) ) {
5983                  return '';
5984              }
5985  
5986              if ( 'pm' === meridian && hour < midDayHour ) {
5987                  hourInTwentyFourHourFormat = hour + midDayHour;
5988              } else if ( 'am' === meridian && midDayHour === hour ) {
5989                  hourInTwentyFourHourFormat = hour - midDayHour;
5990              } else {
5991                  hourInTwentyFourHourFormat = hour;
5992              }
5993  
5994              return String( hourInTwentyFourHourFormat );
5995          },
5996  
5997          /**
5998           * Populates date inputs in date fields.
5999           *
6000           * @since 4.9.0
6001           * @return {boolean} Whether the inputs were populated.
6002           */
6003          populateDateInputs: function populateDateInputs() {
6004              var control = this, parsed;
6005  
6006              parsed = control.parseDateTime( control.setting.get() );
6007  
6008              if ( ! parsed ) {
6009                  return false;
6010              }
6011  
6012              _.each( control.inputElements, function( element, component ) {
6013                  var value = parsed[ component ]; // This will be zero-padded string.
6014  
6015                  // Set month and meridian regardless of focused state since they are dropdowns.
6016                  if ( 'month' === component || 'meridian' === component ) {
6017  
6018                      // Options in dropdowns are not zero-padded.
6019                      value = value.replace( /^0/, '' );
6020  
6021                      element.set( value );
6022                  } else {
6023  
6024                      value = parseInt( value, 10 );
6025                      if ( ! element.element.is( document.activeElement ) ) {
6026  
6027                          // Populate element with zero-padded value if not focused.
6028                          element.set( parsed[ component ] );
6029                      } else if ( value !== parseInt( element(), 10 ) ) {
6030  
6031                          // Forcibly update the value if its underlying value changed, regardless of zero-padding.
6032                          element.set( String( value ) );
6033                      }
6034                  }
6035              } );
6036  
6037              return true;
6038          },
6039  
6040          /**
6041           * Toggle future date notification for date control.
6042           *
6043           * @since 4.9.0
6044           * @param {boolean} notify Add or remove the notification.
6045           * @return {wp.customize.DateTimeControl} The date control instance.
6046           */
6047          toggleFutureDateNotification: function toggleFutureDateNotification( notify ) {
6048              var control = this, notificationCode, notification;
6049  
6050              notificationCode = 'not_future_date';
6051  
6052              if ( notify ) {
6053                  notification = new api.Notification( notificationCode, {
6054                      type: 'error',
6055                      message: api.l10n.futureDateError
6056                  } );
6057                  control.notifications.add( notification );
6058              } else {
6059                  control.notifications.remove( notificationCode );
6060              }
6061  
6062              return control;
6063          }
6064      });
6065  
6066      /**
6067       * Class PreviewLinkControl.
6068       *
6069       * @since 4.9.0
6070       * @class    wp.customize.PreviewLinkControl
6071       * @augments wp.customize.Control
6072       */
6073      api.PreviewLinkControl = api.Control.extend(/** @lends wp.customize.PreviewLinkControl.prototype */{
6074  
6075          defaults: _.extend( {}, api.Control.prototype.defaults, {
6076              templateId: 'customize-preview-link-control'
6077          } ),
6078  
6079          /**
6080           * Initialize behaviors.
6081           *
6082           * @since 4.9.0
6083           * @return {void}
6084           */
6085          ready: function ready() {
6086              var control = this, element, component, node, url, input, button;
6087  
6088              _.bindAll( control, 'updatePreviewLink' );
6089  
6090              if ( ! control.setting ) {
6091                  control.setting = new api.Value();
6092              }
6093  
6094              control.previewElements = {};
6095  
6096              control.container.find( '.preview-control-element' ).each( function() {
6097                  node = $( this );
6098                  component = node.data( 'component' );
6099                  element = new api.Element( node );
6100                  control.previewElements[ component ] = element;
6101                  control.elements.push( element );
6102              } );
6103  
6104              url = control.previewElements.url;
6105              input = control.previewElements.input;
6106              button = control.previewElements.button;
6107  
6108              input.link( control.setting );
6109              url.link( control.setting );
6110  
6111              url.bind( function( value ) {
6112                  url.element.parent().attr( {
6113                      href: value,
6114                      target: api.settings.changeset.uuid
6115                  } );
6116              } );
6117  
6118              api.bind( 'ready', control.updatePreviewLink );
6119              api.state( 'saved' ).bind( control.updatePreviewLink );
6120              api.state( 'changesetStatus' ).bind( control.updatePreviewLink );
6121              api.state( 'activated' ).bind( control.updatePreviewLink );
6122              api.previewer.previewUrl.bind( control.updatePreviewLink );
6123  
6124              button.element.on( 'click', function( event ) {
6125                  event.preventDefault();
6126                  if ( control.setting() ) {
6127                      input.element.select();
6128                      document.execCommand( 'copy' );
6129                      button( button.element.data( 'copied-text' ) );
6130                  }
6131              } );
6132  
6133              url.element.parent().on( 'click', function( event ) {
6134                  if ( $( this ).hasClass( 'disabled' ) ) {
6135                      event.preventDefault();
6136                  }
6137              } );
6138  
6139              button.element.on( 'mouseenter', function() {
6140                  if ( control.setting() ) {
6141                      button( button.element.data( 'copy-text' ) );
6142                  }
6143              } );
6144          },
6145  
6146          /**
6147           * Updates Preview Link
6148           *
6149           * @since 4.9.0
6150           * @return {void}
6151           */
6152          updatePreviewLink: function updatePreviewLink() {
6153              var control = this, unsavedDirtyValues;
6154  
6155              unsavedDirtyValues = ! api.state( 'saved' ).get() || '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get();
6156  
6157              control.toggleSaveNotification( unsavedDirtyValues );
6158              control.previewElements.url.element.parent().toggleClass( 'disabled', unsavedDirtyValues );
6159              control.previewElements.button.element.prop( 'disabled', unsavedDirtyValues );
6160              control.setting.set( api.previewer.getFrontendPreviewUrl() );
6161          },
6162  
6163          /**
6164           * Toggles save notification.
6165           *
6166           * @since 4.9.0
6167           * @param {boolean} notify Add or remove notification.
6168           * @return {void}
6169           */
6170          toggleSaveNotification: function toggleSaveNotification( notify ) {
6171              var control = this, notificationCode, notification;
6172  
6173              notificationCode = 'changes_not_saved';
6174  
6175              if ( notify ) {
6176                  notification = new api.Notification( notificationCode, {
6177                      type: 'info',
6178                      message: api.l10n.saveBeforeShare
6179                  } );
6180                  control.notifications.add( notification );
6181              } else {
6182                  control.notifications.remove( notificationCode );
6183              }
6184          }
6185      });
6186  
6187      /**
6188       * Change objects contained within the main customize object to Settings.
6189       *
6190       * @alias wp.customize.defaultConstructor
6191       */
6192      api.defaultConstructor = api.Setting;
6193  
6194      /**
6195       * Callback for resolved controls.
6196       *
6197       * @callback wp.customize.deferredControlsCallback
6198       * @param {wp.customize.Control[]} controls Resolved controls.
6199       */
6200  
6201      /**
6202       * Collection of all registered controls.
6203       *
6204       * @alias wp.customize.control
6205       *
6206       * @since 3.4.0
6207       *
6208       * @type {Function}
6209       * @param {...string}                             ids        One or more ids for controls to obtain.
6210       * @param {wp.customize.deferredControlsCallback} [callback] Function called when all supplied controls exist.
6211       * @return {wp.customize.Control|undefined|JQuery.Promise<*>} Control instance or undefined (if function called with one id param),
6212       *                                                            or promise resolving to requested controls.
6213       *
6214       * @example <caption>Loop over all registered controls.</caption>
6215       * wp.customize.control.each( function( control ) { ... } );
6216       *
6217       * @example <caption>Getting `background_color` control instance.</caption>
6218       * control = wp.customize.control( 'background_color' );
6219       *
6220       * @example <caption>Check if control exists.</caption>
6221       * hasControl = wp.customize.control.has( 'background_color' );
6222       *
6223       * @example <caption>Deferred getting of `background_color` control until it exists, using callback.</caption>
6224       * wp.customize.control( 'background_color', function( control ) { ... } );
6225       *
6226       * @example <caption>Get title and tagline controls when they both exist, using promise (only available when multiple IDs are present).</caption>
6227       * promise = wp.customize.control( 'blogname', 'blogdescription' );
6228       * promise.done( function( titleControl, taglineControl ) { ... } );
6229       *
6230       * @example <caption>Get title and tagline controls when they both exist, using callback.</caption>
6231       * wp.customize.control( 'blogname', 'blogdescription', function( titleControl, taglineControl ) { ... } );
6232       *
6233       * @example <caption>Getting setting value for `background_color` control.</caption>
6234       * value = wp.customize.control( 'background_color ').setting.get();
6235       * value = wp.customize( 'background_color' ).get(); // Same as above, since setting ID and control ID are the same.
6236       *
6237       * @example <caption>Add new control for site title.</caption>
6238       * wp.customize.control.add( new wp.customize.Control( 'other_blogname', {
6239       *     setting: 'blogname',
6240       *     type: 'text',
6241       *     label: 'Site title',
6242       *     section: 'other_site_identify'
6243       * } ) );
6244       *
6245       * @example <caption>Remove control.</caption>
6246       * wp.customize.control.remove( 'other_blogname' );
6247       *
6248       * @example <caption>Listen for control being added.</caption>
6249       * wp.customize.control.bind( 'add', function( addedControl ) { ... } )
6250       *
6251       * @example <caption>Listen for control being removed.</caption>
6252       * wp.customize.control.bind( 'removed', function( removedControl ) { ... } )
6253       */
6254      api.control = new api.Values({ defaultConstructor: api.Control });
6255  
6256      /**
6257       * Callback for resolved sections.
6258       *
6259       * @callback wp.customize.deferredSectionsCallback
6260       * @param {wp.customize.Section[]} sections Resolved sections.
6261       */
6262  
6263      /**
6264       * Collection of all registered sections.
6265       *
6266       * @alias wp.customize.section
6267       *
6268       * @since 3.4.0
6269       *
6270       * @type {Function}
6271       * @param {...string}                             ids        One or more ids for sections to obtain.
6272       * @param {wp.customize.deferredSectionsCallback} [callback] Function called when all supplied sections exist.
6273       * @return {wp.customize.Section|undefined|JQuery.Promise<*>} Section instance or undefined (if function called with one id param),
6274       *                                                            or promise resolving to requested sections.
6275       *
6276       * @example <caption>Loop over all registered sections.</caption>
6277       * wp.customize.section.each( function( section ) { ... } )
6278       *
6279       * @example <caption>Getting `title_tagline` section instance.</caption>
6280       * section = wp.customize.section( 'title_tagline' )
6281       *
6282       * @example <caption>Expand dynamically-created section when it exists.</caption>
6283       * wp.customize.section( 'dynamically_created', function( section ) {
6284       *     section.expand();
6285       * } );
6286       *
6287       * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
6288       */
6289      api.section = new api.Values({ defaultConstructor: api.Section });
6290  
6291      /**
6292       * Callback for resolved panels.
6293       *
6294       * @callback wp.customize.deferredPanelsCallback
6295       * @param {wp.customize.Panel[]} panels Resolved panels.
6296       */
6297  
6298      /**
6299       * Collection of all registered panels.
6300       *
6301       * @alias wp.customize.panel
6302       *
6303       * @since 4.0.0
6304       *
6305       * @type {Function}
6306       * @param {...string}                           ids        One or more ids for panels to obtain.
6307       * @param {wp.customize.deferredPanelsCallback} [callback] Function called when all supplied panels exist.
6308       * @return {wp.customize.Panel|undefined|JQuery.Promise<*>} Panel instance or undefined (if function called with one id param),
6309       *                                                          or promise resolving to requested panels.
6310       *
6311       * @example <caption>Loop over all registered panels.</caption>
6312       * wp.customize.panel.each( function( panel ) { ... } )
6313       *
6314       * @example <caption>Getting nav_menus panel instance.</caption>
6315       * panel = wp.customize.panel( 'nav_menus' );
6316       *
6317       * @example <caption>Expand dynamically-created panel when it exists.</caption>
6318       * wp.customize.panel( 'dynamically_created', function( panel ) {
6319       *     panel.expand();
6320       * } );
6321       *
6322       * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
6323       */
6324      api.panel = new api.Values({ defaultConstructor: api.Panel });
6325  
6326      /**
6327       * Callback for resolved notifications.
6328       *
6329       * @callback wp.customize.deferredNotificationsCallback
6330       * @param {wp.customize.Notification[]} notifications Resolved notifications.
6331       */
6332  
6333      /**
6334       * Collection of all global notifications.
6335       *
6336       * @alias wp.customize.notifications
6337       *
6338       * @since 4.9.0
6339       *
6340       * @type {Function}
6341       * @param {...string}                                  codes      One or more codes for notifications to obtain.
6342       * @param {wp.customize.deferredNotificationsCallback} [callback] Function called when all supplied notifications exist.
6343       * @return {wp.customize.Notification|undefined|JQuery.Promise<*>} Notification instance or undefined (if function called with one code param),
6344       *                                                                 or promise resolving to requested notifications.
6345       *
6346       * @example <caption>Check if existing notification</caption>
6347       * exists = wp.customize.notifications.has( 'a_new_day_arrived' );
6348       *
6349       * @example <caption>Obtain existing notification</caption>
6350       * notification = wp.customize.notifications( 'a_new_day_arrived' );
6351       *
6352       * @example <caption>Obtain notification that may not exist yet.</caption>
6353       * wp.customize.notifications( 'a_new_day_arrived', function( notification ) { ... } );
6354       *
6355       * @example <caption>Add a warning notification.</caption>
6356       * wp.customize.notifications.add( new wp.customize.Notification( 'midnight_almost_here', {
6357       *     type: 'warning',
6358       *     message: 'Midnight has almost arrived!',
6359       *     dismissible: true
6360       * } ) );
6361       *
6362       * @example <caption>Remove a notification.</caption>
6363       * wp.customize.notifications.remove( 'a_new_day_arrived' );
6364       *
6365       * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
6366       */
6367      api.notifications = new api.Notifications();
6368  
6369      api.PreviewFrame = api.Messenger.extend(/** @lends wp.customize.PreviewFrame.prototype */{
6370          sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity.
6371  
6372          /**
6373           * An object that fetches a preview in the background of the document, which
6374           * allows for seamless replacement of an existing preview.
6375           *
6376           * @constructs wp.customize.PreviewFrame
6377           * @augments   wp.customize.Messenger
6378           *
6379           * @param {Object} params            The parameters object.
6380           * @param {Object} params.container  The container element for the preview frame.
6381           * @param {string} params.previewUrl The URL of the preview.
6382           * @param {Object} params.query      The query parameters for the preview URL.
6383           * @param {Object} options           The options object.
6384           */
6385          initialize: function( params, options ) {
6386              var deferred = $.Deferred();
6387  
6388              /*
6389               * Make the instance of the PreviewFrame the promise object
6390               * so other objects can easily interact with it.
6391               */
6392              deferred.promise( this );
6393  
6394              this.container = params.container;
6395  
6396              $.extend( params, { channel: api.PreviewFrame.uuid() });
6397  
6398              api.Messenger.prototype.initialize.call( this, params, options );
6399  
6400              this.add( 'previewUrl', params.previewUrl );
6401  
6402              this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
6403  
6404              this.run( deferred );
6405          },
6406  
6407          /**
6408           * Run the preview request.
6409           *
6410           * @param {Object} deferred jQuery Deferred object to be resolved with
6411           *                          the request.
6412           */
6413          run: function( deferred ) {
6414              var previewFrame = this,
6415                  loaded = false,
6416                  ready = false,
6417                  readyData = null,
6418                  hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized,
6419                  urlParser,
6420                  params,
6421                  form;
6422  
6423              if ( previewFrame._ready ) {
6424                  previewFrame.unbind( 'ready', previewFrame._ready );
6425              }
6426  
6427              previewFrame._ready = function( data ) {
6428                  ready = true;
6429                  readyData = data;
6430                  previewFrame.container.addClass( 'iframe-ready' );
6431                  if ( ! data ) {
6432                      return;
6433                  }
6434  
6435                  if ( loaded ) {
6436                      deferred.resolveWith( previewFrame, [ data ] );
6437                  }
6438              };
6439  
6440              previewFrame.bind( 'ready', previewFrame._ready );
6441  
6442              urlParser = document.createElement( 'a' );
6443              urlParser.href = previewFrame.previewUrl();
6444  
6445              params = _.extend(
6446                  api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
6447                  {
6448                      customize_changeset_uuid: previewFrame.query.customize_changeset_uuid,
6449                      customize_theme: previewFrame.query.customize_theme,
6450                      customize_messenger_channel: previewFrame.query.customize_messenger_channel
6451                  }
6452              );
6453              if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
6454                  params.customize_autosaved = 'on';
6455              }
6456  
6457              urlParser.search = $.param( params );
6458              previewFrame.iframe = $( '<iframe />', {
6459                  title: api.l10n.previewIframeTitle,
6460                  name: 'customize-' + previewFrame.channel()
6461              } );
6462              previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149.
6463              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' );
6464  
6465              if ( ! hasPendingChangesetUpdate ) {
6466                  previewFrame.iframe.attr( 'src', urlParser.href );
6467              } else {
6468                  previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes.
6469              }
6470  
6471              previewFrame.iframe.appendTo( previewFrame.container );
6472              previewFrame.targetWindow( previewFrame.iframe[0].contentWindow );
6473  
6474              /*
6475               * Submit customized data in POST request to preview frame window since
6476               * there are setting value changes not yet written to changeset.
6477               */
6478              if ( hasPendingChangesetUpdate ) {
6479                  form = $( '<form>', {
6480                      action: urlParser.href,
6481                      target: previewFrame.iframe.attr( 'name' ),
6482                      method: 'post',
6483                      hidden: 'hidden'
6484                  } );
6485                  form.append( $( '<input>', {
6486                      type: 'hidden',
6487                      name: '_method',
6488                      value: 'GET'
6489                  } ) );
6490                  _.each( previewFrame.query, function( value, key ) {
6491                      form.append( $( '<input>', {
6492                          type: 'hidden',
6493                          name: key,
6494                          value: value
6495                      } ) );
6496                  } );
6497                  previewFrame.container.append( form );
6498                  form.trigger( 'submit' );
6499                  form.remove(); // No need to keep the form around after submitted.
6500              }
6501  
6502              previewFrame.bind( 'iframe-loading-error', function( error ) {
6503                  previewFrame.iframe.remove();
6504  
6505                  // Check if the user is not logged in.
6506                  if ( 0 === error ) {
6507                      previewFrame.login( deferred );
6508                      return;
6509                  }
6510  
6511                  // Check for cheaters.
6512                  if ( -1 === error ) {
6513                      deferred.rejectWith( previewFrame, [ 'cheatin' ] );
6514                      return;
6515                  }
6516  
6517                  deferred.rejectWith( previewFrame, [ 'request failure' ] );
6518              } );
6519  
6520              previewFrame.iframe.one( 'load', function() {
6521                  loaded = true;
6522  
6523                  if ( ready ) {
6524                      deferred.resolveWith( previewFrame, [ readyData ] );
6525                  } else {
6526                      setTimeout( function() {
6527                          deferred.rejectWith( previewFrame, [ 'ready timeout' ] );
6528                      }, previewFrame.sensitivity );
6529                  }
6530              });
6531          },
6532  
6533          login: function( deferred ) {
6534              var self = this,
6535                  reject;
6536  
6537              reject = function() {
6538                  deferred.rejectWith( self, [ 'logged out' ] );
6539              };
6540  
6541              if ( this.triedLogin ) {
6542                  return reject();
6543              }
6544  
6545              // Check if we have an admin cookie.
6546              $.get( api.settings.url.ajax, {
6547                  action: 'logged-in'
6548              }).fail( reject ).done( function( response ) {
6549                  var iframe;
6550  
6551                  if ( '1' !== response ) {
6552                      reject();
6553                  }
6554  
6555                  iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
6556                  iframe.appendTo( self.container );
6557                  iframe.on( 'load', function() {
6558                      self.triedLogin = true;
6559  
6560                      iframe.remove();
6561                      self.run( deferred );
6562                  });
6563              });
6564          },
6565  
6566          destroy: function() {
6567              api.Messenger.prototype.destroy.call( this );
6568  
6569              if ( this.iframe ) {
6570                  this.iframe.remove();
6571              }
6572  
6573              delete this.iframe;
6574              delete this.targetWindow;
6575          }
6576      });
6577  
6578      (function(){
6579          var id = 0;
6580          /**
6581           * Return an incremented ID for a preview messenger channel.
6582           *
6583           * This function is named "uuid" for historical reasons, but it is a
6584           * misnomer as it is not an actual UUID, and it is not universally unique.
6585           * This is not to be confused with `api.settings.changeset.uuid`.
6586           *
6587           * @return {string} A unique ID for a preview messenger channel.
6588           */
6589          api.PreviewFrame.uuid = function() {
6590              return 'preview-' + String( id++ );
6591          };
6592      }());
6593  
6594      /**
6595       * Set the document title of the customizer.
6596       *
6597       * @alias wp.customize.setDocumentTitle
6598       *
6599       * @since 4.1.0
6600       *
6601       * @param {string} documentTitle The title to insert into the document title template.
6602       */
6603      api.setDocumentTitle = function ( documentTitle ) {
6604          var tmpl, title;
6605          tmpl = api.settings.documentTitleTmpl;
6606          title = tmpl.replace( '%s', documentTitle );
6607          document.title = title;
6608          api.trigger( 'title', title );
6609      };
6610  
6611      api.Previewer = api.Messenger.extend(/** @lends wp.customize.Previewer.prototype */{
6612          refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh.
6613  
6614          /**
6615           * Initializes the previewer.
6616           *
6617           * @constructs wp.customize.Previewer
6618           * @augments   wp.customize.Messenger
6619           *
6620           * @param {Object}   params             The parameters object.
6621           * @param {string[]} params.allowedUrls An array of allowed URLs for the preview.
6622           * @param {string}   params.container   A selector or jQuery element for the preview
6623           *                                      frame to be placed.
6624           * @param {string}   params.form        A selector or jQuery element for the form to be used for POSTing data to the preview frame.
6625           * @param {string}   params.previewUrl  The URL to preview.
6626           * @param {Object}   [options]          The options object.
6627           */
6628          initialize: function( params, options ) {
6629              var previewer = this,
6630                  urlParser = document.createElement( 'a' );
6631  
6632              $.extend( previewer, options || {} );
6633              previewer.deferred = {
6634                  active: $.Deferred()
6635              };
6636  
6637              // Debounce to prevent hammering server and then wait for any pending update requests.
6638              previewer.refresh = _.debounce(
6639                  ( function( originalRefresh ) {
6640                      return function() {
6641                          var isProcessingComplete, refreshOnceProcessingComplete;
6642                          isProcessingComplete = function() {
6643                              return 0 === api.state( 'processing' ).get();
6644                          };
6645                          if ( isProcessingComplete() ) {
6646                              originalRefresh.call( previewer );
6647                          } else {
6648                              refreshOnceProcessingComplete = function() {
6649                                  if ( isProcessingComplete() ) {
6650                                      originalRefresh.call( previewer );
6651                                      api.state( 'processing' ).unbind( refreshOnceProcessingComplete );
6652                                  }
6653                              };
6654                              api.state( 'processing' ).bind( refreshOnceProcessingComplete );
6655                          }
6656                      };
6657                  }( previewer.refresh ) ),
6658                  previewer.refreshBuffer
6659              );
6660  
6661              previewer.container   = api.ensure( params.container );
6662              previewer.allowedUrls = params.allowedUrls;
6663  
6664              params.url = window.location.href;
6665  
6666              api.Messenger.prototype.initialize.call( previewer, params );
6667  
6668              urlParser.href = previewer.origin();
6669              previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
6670  
6671              /*
6672               * Limit the URL to internal, front-end links.
6673               *
6674               * If the front end and the admin are served from the same domain, load the
6675               * preview over ssl if the Customizer is being loaded over ssl. This avoids
6676               * insecure content warnings. This is not attempted if the admin and front end
6677               * are on different domains to avoid the case where the front end doesn't have
6678               * ssl certs.
6679               */
6680  
6681              previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
6682                  var result = null, urlParser, queryParams, parsedAllowedUrl, parsedCandidateUrls = [];
6683                  urlParser = document.createElement( 'a' );
6684                  urlParser.href = to;
6685  
6686                  // Abort if URL is for admin or (static) files in wp-includes or wp-content.
6687                  if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) {
6688                      return null;
6689                  }
6690  
6691                  // Remove state query params.
6692                  if ( urlParser.search.length > 1 ) {
6693                      queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
6694                      delete queryParams.customize_changeset_uuid;
6695                      delete queryParams.customize_theme;
6696                      delete queryParams.customize_messenger_channel;
6697                      delete queryParams.customize_autosaved;
6698                      if ( _.isEmpty( queryParams ) ) {
6699                          urlParser.search = '';
6700                      } else {
6701                          urlParser.search = $.param( queryParams );
6702                      }
6703                  }
6704  
6705                  parsedCandidateUrls.push( urlParser );
6706  
6707                  // Prepend list with URL that matches the scheme/protocol of the iframe.
6708                  if ( previewer.scheme.get() + ':' !== urlParser.protocol ) {
6709                      urlParser = document.createElement( 'a' );
6710                      urlParser.href = parsedCandidateUrls[0].href;
6711                      urlParser.protocol = previewer.scheme.get() + ':';
6712                      parsedCandidateUrls.unshift( urlParser );
6713                  }
6714  
6715                  // Attempt to match the URL to the control frame's scheme and check if it's allowed. If not, try the original URL.
6716                  parsedAllowedUrl = document.createElement( 'a' );
6717                  _.find( parsedCandidateUrls, function( parsedCandidateUrl ) {
6718                      return ! _.isUndefined( _.find( previewer.allowedUrls, function( allowedUrl ) {
6719                          parsedAllowedUrl.href = allowedUrl;
6720                          if ( urlParser.protocol === parsedAllowedUrl.protocol && urlParser.host === parsedAllowedUrl.host && 0 === urlParser.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) ) ) {
6721                              result = parsedCandidateUrl.href;
6722                              return true;
6723                          }
6724                      } ) );
6725                  } );
6726  
6727                  return result;
6728              });
6729  
6730              previewer.bind( 'ready', previewer.ready );
6731  
6732              // Start listening for keep-alive messages when iframe first loads.
6733              previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) );
6734  
6735              previewer.bind( 'synced', function() {
6736                  previewer.send( 'active' );
6737              } );
6738  
6739              // Refresh the preview when the URL is changed (but not yet).
6740              previewer.previewUrl.bind( previewer.refresh );
6741  
6742              previewer.scroll = 0;
6743              previewer.bind( 'scroll', function( distance ) {
6744                  previewer.scroll = distance;
6745              });
6746  
6747              // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh.
6748              previewer.bind( 'url', function( url ) {
6749                  var onUrlChange, urlChanged = false;
6750                  previewer.scroll = 0;
6751                  onUrlChange = function() {
6752                      urlChanged = true;
6753                  };
6754                  previewer.previewUrl.bind( onUrlChange );
6755                  previewer.previewUrl.set( url );
6756                  previewer.previewUrl.unbind( onUrlChange );
6757                  if ( ! urlChanged ) {
6758                      previewer.refresh();
6759                  }
6760              } );
6761  
6762              // Update the document title when the preview changes.
6763              previewer.bind( 'documentTitle', function ( title ) {
6764                  api.setDocumentTitle( title );
6765              } );
6766          },
6767  
6768          /**
6769           * Handle the preview receiving the ready message.
6770           *
6771           * @since 4.7.0
6772           * @access public
6773           *
6774           * @param {Object} data                Data from preview.
6775           * @param {string} data.currentUrl     Current URL.
6776           * @param {Object} data.activePanels   Active panels.
6777           * @param {Object} data.activeSections Active sections.
6778           * @param {Object} data.activeControls Active controls.
6779           * @return {void}
6780           */
6781          ready: function( data ) {
6782              var previewer = this, synced = {}, constructs;
6783  
6784              synced.settings = api.get();
6785              synced['settings-modified-while-loading'] = previewer.settingsModifiedWhileLoading;
6786              if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) {
6787                  synced.scroll = previewer.scroll;
6788              }
6789              synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get();
6790              previewer.send( 'sync', synced );
6791  
6792              // Set the previewUrl without causing the url to set the iframe.
6793              if ( data.currentUrl ) {
6794                  previewer.previewUrl.unbind( previewer.refresh );
6795                  previewer.previewUrl.set( data.currentUrl );
6796                  previewer.previewUrl.bind( previewer.refresh );
6797              }
6798  
6799              /*
6800               * Walk over all panels, sections, and controls and set their
6801               * respective active states to true if the preview explicitly
6802               * indicates as such.
6803               */
6804              constructs = {
6805                  panel: data.activePanels,
6806                  section: data.activeSections,
6807                  control: data.activeControls
6808              };
6809              _( constructs ).each( function ( activeConstructs, type ) {
6810                  api[ type ].each( function ( construct, id ) {
6811                      var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] );
6812  
6813                      /*
6814                       * If the construct was created statically in PHP (not dynamically in JS)
6815                       * then consider a missing (undefined) value in the activeConstructs to
6816                       * mean it should be deactivated (since it is gone). But if it is
6817                       * dynamically created then only toggle activation if the value is defined,
6818                       * as this means that the construct was also then correspondingly
6819                       * created statically in PHP and the active callback is available.
6820                       * Otherwise, dynamically-created constructs should normally have
6821                       * their active states toggled in JS rather than from PHP.
6822                       */
6823                      if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) {
6824                          if ( activeConstructs[ id ] ) {
6825                              construct.activate();
6826                          } else {
6827                              construct.deactivate();
6828                          }
6829                      }
6830                  } );
6831              } );
6832  
6833              if ( data.settingValidities ) {
6834                  api._handleSettingValidities( {
6835                      settingValidities: data.settingValidities,
6836                      focusInvalidControl: false
6837                  } );
6838              }
6839          },
6840  
6841          /**
6842           * Keep the preview alive by listening for ready and keep-alive messages.
6843           *
6844           * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL.
6845           *
6846           * @since 4.7.0
6847           * @access public
6848           *
6849           * @return {void}
6850           */
6851          keepPreviewAlive: function keepPreviewAlive() {
6852              var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck;
6853  
6854              /**
6855               * Schedule a preview keep-alive check.
6856               *
6857               * Note that if a page load takes longer than keepAliveCheck milliseconds,
6858               * the keep-alive messages will still be getting sent from the previous
6859               * URL.
6860               */
6861              scheduleKeepAliveCheck = function() {
6862                  timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck );
6863              };
6864  
6865              /**
6866               * Set the previewerAlive state to true when receiving a message from the preview.
6867               */
6868              keepAliveTick = function() {
6869                  api.state( 'previewerAlive' ).set( true );
6870                  clearTimeout( timeoutId );
6871                  scheduleKeepAliveCheck();
6872              };
6873  
6874              /**
6875               * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message.
6876               *
6877               * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser
6878               * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage
6879               * transport to use refresh instead, causing the preview frame also to be replaced with the current
6880               * allowed preview URL.
6881               */
6882              handleMissingKeepAlive = function() {
6883                  api.state( 'previewerAlive' ).set( false );
6884              };
6885              scheduleKeepAliveCheck();
6886  
6887              previewer.bind( 'ready', keepAliveTick );
6888              previewer.bind( 'keep-alive', keepAliveTick );
6889          },
6890  
6891          /**
6892           * Query string data sent with each preview request.
6893           *
6894           * @abstract
6895           */
6896          query: function() {},
6897  
6898          abort: function() {
6899              if ( this.loading ) {
6900                  this.loading.destroy();
6901                  delete this.loading;
6902              }
6903          },
6904  
6905          /**
6906           * Refresh the preview seamlessly.
6907           *
6908           * @since 3.4.0
6909           * @access public
6910           *
6911           * @return {void}
6912           */
6913          refresh: function() {
6914              var previewer = this, onSettingChange;
6915  
6916              // Display loading indicator.
6917              previewer.send( 'loading-initiated' );
6918  
6919              previewer.abort();
6920  
6921              previewer.loading = new api.PreviewFrame({
6922                  url:        previewer.url(),
6923                  previewUrl: previewer.previewUrl(),
6924                  query:      previewer.query( { excludeCustomizedSaved: true } ) || {},
6925                  container:  previewer.container
6926              });
6927  
6928              previewer.settingsModifiedWhileLoading = {};
6929              onSettingChange = function( setting ) {
6930                  previewer.settingsModifiedWhileLoading[ setting.id ] = true;
6931              };
6932              api.bind( 'change', onSettingChange );
6933              previewer.loading.always( function() {
6934                  api.unbind( 'change', onSettingChange );
6935              } );
6936  
6937              previewer.loading.done( function( readyData ) {
6938                  var loadingFrame = this, onceSynced;
6939  
6940                  previewer.preview = loadingFrame;
6941                  previewer.targetWindow( loadingFrame.targetWindow() );
6942                  previewer.channel( loadingFrame.channel() );
6943  
6944                  onceSynced = function() {
6945                      loadingFrame.unbind( 'synced', onceSynced );
6946                      if ( previewer._previousPreview ) {
6947                          previewer._previousPreview.destroy();
6948                      }
6949                      previewer._previousPreview = previewer.preview;
6950                      previewer.deferred.active.resolve();
6951                      delete previewer.loading;
6952                  };
6953                  loadingFrame.bind( 'synced', onceSynced );
6954  
6955                  // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh.
6956                  previewer.trigger( 'ready', readyData );
6957              });
6958  
6959              previewer.loading.fail( function( reason ) {
6960                  previewer.send( 'loading-failed' );
6961  
6962                  if ( 'logged out' === reason ) {
6963                      if ( previewer.preview ) {
6964                          previewer.preview.destroy();
6965                          delete previewer.preview;
6966                      }
6967  
6968                      previewer.login().done( previewer.refresh );
6969                  }
6970  
6971                  if ( 'cheatin' === reason ) {
6972                      previewer.cheatin();
6973                  }
6974              });
6975          },
6976  
6977          login: function() {
6978              var previewer = this,
6979                  deferred, messenger, iframe;
6980  
6981              if ( this._login ) {
6982                  return this._login;
6983              }
6984  
6985              deferred = $.Deferred();
6986              this._login = deferred.promise();
6987  
6988              messenger = new api.Messenger({
6989                  channel: 'login',
6990                  url:     api.settings.url.login
6991              });
6992  
6993              iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
6994  
6995              messenger.targetWindow( iframe[0].contentWindow );
6996  
6997              messenger.bind( 'login', function () {
6998                  var refreshNonces = previewer.refreshNonces();
6999  
7000                  refreshNonces.always( function() {
7001                      iframe.remove();
7002                      messenger.destroy();
7003                      delete previewer._login;
7004                  });
7005  
7006                  refreshNonces.done( function() {
7007                      deferred.resolve();
7008                  });
7009  
7010                  refreshNonces.fail( function() {
7011                      previewer.cheatin();
7012                      deferred.reject();
7013                  });
7014              });
7015  
7016              return this._login;
7017          },
7018  
7019          cheatin: function() {
7020              $( document.body ).empty().addClass( 'cheatin' ).append(
7021                  '<h1>' + api.l10n.notAllowedHeading + '</h1>' +
7022                  '<p>' + api.l10n.notAllowed + '</p>'
7023              );
7024          },
7025  
7026          refreshNonces: function() {
7027              var request, deferred = $.Deferred();
7028  
7029              deferred.promise();
7030  
7031              request = wp.ajax.post( 'customize_refresh_nonces', {
7032                  wp_customize: 'on',
7033                  customize_theme: api.settings.theme.stylesheet
7034              });
7035  
7036              request.done( function( response ) {
7037                  api.trigger( 'nonce-refresh', response );
7038                  deferred.resolve();
7039              });
7040  
7041              request.fail( function() {
7042                  deferred.reject();
7043              });
7044  
7045              return deferred;
7046          }
7047      });
7048  
7049      api.settingConstructor = {};
7050      api.controlConstructor = {
7051          color:               api.ColorControl,
7052          media:               api.MediaControl,
7053          upload:              api.UploadControl,
7054          image:               api.ImageControl,
7055          cropped_image:       api.CroppedImageControl,
7056          site_icon:           api.SiteIconControl,
7057          header:              api.HeaderControl,
7058          background:          api.BackgroundControl,
7059          background_position: api.BackgroundPositionControl,
7060          theme:               api.ThemeControl,
7061          date_time:           api.DateTimeControl,
7062          code_editor:         api.CodeEditorControl
7063      };
7064      api.panelConstructor = {
7065          themes: api.ThemesPanel
7066      };
7067      api.sectionConstructor = {
7068          themes: api.ThemesSection,
7069          outer: api.OuterSection
7070      };
7071  
7072      /**
7073       * Handle setting_validities in an error response for the customize-save request.
7074       *
7075       * Add notifications to the settings and focus on the first control that has an invalid setting.
7076       *
7077       * @alias wp.customize._handleSettingValidities
7078       *
7079       * @since 4.6.0
7080       * @private
7081       *
7082       * @param {Object}  args                             Arguments.
7083       * @param {Object}  args.settingValidities           Mapping of setting IDs to true, or to a mapping of error codes to notification params.
7084       * @param {boolean} [args.focusInvalidControl=false] Whether to focus the first control that has an invalid setting.
7085       * @return {void}
7086       */
7087      api._handleSettingValidities = function handleSettingValidities( args ) {
7088          var invalidSettingControls, invalidSettings = [], wasFocused = false;
7089  
7090          // Find the controls that correspond to each invalid setting.
7091          _.each( args.settingValidities, function( validity, settingId ) {
7092              var setting = api( settingId );
7093              if ( setting ) {
7094  
7095                  // Add notifications for invalidities.
7096                  if ( _.isObject( validity ) ) {
7097                      _.each( validity, function( params, code ) {
7098                          var notification, existingNotification, needsReplacement = false;
7099                          notification = new api.Notification( code, _.extend( { fromServer: true }, params ) );
7100  
7101                          // Remove existing notification if already exists for code but differs in parameters.
7102                          existingNotification = setting.notifications( notification.code );
7103                          if ( existingNotification ) {
7104                              needsReplacement = notification.type !== existingNotification.type || notification.message !== existingNotification.message || ! _.isEqual( notification.data, existingNotification.data );
7105                          }
7106                          if ( needsReplacement ) {
7107                              setting.notifications.remove( code );
7108                          }
7109  
7110                          if ( ! setting.notifications.has( notification.code ) ) {
7111                              setting.notifications.add( notification );
7112                          }
7113                          invalidSettings.push( setting.id );
7114                      } );
7115                  }
7116  
7117                  // Remove notification errors that are no longer valid.
7118                  setting.notifications.each( function( notification ) {
7119                      if ( notification.fromServer && 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
7120                          setting.notifications.remove( notification.code );
7121                      }
7122                  } );
7123              }
7124          } );
7125  
7126          if ( args.focusInvalidControl ) {
7127              invalidSettingControls = api.findControlsForSettings( invalidSettings );
7128  
7129              // Focus on the first control that is inside of an expanded section (one that is visible).
7130              _( _.values( invalidSettingControls ) ).find( function( controls ) {
7131                  return _( controls ).find( function( control ) {
7132                      var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
7133                      if ( isExpanded && control.expanded ) {
7134                          isExpanded = control.expanded();
7135                      }
7136                      if ( isExpanded ) {
7137                          control.focus();
7138                          wasFocused = true;
7139                      }
7140                      return wasFocused;
7141                  } );
7142              } );
7143  
7144              // Focus on the first invalid control.
7145              if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
7146                  _.values( invalidSettingControls )[0][0].focus();
7147              }
7148          }
7149      };
7150  
7151      /**
7152       * Find all controls associated with the given settings.
7153       *
7154       * @alias wp.customize.findControlsForSettings
7155       *
7156       * @since 4.6.0
7157       * @param {string[]} settingIds Setting IDs.
7158       * @return {Object.<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
7159       */
7160      api.findControlsForSettings = function findControlsForSettings( settingIds ) {
7161          var controls = {}, settingControls;
7162          _.each( _.unique( settingIds ), function( settingId ) {
7163              var setting = api( settingId );
7164              if ( setting ) {
7165                  settingControls = setting.findControls();
7166                  if ( settingControls && settingControls.length > 0 ) {
7167                      controls[ settingId ] = settingControls;
7168                  }
7169              }
7170          } );
7171          return controls;
7172      };
7173  
7174      /**
7175       * Sort panels, sections, controls by priorities. Hide empty sections and panels.
7176       *
7177       * @alias wp.customize.reflowPaneContents
7178       *
7179       * @since 4.1.0
7180       */
7181      api.reflowPaneContents = _.bind( function () {
7182  
7183          var appendContainer, activeElement, rootHeadContainers, rootNodes = [], wasReflowed = false;
7184  
7185          if ( document.activeElement ) {
7186              activeElement = $( document.activeElement );
7187          }
7188  
7189          // Sort the sections within each panel.
7190          api.panel.each( function ( panel ) {
7191              if ( 'themes' === panel.id ) {
7192                  return; // Don't reflow theme sections, as doing so moves them after the themes container.
7193              }
7194  
7195              var sections = panel.sections(),
7196                  sectionHeadContainers = _.pluck( sections, 'headContainer' );
7197              rootNodes.push( panel );
7198              appendContainer = ( panel.contentContainer.is( 'ul' ) ) ? panel.contentContainer : panel.contentContainer.find( 'ul:first' );
7199              if ( ! api.utils.areElementListsEqual( sectionHeadContainers, appendContainer.children( '[id]' ) ) ) {
7200                  _( sections ).each( function ( section ) {
7201                      appendContainer.append( section.headContainer );
7202                  } );
7203                  wasReflowed = true;
7204              }
7205          } );
7206  
7207          // Sort the controls within each section.
7208          api.section.each( function ( section ) {
7209              var controls = section.controls(),
7210                  controlContainers = _.pluck( controls, 'container' );
7211              if ( ! section.panel() ) {
7212                  rootNodes.push( section );
7213              }
7214              appendContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
7215              if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
7216                  _( controls ).each( function ( control ) {
7217                      appendContainer.append( control.container );
7218                  } );
7219                  wasReflowed = true;
7220              }
7221          } );
7222  
7223          // Sort the root panels and sections.
7224          rootNodes.sort( api.utils.prioritySort );
7225          rootHeadContainers = _.pluck( rootNodes, 'headContainer' );
7226          appendContainer = $( '#customize-theme-controls .customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable.
7227          if ( ! api.utils.areElementListsEqual( rootHeadContainers, appendContainer.children() ) ) {
7228              _( rootNodes ).each( function ( rootNode ) {
7229                  appendContainer.append( rootNode.headContainer );
7230              } );
7231              wasReflowed = true;
7232          }
7233  
7234          // Now re-trigger the active Value callbacks so that the panels and sections can decide whether they can be rendered.
7235          api.panel.each( function ( panel ) {
7236              var value = panel.active();
7237              panel.active.callbacks.fireWith( panel.active, [ value, value ] );
7238          } );
7239          api.section.each( function ( section ) {
7240              var value = section.active();
7241              section.active.callbacks.fireWith( section.active, [ value, value ] );
7242          } );
7243  
7244          // Restore focus if there was a reflow and there was an active (focused) element.
7245          if ( wasReflowed && activeElement ) {
7246              activeElement.trigger( 'focus' );
7247          }
7248          api.trigger( 'pane-contents-reflowed' );
7249      }, api );
7250  
7251      // Define state values.
7252      api.state = new api.Values();
7253      _.each( [
7254          'saved',
7255          'saving',
7256          'trashing',
7257          'activated',
7258          'processing',
7259          'paneVisible',
7260          'expandedPanel',
7261          'expandedSection',
7262          'changesetDate',
7263          'selectedChangesetDate',
7264          'changesetStatus',
7265          'selectedChangesetStatus',
7266          'remainingTimeToPublish',
7267          'previewerAlive',
7268          'editShortcutVisibility',
7269          'changesetLocked',
7270          'previewedDevice'
7271      ], function( name ) {
7272          api.state.create( name );
7273      });
7274  
7275      $( function() {
7276          api.settings = window._wpCustomizeSettings;
7277          api.l10n = window._wpCustomizeControlsL10n;
7278  
7279          // Check if we can run the Customizer.
7280          if ( ! api.settings ) {
7281              return;
7282          }
7283  
7284          // Bail if any incompatibilities are found.
7285          if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
7286              return;
7287          }
7288  
7289          if ( null === api.PreviewFrame.prototype.sensitivity ) {
7290              api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity;
7291          }
7292          if ( null === api.Previewer.prototype.refreshBuffer ) {
7293              api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh;
7294          }
7295  
7296          var parent,
7297              body = $( document.body ),
7298              overlay = body.children( '.wp-full-overlay' ),
7299              title = $( '#customize-info .panel-title.site-title' ),
7300              closeBtn = $( '.customize-controls-close' ),
7301              saveBtn = $( '#save' ),
7302              btnWrapper = $( '#customize-save-button-wrapper' ),
7303              publishSettingsBtn = $( '#publish-settings' ),
7304              footerActions = $( '#customize-footer-actions' );
7305  
7306          // Add publish settings section in JS instead of PHP since the Customizer depends on it to function.
7307          api.bind( 'ready', function() {
7308              api.section.add( new api.OuterSection( 'publish_settings', {
7309                  title: api.l10n.publishSettings,
7310                  priority: 0,
7311                  active: api.settings.theme.active
7312              } ) );
7313          } );
7314  
7315          // Set up publish settings section and its controls.
7316          api.section( 'publish_settings', function( section ) {
7317              var updateButtonsState, trashControl, updateSectionActive, isSectionActive, statusControl, dateControl, toggleDateControl, publishWhenTime, pollInterval, updateTimeArrivedPoller, cancelScheduleButtonReminder, timeArrivedPollingInterval = 1000;
7318  
7319              trashControl = new api.Control( 'trash_changeset', {
7320                  type: 'button',
7321                  section: section.id,
7322                  priority: 30,
7323                  input_attrs: {
7324                      'class': 'button-link button-link-delete',
7325                      value: api.l10n.discardChanges
7326                  }
7327              } );
7328              api.control.add( trashControl );
7329              trashControl.deferred.embedded.done( function() {
7330                  trashControl.container.find( '.button-link' ).on( 'click', function() {
7331                      if ( confirm( api.l10n.trashConfirm ) ) {
7332                          wp.customize.previewer.trash();
7333                      }
7334                  } );
7335              } );
7336  
7337              api.control.add( new api.PreviewLinkControl( 'changeset_preview_link', {
7338                  section: section.id,
7339                  priority: 100
7340              } ) );
7341  
7342              /**
7343               * Return whether the publish settings section should be active.
7344               *
7345               * @return {boolean} Is section active.
7346               */
7347              isSectionActive = function() {
7348                  if ( ! api.state( 'activated' ).get() ) {
7349                      return false;
7350                  }
7351                  if ( api.state( 'trashing' ).get() || 'trash' === api.state( 'changesetStatus' ).get() ) {
7352                      return false;
7353                  }
7354                  if ( '' === api.state( 'changesetStatus' ).get() && api.state( 'saved' ).get() ) {
7355                      return false;
7356                  }
7357                  return true;
7358              };
7359  
7360              // Make sure publish settings are not available while the theme is not active and the customizer is in a published state.
7361              section.active.validate = isSectionActive;
7362              updateSectionActive = function() {
7363                  section.active.set( isSectionActive() );
7364              };
7365              api.state( 'activated' ).bind( updateSectionActive );
7366              api.state( 'trashing' ).bind( updateSectionActive );
7367              api.state( 'saved' ).bind( updateSectionActive );
7368              api.state( 'changesetStatus' ).bind( updateSectionActive );
7369              updateSectionActive();
7370  
7371              // Bind visibility of the publish settings button to whether the section is active.
7372              updateButtonsState = function() {
7373                  publishSettingsBtn.toggle( section.active.get() );
7374                  saveBtn.toggleClass( 'has-next-sibling', section.active.get() );
7375              };
7376              updateButtonsState();
7377              section.active.bind( updateButtonsState );
7378  
7379              /**
7380               * Highlights the schedule button to remind the user to schedule the changes.
7381               */
7382  			function highlightScheduleButton() {
7383                  if ( ! cancelScheduleButtonReminder ) {
7384                      cancelScheduleButtonReminder = api.utils.highlightButton( btnWrapper, {
7385                          delay: 1000,
7386  
7387                          /*
7388                           * Only abort the reminder when the save button is focused.
7389                           * If the user clicks the settings button to toggle the
7390                           * settings closed, we'll still remind them.
7391                           */
7392                          focusTarget: saveBtn
7393                      } );
7394                  }
7395              }
7396  
7397              /**
7398               * Cancels the highlight reminder for the schedule button.
7399               */
7400  			function cancelHighlightScheduleButton() {
7401                  if ( cancelScheduleButtonReminder ) {
7402                      cancelScheduleButtonReminder();
7403                      cancelScheduleButtonReminder = null;
7404                  }
7405              }
7406              api.state( 'selectedChangesetStatus' ).bind( cancelHighlightScheduleButton );
7407  
7408              section.contentContainer.find( '.customize-action' ).text( api.l10n.updating );
7409              section.contentContainer.find( '.customize-section-back' ).removeAttr( 'tabindex' );
7410              publishSettingsBtn.prop( 'disabled', false );
7411  
7412              publishSettingsBtn.on( 'click', function( event ) {
7413                  event.preventDefault();
7414                  section.expanded.set( ! section.expanded.get() );
7415              } );
7416  
7417              section.expanded.bind( function( isExpanded ) {
7418                  var defaultChangesetStatus;
7419                  publishSettingsBtn.attr( 'aria-expanded', String( isExpanded ) );
7420                  publishSettingsBtn.toggleClass( 'active', isExpanded );
7421  
7422                  if ( isExpanded ) {
7423                      cancelHighlightScheduleButton();
7424                      return;
7425                  }
7426  
7427                  defaultChangesetStatus = api.state( 'changesetStatus' ).get();
7428                  if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
7429                      defaultChangesetStatus = 'publish';
7430                  }
7431  
7432                  if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
7433                      highlightScheduleButton();
7434                  } else if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
7435                      highlightScheduleButton();
7436                  }
7437              } );
7438  
7439              statusControl = new api.Control( 'changeset_status', {
7440                  priority: 10,
7441                  type: 'radio',
7442                  section: 'publish_settings',
7443                  setting: api.state( 'selectedChangesetStatus' ),
7444                  templateId: 'customize-selected-changeset-status-control',
7445                  label: api.l10n.action,
7446                  choices: api.settings.changeset.statusChoices
7447              } );
7448              api.control.add( statusControl );
7449  
7450              dateControl = new api.DateTimeControl( 'changeset_scheduled_date', {
7451                  priority: 20,
7452                  section: 'publish_settings',
7453                  setting: api.state( 'selectedChangesetDate' ),
7454                  minYear: ( new Date() ).getFullYear(),
7455                  allowPastDate: false,
7456                  includeTime: true,
7457                  twelveHourFormat: /a/i.test( api.settings.timeFormat ),
7458                  description: api.l10n.scheduleDescription
7459              } );
7460              dateControl.notifications.alt = true;
7461              api.control.add( dateControl );
7462  
7463              publishWhenTime = function() {
7464                  api.state( 'selectedChangesetStatus' ).set( 'publish' );
7465                  api.previewer.save();
7466              };
7467  
7468              // Start countdown for when the dateTime arrives, or clear interval when it is .
7469              updateTimeArrivedPoller = function() {
7470                  var shouldPoll = (
7471                      'future' === api.state( 'changesetStatus' ).get() &&
7472                      'future' === api.state( 'selectedChangesetStatus' ).get() &&
7473                      api.state( 'changesetDate' ).get() &&
7474                      api.state( 'selectedChangesetDate' ).get() === api.state( 'changesetDate' ).get() &&
7475                      api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ) >= 0
7476                  );
7477  
7478                  if ( shouldPoll && ! pollInterval ) {
7479                      pollInterval = setInterval( function() {
7480                          var remainingTime = api.utils.getRemainingTime( api.state( 'changesetDate' ).get() );
7481                          api.state( 'remainingTimeToPublish' ).set( remainingTime );
7482                          if ( remainingTime <= 0 ) {
7483                              clearInterval( pollInterval );
7484                              pollInterval = 0;
7485                              publishWhenTime();
7486                          }
7487                      }, timeArrivedPollingInterval );
7488                  } else if ( ! shouldPoll && pollInterval ) {
7489                      clearInterval( pollInterval );
7490                      pollInterval = 0;
7491                  }
7492              };
7493  
7494              api.state( 'changesetDate' ).bind( updateTimeArrivedPoller );
7495              api.state( 'selectedChangesetDate' ).bind( updateTimeArrivedPoller );
7496              api.state( 'changesetStatus' ).bind( updateTimeArrivedPoller );
7497              api.state( 'selectedChangesetStatus' ).bind( updateTimeArrivedPoller );
7498              updateTimeArrivedPoller();
7499  
7500              // Ensure dateControl only appears when selected status is future.
7501              dateControl.active.validate = function() {
7502                  return 'future' === api.state( 'selectedChangesetStatus' ).get();
7503              };
7504              toggleDateControl = function( value ) {
7505                  dateControl.active.set( 'future' === value );
7506              };
7507              toggleDateControl( api.state( 'selectedChangesetStatus' ).get() );
7508              api.state( 'selectedChangesetStatus' ).bind( toggleDateControl );
7509  
7510              // Show notification on date control when status is future but it isn't a future date.
7511              api.state( 'saving' ).bind( function( isSaving ) {
7512                  if ( isSaving && 'future' === api.state( 'selectedChangesetStatus' ).get() ) {
7513                      dateControl.toggleFutureDateNotification( ! dateControl.isFutureDate() );
7514                  }
7515              } );
7516          } );
7517  
7518          // Prevent the form from saving when enter is pressed on an input or select element.
7519          $('#customize-controls').on( 'keydown', function( e ) {
7520              var isEnter = ( 13 === e.which ),
7521                  $el = $( e.target );
7522  
7523              if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
7524                  e.preventDefault();
7525              }
7526          });
7527  
7528          // Expand/Collapse the main customizer customize info.
7529          $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
7530              var section = $( this ).closest( '.accordion-section' ),
7531                  content = section.find( '.customize-panel-description:first' );
7532  
7533              if ( section.hasClass( 'cannot-expand' ) ) {
7534                  return;
7535              }
7536  
7537              if ( section.hasClass( 'open' ) ) {
7538                  section.toggleClass( 'open' );
7539                  content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration, function() {
7540                      content.trigger( 'toggled' );
7541                  } );
7542                  $( this ).attr( 'aria-expanded', false );
7543              } else {
7544                  content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration, function() {
7545                      content.trigger( 'toggled' );
7546                  } );
7547                  section.toggleClass( 'open' );
7548                  $( this ).attr( 'aria-expanded', true );
7549              }
7550          });
7551  
7552          /**
7553           * Initialize Previewer
7554           *
7555           * @alias wp.customize.previewer
7556           */
7557          api.previewer = new api.Previewer({
7558              container:   '#customize-preview',
7559              form:        '#customize-controls',
7560              previewUrl:  api.settings.url.preview,
7561              allowedUrls: api.settings.url.allowed
7562          },/** @lends wp.customize.previewer */{
7563  
7564              nonce: api.settings.nonce,
7565  
7566              /**
7567               * Build the query to send along with the Preview request.
7568               *
7569               * @since 3.4.0
7570               * @since 4.7.0 Added options param.
7571               * @access public
7572               *
7573               * @param {Object}  [options]                              Options.
7574               * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset).
7575               * @return {Object} Query vars.
7576               */
7577              query: function( options ) {
7578                  var queryVars = {
7579                      wp_customize: 'on',
7580                      customize_theme: api.settings.theme.stylesheet,
7581                      nonce: this.nonce.preview,
7582                      customize_changeset_uuid: api.settings.changeset.uuid
7583                  };
7584                  if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
7585                      queryVars.customize_autosaved = 'on';
7586                  }
7587  
7588                  /*
7589                   * Exclude customized data if requested especially for calls to requestChangesetUpdate.
7590                   * Changeset updates are differential and so it is a performance waste to send all of
7591                   * the dirty settings with each update.
7592                   */
7593                  queryVars.customized = JSON.stringify( api.dirtyValues( {
7594                      unsaved: options && options.excludeCustomizedSaved
7595                  } ) );
7596  
7597                  return queryVars;
7598              },
7599  
7600              /**
7601               * Save (and publish) the customizer changeset.
7602               *
7603               * Updates to the changeset are transactional. If any of the settings
7604               * are invalid then none of them will be written into the changeset.
7605               * A revision will be made for the changeset post if revisions support
7606               * has been added to the post type.
7607               *
7608               * @since 3.4.0
7609               * @since 4.7.0 Added args param and return value.
7610               *
7611               * @param {Object} [args]                Args.
7612               * @param {string} [args.status=publish] Status.
7613               * @param {string} [args.date]           Date, in local time in MySQL format.
7614               * @param {string} [args.title]          Title.
7615               * @return {JQuery.Promise<*>} Promise.
7616               */
7617              save: function( args ) {
7618                  var previewer = this,
7619                      deferred = $.Deferred(),
7620                      changesetStatus = api.state( 'selectedChangesetStatus' ).get(),
7621                      selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(),
7622                      processing = api.state( 'processing' ),
7623                      submitWhenDoneProcessing,
7624                      submit,
7625                      modifiedWhileSaving = {},
7626                      invalidSettings = [],
7627                      invalidControls = [],
7628                      invalidSettingLessControls = [];
7629  
7630                  if ( args && args.status ) {
7631                      changesetStatus = args.status;
7632                  }
7633  
7634                  if ( api.state( 'saving' ).get() ) {
7635                      deferred.reject( 'already_saving' );
7636                      deferred.promise();
7637                  }
7638  
7639                  api.state( 'saving' ).set( true );
7640  
7641                  /**
7642                   * Captures a setting as modified during the save process.
7643                   *
7644                   * @param {wp.customize.Setting} setting The setting that was modified.
7645                   */
7646  				function captureSettingModifiedDuringSave( setting ) {
7647                      modifiedWhileSaving[ setting.id ] = true;
7648                  }
7649  
7650                  submit = function () {
7651                      var request, query, settingInvalidities = {}, latestRevision = api._latestRevision, errorCode = 'client_side_error';
7652  
7653                      api.bind( 'change', captureSettingModifiedDuringSave );
7654                      api.notifications.remove( errorCode );
7655  
7656                      /*
7657                       * Block saving if there are any settings that are marked as
7658                       * invalid from the client (not from the server). Focus on
7659                       * the control.
7660                       */
7661                      api.each( function( setting ) {
7662                          setting.notifications.each( function( notification ) {
7663                              if ( 'error' === notification.type && ! notification.fromServer ) {
7664                                  invalidSettings.push( setting.id );
7665                                  if ( ! settingInvalidities[ setting.id ] ) {
7666                                      settingInvalidities[ setting.id ] = {};
7667                                  }
7668                                  settingInvalidities[ setting.id ][ notification.code ] = notification;
7669                              }
7670                          } );
7671                      } );
7672  
7673                      // Find all invalid setting less controls with notification type error.
7674                      api.control.each( function( control ) {
7675                          if ( ! control.setting || ! control.setting.id && control.active.get() ) {
7676                              control.notifications.each( function( notification ) {
7677                                  if ( 'error' === notification.type ) {
7678                                      invalidSettingLessControls.push( [ control ] );
7679                                  }
7680                              } );
7681                          }
7682                      } );
7683  
7684                      invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) );
7685                      if ( ! _.isEmpty( invalidControls ) ) {
7686  
7687                          invalidControls[0][0].focus();
7688                          api.unbind( 'change', captureSettingModifiedDuringSave );
7689  
7690                          if ( invalidSettings.length ) {
7691                              api.notifications.add( new api.Notification( errorCode, {
7692                                  message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ),
7693                                  type: 'error',
7694                                  dismissible: true,
7695                                  saveFailure: true
7696                              } ) );
7697                          }
7698  
7699                          deferred.rejectWith( previewer, [
7700                              { setting_invalidities: settingInvalidities }
7701                          ] );
7702                          api.state( 'saving' ).set( false );
7703                          return deferred.promise();
7704                      }
7705  
7706                      /*
7707                       * Note that excludeCustomizedSaved is intentionally false so that the entire
7708                       * set of customized data will be included if bypassed changeset update.
7709                       */
7710                      query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), {
7711                          nonce: previewer.nonce.save,
7712                          customize_changeset_status: changesetStatus
7713                      } );
7714  
7715                      if ( args && args.date ) {
7716                          query.customize_changeset_date = args.date;
7717                      } else if ( 'future' === changesetStatus && selectedChangesetDate ) {
7718                          query.customize_changeset_date = selectedChangesetDate;
7719                      }
7720  
7721                      if ( args && args.title ) {
7722                          query.customize_changeset_title = args.title;
7723                      }
7724  
7725                      // Allow plugins to modify the params included with the save request.
7726                      api.trigger( 'save-request-params', query );
7727  
7728                      /*
7729                       * Note that the dirty customized values will have already been set in the
7730                       * changeset and so technically query.customized could be deleted. However,
7731                       * it is remaining here to make sure that any settings that got updated
7732                       * quietly which may have not triggered an update request will also get
7733                       * included in the values that get saved to the changeset. This will ensure
7734                       * that values that get injected via the saved event will be included in
7735                       * the changeset. This also ensures that setting values that were invalid
7736                       * will get re-validated, perhaps in the case of settings that are invalid
7737                       * due to dependencies on other settings.
7738                       */
7739                      request = wp.ajax.post( 'customize_save', query );
7740                      api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
7741  
7742                      api.trigger( 'save', request );
7743  
7744                      request.always( function () {
7745                          api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
7746                          api.state( 'saving' ).set( false );
7747                          api.unbind( 'change', captureSettingModifiedDuringSave );
7748                      } );
7749  
7750                      // Remove notifications that were added due to save failures.
7751                      api.notifications.each( function( notification ) {
7752                          if ( notification.saveFailure ) {
7753                              api.notifications.remove( notification.code );
7754                          }
7755                      });
7756  
7757                      request.fail( function ( response ) {
7758                          var notification, notificationArgs;
7759                          notificationArgs = {
7760                              type: 'error',
7761                              dismissible: true,
7762                              fromServer: true,
7763                              saveFailure: true
7764                          };
7765  
7766                          if ( '0' === response ) {
7767                              response = 'not_logged_in';
7768                          } else if ( '-1' === response ) {
7769                              // Back-compat in case any other check_ajax_referer() call is dying.
7770                              response = 'invalid_nonce';
7771                          }
7772  
7773                          if ( 'invalid_nonce' === response ) {
7774                              previewer.cheatin();
7775                          } else if ( 'not_logged_in' === response ) {
7776                              previewer.preview.iframe.hide();
7777                              previewer.login().done( function() {
7778                                  previewer.save();
7779                                  previewer.preview.iframe.show();
7780                              } );
7781                          } else if ( response.code ) {
7782                              if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) {
7783                                  api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus();
7784                              } else if ( 'changeset_locked' !== response.code ) {
7785                                  notification = new api.Notification( response.code, _.extend( notificationArgs, {
7786                                      message: response.message
7787                                  } ) );
7788                              }
7789                          } else {
7790                              notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
7791                                  message: api.l10n.unknownRequestFail
7792                              } ) );
7793                          }
7794  
7795                          if ( notification ) {
7796                              api.notifications.add( notification );
7797                          }
7798  
7799                          if ( response.setting_validities ) {
7800                              api._handleSettingValidities( {
7801                                  settingValidities: response.setting_validities,
7802                                  focusInvalidControl: true
7803                              } );
7804                          }
7805  
7806                          deferred.rejectWith( previewer, [ response ] );
7807                          api.trigger( 'error', response );
7808  
7809                          // Start a new changeset if the underlying changeset was published.
7810                          if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) {
7811                              api.settings.changeset.uuid = response.next_changeset_uuid;
7812                              api.state( 'changesetStatus' ).set( '' );
7813                              if ( api.settings.changeset.branching ) {
7814                                  parent.send( 'changeset-uuid', api.settings.changeset.uuid );
7815                              }
7816                              api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid );
7817                          }
7818                      } );
7819  
7820                      request.done( function( response ) {
7821  
7822                          previewer.send( 'saved', response );
7823  
7824                          api.state( 'changesetStatus' ).set( response.changeset_status );
7825                          if ( response.changeset_date ) {
7826                              api.state( 'changesetDate' ).set( response.changeset_date );
7827                          }
7828  
7829                          if ( 'publish' === response.changeset_status ) {
7830  
7831                              // Mark all published as clean if they haven't been modified during the request.
7832                              api.each( function( setting ) {
7833                                  /*
7834                                   * Note that the setting revision will be undefined in the case of setting
7835                                   * values that are marked as dirty when the customizer is loaded, such as
7836                                   * when applying starter content. All other dirty settings will have an
7837                                   * associated revision due to their modification triggering a change event.
7838                                   */
7839                                  if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) {
7840                                      setting._dirty = false;
7841                                  }
7842                              } );
7843  
7844                              api.state( 'changesetStatus' ).set( '' );
7845                              api.settings.changeset.uuid = response.next_changeset_uuid;
7846                              if ( api.settings.changeset.branching ) {
7847                                  parent.send( 'changeset-uuid', api.settings.changeset.uuid );
7848                              }
7849                          }
7850  
7851                          // Prevent subsequent requestChangesetUpdate() calls from including the settings that have been saved.
7852                          api._lastSavedRevision = Math.max( latestRevision, api._lastSavedRevision );
7853  
7854                          if ( response.setting_validities ) {
7855                              api._handleSettingValidities( {
7856                                  settingValidities: response.setting_validities,
7857                                  focusInvalidControl: true
7858                              } );
7859                          }
7860  
7861                          deferred.resolveWith( previewer, [ response ] );
7862                          api.trigger( 'saved', response );
7863  
7864                          // Restore the global dirty state if any settings were modified during save.
7865                          if ( ! _.isEmpty( modifiedWhileSaving ) ) {
7866                              api.state( 'saved' ).set( false );
7867                          }
7868                      } );
7869                  };
7870  
7871                  if ( 0 === processing() ) {
7872                      submit();
7873                  } else {
7874                      submitWhenDoneProcessing = function () {
7875                          if ( 0 === processing() ) {
7876                              api.state.unbind( 'change', submitWhenDoneProcessing );
7877                              submit();
7878                          }
7879                      };
7880                      api.state.bind( 'change', submitWhenDoneProcessing );
7881                  }
7882  
7883                  return deferred.promise();
7884              },
7885  
7886              /**
7887               * Trash the current changes.
7888               *
7889               * Revert the Customizer to its previously-published state.
7890               *
7891               * @since 4.9.0
7892               */
7893              trash: function trash() {
7894                  var request, success, fail;
7895  
7896                  api.state( 'trashing' ).set( true );
7897                  api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
7898  
7899                  request = wp.ajax.post( 'customize_trash', {
7900                      customize_changeset_uuid: api.settings.changeset.uuid,
7901                      nonce: api.settings.nonce.trash
7902                  } );
7903                  api.notifications.add( new api.OverlayNotification( 'changeset_trashing', {
7904                      type: 'info',
7905                      message: api.l10n.revertingChanges,
7906                      loading: true
7907                  } ) );
7908  
7909                  success = function() {
7910                      var urlParser = document.createElement( 'a' ), queryParams;
7911  
7912                      api.state( 'changesetStatus' ).set( 'trash' );
7913                      api.each( function( setting ) {
7914                          setting._dirty = false;
7915                      } );
7916                      api.state( 'saved' ).set( true );
7917  
7918                      // Go back to Customizer without changeset.
7919                      urlParser.href = location.href;
7920                      queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
7921                      delete queryParams.changeset_uuid;
7922                      queryParams['return'] = api.settings.url['return'];
7923                      urlParser.search = $.param( queryParams );
7924                      location.replace( urlParser.href );
7925                  };
7926  
7927                  fail = function( code, message ) {
7928                      var notificationCode = code || 'unknown_error';
7929                      api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
7930                      api.state( 'trashing' ).set( false );
7931                      api.notifications.remove( 'changeset_trashing' );
7932                      api.notifications.add( new api.Notification( notificationCode, {
7933                          message: message || api.l10n.unknownError,
7934                          dismissible: true,
7935                          type: 'error'
7936                      } ) );
7937                  };
7938  
7939                  request.done( function( response ) {
7940                      success( response.message );
7941                  } );
7942  
7943                  request.fail( function( response ) {
7944                      var code = response.code || 'trashing_failed';
7945                      if ( response.success || 'non_existent_changeset' === code || 'changeset_already_trashed' === code ) {
7946                          success( response.message );
7947                      } else {
7948                          fail( code, response.message );
7949                      }
7950                  } );
7951              },
7952  
7953              /**
7954               * Builds the front preview URL with the current state of customizer.
7955               *
7956               * @since 4.9.0
7957               *
7958               * @return {string} Preview URL.
7959               */
7960              getFrontendPreviewUrl: function() {
7961                  var previewer = this, params, urlParser;
7962                  urlParser = document.createElement( 'a' );
7963                  urlParser.href = previewer.previewUrl.get();
7964                  params = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
7965  
7966                  if ( api.state( 'changesetStatus' ).get() && 'publish' !== api.state( 'changesetStatus' ).get() ) {
7967                      params.customize_changeset_uuid = api.settings.changeset.uuid;
7968                  }
7969                  if ( ! api.state( 'activated' ).get() ) {
7970                      params.customize_theme = api.settings.theme.stylesheet;
7971                  }
7972  
7973                  urlParser.search = $.param( params );
7974                  return urlParser.href;
7975              }
7976          });
7977  
7978          // Ensure preview nonce is included with every customized request, to allow post data to be read.
7979          $.ajaxPrefilter( function injectPreviewNonce( options ) {
7980              if ( ! /wp_customize=on/.test( options.data ) ) {
7981                  return;
7982              }
7983              options.data += '&' + $.param({
7984                  customize_preview_nonce: api.settings.nonce.preview
7985              });
7986          });
7987  
7988          // Refresh the nonces if the preview sends updated nonces over.
7989          api.previewer.bind( 'nonce', function( nonce ) {
7990              $.extend( this.nonce, nonce );
7991          });
7992  
7993          // Refresh the nonces if login sends updated nonces over.
7994          api.bind( 'nonce-refresh', function( nonce ) {
7995              $.extend( api.settings.nonce, nonce );
7996              $.extend( api.previewer.nonce, nonce );
7997              api.previewer.send( 'nonce-refresh', nonce );
7998          });
7999  
8000          // Create Settings.
8001          $.each( api.settings.settings, function( id, data ) {
8002              var Constructor = api.settingConstructor[ data.type ] || api.Setting;
8003              api.add( new Constructor( id, data.value, {
8004                  transport: data.transport,
8005                  previewer: api.previewer,
8006                  dirty: !! data.dirty
8007              } ) );
8008          });
8009  
8010          // Create Panels.
8011          $.each( api.settings.panels, function ( id, data ) {
8012              var Constructor = api.panelConstructor[ data.type ] || api.Panel, options;
8013              // Inclusion of params alias is for back-compat for custom panels that expect to augment this property.
8014              options = _.extend( { params: data }, data );
8015              api.panel.add( new Constructor( id, options ) );
8016          });
8017  
8018          // Create Sections.
8019          $.each( api.settings.sections, function ( id, data ) {
8020              var Constructor = api.sectionConstructor[ data.type ] || api.Section, options;
8021              // Inclusion of params alias is for back-compat for custom sections that expect to augment this property.
8022              options = _.extend( { params: data }, data );
8023              api.section.add( new Constructor( id, options ) );
8024          });
8025  
8026          // Create Controls.
8027          $.each( api.settings.controls, function( id, data ) {
8028              var Constructor = api.controlConstructor[ data.type ] || api.Control, options;
8029              // Inclusion of params alias is for back-compat for custom controls that expect to augment this property.
8030              options = _.extend( { params: data }, data );
8031              api.control.add( new Constructor( id, options ) );
8032          });
8033  
8034          // Focus the autofocused element.
8035          _.each( [ 'panel', 'section', 'control' ], function( type ) {
8036              var id = api.settings.autofocus[ type ];
8037              if ( ! id ) {
8038                  return;
8039              }
8040  
8041              /*
8042               * Defer focus until:
8043               * 1. The panel, section, or control exists (especially for dynamically-created ones).
8044               * 2. The instance is embedded in the document (and so is focusable).
8045               * 3. The preview has finished loading so that the active states have been set.
8046               */
8047              api[ type ]( id, function( instance ) {
8048                  instance.deferred.embedded.done( function() {
8049                      api.previewer.deferred.active.done( function() {
8050                          instance.focus();
8051                      });
8052                  });
8053              });
8054          });
8055  
8056          api.bind( 'ready', api.reflowPaneContents );
8057          $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
8058              var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents );
8059              values.bind( 'add', debouncedReflowPaneContents );
8060              values.bind( 'change', debouncedReflowPaneContents );
8061              values.bind( 'remove', debouncedReflowPaneContents );
8062          } );
8063  
8064          // Set up global notifications area.
8065          api.bind( 'ready', function setUpGlobalNotificationsArea() {
8066              var sidebar, containerHeight, containerInitialTop;
8067              api.notifications.container = $( '#customize-notifications-area' );
8068  
8069              api.notifications.bind( 'change', _.debounce( function() {
8070                  api.notifications.render();
8071              } ) );
8072  
8073              sidebar = $( '.wp-full-overlay-sidebar-content' );
8074              api.notifications.bind( 'rendered', function updateSidebarTop() {
8075                  sidebar.css( 'top', '' );
8076                  if ( 0 !== api.notifications.count() ) {
8077                      containerHeight = api.notifications.container.outerHeight() + 1;
8078                      containerInitialTop = parseInt( sidebar.css( 'top' ), 10 );
8079                      sidebar.css( 'top', containerInitialTop + containerHeight + 'px' );
8080                  }
8081                  api.notifications.trigger( 'sidebarTopUpdated' );
8082              });
8083  
8084              api.notifications.render();
8085          });
8086  
8087          // Save and activated states.
8088          (function( state ) {
8089              var saved = state.instance( 'saved' ),
8090                  saving = state.instance( 'saving' ),
8091                  trashing = state.instance( 'trashing' ),
8092                  activated = state.instance( 'activated' ),
8093                  processing = state.instance( 'processing' ),
8094                  paneVisible = state.instance( 'paneVisible' ),
8095                  expandedPanel = state.instance( 'expandedPanel' ),
8096                  expandedSection = state.instance( 'expandedSection' ),
8097                  changesetStatus = state.instance( 'changesetStatus' ),
8098                  selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ),
8099                  changesetDate = state.instance( 'changesetDate' ),
8100                  selectedChangesetDate = state.instance( 'selectedChangesetDate' ),
8101                  previewerAlive = state.instance( 'previewerAlive' ),
8102                  editShortcutVisibility  = state.instance( 'editShortcutVisibility' ),
8103                  changesetLocked = state.instance( 'changesetLocked' ),
8104                  populateChangesetUuidParam, defaultSelectedChangesetStatus;
8105  
8106              state.bind( 'change', function() {
8107                  var canSave;
8108  
8109                  if ( ! activated() ) {
8110                      saveBtn.val( api.l10n.activate );
8111                      closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
8112  
8113                  } else if ( '' === changesetStatus.get() && saved() ) {
8114                      if ( api.settings.changeset.currentUserCanPublish ) {
8115                          saveBtn.val( api.l10n.published );
8116                      } else {
8117                          saveBtn.val( api.l10n.saved );
8118                      }
8119                      closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
8120  
8121                  } else {
8122                      if ( 'draft' === selectedChangesetStatus() ) {
8123                          if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
8124                              saveBtn.val( api.l10n.draftSaved );
8125                          } else {
8126                              saveBtn.val( api.l10n.saveDraft );
8127                          }
8128                      } else if ( 'future' === selectedChangesetStatus() ) {
8129                          if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
8130                              if ( changesetDate.get() !== selectedChangesetDate.get() ) {
8131                                  saveBtn.val( api.l10n.schedule );
8132                              } else {
8133                                  saveBtn.val( api.l10n.scheduled );
8134                              }
8135                          } else {
8136                              saveBtn.val( api.l10n.schedule );
8137                          }
8138                      } else if ( api.settings.changeset.currentUserCanPublish ) {
8139                          saveBtn.val( api.l10n.publish );
8140                      }
8141                      closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
8142                  }
8143  
8144                  /*
8145                   * Save (publish) button should be enabled if saving is not currently happening,
8146                   * and if the theme is not active or the changeset exists but is not published.
8147                   */
8148                  canSave = ! saving() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
8149  
8150                  saveBtn.prop( 'disabled', ! canSave );
8151              });
8152  
8153              selectedChangesetStatus.validate = function( status ) {
8154                  if ( '' === status || 'auto-draft' === status ) {
8155                      return null;
8156                  }
8157                  return status;
8158              };
8159  
8160              defaultSelectedChangesetStatus = api.settings.changeset.currentUserCanPublish ? 'publish' : 'draft';
8161  
8162              // Set default states.
8163              changesetStatus( api.settings.changeset.status );
8164              changesetLocked( Boolean( api.settings.changeset.lockUser ) );
8165              changesetDate( api.settings.changeset.publishDate );
8166              selectedChangesetDate( api.settings.changeset.publishDate );
8167              selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? defaultSelectedChangesetStatus : api.settings.changeset.status );
8168              selectedChangesetStatus.link( changesetStatus ); // Ensure that direct updates to status on server via wp.customizer.previewer.save() will update selection.
8169              saved( true );
8170              if ( '' === changesetStatus() ) { // Handle case for loading starter content.
8171                  api.each( function( setting ) {
8172                      if ( setting._dirty ) {
8173                          saved( false );
8174                      }
8175                  } );
8176              }
8177              saving( false );
8178              activated( api.settings.theme.active );
8179              processing( 0 );
8180              paneVisible( true );
8181              expandedPanel( false );
8182              expandedSection( false );
8183              previewerAlive( true );
8184              editShortcutVisibility( 'visible' );
8185  
8186              api.bind( 'change', function() {
8187                  if ( state( 'saved' ).get() ) {
8188                      state( 'saved' ).set( false );
8189                  }
8190              });
8191  
8192              // Populate changeset UUID param when state becomes dirty.
8193              if ( api.settings.changeset.branching ) {
8194                  saved.bind( function( isSaved ) {
8195                      if ( ! isSaved ) {
8196                          populateChangesetUuidParam( true );
8197                      }
8198                  });
8199              }
8200  
8201              saving.bind( function( isSaving ) {
8202                  body.toggleClass( 'saving', isSaving );
8203              } );
8204              trashing.bind( function( isTrashing ) {
8205                  body.toggleClass( 'trashing', isTrashing );
8206              } );
8207  
8208              api.bind( 'saved', function( response ) {
8209                  state('saved').set( true );
8210                  if ( 'publish' === response.changeset_status ) {
8211                      state( 'activated' ).set( true );
8212                  }
8213              });
8214  
8215              activated.bind( function( to ) {
8216                  if ( to ) {
8217                      api.trigger( 'activated' );
8218                  }
8219              });
8220  
8221              /**
8222               * Populate URL with UUID via `history.replaceState()`.
8223               *
8224               * @since 4.7.0
8225               * @access private
8226               *
8227               * @param {boolean} isIncluded Is UUID included.
8228               * @return {void}
8229               */
8230              populateChangesetUuidParam = function( isIncluded ) {
8231                  var urlParser, queryParams;
8232  
8233                  // Abort on IE9 which doesn't support history management.
8234                  if ( ! history.replaceState ) {
8235                      return;
8236                  }
8237  
8238                  urlParser = document.createElement( 'a' );
8239                  urlParser.href = location.href;
8240                  queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
8241                  if ( isIncluded ) {
8242                      if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) {
8243                          return;
8244                      }
8245                      queryParams.changeset_uuid = api.settings.changeset.uuid;
8246                  } else {
8247                      if ( ! queryParams.changeset_uuid ) {
8248                          return;
8249                      }
8250                      delete queryParams.changeset_uuid;
8251                  }
8252                  urlParser.search = $.param( queryParams );
8253                  history.replaceState( {}, document.title, urlParser.href );
8254              };
8255  
8256              // Show changeset UUID in URL when in branching mode and there is a saved changeset.
8257              if ( api.settings.changeset.branching ) {
8258                  changesetStatus.bind( function( newStatus ) {
8259                      populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus && 'trash' !== newStatus );
8260                  } );
8261              }
8262          }( api.state ) );
8263  
8264          /**
8265           * Handles lock notice and take over request.
8266           *
8267           * @since 4.9.0
8268           */
8269          ( function checkAndDisplayLockNotice() {
8270  
8271              var LockedNotification = api.OverlayNotification.extend(/** @lends wp.customize~LockedNotification.prototype */{
8272  
8273                  /**
8274                   * Template ID.
8275                   *
8276                   * @type {string}
8277                   */
8278                  templateId: 'customize-changeset-locked-notification',
8279  
8280                  /**
8281                   * Lock user.
8282                   *
8283                   * @type {Object}
8284                   */
8285                  lockUser: null,
8286  
8287                  /**
8288                   * A notification that is displayed in a full-screen overlay with information about the locked changeset.
8289                   *
8290                   * @constructs wp.customize~LockedNotification
8291                   * @augments   wp.customize.OverlayNotification
8292                   *
8293                   * @since 4.9.0
8294                   *
8295                   * @param {string} [code]   Code.
8296                   * @param {Object} [params] Params.
8297                   */
8298                  initialize: function( code, params ) {
8299                      var notification = this, _code, _params;
8300                      _code = code || 'changeset_locked';
8301                      _params = _.extend(
8302                          {
8303                              message: '',
8304                              type: 'warning',
8305                              containerClasses: '',
8306                              lockUser: {}
8307                          },
8308                          params
8309                      );
8310                      _params.containerClasses += ' notification-changeset-locked';
8311                      api.OverlayNotification.prototype.initialize.call( notification, _code, _params );
8312                  },
8313  
8314                  /**
8315                   * Render notification.
8316                   *
8317                   * @since 4.9.0
8318                   *
8319                   * @return {JQuery} Notification container.
8320                   */
8321                  render: function() {
8322                      var notification = this, li, data, takeOverButton, request;
8323                      data = _.extend(
8324                          {
8325                              allowOverride: false,
8326                              returnUrl: api.settings.url['return'],
8327                              previewUrl: api.previewer.previewUrl.get(),
8328                              frontendPreviewUrl: api.previewer.getFrontendPreviewUrl()
8329                          },
8330                          this
8331                      );
8332  
8333                      li = api.OverlayNotification.prototype.render.call( data );
8334  
8335                      // Try to autosave the changeset now.
8336                      api.requestChangesetUpdate( {}, { autosave: true } ).fail( function( response ) {
8337                          if ( ! response.autosaved ) {
8338                              li.find( '.notice-error' ).prop( 'hidden', false ).text( response.message || api.l10n.unknownRequestFail );
8339                          }
8340                      } );
8341  
8342                      takeOverButton = li.find( '.customize-notice-take-over-button' );
8343                      takeOverButton.on( 'click', function( event ) {
8344                          event.preventDefault();
8345                          if ( request ) {
8346                              return;
8347                          }
8348  
8349                          takeOverButton.addClass( 'disabled' );
8350                          request = wp.ajax.post( 'customize_override_changeset_lock', {
8351                              wp_customize: 'on',
8352                              customize_theme: api.settings.theme.stylesheet,
8353                              customize_changeset_uuid: api.settings.changeset.uuid,
8354                              nonce: api.settings.nonce.override_lock
8355                          } );
8356  
8357                          request.done( function() {
8358                              api.notifications.remove( notification.code ); // Remove self.
8359                              api.state( 'changesetLocked' ).set( false );
8360                          } );
8361  
8362                          request.fail( function( response ) {
8363                              var message = response.message || api.l10n.unknownRequestFail;
8364                              li.find( '.notice-error' ).prop( 'hidden', false ).text( message );
8365  
8366                              request.always( function() {
8367                                  takeOverButton.removeClass( 'disabled' );
8368                              } );
8369                          } );
8370  
8371                          request.always( function() {
8372                              request = null;
8373                          } );
8374                      } );
8375  
8376                      return li;
8377                  }
8378              });
8379  
8380              /**
8381               * Start lock.
8382               *
8383               * @since 4.9.0
8384               *
8385               * @param {Object}  [args]                     Args.
8386               * @param {Object}  [args.lockUser]            Lock user data.
8387               * @param {boolean} [args.allowOverride=false] Whether override is allowed.
8388               * @return {void}
8389               */
8390  			function startLock( args ) {
8391                  if ( args && args.lockUser ) {
8392                      api.settings.changeset.lockUser = args.lockUser;
8393                  }
8394                  api.state( 'changesetLocked' ).set( true );
8395                  api.notifications.add( new LockedNotification( 'changeset_locked', {
8396                      lockUser: api.settings.changeset.lockUser,
8397                      allowOverride: Boolean( args && args.allowOverride )
8398                  } ) );
8399              }
8400  
8401              // Show initial notification.
8402              if ( api.settings.changeset.lockUser ) {
8403                  startLock( { allowOverride: true } );
8404              }
8405  
8406              // Check for lock when sending heartbeat requests.
8407              $( document ).on( 'heartbeat-send.update_lock_notice', function( event, data ) {
8408                  data.check_changeset_lock = true;
8409                  data.changeset_uuid = api.settings.changeset.uuid;
8410              } );
8411  
8412              // Handle heartbeat ticks.
8413              $( document ).on( 'heartbeat-tick.update_lock_notice', function( event, data ) {
8414                  var notification, code = 'changeset_locked';
8415                  if ( ! data.customize_changeset_lock_user ) {
8416                      return;
8417                  }
8418  
8419                  // Update notification when a different user takes over.
8420                  notification = api.notifications( code );
8421                  if ( notification && notification.lockUser.id !== api.settings.changeset.lockUser.id ) {
8422                      api.notifications.remove( code );
8423                  }
8424  
8425                  startLock( {
8426                      lockUser: data.customize_changeset_lock_user
8427                  } );
8428              } );
8429  
8430              // Handle locking in response to changeset save errors.
8431              api.bind( 'error', function( response ) {
8432                  if ( 'changeset_locked' === response.code && response.lock_user ) {
8433                      startLock( {
8434                          lockUser: response.lock_user
8435                      } );
8436                  }
8437              } );
8438          } )();
8439  
8440          // Set up initial notifications.
8441          (function() {
8442              var removedQueryParams = [], autosaveDismissed = false;
8443  
8444              /**
8445               * Obtain the URL to restore the autosave.
8446               *
8447               * @return {string} Customizer URL.
8448               */
8449  			function getAutosaveRestorationUrl() {
8450                  var urlParser, queryParams;
8451                  urlParser = document.createElement( 'a' );
8452                  urlParser.href = location.href;
8453                  queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
8454                  if ( api.settings.changeset.latestAutoDraftUuid ) {
8455                      queryParams.changeset_uuid = api.settings.changeset.latestAutoDraftUuid;
8456                  } else {
8457                      queryParams.customize_autosaved = 'on';
8458                  }
8459                  queryParams['return'] = api.settings.url['return'];
8460                  urlParser.search = $.param( queryParams );
8461                  return urlParser.href;
8462              }
8463  
8464              /**
8465               * Remove parameter from the URL.
8466               *
8467               * @param {string[]} params Parameter names to remove.
8468               * @return {void}
8469               */
8470  			function stripParamsFromLocation( params ) {
8471                  var urlParser = document.createElement( 'a' ), queryParams, strippedParams = 0;
8472                  urlParser.href = location.href;
8473                  queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
8474                  _.each( params, function( param ) {
8475                      if ( 'undefined' !== typeof queryParams[ param ] ) {
8476                          strippedParams += 1;
8477                          delete queryParams[ param ];
8478                      }
8479                  } );
8480                  if ( 0 === strippedParams ) {
8481                      return;
8482                  }
8483  
8484                  urlParser.search = $.param( queryParams );
8485                  history.replaceState( {}, document.title, urlParser.href );
8486              }
8487  
8488              /**
8489               * Displays a Site Editor notification when a block theme is activated.
8490               *
8491               * @since 4.9.0
8492               *
8493               * @param {string} [notification] A notification to display.
8494               * @return {void}
8495               */
8496  			function addSiteEditorNotification( notification ) {
8497                  api.notifications.add( new api.Notification( 'site_editor_block_theme_notice', {
8498                      message: notification,
8499                      type: 'info',
8500                      dismissible: false,
8501                      render: function() {
8502                          var notification = api.Notification.prototype.render.call( this ),
8503                              button = notification.find( 'button.switch-to-editor' );
8504  
8505                          button.on( 'click', function( event ) {
8506                              event.preventDefault();
8507                              location.assign( button.data( 'action' ) );
8508                          } );
8509  
8510                          return notification;
8511                      }
8512                  } ) );
8513              }
8514  
8515              /**
8516               * Dismiss autosave.
8517               *
8518               * @return {void}
8519               */
8520  			function dismissAutosave() {
8521                  if ( autosaveDismissed ) {
8522                      return;
8523                  }
8524                  wp.ajax.post( 'customize_dismiss_autosave_or_lock', {
8525                      wp_customize: 'on',
8526                      customize_theme: api.settings.theme.stylesheet,
8527                      customize_changeset_uuid: api.settings.changeset.uuid,
8528                      nonce: api.settings.nonce.dismiss_autosave_or_lock,
8529                      dismiss_autosave: true
8530                  } );
8531                  autosaveDismissed = true;
8532              }
8533  
8534              /**
8535               * Add notification regarding the availability of an autosave to restore.
8536               *
8537               * @return {void}
8538               */
8539  			function addAutosaveRestoreNotification() {
8540                  var code = 'autosave_available', onStateChange;
8541  
8542                  // Since there is an autosave revision and the user hasn't loaded with autosaved, add notification to prompt to load autosaved version.
8543                  api.notifications.add( new api.Notification( code, {
8544                      message: api.l10n.autosaveNotice,
8545                      type: 'warning',
8546                      dismissible: true,
8547                      render: function() {
8548                          var li = api.Notification.prototype.render.call( this ), link;
8549  
8550                          // Handle clicking on restoration link.
8551                          link = li.find( 'a' );
8552                          link.prop( 'href', getAutosaveRestorationUrl() );
8553                          link.on( 'click', function( event ) {
8554                              event.preventDefault();
8555                              location.replace( getAutosaveRestorationUrl() );
8556                          } );
8557  
8558                          // Handle dismissal of notice.
8559                          li.find( '.notice-dismiss' ).on( 'click', dismissAutosave );
8560  
8561                          return li;
8562                      }
8563                  } ) );
8564  
8565                  // Remove the notification once the user starts making changes.
8566                  onStateChange = function() {
8567                      dismissAutosave();
8568                      api.notifications.remove( code );
8569                      api.unbind( 'change', onStateChange );
8570                      api.state( 'changesetStatus' ).unbind( onStateChange );
8571                  };
8572                  api.bind( 'change', onStateChange );
8573                  api.state( 'changesetStatus' ).bind( onStateChange );
8574              }
8575  
8576              if ( api.settings.changeset.autosaved ) {
8577                  api.state( 'saved' ).set( false );
8578                  removedQueryParams.push( 'customize_autosaved' );
8579              }
8580              if ( ! api.settings.changeset.branching && ( ! api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ) ) {
8581                  removedQueryParams.push( 'changeset_uuid' ); // Remove UUID when restoring autosave auto-draft.
8582              }
8583              if ( removedQueryParams.length > 0 ) {
8584                  stripParamsFromLocation( removedQueryParams );
8585              }
8586              if ( api.settings.changeset.latestAutoDraftUuid || api.settings.changeset.hasAutosaveRevision ) {
8587                  addAutosaveRestoreNotification();
8588              }
8589              var shouldDisplayBlockThemeNotification = !! parseInt( $( '#customize-info' ).data( 'block-theme' ), 10 );
8590              if (shouldDisplayBlockThemeNotification) {
8591                  addSiteEditorNotification( api.l10n.blockThemeNotification );
8592              }
8593          })();
8594  
8595          // Check if preview url is valid and load the preview frame.
8596          if ( api.previewer.previewUrl() ) {
8597              api.previewer.refresh();
8598          } else {
8599              api.previewer.previewUrl( api.settings.url.home );
8600          }
8601  
8602          // Button bindings.
8603          saveBtn.on( 'click', function( event ) {
8604              api.previewer.save();
8605              event.preventDefault();
8606          }).on( 'keydown', function( event ) {
8607              if ( 9 === event.which ) { // Tab.
8608                  return;
8609              }
8610              if ( 13 === event.which ) { // Enter.
8611                  api.previewer.save();
8612              }
8613              event.preventDefault();
8614          });
8615  
8616          closeBtn.on( 'keydown', function( event ) {
8617              if ( 9 === event.which ) { // Tab.
8618                  return;
8619              }
8620              if ( 13 === event.which ) { // Enter.
8621                  this.click();
8622              }
8623              event.preventDefault();
8624          });
8625  
8626          $( '.collapse-sidebar' ).on( 'click', function() {
8627              api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
8628          });
8629  
8630          api.state( 'paneVisible' ).bind( function( paneVisible ) {
8631              overlay.toggleClass( 'preview-only', ! paneVisible );
8632              overlay.toggleClass( 'expanded', paneVisible );
8633              overlay.toggleClass( 'collapsed', ! paneVisible );
8634  
8635              if ( ! paneVisible ) {
8636                  $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
8637              } else {
8638                  $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
8639              }
8640          });
8641  
8642          // Keyboard shortcuts - esc to exit section/panel.
8643          body.on( 'keydown', function( event ) {
8644              var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
8645  
8646              if ( 27 !== event.which ) { // Esc.
8647                  return;
8648              }
8649  
8650              /*
8651               * Abort if the event target is not the body (the default) and not inside of #customize-controls.
8652               * This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else.
8653               */
8654              if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) {
8655                  return;
8656              }
8657  
8658              // Abort if we're inside of a block editor instance.
8659              if ( event.target.closest( '.block-editor-writing-flow' ) !== null ||
8660                  event.target.closest( '.block-editor-block-list__block-popover' ) !== null
8661              ) {
8662                  return;
8663              }
8664  
8665              // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
8666              api.control.each( function( control ) {
8667                  if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
8668                      expandedControls.push( control );
8669                  }
8670              });
8671              api.section.each( function( section ) {
8672                  if ( section.expanded() ) {
8673                      expandedSections.push( section );
8674                  }
8675              });
8676              api.panel.each( function( panel ) {
8677                  if ( panel.expanded() ) {
8678                      expandedPanels.push( panel );
8679                  }
8680              });
8681  
8682              // Skip collapsing expanded controls if there are no expanded sections.
8683              if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
8684                  expandedControls.length = 0;
8685              }
8686  
8687              // Collapse the most granular expanded object.
8688              collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
8689              if ( collapsedObject ) {
8690                  if ( 'themes' === collapsedObject.params.type ) {
8691  
8692                      // Themes panel or section.
8693                      if ( body.hasClass( 'modal-open' ) ) {
8694                          collapsedObject.closeDetails();
8695                      } else if ( api.panel.has( 'themes' ) ) {
8696  
8697                          // If we're collapsing a section, collapse the panel also.
8698                          api.panel( 'themes' ).collapse();
8699                      }
8700                      return;
8701                  }
8702                  collapsedObject.collapse();
8703                  event.preventDefault();
8704              }
8705          });
8706  
8707          $( '.customize-controls-preview-toggle' ).on( 'click', function() {
8708              api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
8709          });
8710  
8711          /*
8712           * Sticky header feature.
8713           */
8714          (function initStickyHeaders() {
8715              var parentContainer = $( '.wp-full-overlay-sidebar-content' ),
8716                  changeContainer, updateHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader,
8717                  activeHeader, lastScrollTop;
8718  
8719              /**
8720               * Determine which panel or section is currently expanded.
8721               *
8722               * @since 4.7.0
8723               * @access private
8724               *
8725               * @param {wp.customize.Panel|wp.customize.Section} container Construct.
8726               * @return {void}
8727               */
8728              changeContainer = function( container ) {
8729                  var newInstance = container,
8730                      expandedSection = api.state( 'expandedSection' ).get(),
8731                      expandedPanel = api.state( 'expandedPanel' ).get(),
8732                      headerElement;
8733  
8734                  if ( activeHeader && activeHeader.element ) {
8735                      // Release previously active header element.
8736                      releaseStickyHeader( activeHeader.element );
8737  
8738                      // Remove event listener in the previous panel or section.
8739                      activeHeader.element.find( '.description' ).off( 'toggled', updateHeaderHeight );
8740                  }
8741  
8742                  if ( ! newInstance ) {
8743                      if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) {
8744                          newInstance = expandedPanel;
8745                      } else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) {
8746                          newInstance = expandedSection;
8747                      } else {
8748                          activeHeader = false;
8749                          return;
8750                      }
8751                  }
8752  
8753                  headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first();
8754                  if ( headerElement.length ) {
8755                      activeHeader = {
8756                          instance: newInstance,
8757                          element:  headerElement,
8758                          parent:   headerElement.closest( '.customize-pane-child' ),
8759                          height:   headerElement.outerHeight()
8760                      };
8761  
8762                      // Update header height whenever help text is expanded or collapsed.
8763                      activeHeader.element.find( '.description' ).on( 'toggled', updateHeaderHeight );
8764  
8765                      if ( expandedSection ) {
8766                          resetStickyHeader( activeHeader.element, activeHeader.parent );
8767                      }
8768                  } else {
8769                      activeHeader = false;
8770                  }
8771              };
8772              api.state( 'expandedSection' ).bind( changeContainer );
8773              api.state( 'expandedPanel' ).bind( changeContainer );
8774  
8775              // Throttled scroll event handler.
8776              parentContainer.on( 'scroll', _.throttle( function() {
8777                  if ( ! activeHeader ) {
8778                      return;
8779                  }
8780  
8781                  var scrollTop = parentContainer.scrollTop(),
8782                      scrollDirection;
8783  
8784                  if ( ! lastScrollTop ) {
8785                      scrollDirection = 1;
8786                  } else {
8787                      if ( scrollTop === lastScrollTop ) {
8788                          scrollDirection = 0;
8789                      } else if ( scrollTop > lastScrollTop ) {
8790                          scrollDirection = 1;
8791                      } else {
8792                          scrollDirection = -1;
8793                      }
8794                  }
8795                  lastScrollTop = scrollTop;
8796                  if ( 0 !== scrollDirection ) {
8797                      positionStickyHeader( activeHeader, scrollTop, scrollDirection );
8798                  }
8799              }, 8 ) );
8800  
8801              // Update header position on sidebar layout change.
8802              api.notifications.bind( 'sidebarTopUpdated', function() {
8803                  if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) {
8804                      activeHeader.element.css( 'top', parentContainer.css( 'top' ) );
8805                  }
8806              });
8807  
8808              // Release header element if it is sticky.
8809              releaseStickyHeader = function( headerElement ) {
8810                  if ( ! headerElement.hasClass( 'is-sticky' ) ) {
8811                      return;
8812                  }
8813                  headerElement
8814                      .removeClass( 'is-sticky' )
8815                      .addClass( 'maybe-sticky is-in-view' )
8816                      .css( 'top', parentContainer.scrollTop() + 'px' );
8817              };
8818  
8819              // Reset position of the sticky header.
8820              resetStickyHeader = function( headerElement, headerParent ) {
8821                  if ( headerElement.hasClass( 'is-in-view' ) ) {
8822                      headerElement
8823                          .removeClass( 'maybe-sticky is-in-view' )
8824                          .css( {
8825                              width: '',
8826                              top:   ''
8827                          } );
8828                      headerParent.css( 'padding-top', '' );
8829                  }
8830              };
8831  
8832              /**
8833               * Update active header height.
8834               *
8835               * @since 4.7.0
8836               * @access private
8837               *
8838               * @return {void}
8839               */
8840              updateHeaderHeight = function() {
8841                  activeHeader.height = activeHeader.element.outerHeight();
8842              };
8843  
8844              /**
8845               * Reposition header on throttled `scroll` event.
8846               *
8847               * @since 4.7.0
8848               * @access private
8849               *
8850               * @param {Object} header          Header.
8851               * @param {number} scrollTop       Scroll top.
8852               * @param {number} scrollDirection Scroll direction, negative number being up and positive being down.
8853               * @return {void}
8854               */
8855              positionStickyHeader = function( header, scrollTop, scrollDirection ) {
8856                  var headerElement = header.element,
8857                      headerParent = header.parent,
8858                      headerHeight = header.height,
8859                      headerTop = parseInt( headerElement.css( 'top' ), 10 ),
8860                      maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
8861                      isSticky = headerElement.hasClass( 'is-sticky' ),
8862                      isInView = headerElement.hasClass( 'is-in-view' ),
8863                      isScrollingUp = ( -1 === scrollDirection );
8864  
8865                  // When scrolling down, gradually hide sticky header.
8866                  if ( ! isScrollingUp ) {
8867                      if ( isSticky ) {
8868                          headerTop = scrollTop;
8869                          headerElement
8870                              .removeClass( 'is-sticky' )
8871                              .css( {
8872                                  top:   headerTop + 'px',
8873                                  width: ''
8874                              } );
8875                      }
8876                      if ( isInView && scrollTop > headerTop + headerHeight ) {
8877                          headerElement.removeClass( 'is-in-view' );
8878                          headerParent.css( 'padding-top', '' );
8879                      }
8880                      return;
8881                  }
8882  
8883                  // Scrolling up.
8884                  if ( ! maybeSticky && scrollTop >= headerHeight ) {
8885                      maybeSticky = true;
8886                      headerElement.addClass( 'maybe-sticky' );
8887                  } else if ( 0 === scrollTop ) {
8888                      // Reset header in base position.
8889                      headerElement
8890                          .removeClass( 'maybe-sticky is-in-view is-sticky' )
8891                          .css( {
8892                              top:   '',
8893                              width: ''
8894                          } );
8895                      headerParent.css( 'padding-top', '' );
8896                      return;
8897                  }
8898  
8899                  if ( isInView && ! isSticky ) {
8900                      // Header is in the view but is not yet sticky.
8901                      if ( headerTop >= scrollTop ) {
8902                          // Header is fully visible.
8903                          headerElement
8904                              .addClass( 'is-sticky' )
8905                              .css( {
8906                                  top:   parentContainer.css( 'top' ),
8907                                  width: headerParent.outerWidth() + 'px'
8908                              } );
8909                      }
8910                  } else if ( maybeSticky && ! isInView ) {
8911                      // Header is out of the view.
8912                      headerElement
8913                          .addClass( 'is-in-view' )
8914                          .css( 'top', ( scrollTop - headerHeight ) + 'px' );
8915                      headerParent.css( 'padding-top', headerHeight + 'px' );
8916                  }
8917              };
8918          }());
8919  
8920          // Previewed device bindings. (The api.previewedDevice property
8921          // is how this Value was first introduced, but since it has moved to api.state.)
8922          api.previewedDevice = api.state( 'previewedDevice' );
8923  
8924          // Set the default device.
8925          api.bind( 'ready', function() {
8926              _.find( api.settings.previewableDevices, function( value, key ) {
8927                  if ( true === value['default'] ) {
8928                      api.previewedDevice.set( key );
8929                      return true;
8930                  }
8931              } );
8932          } );
8933  
8934          // Set the toggled device.
8935          footerActions.find( '.devices button' ).on( 'click', function( event ) {
8936              api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
8937          });
8938  
8939          // Bind device changes.
8940          api.previewedDevice.bind( function( newDevice ) {
8941              var overlay = $( '.wp-full-overlay' ),
8942                  devices = '';
8943  
8944              footerActions.find( '.devices button' )
8945                  .removeClass( 'active' )
8946                  .attr( 'aria-pressed', false );
8947  
8948              footerActions.find( '.devices .preview-' + newDevice )
8949                  .addClass( 'active' )
8950                  .attr( 'aria-pressed', true );
8951  
8952              $.each( api.settings.previewableDevices, function( device ) {
8953                  devices += ' preview-' + device;
8954              } );
8955  
8956              overlay
8957                  .removeClass( devices )
8958                  .addClass( 'preview-' + newDevice );
8959          } );
8960  
8961          // Bind site title display to the corresponding field.
8962          if ( title.length ) {
8963              api( 'blogname', function( setting ) {
8964                  var updateTitle = function() {
8965                      var blogTitle = setting() || '';
8966                      title.text( blogTitle.toString().trim() || api.l10n.untitledBlogName );
8967                  };
8968                  setting.bind( updateTitle );
8969                  updateTitle();
8970              } );
8971          }
8972  
8973          /*
8974           * Create a postMessage connection with a parent frame,
8975           * in case the Customizer frame was opened with the Customize loader.
8976           *
8977           * @see wp.customize.Loader
8978           */
8979          parent = new api.Messenger({
8980              url: api.settings.url.parent,
8981              channel: 'loader'
8982          });
8983  
8984          // Handle exiting of Customizer.
8985          (function() {
8986              var isInsideIframe = false;
8987  
8988              /**
8989               * Determine whether the Customizer is in a clean state.
8990               *
8991               * @return {boolean} True if the Customizer is in a clean state, false otherwise.
8992               */
8993  			function isCleanState() {
8994                  var defaultChangesetStatus;
8995  
8996                  /*
8997                   * Handle special case of previewing theme switch since some settings (for nav menus and widgets)
8998                   * are pre-dirty and non-active themes can only ever be auto-drafts.
8999                   */
9000                  if ( ! api.state( 'activated' ).get() ) {
9001                      return 0 === api._latestRevision;
9002                  }
9003  
9004                  // Dirty if the changeset status has been changed but not saved yet.
9005                  defaultChangesetStatus = api.state( 'changesetStatus' ).get();
9006                  if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
9007                      defaultChangesetStatus = 'publish';
9008                  }
9009                  if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
9010                      return false;
9011                  }
9012  
9013                  // Dirty if scheduled but the changeset date hasn't been saved yet.
9014                  if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
9015                      return false;
9016                  }
9017  
9018                  return api.state( 'saved' ).get() && 'auto-draft' !== api.state( 'changesetStatus' ).get();
9019              }
9020  
9021              /*
9022               * If we receive a 'back' event, we're inside an iframe.
9023               * Send any clicks to the 'Return' link to the parent page.
9024               */
9025              parent.bind( 'back', function() {
9026                  isInsideIframe = true;
9027              });
9028  
9029              /**
9030               * Starts prompting the user before unloading the Customizer if there are unsaved changes.
9031               */
9032  			function startPromptingBeforeUnload() {
9033                  api.unbind( 'change', startPromptingBeforeUnload );
9034                  api.state( 'selectedChangesetStatus' ).unbind( startPromptingBeforeUnload );
9035                  api.state( 'selectedChangesetDate' ).unbind( startPromptingBeforeUnload );
9036  
9037                  // Prompt user with AYS dialog if leaving the Customizer with unsaved changes.
9038                  $( window ).on( 'beforeunload.customize-confirm', function() {
9039                      if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) {
9040                          setTimeout( function() {
9041                              overlay.removeClass( 'customize-loading' );
9042                          }, 1 );
9043                          return api.l10n.saveAlert;
9044                      }
9045                  });
9046              }
9047              api.bind( 'change', startPromptingBeforeUnload );
9048              api.state( 'selectedChangesetStatus' ).bind( startPromptingBeforeUnload );
9049              api.state( 'selectedChangesetDate' ).bind( startPromptingBeforeUnload );
9050  
9051              /**
9052               * Requests to close the Customizer, prompting the user to save changes if necessary.
9053               *
9054               * @return {JQuery.Promise} A promise that resolves if the user is cleared to close the Customizer, or rejects if the user cancels the close.
9055               */
9056  			function requestClose() {
9057                  var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false;
9058  
9059                  if ( isCleanState() ) {
9060                      dismissLock = true;
9061                  } else if ( confirm( api.l10n.saveAlert ) ) {
9062  
9063                      dismissLock = true;
9064  
9065                      // Mark all settings as clean to prevent another call to requestChangesetUpdate.
9066                      api.each( function( setting ) {
9067                          setting._dirty = false;
9068                      });
9069                      $( document ).off( 'visibilitychange.wp-customize-changeset-update' );
9070                      $( window ).off( 'beforeunload.wp-customize-changeset-update' );
9071  
9072                      closeBtn.css( 'cursor', 'progress' );
9073                      if ( '' !== api.state( 'changesetStatus' ).get() ) {
9074                          dismissAutoSave = true;
9075                      }
9076                  } else {
9077                      clearedToClose.reject();
9078                  }
9079  
9080                  if ( dismissLock || dismissAutoSave ) {
9081                      wp.ajax.send( 'customize_dismiss_autosave_or_lock', {
9082                          timeout: 500, // Don't wait too long.
9083                          data: {
9084                              wp_customize: 'on',
9085                              customize_theme: api.settings.theme.stylesheet,
9086                              customize_changeset_uuid: api.settings.changeset.uuid,
9087                              nonce: api.settings.nonce.dismiss_autosave_or_lock,
9088                              dismiss_autosave: dismissAutoSave,
9089                              dismiss_lock: dismissLock
9090                          }
9091                      } ).always( function() {
9092                          clearedToClose.resolve();
9093                      } );
9094                  }
9095  
9096                  return clearedToClose.promise();
9097              }
9098  
9099              parent.bind( 'confirm-close', function() {
9100                  requestClose().done( function() {
9101                      parent.send( 'confirmed-close', true );
9102                  } ).fail( function() {
9103                      parent.send( 'confirmed-close', false );
9104                  } );
9105              } );
9106  
9107              closeBtn.on( 'click.customize-controls-close', function( event ) {
9108                  event.preventDefault();
9109                  if ( isInsideIframe ) {
9110                      parent.send( 'close' ); // See confirm-close logic above.
9111                  } else {
9112                      requestClose().done( function() {
9113                          $( window ).off( 'beforeunload.customize-confirm' );
9114                          window.location.href = closeBtn.prop( 'href' );
9115                      } );
9116                  }
9117              });
9118          })();
9119  
9120          // Pass events through to the parent.
9121          $.each( [ 'saved', 'change' ], function ( i, event ) {
9122              api.bind( event, function() {
9123                  parent.send( event );
9124              });
9125          } );
9126  
9127          // Pass titles to the parent.
9128          api.bind( 'title', function( newTitle ) {
9129              parent.send( 'title', newTitle );
9130          });
9131  
9132          if ( api.settings.changeset.branching ) {
9133              parent.send( 'changeset-uuid', api.settings.changeset.uuid );
9134          }
9135  
9136          // Initialize the connection with the parent frame.
9137          parent.send( 'ready' );
9138  
9139          // Control visibility for default controls.
9140          $.each({
9141              'background_image': {
9142                  controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ],
9143                  callback: function( to ) { return !! to; }
9144              },
9145              'show_on_front': {
9146                  controls: [ 'page_on_front', 'page_for_posts' ],
9147                  callback: function( to ) { return 'page' === to; }
9148              },
9149              'header_textcolor': {
9150                  controls: [ 'header_textcolor' ],
9151                  callback: function( to ) { return 'blank' !== to; }
9152              }
9153          }, function( settingId, o ) {
9154              api( settingId, function( setting ) {
9155                  $.each( o.controls, function( i, controlId ) {
9156                      api.control( controlId, function( control ) {
9157                          var visibility = function( to ) {
9158                              control.container.toggle( o.callback( to ) );
9159                          };
9160  
9161                          visibility( setting.get() );
9162                          setting.bind( visibility );
9163                      });
9164                  });
9165              });
9166          });
9167  
9168          api.control( 'background_preset', function( control ) {
9169              var visibility, defaultValues, values, toggleVisibility, updateSettings, preset;
9170  
9171              visibility = { // position, size, repeat, attachment.
9172                  'default': [ false, false, false, false ],
9173                  'fill': [ true, false, false, false ],
9174                  'fit': [ true, false, true, false ],
9175                  'repeat': [ true, false, false, true ],
9176                  'custom': [ true, true, true, true ]
9177              };
9178  
9179              defaultValues = [
9180                  _wpCustomizeBackground.defaults['default-position-x'],
9181                  _wpCustomizeBackground.defaults['default-position-y'],
9182                  _wpCustomizeBackground.defaults['default-size'],
9183                  _wpCustomizeBackground.defaults['default-repeat'],
9184                  _wpCustomizeBackground.defaults['default-attachment']
9185              ];
9186  
9187              values = { // position_x, position_y, size, repeat, attachment.
9188                  'default': defaultValues,
9189                  'fill': [ 'left', 'top', 'cover', 'no-repeat', 'fixed' ],
9190                  'fit': [ 'left', 'top', 'contain', 'no-repeat', 'fixed' ],
9191                  'repeat': [ 'left', 'top', 'auto', 'repeat', 'scroll' ]
9192              };
9193  
9194              // @todo These should actually toggle the active state,
9195              // but without the preview overriding the state in data.activeControls.
9196              toggleVisibility = function( preset ) {
9197                  _.each( [ 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], function( controlId, i ) {
9198                      var control = api.control( controlId );
9199                      if ( control ) {
9200                          control.container.toggle( visibility[ preset ][ i ] );
9201                      }
9202                  } );
9203              };
9204  
9205              updateSettings = function( preset ) {
9206                  _.each( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ], function( settingId, i ) {
9207                      var setting = api( settingId );
9208                      if ( setting ) {
9209                          setting.set( values[ preset ][ i ] );
9210                      }
9211                  } );
9212              };
9213  
9214              preset = control.setting.get();
9215              toggleVisibility( preset );
9216  
9217              control.setting.bind( 'change', function( preset ) {
9218                  toggleVisibility( preset );
9219                  if ( 'custom' !== preset ) {
9220                      updateSettings( preset );
9221                  }
9222              } );
9223          } );
9224  
9225          api.control( 'background_repeat', function( control ) {
9226              control.elements[0].unsync( api( 'background_repeat' ) );
9227  
9228              control.element = new api.Element( control.container.find( 'input' ) );
9229              control.element.set( 'no-repeat' !== control.setting() );
9230  
9231              control.element.bind( function( to ) {
9232                  control.setting.set( to ? 'repeat' : 'no-repeat' );
9233              } );
9234  
9235              control.setting.bind( function( to ) {
9236                  control.element.set( 'no-repeat' !== to );
9237              } );
9238          } );
9239  
9240          api.control( 'background_attachment', function( control ) {
9241              control.elements[0].unsync( api( 'background_attachment' ) );
9242  
9243              control.element = new api.Element( control.container.find( 'input' ) );
9244              control.element.set( 'fixed' !== control.setting() );
9245  
9246              control.element.bind( function( to ) {
9247                  control.setting.set( to ? 'scroll' : 'fixed' );
9248              } );
9249  
9250              control.setting.bind( function( to ) {
9251                  control.element.set( 'fixed' !== to );
9252              } );
9253          } );
9254  
9255          // Juggle the two controls that use header_textcolor.
9256          api.control( 'display_header_text', function( control ) {
9257              var last = '';
9258  
9259              control.elements[0].unsync( api( 'header_textcolor' ) );
9260  
9261              control.element = new api.Element( control.container.find('input') );
9262              control.element.set( 'blank' !== control.setting() );
9263  
9264              control.element.bind( function( to ) {
9265                  if ( ! to ) {
9266                      last = api( 'header_textcolor' ).get();
9267                  }
9268  
9269                  control.setting.set( to ? last : 'blank' );
9270              });
9271  
9272              control.setting.bind( function( to ) {
9273                  control.element.set( 'blank' !== to );
9274              });
9275          });
9276  
9277          // Add behaviors to the static front page controls.
9278          api( 'show_on_front', 'page_on_front', 'page_for_posts', function( showOnFront, pageOnFront, pageForPosts ) {
9279              var handleChange = function() {
9280                  var setting = this, pageOnFrontId, pageForPostsId, errorCode = 'show_on_front_page_collision';
9281                  pageOnFrontId = parseInt( pageOnFront(), 10 );
9282                  pageForPostsId = parseInt( pageForPosts(), 10 );
9283  
9284                  if ( 'page' === showOnFront() ) {
9285  
9286                      // Change previewed URL to the homepage when changing the page_on_front.
9287                      if ( setting === pageOnFront && pageOnFrontId > 0 ) {
9288                          api.previewer.previewUrl.set( api.settings.url.home );
9289                      }
9290  
9291                      // Change the previewed URL to the selected page when changing the page_for_posts.
9292                      if ( setting === pageForPosts && pageForPostsId > 0 ) {
9293                          api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageForPostsId );
9294                      }
9295                  }
9296  
9297                  // Toggle notification when the homepage and posts page are both set and the same.
9298                  if ( 'page' === showOnFront() && pageOnFrontId && pageForPostsId && pageOnFrontId === pageForPostsId ) {
9299                      showOnFront.notifications.add( new api.Notification( errorCode, {
9300                          type: 'error',
9301                          message: api.l10n.pageOnFrontError
9302                      } ) );
9303                  } else {
9304                      showOnFront.notifications.remove( errorCode );
9305                  }
9306              };
9307              showOnFront.bind( handleChange );
9308              pageOnFront.bind( handleChange );
9309              pageForPosts.bind( handleChange );
9310              handleChange.call( showOnFront, showOnFront() ); // Make sure initial notification is added after loading existing changeset.
9311  
9312              // Move notifications container to the bottom.
9313              api.control( 'show_on_front', function( showOnFrontControl ) {
9314                  showOnFrontControl.deferred.embedded.done( function() {
9315                      showOnFrontControl.container.append( showOnFrontControl.getNotificationsContainerElement() );
9316                  });
9317              });
9318          });
9319  
9320          // Add code editor for Custom CSS.
9321          (function() {
9322              var sectionReady = $.Deferred();
9323  
9324              api.section( 'custom_css', function( section ) {
9325                  section.deferred.embedded.done( function() {
9326                      if ( section.expanded() ) {
9327                          sectionReady.resolve( section );
9328                      } else {
9329                          section.expanded.bind( function( isExpanded ) {
9330                              if ( isExpanded ) {
9331                                  sectionReady.resolve( section );
9332                              }
9333                          } );
9334                      }
9335                  });
9336              });
9337  
9338              // Set up the section description behaviors.
9339              sectionReady.done( function setupSectionDescription( section ) {
9340                  var control = api.control( 'custom_css' );
9341  
9342                  // Hide redundant label for visual users.
9343                  control.container.find( '.customize-control-title:first' ).addClass( 'screen-reader-text' );
9344  
9345                  // Close the section description when clicking the close button.
9346                  section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() {
9347                      section.container.find( '.section-meta .customize-section-description:first' )
9348                          .removeClass( 'open' )
9349                          .slideUp();
9350  
9351                      section.container.find( '.customize-help-toggle' )
9352                          .attr( 'aria-expanded', 'false' )
9353                          .focus(); // Avoid focus loss.
9354                  });
9355  
9356                  // Reveal help text if setting is empty.
9357                  if ( control && ! control.setting.get() ) {
9358                      section.container.find( '.section-meta .customize-section-description:first' )
9359                          .addClass( 'open' )
9360                          .show()
9361                          .trigger( 'toggled' );
9362  
9363                      section.container.find( '.customize-help-toggle' ).attr( 'aria-expanded', 'true' );
9364                  }
9365              });
9366          })();
9367  
9368          // Toggle visibility of Header Video notice when active state change.
9369          api.control( 'header_video', function( headerVideoControl ) {
9370              headerVideoControl.deferred.embedded.done( function() {
9371                  var toggleNotice = function() {
9372                      var section = api.section( headerVideoControl.section() ), noticeCode = 'video_header_not_available';
9373                      if ( ! section ) {
9374                          return;
9375                      }
9376                      if ( headerVideoControl.active.get() ) {
9377                          section.notifications.remove( noticeCode );
9378                      } else {
9379                          section.notifications.add( new api.Notification( noticeCode, {
9380                              type: 'info',
9381                              message: api.l10n.videoHeaderNotice
9382                          } ) );
9383                      }
9384                  };
9385                  toggleNotice();
9386                  headerVideoControl.active.bind( toggleNotice );
9387              } );
9388          } );
9389  
9390          // Update the setting validities.
9391          api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
9392              api._handleSettingValidities( {
9393                  settingValidities: settingValidities,
9394                  focusInvalidControl: false
9395              } );
9396          } );
9397  
9398          // Focus on the control that is associated with the given setting.
9399          api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
9400              var matchedControls = [];
9401              api.control.each( function( control ) {
9402                  var settingIds = _.pluck( control.settings, 'id' );
9403                  if ( -1 !== _.indexOf( settingIds, settingId ) ) {
9404                      matchedControls.push( control );
9405                  }
9406              } );
9407  
9408              // Focus on the matched control with the lowest priority (appearing higher).
9409              if ( matchedControls.length ) {
9410                  matchedControls.sort( function( a, b ) {
9411                      return a.priority() - b.priority();
9412                  } );
9413                  matchedControls[0].focus();
9414              }
9415          } );
9416  
9417          // Refresh the preview when it requests.
9418          api.previewer.bind( 'refresh', function() {
9419              api.previewer.refresh();
9420          });
9421  
9422          // Update the edit shortcut visibility state.
9423          api.state( 'paneVisible' ).bind( function( isPaneVisible ) {
9424              var isMobileScreen;
9425              if ( window.matchMedia ) {
9426                  isMobileScreen = window.matchMedia( 'screen and ( max-width: 640px )' ).matches;
9427              } else {
9428                  isMobileScreen = $( window ).width() <= 640;
9429              }
9430              api.state( 'editShortcutVisibility' ).set( isPaneVisible || isMobileScreen ? 'visible' : 'hidden' );
9431          } );
9432          if ( window.matchMedia ) {
9433              window.matchMedia( 'screen and ( max-width: 640px )' ).addListener( function() {
9434                  var state = api.state( 'paneVisible' );
9435                  state.callbacks.fireWith( state, [ state.get(), state.get() ] );
9436              } );
9437          }
9438          api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) {
9439              api.state( 'editShortcutVisibility' ).set( visibility );
9440          } );
9441          api.state( 'editShortcutVisibility' ).bind( function( visibility ) {
9442              api.previewer.send( 'edit-shortcut-visibility', visibility );
9443          } );
9444  
9445          /**
9446           * Starts autosaving changeset when a change is made.
9447           */
9448  		function startAutosaving() {
9449              var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
9450  
9451              api.unbind( 'change', startAutosaving ); // Ensure startAutosaving only fires once.
9452  
9453              /**
9454               * Handles changes to the saved state of the changeset.
9455               *
9456               * @param {boolean} isSaved Whether the changeset is saved or not.
9457               */
9458  			function onChangeSaved( isSaved ) {
9459                  if ( ! isSaved && ! api.settings.changeset.autosaved ) {
9460                      api.settings.changeset.autosaved = true; // Once a change is made then autosaving kicks in.
9461                      api.previewer.send( 'autosaving' );
9462                  }
9463              }
9464              api.state( 'saved' ).bind( onChangeSaved );
9465              onChangeSaved( api.state( 'saved' ).get() );
9466  
9467              /**
9468               * Request changeset update and then re-schedule the next changeset update time.
9469               *
9470               * @since 4.7.0
9471               * @private
9472               */
9473              updateChangesetWithReschedule = function() {
9474                  if ( ! updatePending ) {
9475                      updatePending = true;
9476                      api.requestChangesetUpdate( {}, { autosave: true } ).always( function() {
9477                          updatePending = false;
9478                      } );
9479                  }
9480                  scheduleChangesetUpdate();
9481              };
9482  
9483              /**
9484               * Schedule changeset update.
9485               *
9486               * @since 4.7.0
9487               * @private
9488               */
9489              scheduleChangesetUpdate = function() {
9490                  clearTimeout( timeoutId );
9491                  timeoutId = setTimeout( function() {
9492                      updateChangesetWithReschedule();
9493                  }, api.settings.timeouts.changesetAutoSave );
9494              };
9495  
9496              // Start auto-save interval for updating changeset.
9497              scheduleChangesetUpdate();
9498  
9499              // Save changeset when focus removed from window.
9500              $( document ).on( 'visibilitychange.wp-customize-changeset-update', function() {
9501                  if ( document.hidden ) {
9502                      updateChangesetWithReschedule();
9503                  }
9504              } );
9505  
9506              // Save changeset before unloading window.
9507              $( window ).on( 'beforeunload.wp-customize-changeset-update', function() {
9508                  updateChangesetWithReschedule();
9509              } );
9510          }
9511          api.bind( 'change', startAutosaving );
9512  
9513          // Make sure TinyMCE dialogs appear above Customizer UI.
9514          $( document ).one( 'tinymce-editor-setup', function() {
9515              if ( window.tinymce.ui.FloatPanel && ( ! window.tinymce.ui.FloatPanel.zIndex || window.tinymce.ui.FloatPanel.zIndex < 500001 ) ) {
9516                  window.tinymce.ui.FloatPanel.zIndex = 500001;
9517              }
9518          } );
9519  
9520          body.addClass( 'ready' );
9521          api.trigger( 'ready' );
9522      });
9523  
9524  })( wp, jQuery );


Generated : Wed Oct 14 08:20:33 2026 Cross-referenced by PHPXref