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