[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Fri Aug 14 08:20:23 2026 Cross-referenced by PHPXref