customize-selective-refresh.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062
  1. /* global jQuery, JSON, _customizePartialRefreshExports, console */
  2. /** @namespace wp.customize.selectiveRefresh */
  3. wp.customize.selectiveRefresh = ( function( $, api ) {
  4. 'use strict';
  5. var self, Partial, Placement;
  6. self = {
  7. ready: $.Deferred(),
  8. editShortcutVisibility: new api.Value(),
  9. data: {
  10. partials: {},
  11. renderQueryVar: '',
  12. l10n: {
  13. shiftClickToEdit: ''
  14. }
  15. },
  16. currentRequest: null
  17. };
  18. _.extend( self, api.Events );
  19. /**
  20. * A Customizer Partial.
  21. *
  22. * A partial provides a rendering of one or more settings according to a template.
  23. *
  24. * @memberOf wp.customize.selectiveRefresh
  25. *
  26. * @see PHP class WP_Customize_Partial.
  27. *
  28. * @class
  29. * @augments wp.customize.Class
  30. * @since 4.5.0
  31. */
  32. Partial = self.Partial = api.Class.extend(/** @lends wp.customize.SelectiveRefresh.Partial.prototype */{
  33. id: null,
  34. /**
  35. * Default params.
  36. *
  37. * @since 4.9.0
  38. * @var {object}
  39. */
  40. defaults: {
  41. selector: null,
  42. primarySetting: null,
  43. containerInclusive: false,
  44. fallbackRefresh: true // Note this needs to be false in a front-end editing context.
  45. },
  46. /**
  47. * Constructor.
  48. *
  49. * @since 4.5.0
  50. *
  51. * @param {string} id - Unique identifier for the partial instance.
  52. * @param {object} options - Options hash for the partial instance.
  53. * @param {string} options.type - Type of partial (e.g. nav_menu, widget, etc)
  54. * @param {string} options.selector - jQuery selector to find the container element in the page.
  55. * @param {array} options.settings - The IDs for the settings the partial relates to.
  56. * @param {string} options.primarySetting - The ID for the primary setting the partial renders.
  57. * @param {bool} options.fallbackRefresh - Whether to refresh the entire preview in case of a partial refresh failure.
  58. * @param {object} [options.params] - Deprecated wrapper for the above properties.
  59. */
  60. initialize: function( id, options ) {
  61. var partial = this;
  62. options = options || {};
  63. partial.id = id;
  64. partial.params = _.extend(
  65. {
  66. settings: []
  67. },
  68. partial.defaults,
  69. options.params || options
  70. );
  71. partial.deferred = {};
  72. partial.deferred.ready = $.Deferred();
  73. partial.deferred.ready.done( function() {
  74. partial.ready();
  75. } );
  76. },
  77. /**
  78. * Set up the partial.
  79. *
  80. * @since 4.5.0
  81. */
  82. ready: function() {
  83. var partial = this;
  84. _.each( partial.placements(), function( placement ) {
  85. $( placement.container ).attr( 'title', self.data.l10n.shiftClickToEdit );
  86. partial.createEditShortcutForPlacement( placement );
  87. } );
  88. $( document ).on( 'click', partial.params.selector, function( e ) {
  89. if ( ! e.shiftKey ) {
  90. return;
  91. }
  92. e.preventDefault();
  93. _.each( partial.placements(), function( placement ) {
  94. if ( $( placement.container ).is( e.currentTarget ) ) {
  95. partial.showControl();
  96. }
  97. } );
  98. } );
  99. },
  100. /**
  101. * Create and show the edit shortcut for a given partial placement container.
  102. *
  103. * @since 4.7.0
  104. * @access public
  105. *
  106. * @param {Placement} placement The placement container element.
  107. * @returns {void}
  108. */
  109. createEditShortcutForPlacement: function( placement ) {
  110. var partial = this, $shortcut, $placementContainer, illegalAncestorSelector, illegalContainerSelector;
  111. if ( ! placement.container ) {
  112. return;
  113. }
  114. $placementContainer = $( placement.container );
  115. illegalAncestorSelector = 'head';
  116. illegalContainerSelector = 'area, audio, base, bdi, bdo, br, button, canvas, col, colgroup, command, datalist, embed, head, hr, html, iframe, img, input, keygen, label, link, map, math, menu, meta, noscript, object, optgroup, option, param, progress, rp, rt, ruby, script, select, source, style, svg, table, tbody, textarea, tfoot, thead, title, tr, track, video, wbr';
  117. if ( ! $placementContainer.length || $placementContainer.is( illegalContainerSelector ) || $placementContainer.closest( illegalAncestorSelector ).length ) {
  118. return;
  119. }
  120. $shortcut = partial.createEditShortcut();
  121. $shortcut.on( 'click', function( event ) {
  122. event.preventDefault();
  123. event.stopPropagation();
  124. partial.showControl();
  125. } );
  126. partial.addEditShortcutToPlacement( placement, $shortcut );
  127. },
  128. /**
  129. * Add an edit shortcut to the placement container.
  130. *
  131. * @since 4.7.0
  132. * @access public
  133. *
  134. * @param {Placement} placement The placement for the partial.
  135. * @param {jQuery} $editShortcut The shortcut element as a jQuery object.
  136. * @returns {void}
  137. */
  138. addEditShortcutToPlacement: function( placement, $editShortcut ) {
  139. var $placementContainer = $( placement.container );
  140. $placementContainer.prepend( $editShortcut );
  141. if ( ! $placementContainer.is( ':visible' ) || 'none' === $placementContainer.css( 'display' ) ) {
  142. $editShortcut.addClass( 'customize-partial-edit-shortcut-hidden' );
  143. }
  144. },
  145. /**
  146. * Return the unique class name for the edit shortcut button for this partial.
  147. *
  148. * @since 4.7.0
  149. * @access public
  150. *
  151. * @return {string} Partial ID converted into a class name for use in shortcut.
  152. */
  153. getEditShortcutClassName: function() {
  154. var partial = this, cleanId;
  155. cleanId = partial.id.replace( /]/g, '' ).replace( /\[/g, '-' );
  156. return 'customize-partial-edit-shortcut-' + cleanId;
  157. },
  158. /**
  159. * Return the appropriate translated string for the edit shortcut button.
  160. *
  161. * @since 4.7.0
  162. * @access public
  163. *
  164. * @return {string} Tooltip for edit shortcut.
  165. */
  166. getEditShortcutTitle: function() {
  167. var partial = this, l10n = self.data.l10n;
  168. switch ( partial.getType() ) {
  169. case 'widget':
  170. return l10n.clickEditWidget;
  171. case 'blogname':
  172. return l10n.clickEditTitle;
  173. case 'blogdescription':
  174. return l10n.clickEditTitle;
  175. case 'nav_menu':
  176. return l10n.clickEditMenu;
  177. default:
  178. return l10n.clickEditMisc;
  179. }
  180. },
  181. /**
  182. * Return the type of this partial
  183. *
  184. * Will use `params.type` if set, but otherwise will try to infer type from settingId.
  185. *
  186. * @since 4.7.0
  187. * @access public
  188. *
  189. * @return {string} Type of partial derived from type param or the related setting ID.
  190. */
  191. getType: function() {
  192. var partial = this, settingId;
  193. settingId = partial.params.primarySetting || _.first( partial.settings() ) || 'unknown';
  194. if ( partial.params.type ) {
  195. return partial.params.type;
  196. }
  197. if ( settingId.match( /^nav_menu_instance\[/ ) ) {
  198. return 'nav_menu';
  199. }
  200. if ( settingId.match( /^widget_.+\[\d+]$/ ) ) {
  201. return 'widget';
  202. }
  203. return settingId;
  204. },
  205. /**
  206. * Create an edit shortcut button for this partial.
  207. *
  208. * @since 4.7.0
  209. * @access public
  210. *
  211. * @return {jQuery} The edit shortcut button element.
  212. */
  213. createEditShortcut: function() {
  214. var partial = this, shortcutTitle, $buttonContainer, $button, $image;
  215. shortcutTitle = partial.getEditShortcutTitle();
  216. $buttonContainer = $( '<span>', {
  217. 'class': 'customize-partial-edit-shortcut ' + partial.getEditShortcutClassName()
  218. } );
  219. $button = $( '<button>', {
  220. 'aria-label': shortcutTitle,
  221. 'title': shortcutTitle,
  222. 'class': 'customize-partial-edit-shortcut-button'
  223. } );
  224. $image = $( '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13.89 3.39l2.71 2.72c.46.46.42 1.24.03 1.64l-8.01 8.02-5.56 1.16 1.16-5.58s7.6-7.63 7.99-8.03c.39-.39 1.22-.39 1.68.07zm-2.73 2.79l-5.59 5.61 1.11 1.11 5.54-5.65zm-2.97 8.23l5.58-5.6-1.07-1.08-5.59 5.6z"/></svg>' );
  225. $button.append( $image );
  226. $buttonContainer.append( $button );
  227. return $buttonContainer;
  228. },
  229. /**
  230. * Find all placements for this partial int he document.
  231. *
  232. * @since 4.5.0
  233. *
  234. * @return {Array.<Placement>}
  235. */
  236. placements: function() {
  237. var partial = this, selector;
  238. selector = partial.params.selector || '';
  239. if ( selector ) {
  240. selector += ', ';
  241. }
  242. selector += '[data-customize-partial-id="' + partial.id + '"]'; // @todo Consider injecting customize-partial-id-${id} classnames instead.
  243. return $( selector ).map( function() {
  244. var container = $( this ), context;
  245. context = container.data( 'customize-partial-placement-context' );
  246. if ( _.isString( context ) && '{' === context.substr( 0, 1 ) ) {
  247. throw new Error( 'context JSON parse error' );
  248. }
  249. return new Placement( {
  250. partial: partial,
  251. container: container,
  252. context: context
  253. } );
  254. } ).get();
  255. },
  256. /**
  257. * Get list of setting IDs related to this partial.
  258. *
  259. * @since 4.5.0
  260. *
  261. * @return {String[]}
  262. */
  263. settings: function() {
  264. var partial = this;
  265. if ( partial.params.settings && 0 !== partial.params.settings.length ) {
  266. return partial.params.settings;
  267. } else if ( partial.params.primarySetting ) {
  268. return [ partial.params.primarySetting ];
  269. } else {
  270. return [ partial.id ];
  271. }
  272. },
  273. /**
  274. * Return whether the setting is related to the partial.
  275. *
  276. * @since 4.5.0
  277. *
  278. * @param {wp.customize.Value|string} setting ID or object for setting.
  279. * @return {boolean} Whether the setting is related to the partial.
  280. */
  281. isRelatedSetting: function( setting /*... newValue, oldValue */ ) {
  282. var partial = this;
  283. if ( _.isString( setting ) ) {
  284. setting = api( setting );
  285. }
  286. if ( ! setting ) {
  287. return false;
  288. }
  289. return -1 !== _.indexOf( partial.settings(), setting.id );
  290. },
  291. /**
  292. * Show the control to modify this partial's setting(s).
  293. *
  294. * This may be overridden for inline editing.
  295. *
  296. * @since 4.5.0
  297. */
  298. showControl: function() {
  299. var partial = this, settingId = partial.params.primarySetting;
  300. if ( ! settingId ) {
  301. settingId = _.first( partial.settings() );
  302. }
  303. if ( partial.getType() === 'nav_menu' ) {
  304. if ( partial.params.navMenuArgs.theme_location ) {
  305. settingId = 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']';
  306. } else if ( partial.params.navMenuArgs.menu ) {
  307. settingId = 'nav_menu[' + String( partial.params.navMenuArgs.menu ) + ']';
  308. }
  309. }
  310. api.preview.send( 'focus-control-for-setting', settingId );
  311. },
  312. /**
  313. * Prepare container for selective refresh.
  314. *
  315. * @since 4.5.0
  316. *
  317. * @param {Placement} placement
  318. */
  319. preparePlacement: function( placement ) {
  320. $( placement.container ).addClass( 'customize-partial-refreshing' );
  321. },
  322. /**
  323. * Reference to the pending promise returned from self.requestPartial().
  324. *
  325. * @since 4.5.0
  326. * @private
  327. */
  328. _pendingRefreshPromise: null,
  329. /**
  330. * Request the new partial and render it into the placements.
  331. *
  332. * @since 4.5.0
  333. *
  334. * @this {wp.customize.selectiveRefresh.Partial}
  335. * @return {jQuery.Promise}
  336. */
  337. refresh: function() {
  338. var partial = this, refreshPromise;
  339. refreshPromise = self.requestPartial( partial );
  340. if ( ! partial._pendingRefreshPromise ) {
  341. _.each( partial.placements(), function( placement ) {
  342. partial.preparePlacement( placement );
  343. } );
  344. refreshPromise.done( function( placements ) {
  345. _.each( placements, function( placement ) {
  346. partial.renderContent( placement );
  347. } );
  348. } );
  349. refreshPromise.fail( function( data, placements ) {
  350. partial.fallback( data, placements );
  351. } );
  352. // Allow new request when this one finishes.
  353. partial._pendingRefreshPromise = refreshPromise;
  354. refreshPromise.always( function() {
  355. partial._pendingRefreshPromise = null;
  356. } );
  357. }
  358. return refreshPromise;
  359. },
  360. /**
  361. * Apply the addedContent in the placement to the document.
  362. *
  363. * Note the placement object will have its container and removedNodes
  364. * properties updated.
  365. *
  366. * @since 4.5.0
  367. *
  368. * @param {Placement} placement
  369. * @param {Element|jQuery} [placement.container] - This param will be empty if there was no element matching the selector.
  370. * @param {string|object|boolean} placement.addedContent - Rendered HTML content, a data object for JS templates to render, or false if no render.
  371. * @param {object} [placement.context] - Optional context information about the container.
  372. * @returns {boolean} Whether the rendering was successful and the fallback was not invoked.
  373. */
  374. renderContent: function( placement ) {
  375. var partial = this, content, newContainerElement;
  376. if ( ! placement.container ) {
  377. partial.fallback( new Error( 'no_container' ), [ placement ] );
  378. return false;
  379. }
  380. placement.container = $( placement.container );
  381. if ( false === placement.addedContent ) {
  382. partial.fallback( new Error( 'missing_render' ), [ placement ] );
  383. return false;
  384. }
  385. // Currently a subclass needs to override renderContent to handle partials returning data object.
  386. if ( ! _.isString( placement.addedContent ) ) {
  387. partial.fallback( new Error( 'non_string_content' ), [ placement ] );
  388. return false;
  389. }
  390. /* jshint ignore:start */
  391. self.orginalDocumentWrite = document.write;
  392. document.write = function() {
  393. throw new Error( self.data.l10n.badDocumentWrite );
  394. };
  395. /* jshint ignore:end */
  396. try {
  397. content = placement.addedContent;
  398. if ( wp.emoji && wp.emoji.parse && ! $.contains( document.head, placement.container[0] ) ) {
  399. content = wp.emoji.parse( content );
  400. }
  401. if ( partial.params.containerInclusive ) {
  402. // Note that content may be an empty string, and in this case jQuery will just remove the oldContainer
  403. newContainerElement = $( content );
  404. // Merge the new context on top of the old context.
  405. placement.context = _.extend(
  406. placement.context,
  407. newContainerElement.data( 'customize-partial-placement-context' ) || {}
  408. );
  409. newContainerElement.data( 'customize-partial-placement-context', placement.context );
  410. placement.removedNodes = placement.container;
  411. placement.container = newContainerElement;
  412. placement.removedNodes.replaceWith( placement.container );
  413. placement.container.attr( 'title', self.data.l10n.shiftClickToEdit );
  414. } else {
  415. placement.removedNodes = document.createDocumentFragment();
  416. while ( placement.container[0].firstChild ) {
  417. placement.removedNodes.appendChild( placement.container[0].firstChild );
  418. }
  419. placement.container.html( content );
  420. }
  421. placement.container.removeClass( 'customize-render-content-error' );
  422. } catch ( error ) {
  423. if ( 'undefined' !== typeof console && console.error ) {
  424. console.error( partial.id, error );
  425. }
  426. partial.fallback( error, [ placement ] );
  427. }
  428. /* jshint ignore:start */
  429. document.write = self.orginalDocumentWrite;
  430. self.orginalDocumentWrite = null;
  431. /* jshint ignore:end */
  432. partial.createEditShortcutForPlacement( placement );
  433. placement.container.removeClass( 'customize-partial-refreshing' );
  434. // Prevent placement container from being re-triggered as being rendered among nested partials.
  435. placement.container.data( 'customize-partial-content-rendered', true );
  436. /*
  437. * Note that the 'wp_audio_shortcode_library' and 'wp_video_shortcode_library' filters
  438. * will determine whether or not wp.mediaelement is loaded and whether it will
  439. * initialize audio and video respectively. See also https://core.trac.wordpress.org/ticket/40144
  440. */
  441. if ( wp.mediaelement ) {
  442. wp.mediaelement.initialize();
  443. }
  444. if ( wp.playlist ) {
  445. wp.playlist.initialize();
  446. }
  447. /**
  448. * Announce when a partial's placement has been rendered so that dynamic elements can be re-built.
  449. */
  450. self.trigger( 'partial-content-rendered', placement );
  451. return true;
  452. },
  453. /**
  454. * Handle fail to render partial.
  455. *
  456. * The first argument is either the failing jqXHR or an Error object, and the second argument is the array of containers.
  457. *
  458. * @since 4.5.0
  459. */
  460. fallback: function() {
  461. var partial = this;
  462. if ( partial.params.fallbackRefresh ) {
  463. self.requestFullRefresh();
  464. }
  465. }
  466. } );
  467. /**
  468. * A Placement for a Partial.
  469. *
  470. * A partial placement is the actual physical representation of a partial for a given context.
  471. * It also may have information in relation to how a placement may have just changed.
  472. * The placement is conceptually similar to a DOM Range or MutationRecord.
  473. *
  474. * @memberOf wp.customize.selectiveRefresh
  475. *
  476. * @class Placement
  477. * @augments wp.customize.Class
  478. * @since 4.5.0
  479. */
  480. self.Placement = Placement = api.Class.extend(/** @lends wp.customize.selectiveRefresh.prototype */{
  481. /**
  482. * The partial with which the container is associated.
  483. *
  484. * @param {wp.customize.selectiveRefresh.Partial}
  485. */
  486. partial: null,
  487. /**
  488. * DOM element which contains the placement's contents.
  489. *
  490. * This will be null if the startNode and endNode do not point to the same
  491. * DOM element, such as in the case of a sidebar partial.
  492. * This container element itself will be replaced for partials that
  493. * have containerInclusive param defined as true.
  494. */
  495. container: null,
  496. /**
  497. * DOM node for the initial boundary of the placement.
  498. *
  499. * This will normally be the same as endNode since most placements appear as elements.
  500. * This is primarily useful for widget sidebars which do not have intrinsic containers, but
  501. * for which an HTML comment is output before to mark the starting position.
  502. */
  503. startNode: null,
  504. /**
  505. * DOM node for the terminal boundary of the placement.
  506. *
  507. * This will normally be the same as startNode since most placements appear as elements.
  508. * This is primarily useful for widget sidebars which do not have intrinsic containers, but
  509. * for which an HTML comment is output before to mark the ending position.
  510. */
  511. endNode: null,
  512. /**
  513. * Context data.
  514. *
  515. * This provides information about the placement which is included in the request
  516. * in order to render the partial properly.
  517. *
  518. * @param {object}
  519. */
  520. context: null,
  521. /**
  522. * The content for the partial when refreshed.
  523. *
  524. * @param {string}
  525. */
  526. addedContent: null,
  527. /**
  528. * DOM node(s) removed when the partial is refreshed.
  529. *
  530. * If the partial is containerInclusive, then the removedNodes will be
  531. * the single Element that was the partial's former placement. If the
  532. * partial is not containerInclusive, then the removedNodes will be a
  533. * documentFragment containing the nodes removed.
  534. *
  535. * @param {Element|DocumentFragment}
  536. */
  537. removedNodes: null,
  538. /**
  539. * Constructor.
  540. *
  541. * @since 4.5.0
  542. *
  543. * @param {object} args
  544. * @param {Partial} args.partial
  545. * @param {jQuery|Element} [args.container]
  546. * @param {Node} [args.startNode]
  547. * @param {Node} [args.endNode]
  548. * @param {object} [args.context]
  549. * @param {string} [args.addedContent]
  550. * @param {jQuery|DocumentFragment} [args.removedNodes]
  551. */
  552. initialize: function( args ) {
  553. var placement = this;
  554. args = _.extend( {}, args || {} );
  555. if ( ! args.partial || ! args.partial.extended( Partial ) ) {
  556. throw new Error( 'Missing partial' );
  557. }
  558. args.context = args.context || {};
  559. if ( args.container ) {
  560. args.container = $( args.container );
  561. }
  562. _.extend( placement, args );
  563. }
  564. });
  565. /**
  566. * Mapping of type names to Partial constructor subclasses.
  567. *
  568. * @since 4.5.0
  569. *
  570. * @type {Object.<string, wp.customize.selectiveRefresh.Partial>}
  571. */
  572. self.partialConstructor = {};
  573. self.partial = new api.Values({ defaultConstructor: Partial });
  574. /**
  575. * Get the POST vars for a Customizer preview request.
  576. *
  577. * @since 4.5.0
  578. * @see wp.customize.previewer.query()
  579. *
  580. * @return {object}
  581. */
  582. self.getCustomizeQuery = function() {
  583. var dirtyCustomized = {};
  584. api.each( function( value, key ) {
  585. if ( value._dirty ) {
  586. dirtyCustomized[ key ] = value();
  587. }
  588. } );
  589. return {
  590. wp_customize: 'on',
  591. nonce: api.settings.nonce.preview,
  592. customize_theme: api.settings.theme.stylesheet,
  593. customized: JSON.stringify( dirtyCustomized ),
  594. customize_changeset_uuid: api.settings.changeset.uuid
  595. };
  596. };
  597. /**
  598. * Currently-requested partials and their associated deferreds.
  599. *
  600. * @since 4.5.0
  601. * @type {Object<string, { deferred: jQuery.Promise, partial: wp.customize.selectiveRefresh.Partial }>}
  602. */
  603. self._pendingPartialRequests = {};
  604. /**
  605. * Timeout ID for the current requesr, or null if no request is current.
  606. *
  607. * @since 4.5.0
  608. * @type {number|null}
  609. * @private
  610. */
  611. self._debouncedTimeoutId = null;
  612. /**
  613. * Current jqXHR for the request to the partials.
  614. *
  615. * @since 4.5.0
  616. * @type {jQuery.jqXHR|null}
  617. * @private
  618. */
  619. self._currentRequest = null;
  620. /**
  621. * Request full page refresh.
  622. *
  623. * When selective refresh is embedded in the context of front-end editing, this request
  624. * must fail or else changes will be lost, unless transactions are implemented.
  625. *
  626. * @since 4.5.0
  627. */
  628. self.requestFullRefresh = function() {
  629. api.preview.send( 'refresh' );
  630. };
  631. /**
  632. * Request a re-rendering of a partial.
  633. *
  634. * @since 4.5.0
  635. *
  636. * @param {wp.customize.selectiveRefresh.Partial} partial
  637. * @return {jQuery.Promise}
  638. */
  639. self.requestPartial = function( partial ) {
  640. var partialRequest;
  641. if ( self._debouncedTimeoutId ) {
  642. clearTimeout( self._debouncedTimeoutId );
  643. self._debouncedTimeoutId = null;
  644. }
  645. if ( self._currentRequest ) {
  646. self._currentRequest.abort();
  647. self._currentRequest = null;
  648. }
  649. partialRequest = self._pendingPartialRequests[ partial.id ];
  650. if ( ! partialRequest || 'pending' !== partialRequest.deferred.state() ) {
  651. partialRequest = {
  652. deferred: $.Deferred(),
  653. partial: partial
  654. };
  655. self._pendingPartialRequests[ partial.id ] = partialRequest;
  656. }
  657. // Prevent leaking partial into debounced timeout callback.
  658. partial = null;
  659. self._debouncedTimeoutId = setTimeout(
  660. function() {
  661. var data, partialPlacementContexts, partialsPlacements, request;
  662. self._debouncedTimeoutId = null;
  663. data = self.getCustomizeQuery();
  664. /*
  665. * It is key that the containers be fetched exactly at the point of the request being
  666. * made, because the containers need to be mapped to responses by array indices.
  667. */
  668. partialsPlacements = {};
  669. partialPlacementContexts = {};
  670. _.each( self._pendingPartialRequests, function( pending, partialId ) {
  671. partialsPlacements[ partialId ] = pending.partial.placements();
  672. if ( ! self.partial.has( partialId ) ) {
  673. pending.deferred.rejectWith( pending.partial, [ new Error( 'partial_removed' ), partialsPlacements[ partialId ] ] );
  674. } else {
  675. /*
  676. * Note that this may in fact be an empty array. In that case, it is the responsibility
  677. * of the Partial subclass instance to know where to inject the response, or else to
  678. * just issue a refresh (default behavior). The data being returned with each container
  679. * is the context information that may be needed to render certain partials, such as
  680. * the contained sidebar for rendering widgets or what the nav menu args are for a menu.
  681. */
  682. partialPlacementContexts[ partialId ] = _.map( partialsPlacements[ partialId ], function( placement ) {
  683. return placement.context || {};
  684. } );
  685. }
  686. } );
  687. data.partials = JSON.stringify( partialPlacementContexts );
  688. data[ self.data.renderQueryVar ] = '1';
  689. request = self._currentRequest = wp.ajax.send( null, {
  690. data: data,
  691. url: api.settings.url.self
  692. } );
  693. request.done( function( data ) {
  694. /**
  695. * Announce the data returned from a request to render partials.
  696. *
  697. * The data is filtered on the server via customize_render_partials_response
  698. * so plugins can inject data from the server to be utilized
  699. * on the client via this event. Plugins may use this filter
  700. * to communicate script and style dependencies that need to get
  701. * injected into the page to support the rendered partials.
  702. * This is similar to the 'saved' event.
  703. */
  704. self.trigger( 'render-partials-response', data );
  705. // Relay errors (warnings) captured during rendering and relay to console.
  706. if ( data.errors && 'undefined' !== typeof console && console.warn ) {
  707. _.each( data.errors, function( error ) {
  708. console.warn( error );
  709. } );
  710. }
  711. /*
  712. * Note that data is an array of items that correspond to the array of
  713. * containers that were submitted in the request. So we zip up the
  714. * array of containers with the array of contents for those containers,
  715. * and send them into .
  716. */
  717. _.each( self._pendingPartialRequests, function( pending, partialId ) {
  718. var placementsContents;
  719. if ( ! _.isArray( data.contents[ partialId ] ) ) {
  720. pending.deferred.rejectWith( pending.partial, [ new Error( 'unrecognized_partial' ), partialsPlacements[ partialId ] ] );
  721. } else {
  722. placementsContents = _.map( data.contents[ partialId ], function( content, i ) {
  723. var partialPlacement = partialsPlacements[ partialId ][ i ];
  724. if ( partialPlacement ) {
  725. partialPlacement.addedContent = content;
  726. } else {
  727. partialPlacement = new Placement( {
  728. partial: pending.partial,
  729. addedContent: content
  730. } );
  731. }
  732. return partialPlacement;
  733. } );
  734. pending.deferred.resolveWith( pending.partial, [ placementsContents ] );
  735. }
  736. } );
  737. self._pendingPartialRequests = {};
  738. } );
  739. request.fail( function( data, statusText ) {
  740. /*
  741. * Ignore failures caused by partial.currentRequest.abort()
  742. * The pending deferreds will remain in self._pendingPartialRequests
  743. * for re-use with the next request.
  744. */
  745. if ( 'abort' === statusText ) {
  746. return;
  747. }
  748. _.each( self._pendingPartialRequests, function( pending, partialId ) {
  749. pending.deferred.rejectWith( pending.partial, [ data, partialsPlacements[ partialId ] ] );
  750. } );
  751. self._pendingPartialRequests = {};
  752. } );
  753. },
  754. api.settings.timeouts.selectiveRefresh
  755. );
  756. return partialRequest.deferred.promise();
  757. };
  758. /**
  759. * Add partials for any nav menu container elements in the document.
  760. *
  761. * This method may be called multiple times. Containers that already have been
  762. * seen will be skipped.
  763. *
  764. * @since 4.5.0
  765. *
  766. * @param {jQuery|HTMLElement} [rootElement]
  767. * @param {object} [options]
  768. * @param {boolean=true} [options.triggerRendered]
  769. */
  770. self.addPartials = function( rootElement, options ) {
  771. var containerElements;
  772. if ( ! rootElement ) {
  773. rootElement = document.documentElement;
  774. }
  775. rootElement = $( rootElement );
  776. options = _.extend(
  777. {
  778. triggerRendered: true
  779. },
  780. options || {}
  781. );
  782. containerElements = rootElement.find( '[data-customize-partial-id]' );
  783. if ( rootElement.is( '[data-customize-partial-id]' ) ) {
  784. containerElements = containerElements.add( rootElement );
  785. }
  786. containerElements.each( function() {
  787. var containerElement = $( this ), partial, placement, id, Constructor, partialOptions, containerContext;
  788. id = containerElement.data( 'customize-partial-id' );
  789. if ( ! id ) {
  790. return;
  791. }
  792. containerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
  793. partial = self.partial( id );
  794. if ( ! partial ) {
  795. partialOptions = containerElement.data( 'customize-partial-options' ) || {};
  796. partialOptions.constructingContainerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
  797. Constructor = self.partialConstructor[ containerElement.data( 'customize-partial-type' ) ] || self.Partial;
  798. partial = new Constructor( id, partialOptions );
  799. self.partial.add( partial );
  800. }
  801. /*
  802. * Only trigger renders on (nested) partials that have been not been
  803. * handled yet. An example where this would apply is a nav menu
  804. * embedded inside of a navigation menu widget. When the widget's title
  805. * is updated, the entire widget will re-render and then the event
  806. * will be triggered for the nested nav menu to do any initialization.
  807. */
  808. if ( options.triggerRendered && ! containerElement.data( 'customize-partial-content-rendered' ) ) {
  809. placement = new Placement( {
  810. partial: partial,
  811. context: containerContext,
  812. container: containerElement
  813. } );
  814. $( placement.container ).attr( 'title', self.data.l10n.shiftClickToEdit );
  815. partial.createEditShortcutForPlacement( placement );
  816. /**
  817. * Announce when a partial's nested placement has been re-rendered.
  818. */
  819. self.trigger( 'partial-content-rendered', placement );
  820. }
  821. containerElement.data( 'customize-partial-content-rendered', true );
  822. } );
  823. };
  824. api.bind( 'preview-ready', function() {
  825. var handleSettingChange, watchSettingChange, unwatchSettingChange;
  826. _.extend( self.data, _customizePartialRefreshExports );
  827. // Create the partial JS models.
  828. _.each( self.data.partials, function( data, id ) {
  829. var Constructor, partial = self.partial( id );
  830. if ( ! partial ) {
  831. Constructor = self.partialConstructor[ data.type ] || self.Partial;
  832. partial = new Constructor(
  833. id,
  834. _.extend( { params: data }, data ) // Inclusion of params alias is for back-compat for custom partials that expect to augment this property.
  835. );
  836. self.partial.add( partial );
  837. } else {
  838. _.extend( partial.params, data );
  839. }
  840. } );
  841. /**
  842. * Handle change to a setting.
  843. *
  844. * Note this is largely needed because adding a 'change' event handler to wp.customize
  845. * will only include the changed setting object as an argument, not including the
  846. * new value or the old value.
  847. *
  848. * @since 4.5.0
  849. * @this {wp.customize.Setting}
  850. *
  851. * @param {*|null} newValue New value, or null if the setting was just removed.
  852. * @param {*|null} oldValue Old value, or null if the setting was just added.
  853. */
  854. handleSettingChange = function( newValue, oldValue ) {
  855. var setting = this;
  856. self.partial.each( function( partial ) {
  857. if ( partial.isRelatedSetting( setting, newValue, oldValue ) ) {
  858. partial.refresh();
  859. }
  860. } );
  861. };
  862. /**
  863. * Trigger the initial change for the added setting, and watch for changes.
  864. *
  865. * @since 4.5.0
  866. * @this {wp.customize.Values}
  867. *
  868. * @param {wp.customize.Setting} setting
  869. */
  870. watchSettingChange = function( setting ) {
  871. handleSettingChange.call( setting, setting(), null );
  872. setting.bind( handleSettingChange );
  873. };
  874. /**
  875. * Trigger the final change for the removed setting, and unwatch for changes.
  876. *
  877. * @since 4.5.0
  878. * @this {wp.customize.Values}
  879. *
  880. * @param {wp.customize.Setting} setting
  881. */
  882. unwatchSettingChange = function( setting ) {
  883. handleSettingChange.call( setting, null, setting() );
  884. setting.unbind( handleSettingChange );
  885. };
  886. api.bind( 'add', watchSettingChange );
  887. api.bind( 'remove', unwatchSettingChange );
  888. api.each( function( setting ) {
  889. setting.bind( handleSettingChange );
  890. } );
  891. // Add (dynamic) initial partials that are declared via data-* attributes.
  892. self.addPartials( document.documentElement, {
  893. triggerRendered: false
  894. } );
  895. // Add new dynamic partials when the document changes.
  896. if ( 'undefined' !== typeof MutationObserver ) {
  897. self.mutationObserver = new MutationObserver( function( mutations ) {
  898. _.each( mutations, function( mutation ) {
  899. self.addPartials( $( mutation.target ) );
  900. } );
  901. } );
  902. self.mutationObserver.observe( document.documentElement, {
  903. childList: true,
  904. subtree: true
  905. } );
  906. }
  907. /**
  908. * Handle rendering of partials.
  909. *
  910. * @param {api.selectiveRefresh.Placement} placement
  911. */
  912. api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
  913. if ( placement.container ) {
  914. self.addPartials( placement.container );
  915. }
  916. } );
  917. /**
  918. * Handle setting validities in partial refresh response.
  919. *
  920. * @param {object} data Response data.
  921. * @param {object} data.setting_validities Setting validities.
  922. */
  923. api.selectiveRefresh.bind( 'render-partials-response', function handleSettingValiditiesResponse( data ) {
  924. if ( data.setting_validities ) {
  925. api.preview.send( 'selective-refresh-setting-validities', data.setting_validities );
  926. }
  927. } );
  928. api.preview.bind( 'edit-shortcut-visibility', function( visibility ) {
  929. api.selectiveRefresh.editShortcutVisibility.set( visibility );
  930. } );
  931. api.selectiveRefresh.editShortcutVisibility.bind( function( visibility ) {
  932. var body = $( document.body ), shouldAnimateHide;
  933. shouldAnimateHide = ( 'hidden' === visibility && body.hasClass( 'customize-partial-edit-shortcuts-shown' ) && ! body.hasClass( 'customize-partial-edit-shortcuts-hidden' ) );
  934. body.toggleClass( 'customize-partial-edit-shortcuts-hidden', shouldAnimateHide );
  935. body.toggleClass( 'customize-partial-edit-shortcuts-shown', 'visible' === visibility );
  936. } );
  937. api.preview.bind( 'active', function() {
  938. // Make all partials ready.
  939. self.partial.each( function( partial ) {
  940. partial.deferred.ready.resolve();
  941. } );
  942. // Make all partials added henceforth as ready upon add.
  943. self.partial.bind( 'add', function( partial ) {
  944. partial.deferred.ready.resolve();
  945. } );
  946. } );
  947. } );
  948. return self;
  949. }( jQuery, wp.customize ) );