[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

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