Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 

438 rindas
14 KiB

  1. /* global _wpCustomizePreviewNavMenusExports */
  2. wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) {
  3. 'use strict';
  4. var self = {
  5. data: {
  6. navMenuInstanceArgs: {}
  7. }
  8. };
  9. if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
  10. _.extend( self.data, _wpCustomizePreviewNavMenusExports );
  11. }
  12. /**
  13. * Initialize nav menus preview.
  14. */
  15. self.init = function() {
  16. var self = this, synced = false;
  17. /*
  18. * Keep track of whether we synced to determine whether or not bindSettingListener
  19. * should also initially fire the listener. This initial firing needs to wait until
  20. * after all of the settings have been synced from the pane in order to prevent
  21. * an infinite selective fallback-refresh. Note that this sync handler will be
  22. * added after the sync handler in customize-preview.js, so it will be triggered
  23. * after all of the settings are added.
  24. */
  25. api.preview.bind( 'sync', function() {
  26. synced = true;
  27. } );
  28. if ( api.selectiveRefresh ) {
  29. // Listen for changes to settings related to nav menus.
  30. api.each( function( setting ) {
  31. self.bindSettingListener( setting );
  32. } );
  33. api.bind( 'add', function( setting ) {
  34. /*
  35. * Handle case where an invalid nav menu item (one for which its associated object has been deleted)
  36. * is synced from the controls into the preview. Since invalid nav menu items are filtered out from
  37. * being exported to the frontend by the _is_valid_nav_menu_item filter in wp_get_nav_menu_items(),
  38. * the customizer controls will have a nav_menu_item setting where the preview will have none, and
  39. * this can trigger an infinite fallback refresh when the nav menu item lacks any valid items.
  40. */
  41. if ( setting.get() && ! setting.get()._invalid ) {
  42. self.bindSettingListener( setting, { fire: synced } );
  43. }
  44. } );
  45. api.bind( 'remove', function( setting ) {
  46. self.unbindSettingListener( setting );
  47. } );
  48. /*
  49. * Ensure that wp_nav_menu() instances nested inside of other partials
  50. * will be recognized as being present on the page.
  51. */
  52. api.selectiveRefresh.bind( 'render-partials-response', function( response ) {
  53. if ( response.nav_menu_instance_args ) {
  54. _.extend( self.data.navMenuInstanceArgs, response.nav_menu_instance_args );
  55. }
  56. } );
  57. }
  58. api.preview.bind( 'active', function() {
  59. self.highlightControls();
  60. } );
  61. };
  62. if ( api.selectiveRefresh ) {
  63. /**
  64. * Partial representing an invocation of wp_nav_menu().
  65. *
  66. * @class
  67. * @augments wp.customize.selectiveRefresh.Partial
  68. * @since 4.5.0
  69. */
  70. self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend({
  71. /**
  72. * Constructor.
  73. *
  74. * @since 4.5.0
  75. * @param {string} id - Partial ID.
  76. * @param {Object} options
  77. * @param {Object} options.params
  78. * @param {Object} options.params.navMenuArgs
  79. * @param {string} options.params.navMenuArgs.args_hmac
  80. * @param {string} [options.params.navMenuArgs.theme_location]
  81. * @param {number} [options.params.navMenuArgs.menu]
  82. * @param {object} [options.constructingContainerContext]
  83. */
  84. initialize: function( id, options ) {
  85. var partial = this, matches, argsHmac;
  86. matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ );
  87. if ( ! matches ) {
  88. throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' );
  89. }
  90. argsHmac = matches[1];
  91. options = options || {};
  92. options.params = _.extend(
  93. {
  94. selector: '[data-customize-partial-id="' + id + '"]',
  95. navMenuArgs: options.constructingContainerContext || {},
  96. containerInclusive: true
  97. },
  98. options.params || {}
  99. );
  100. api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
  101. if ( ! _.isObject( partial.params.navMenuArgs ) ) {
  102. throw new Error( 'Missing navMenuArgs' );
  103. }
  104. if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) {
  105. throw new Error( 'args_hmac mismatch with id' );
  106. }
  107. },
  108. /**
  109. * Return whether the setting is related to this partial.
  110. *
  111. * @since 4.5.0
  112. * @param {wp.customize.Value|string} setting - Object or ID.
  113. * @param {number|object|false|null} newValue - New value, or null if the setting was just removed.
  114. * @param {number|object|false|null} oldValue - Old value, or null if the setting was just added.
  115. * @returns {boolean}
  116. */
  117. isRelatedSetting: function( setting, newValue, oldValue ) {
  118. var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting, _newValue, _oldValue, urlParser;
  119. if ( _.isString( setting ) ) {
  120. setting = api( setting );
  121. }
  122. /*
  123. * Prevent nav_menu_item changes only containing type_label differences triggering a refresh.
  124. * These settings in the preview do not include type_label property, and so if one of these
  125. * nav_menu_item settings is dirty, after a refresh the nav menu instance would do a selective
  126. * refresh immediately because the setting from the pane would have the type_label whereas
  127. * the setting in the preview would not, thus triggering a change event. The following
  128. * condition short-circuits this unnecessary selective refresh and also prevents an infinite
  129. * loop in the case where a nav_menu_instance partial had done a fallback refresh.
  130. * @todo Nav menu item settings should not include a type_label property to begin with.
  131. */
  132. isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id );
  133. if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) {
  134. _newValue = _.clone( newValue );
  135. _oldValue = _.clone( oldValue );
  136. delete _newValue.type_label;
  137. delete _oldValue.type_label;
  138. // Normalize URL scheme when parent frame is HTTPS to prevent selective refresh upon initial page load.
  139. if ( 'https' === api.preview.scheme.get() ) {
  140. urlParser = document.createElement( 'a' );
  141. urlParser.href = _newValue.url;
  142. urlParser.protocol = 'https:';
  143. _newValue.url = urlParser.href;
  144. urlParser.href = _oldValue.url;
  145. urlParser.protocol = 'https:';
  146. _oldValue.url = urlParser.href;
  147. }
  148. // Prevent original_title differences from causing refreshes if title is present.
  149. if ( newValue.title ) {
  150. delete _oldValue.original_title;
  151. delete _newValue.original_title;
  152. }
  153. if ( _.isEqual( _oldValue, _newValue ) ) {
  154. return false;
  155. }
  156. }
  157. if ( partial.params.navMenuArgs.theme_location ) {
  158. if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) {
  159. return true;
  160. }
  161. navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' );
  162. }
  163. navMenuId = partial.params.navMenuArgs.menu;
  164. if ( ! navMenuId && navMenuLocationSetting ) {
  165. navMenuId = navMenuLocationSetting();
  166. }
  167. if ( ! navMenuId ) {
  168. return false;
  169. }
  170. return (
  171. ( 'nav_menu[' + navMenuId + ']' === setting.id ) ||
  172. ( isNavMenuItemSetting && (
  173. ( newValue && newValue.nav_menu_term_id === navMenuId ) ||
  174. ( oldValue && oldValue.nav_menu_term_id === navMenuId )
  175. ) )
  176. );
  177. },
  178. /**
  179. * Make sure that partial fallback behavior is invoked if there is no associated menu.
  180. *
  181. * @since 4.5.0
  182. *
  183. * @returns {Promise}
  184. */
  185. refresh: function() {
  186. var partial = this, menuId, deferred = $.Deferred();
  187. // Make sure the fallback behavior is invoked when the partial is no longer associated with a menu.
  188. if ( _.isNumber( partial.params.navMenuArgs.menu ) ) {
  189. menuId = partial.params.navMenuArgs.menu;
  190. } else if ( partial.params.navMenuArgs.theme_location && api.has( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ) ) {
  191. menuId = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ).get();
  192. }
  193. if ( ! menuId ) {
  194. partial.fallback();
  195. deferred.reject();
  196. return deferred.promise();
  197. }
  198. return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
  199. },
  200. /**
  201. * Render content.
  202. *
  203. * @inheritdoc
  204. * @param {wp.customize.selectiveRefresh.Placement} placement
  205. */
  206. renderContent: function( placement ) {
  207. var partial = this, previousContainer = placement.container;
  208. // Do fallback behavior to refresh preview if menu is now empty.
  209. if ( '' === placement.addedContent ) {
  210. placement.partial.fallback();
  211. }
  212. if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
  213. // Trigger deprecated event.
  214. $( document ).trigger( 'customize-preview-menu-refreshed', [ {
  215. instanceNumber: null, // @deprecated
  216. wpNavArgs: placement.context, // @deprecated
  217. wpNavMenuArgs: placement.context,
  218. oldContainer: previousContainer,
  219. newContainer: placement.container
  220. } ] );
  221. }
  222. }
  223. });
  224. api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
  225. /**
  226. * Request full refresh if there are nav menu instances that lack partials which also match the supplied args.
  227. *
  228. * @param {object} navMenuInstanceArgs
  229. */
  230. self.handleUnplacedNavMenuInstances = function( navMenuInstanceArgs ) {
  231. var unplacedNavMenuInstances;
  232. unplacedNavMenuInstances = _.filter( _.values( self.data.navMenuInstanceArgs ), function( args ) {
  233. return ! api.selectiveRefresh.partial.has( 'nav_menu_instance[' + args.args_hmac + ']' );
  234. } );
  235. if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) {
  236. api.selectiveRefresh.requestFullRefresh();
  237. return true;
  238. }
  239. return false;
  240. };
  241. /**
  242. * Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
  243. *
  244. * @since 4.5.0
  245. *
  246. * @param {wp.customize.Value} setting
  247. * @param {object} [options]
  248. * @param {boolean} options.fire Whether to invoke the callback after binding.
  249. * This is used when a dynamic setting is added.
  250. * @return {boolean} Whether the setting was bound.
  251. */
  252. self.bindSettingListener = function( setting, options ) {
  253. var matches;
  254. options = options || {};
  255. matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
  256. if ( matches ) {
  257. setting._navMenuId = parseInt( matches[1], 10 );
  258. setting.bind( this.onChangeNavMenuSetting );
  259. if ( options.fire ) {
  260. this.onChangeNavMenuSetting.call( setting, setting(), false );
  261. }
  262. return true;
  263. }
  264. matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
  265. if ( matches ) {
  266. setting._navMenuItemId = parseInt( matches[1], 10 );
  267. setting.bind( this.onChangeNavMenuItemSetting );
  268. if ( options.fire ) {
  269. this.onChangeNavMenuItemSetting.call( setting, setting(), false );
  270. }
  271. return true;
  272. }
  273. matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
  274. if ( matches ) {
  275. setting._navMenuThemeLocation = matches[1];
  276. setting.bind( this.onChangeNavMenuLocationsSetting );
  277. if ( options.fire ) {
  278. this.onChangeNavMenuLocationsSetting.call( setting, setting(), false );
  279. }
  280. return true;
  281. }
  282. return false;
  283. };
  284. /**
  285. * Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
  286. *
  287. * @since 4.5.0
  288. *
  289. * @param {wp.customize.Value} setting
  290. */
  291. self.unbindSettingListener = function( setting ) {
  292. setting.unbind( this.onChangeNavMenuSetting );
  293. setting.unbind( this.onChangeNavMenuItemSetting );
  294. setting.unbind( this.onChangeNavMenuLocationsSetting );
  295. };
  296. /**
  297. * Handle change for nav_menu[] setting for nav menu instances lacking partials.
  298. *
  299. * @since 4.5.0
  300. *
  301. * @this {wp.customize.Value}
  302. */
  303. self.onChangeNavMenuSetting = function() {
  304. var setting = this;
  305. self.handleUnplacedNavMenuInstances( {
  306. menu: setting._navMenuId
  307. } );
  308. // Ensure all nav menu instances with a theme_location assigned to this menu are handled.
  309. api.each( function( otherSetting ) {
  310. if ( ! otherSetting._navMenuThemeLocation ) {
  311. return;
  312. }
  313. if ( setting._navMenuId === otherSetting() ) {
  314. self.handleUnplacedNavMenuInstances( {
  315. theme_location: otherSetting._navMenuThemeLocation
  316. } );
  317. }
  318. } );
  319. };
  320. /**
  321. * Handle change for nav_menu_item[] setting for nav menu instances lacking partials.
  322. *
  323. * @since 4.5.0
  324. *
  325. * @param {object} newItem New value for nav_menu_item[] setting.
  326. * @param {object} oldItem Old value for nav_menu_item[] setting.
  327. * @this {wp.customize.Value}
  328. */
  329. self.onChangeNavMenuItemSetting = function( newItem, oldItem ) {
  330. var item = newItem || oldItem, navMenuSetting;
  331. navMenuSetting = api( 'nav_menu[' + String( item.nav_menu_term_id ) + ']' );
  332. if ( navMenuSetting ) {
  333. self.onChangeNavMenuSetting.call( navMenuSetting );
  334. }
  335. };
  336. /**
  337. * Handle change for nav_menu_locations[] setting for nav menu instances lacking partials.
  338. *
  339. * @since 4.5.0
  340. *
  341. * @this {wp.customize.Value}
  342. */
  343. self.onChangeNavMenuLocationsSetting = function() {
  344. var setting = this, hasNavMenuInstance;
  345. self.handleUnplacedNavMenuInstances( {
  346. theme_location: setting._navMenuThemeLocation
  347. } );
  348. // If there are no wp_nav_menu() instances that refer to the theme location, do full refresh.
  349. hasNavMenuInstance = !! _.findWhere( _.values( self.data.navMenuInstanceArgs ), {
  350. theme_location: setting._navMenuThemeLocation
  351. } );
  352. if ( ! hasNavMenuInstance ) {
  353. api.selectiveRefresh.requestFullRefresh();
  354. }
  355. };
  356. }
  357. /**
  358. * Connect nav menu items with their corresponding controls in the pane.
  359. *
  360. * Setup shift-click on nav menu items which are more granular than the nav menu partial itself.
  361. * Also this applies even if a nav menu is not partial-refreshable.
  362. *
  363. * @since 4.5.0
  364. */
  365. self.highlightControls = function() {
  366. var selector = '.menu-item';
  367. // Skip adding highlights if not in the customizer preview iframe.
  368. if ( ! api.settings.channel ) {
  369. return;
  370. }
  371. // Focus on the menu item control when shift+clicking the menu item.
  372. $( document ).on( 'click', selector, function( e ) {
  373. var navMenuItemParts;
  374. if ( ! e.shiftKey ) {
  375. return;
  376. }
  377. navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(-?\d+)(?:\s|$)/ );
  378. if ( navMenuItemParts ) {
  379. e.preventDefault();
  380. e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items.
  381. api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) );
  382. }
  383. });
  384. };
  385. api.bind( 'preview-ready', function() {
  386. self.init();
  387. } );
  388. return self;
  389. }( jQuery, _, wp, wp.customize ) );