You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9353 lines
286 KiB

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