[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Thu Jul 31 08:20:01 2025 Cross-referenced by PHPXref