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


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