[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/js/jquery/ui/ -> menu.js (source)

   1  /*!
   2   * jQuery UI Menu 1.14.2
   3   * https://jqueryui.com
   4   *
   5   * Copyright OpenJS Foundation and other contributors
   6   * Released under the MIT license.
   7   * https://jquery.org/license
   8   */
   9  
  10  //>>label: Menu
  11  //>>group: Widgets
  12  //>>description: Creates nestable menus.
  13  //>>docs: https://api.jqueryui.com/menu/
  14  //>>demos: https://jqueryui.com/menu/
  15  //>>css.structure: ../../themes/base/core.css
  16  //>>css.structure: ../../themes/base/menu.css
  17  //>>css.theme: ../../themes/base/theme.css
  18  
  19  ( function( factory ) {
  20      "use strict";
  21  
  22      if ( typeof define === "function" && define.amd ) {
  23  
  24          // AMD. Register as an anonymous module.
  25          define( [
  26              "jquery",
  27              "../keycode",
  28              "../position",
  29              "../unique-id",
  30              "../version",
  31              "../widget"
  32          ], factory );
  33      } else {
  34  
  35          // Browser globals
  36          factory( jQuery );
  37      }
  38  } )( function( $ ) {
  39  "use strict";
  40  
  41  return $.widget( "ui.menu", {
  42      version: "1.14.2",
  43      defaultElement: "<ul>",
  44      delay: 300,
  45      options: {
  46          icons: {
  47              submenu: "ui-icon-caret-1-e"
  48          },
  49          items: "> *",
  50          menus: "ul",
  51          position: {
  52              my: "left top",
  53              at: "right top"
  54          },
  55          role: "menu",
  56  
  57          // Callbacks
  58          blur: null,
  59          focus: null,
  60          select: null
  61      },
  62  
  63      _create: function() {
  64          this.activeMenu = this.element;
  65  
  66          // Flag used to prevent firing of the click handler
  67          // as the event bubbles up through nested menus
  68          this.mouseHandled = false;
  69          this.lastMousePosition = { x: null, y: null };
  70          this.element
  71              .uniqueId()
  72              .attr( {
  73                  role: this.options.role,
  74                  tabIndex: 0
  75              } );
  76  
  77          this._addClass( "ui-menu", "ui-widget ui-widget-content" );
  78          this._on( {
  79  
  80              // Prevent focus from sticking to links inside menu after clicking
  81              // them (focus should always stay on UL during navigation).
  82              "mousedown .ui-menu-item": function( event ) {
  83                  event.preventDefault();
  84  
  85                  this._activateItem( event );
  86              },
  87              "click .ui-menu-item": function( event ) {
  88                  var target = $( event.target );
  89                  var active = $( this.document[ 0 ].activeElement );
  90                  if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) {
  91                      this.select( event );
  92  
  93                      // Only set the mouseHandled flag if the event will bubble, see #9469.
  94                      if ( !event.isPropagationStopped() ) {
  95                          this.mouseHandled = true;
  96                      }
  97  
  98                      // Open submenu on click
  99                      if ( target.has( ".ui-menu" ).length ) {
 100                          this.expand( event );
 101                      } else if ( !this.element.is( ":focus" ) &&
 102                              active.closest( ".ui-menu" ).length ) {
 103  
 104                          // Redirect focus to the menu
 105                          this.element.trigger( "focus", [ true ] );
 106  
 107                          // If the active item is on the top level, let it stay active.
 108                          // Otherwise, blur the active item since it is no longer visible.
 109                          if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) {
 110                              clearTimeout( this.timer );
 111                          }
 112                      }
 113                  }
 114              },
 115              "mouseenter .ui-menu-item": "_activateItem",
 116              "mousemove .ui-menu-item": "_activateItem",
 117              mouseleave: "collapseAll",
 118              "mouseleave .ui-menu": "collapseAll",
 119              focus: function( event, keepActiveItem ) {
 120  
 121                  // If there's already an active item, keep it active
 122                  // If not, activate the first item
 123                  var item = this.active || this._menuItems().first();
 124  
 125                  if ( !keepActiveItem ) {
 126                      this.focus( event, item );
 127                  }
 128              },
 129              blur: function( event ) {
 130                  this._delay( function() {
 131                      var notContained = !$.contains(
 132                          this.element[ 0 ],
 133                          this.document[ 0 ].activeElement
 134                      );
 135                      if ( notContained ) {
 136                          this.collapseAll( event );
 137                      }
 138                  } );
 139              },
 140              keydown: "_keydown"
 141          } );
 142  
 143          this.refresh();
 144  
 145          // Clicks outside of a menu collapse any open menus
 146          this._on( this.document, {
 147              click: function( event ) {
 148                  if ( this._closeOnDocumentClick( event ) ) {
 149                      this.collapseAll( event, true );
 150                  }
 151  
 152                  // Reset the mouseHandled flag
 153                  this.mouseHandled = false;
 154              }
 155          } );
 156      },
 157  
 158      _activateItem: function( event ) {
 159  
 160          // Ignore mouse events while typeahead is active, see #10458.
 161          // Prevents focusing the wrong item when typeahead causes a scroll while the mouse
 162          // is over an item in the menu
 163          if ( this.previousFilter ) {
 164              return;
 165          }
 166  
 167          // If the mouse didn't actually move, but the page was scrolled, ignore the event (#9356)
 168          if ( event.clientX === this.lastMousePosition.x &&
 169                  event.clientY === this.lastMousePosition.y ) {
 170              return;
 171          }
 172  
 173          this.lastMousePosition = {
 174              x: event.clientX,
 175              y: event.clientY
 176          };
 177  
 178          var actualTarget = $( event.target ).closest( ".ui-menu-item" ),
 179              target = $( event.currentTarget );
 180  
 181          // Ignore bubbled events on parent items, see #11641
 182          if ( actualTarget[ 0 ] !== target[ 0 ] ) {
 183              return;
 184          }
 185  
 186          // If the item is already active, there's nothing to do
 187          if ( target.is( ".ui-state-active" ) ) {
 188              return;
 189          }
 190  
 191          // Remove ui-state-active class from siblings of the newly focused menu item
 192          // to avoid a jump caused by adjacent elements both having a class with a border
 193          this._removeClass( target.siblings().children( ".ui-state-active" ),
 194              null, "ui-state-active" );
 195          this.focus( event, target );
 196      },
 197  
 198      _destroy: function() {
 199          var items = this.element.find( ".ui-menu-item" )
 200                  .removeAttr( "role aria-disabled" ),
 201              submenus = items.children( ".ui-menu-item-wrapper" )
 202                  .removeUniqueId()
 203                  .removeAttr( "tabIndex role aria-haspopup" );
 204  
 205          // Destroy (sub)menus
 206          this.element
 207              .removeAttr( "aria-activedescendant" )
 208              .find( ".ui-menu" ).addBack()
 209                  .removeAttr( "role aria-labelledby aria-expanded aria-hidden aria-disabled " +
 210                      "tabIndex" )
 211                  .removeUniqueId()
 212                  .show();
 213  
 214          submenus.children().each( function() {
 215              var elem = $( this );
 216              if ( elem.data( "ui-menu-submenu-caret" ) ) {
 217                  elem.remove();
 218              }
 219          } );
 220      },
 221  
 222      _keydown: function( event ) {
 223          var match, prev, character, skip,
 224              preventDefault = true;
 225  
 226          switch ( event.keyCode ) {
 227          case $.ui.keyCode.PAGE_UP:
 228              this.previousPage( event );
 229              break;
 230          case $.ui.keyCode.PAGE_DOWN:
 231              this.nextPage( event );
 232              break;
 233          case $.ui.keyCode.HOME:
 234              this._move( "first", "first", event );
 235              break;
 236          case $.ui.keyCode.END:
 237              this._move( "last", "last", event );
 238              break;
 239          case $.ui.keyCode.UP:
 240              this.previous( event );
 241              break;
 242          case $.ui.keyCode.DOWN:
 243              this.next( event );
 244              break;
 245          case $.ui.keyCode.LEFT:
 246              this.collapse( event );
 247              break;
 248          case $.ui.keyCode.RIGHT:
 249              if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
 250                  this.expand( event );
 251              }
 252              break;
 253          case $.ui.keyCode.ENTER:
 254          case $.ui.keyCode.SPACE:
 255              this._activate( event );
 256              break;
 257          case $.ui.keyCode.ESCAPE:
 258              this.collapse( event );
 259              break;
 260          default:
 261              preventDefault = false;
 262              prev = this.previousFilter || "";
 263              skip = false;
 264  
 265              // Support number pad values
 266              character = event.keyCode >= 96 && event.keyCode <= 105 ?
 267                  ( event.keyCode - 96 ).toString() : String.fromCharCode( event.keyCode );
 268  
 269              clearTimeout( this.filterTimer );
 270  
 271              if ( character === prev ) {
 272                  skip = true;
 273              } else {
 274                  character = prev + character;
 275              }
 276  
 277              match = this._filterMenuItems( character );
 278              match = skip && match.index( this.active.next() ) !== -1 ?
 279                  this.active.nextAll( ".ui-menu-item" ) :
 280                  match;
 281  
 282              // If no matches on the current filter, reset to the last character pressed
 283              // to move down the menu to the first item that starts with that character
 284              if ( !match.length ) {
 285                  character = String.fromCharCode( event.keyCode );
 286                  match = this._filterMenuItems( character );
 287              }
 288  
 289              if ( match.length ) {
 290                  this.focus( event, match );
 291                  this.previousFilter = character;
 292                  this.filterTimer = this._delay( function() {
 293                      delete this.previousFilter;
 294                  }, 1000 );
 295              } else {
 296                  delete this.previousFilter;
 297              }
 298          }
 299  
 300          if ( preventDefault ) {
 301              event.preventDefault();
 302          }
 303      },
 304  
 305      _activate: function( event ) {
 306          if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
 307              if ( this.active.children( "[aria-haspopup='true']" ).length ) {
 308                  this.expand( event );
 309              } else {
 310                  this.select( event );
 311              }
 312          }
 313      },
 314  
 315      refresh: function() {
 316          var menus, items, newSubmenus, newItems, newWrappers,
 317              that = this,
 318              icon = this.options.icons.submenu,
 319              submenus = this.element.find( this.options.menus );
 320  
 321          this._toggleClass( "ui-menu-icons", null, !!this.element.find( ".ui-icon" ).length );
 322  
 323          // Initialize nested menus
 324          newSubmenus = submenus.filter( ":not(.ui-menu)" )
 325              .hide()
 326              .attr( {
 327                  role: this.options.role,
 328                  "aria-hidden": "true",
 329                  "aria-expanded": "false"
 330              } )
 331              .each( function() {
 332                  var menu = $( this ),
 333                      item = menu.prev(),
 334                      submenuCaret = $( "<span>" ).data( "ui-menu-submenu-caret", true );
 335  
 336                  that._addClass( submenuCaret, "ui-menu-icon", "ui-icon " + icon );
 337                  item
 338                      .attr( "aria-haspopup", "true" )
 339                      .prepend( submenuCaret );
 340                  menu.attr( "aria-labelledby", item.attr( "id" ) );
 341              } );
 342  
 343          this._addClass( newSubmenus, "ui-menu", "ui-widget ui-widget-content ui-front" );
 344  
 345          menus = submenus.add( this.element );
 346          items = menus.find( this.options.items );
 347  
 348          // Initialize menu-items containing spaces and/or dashes only as dividers
 349          items.not( ".ui-menu-item" ).each( function() {
 350              var item = $( this );
 351              if ( that._isDivider( item ) ) {
 352                  that._addClass( item, "ui-menu-divider", "ui-widget-content" );
 353              }
 354          } );
 355  
 356          // Don't refresh list items that are already adapted
 357          newItems = items.not( ".ui-menu-item, .ui-menu-divider" );
 358          newWrappers = newItems.children()
 359              .not( ".ui-menu" )
 360                  .uniqueId()
 361                  .attr( {
 362                      tabIndex: -1,
 363                      role: this._itemRole()
 364                  } );
 365          this._addClass( newItems, "ui-menu-item" )
 366              ._addClass( newWrappers, "ui-menu-item-wrapper" );
 367  
 368          // Add aria-disabled attribute to any disabled menu item
 369          items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
 370  
 371          // If the active item has been removed, blur the menu
 372          if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
 373              this.blur();
 374          }
 375      },
 376  
 377      _itemRole: function() {
 378          return {
 379              menu: "menuitem",
 380              listbox: "option"
 381          }[ this.options.role ];
 382      },
 383  
 384      _setOption: function( key, value ) {
 385          if ( key === "icons" ) {
 386              var icons = this.element.find( ".ui-menu-icon" );
 387              this._removeClass( icons, null, this.options.icons.submenu )
 388                  ._addClass( icons, null, value.submenu );
 389          }
 390          this._super( key, value );
 391      },
 392  
 393      _setOptionDisabled: function( value ) {
 394          this._super( value );
 395  
 396          this.element.attr( "aria-disabled", String( value ) );
 397          this._toggleClass( null, "ui-state-disabled", !!value );
 398      },
 399  
 400      focus: function( event, item ) {
 401          var nested, focused, activeParent;
 402          this.blur( event, event && event.type === "focus" );
 403  
 404          this._scrollIntoView( item );
 405  
 406          this.active = item.first();
 407  
 408          focused = this.active.children( ".ui-menu-item-wrapper" );
 409          this._addClass( focused, null, "ui-state-active" );
 410  
 411          // Only update aria-activedescendant if there's a role
 412          // otherwise we assume focus is managed elsewhere
 413          if ( this.options.role ) {
 414              this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
 415          }
 416  
 417          // Highlight active parent menu item, if any
 418          activeParent = this.active
 419              .parent()
 420                  .closest( ".ui-menu-item" )
 421                      .children( ".ui-menu-item-wrapper" );
 422          this._addClass( activeParent, null, "ui-state-active" );
 423  
 424          if ( event && event.type === "keydown" ) {
 425              this._close();
 426          } else {
 427              this.timer = this._delay( function() {
 428                  this._close();
 429              }, this.delay );
 430          }
 431  
 432          nested = item.children( ".ui-menu" );
 433          if ( nested.length && event && ( /^mouse/.test( event.type ) ) ) {
 434              this._startOpening( nested );
 435          }
 436          this.activeMenu = item.parent();
 437  
 438          this._trigger( "focus", event, { item: item } );
 439      },
 440  
 441      _scrollIntoView: function( item ) {
 442          var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
 443          if ( this._hasScroll() ) {
 444              borderTop = parseFloat( $.css( this.activeMenu[ 0 ], "borderTopWidth" ) ) || 0;
 445              paddingTop = parseFloat( $.css( this.activeMenu[ 0 ], "paddingTop" ) ) || 0;
 446              offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
 447              scroll = this.activeMenu.scrollTop();
 448              elementHeight = this.activeMenu.height();
 449              itemHeight = item.outerHeight();
 450  
 451              if ( offset < 0 ) {
 452                  this.activeMenu.scrollTop( scroll + offset );
 453              } else if ( offset + itemHeight > elementHeight ) {
 454                  this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
 455              }
 456          }
 457      },
 458  
 459      blur: function( event, fromFocus ) {
 460          if ( !fromFocus ) {
 461              clearTimeout( this.timer );
 462          }
 463  
 464          if ( !this.active ) {
 465              return;
 466          }
 467  
 468          this._removeClass( this.active.children( ".ui-menu-item-wrapper" ),
 469              null, "ui-state-active" );
 470  
 471          this._trigger( "blur", event, { item: this.active } );
 472          this.active = null;
 473      },
 474  
 475      _startOpening: function( submenu ) {
 476          clearTimeout( this.timer );
 477  
 478          // Don't open if already open fixes a Firefox bug that caused a .5 pixel
 479          // shift in the submenu position when mousing over the caret icon
 480          if ( submenu.attr( "aria-hidden" ) !== "true" ) {
 481              return;
 482          }
 483  
 484          this.timer = this._delay( function() {
 485              this._close();
 486              this._open( submenu );
 487          }, this.delay );
 488      },
 489  
 490      _open: function( submenu ) {
 491          var position = $.extend( {
 492              of: this.active
 493          }, this.options.position );
 494  
 495          clearTimeout( this.timer );
 496          this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) )
 497              .hide()
 498              .attr( "aria-hidden", "true" );
 499  
 500          submenu
 501              .show()
 502              .removeAttr( "aria-hidden" )
 503              .attr( "aria-expanded", "true" )
 504              .position( position );
 505      },
 506  
 507      collapseAll: function( event, all ) {
 508          clearTimeout( this.timer );
 509          this.timer = this._delay( function() {
 510  
 511              // If we were passed an event, look for the submenu that contains the event
 512              var currentMenu = all ? this.element :
 513                  $( event && event.target ).closest( this.element.find( ".ui-menu" ) );
 514  
 515              // If we found no valid submenu ancestor, use the main menu to close all
 516              // sub menus anyway
 517              if ( !currentMenu.length ) {
 518                  currentMenu = this.element;
 519              }
 520  
 521              this._close( currentMenu );
 522  
 523              this.blur( event );
 524  
 525              // Work around active item staying active after menu is blurred
 526              this._removeClass( currentMenu.find( ".ui-state-active" ), null, "ui-state-active" );
 527  
 528              this.activeMenu = currentMenu;
 529          }, all ? 0 : this.delay );
 530      },
 531  
 532      // With no arguments, closes the currently active menu - if nothing is active
 533      // it closes all menus.  If passed an argument, it will search for menus BELOW
 534      _close: function( startMenu ) {
 535          if ( !startMenu ) {
 536              startMenu = this.active ? this.active.parent() : this.element;
 537          }
 538  
 539          startMenu.find( ".ui-menu" )
 540              .hide()
 541              .attr( "aria-hidden", "true" )
 542              .attr( "aria-expanded", "false" );
 543      },
 544  
 545      _closeOnDocumentClick: function( event ) {
 546          return !$( event.target ).closest( ".ui-menu" ).length;
 547      },
 548  
 549      _isDivider: function( item ) {
 550  
 551          // Match hyphen, em dash, en dash
 552          return !/[^\-\u2014\u2013\s]/.test( item.text() );
 553      },
 554  
 555      collapse: function( event ) {
 556          var newItem = this.active &&
 557              this.active.parent().closest( ".ui-menu-item", this.element );
 558          if ( newItem && newItem.length ) {
 559              this._close();
 560              this.focus( event, newItem );
 561          }
 562      },
 563  
 564      expand: function( event ) {
 565          var newItem = this.active && this._menuItems( this.active.children( ".ui-menu" ) ).first();
 566  
 567          if ( newItem && newItem.length ) {
 568              this._open( newItem.parent() );
 569  
 570              // Delay so Firefox will not hide activedescendant change in expanding submenu from AT
 571              this._delay( function() {
 572                  this.focus( event, newItem );
 573              } );
 574          }
 575      },
 576  
 577      next: function( event ) {
 578          this._move( "next", "first", event );
 579      },
 580  
 581      previous: function( event ) {
 582          this._move( "prev", "last", event );
 583      },
 584  
 585      isFirstItem: function() {
 586          return this.active && !this.active.prevAll( ".ui-menu-item" ).length;
 587      },
 588  
 589      isLastItem: function() {
 590          return this.active && !this.active.nextAll( ".ui-menu-item" ).length;
 591      },
 592  
 593      _menuItems: function( menu ) {
 594          return ( menu || this.element )
 595              .find( this.options.items )
 596              .filter( ".ui-menu-item" );
 597      },
 598  
 599      _move: function( direction, filter, event ) {
 600          var next;
 601          if ( this.active ) {
 602              if ( direction === "first" || direction === "last" ) {
 603                  next = this.active
 604                      [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" )
 605                      .last();
 606              } else {
 607                  next = this.active
 608                      [ direction + "All" ]( ".ui-menu-item" )
 609                      .first();
 610              }
 611          }
 612          if ( !next || !next.length || !this.active ) {
 613              next = this._menuItems( this.activeMenu )[ filter ]();
 614          }
 615  
 616          this.focus( event, next );
 617      },
 618  
 619      nextPage: function( event ) {
 620          var item, base, height;
 621  
 622          if ( !this.active ) {
 623              this.next( event );
 624              return;
 625          }
 626          if ( this.isLastItem() ) {
 627              return;
 628          }
 629          if ( this._hasScroll() ) {
 630              base = this.active.offset().top;
 631              height = this.element.innerHeight();
 632  
 633              // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back.
 634              if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) {
 635                  height += this.element[ 0 ].offsetHeight - this.element.outerHeight();
 636              }
 637  
 638              this.active.nextAll( ".ui-menu-item" ).each( function() {
 639                  item = $( this );
 640                  return item.offset().top - base - height < 0;
 641              } );
 642  
 643              this.focus( event, item );
 644          } else {
 645              this.focus( event, this._menuItems( this.activeMenu )
 646                  [ !this.active ? "first" : "last" ]() );
 647          }
 648      },
 649  
 650      previousPage: function( event ) {
 651          var item, base, height;
 652          if ( !this.active ) {
 653              this.next( event );
 654              return;
 655          }
 656          if ( this.isFirstItem() ) {
 657              return;
 658          }
 659          if ( this._hasScroll() ) {
 660              base = this.active.offset().top;
 661              height = this.element.innerHeight();
 662  
 663              // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back.
 664              if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) {
 665                  height += this.element[ 0 ].offsetHeight - this.element.outerHeight();
 666              }
 667  
 668              this.active.prevAll( ".ui-menu-item" ).each( function() {
 669                  item = $( this );
 670                  return item.offset().top - base + height > 0;
 671              } );
 672  
 673              this.focus( event, item );
 674          } else {
 675              this.focus( event, this._menuItems( this.activeMenu ).first() );
 676          }
 677      },
 678  
 679      _hasScroll: function() {
 680          return this.element.outerHeight() < this.element.prop( "scrollHeight" );
 681      },
 682  
 683      select: function( event ) {
 684  
 685          // TODO: It should never be possible to not have an active item at this
 686          // point, but the tests don't trigger mouseenter before click.
 687          this.active = this.active || $( event.target ).closest( ".ui-menu-item" );
 688          var ui = { item: this.active };
 689          if ( !this.active.has( ".ui-menu" ).length ) {
 690              this.collapseAll( event, true );
 691          }
 692          this._trigger( "select", event, ui );
 693      },
 694  
 695      _filterMenuItems: function( character ) {
 696          var escapedCharacter = character.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ),
 697              regex = new RegExp( "^" + escapedCharacter, "i" );
 698  
 699          return this.activeMenu
 700              .find( this.options.items )
 701  
 702                  // Only match on items, not dividers or other content (#10571)
 703                  .filter( ".ui-menu-item" )
 704                      .filter( function() {
 705                          return regex.test(
 706                              String.prototype.trim.call(
 707                                  $( this ).children( ".ui-menu-item-wrapper" ).text() ) );
 708                      } );
 709      }
 710  } );
 711  
 712  } );


Generated : Fri Jul 24 08:20:19 2026 Cross-referenced by PHPXref