[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/js/ -> customize-preview-nav-menus.js (source)

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


Generated : Fri Sep 4 08:20:24 2026 Cross-referenced by PHPXref