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.

customize-selective-refresh.js 32 KiB

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