[ Index ]

PHP Cross Reference of WordPress

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


Generated: Thu Nov 21 01:00:03 2024 Cross-referenced by PHPXref 0.7.1