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


Generated : Thu Nov 21 08:20:01 2024 Cross-referenced by PHPXref