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


Generated : Thu Nov 21 08:20:01 2024 Cross-referenced by PHPXref