[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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