angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

customize-preview-nav-menus.js (15024B)


      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 ) );