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