[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Fri Oct 10 08:20:03 2025 Cross-referenced by PHPXref