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


Generated : Tue Sep 9 08:20:04 2025 Cross-referenced by PHPXref