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