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