[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

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


Generated: Sat Apr 20 01:00:03 2024 Cross-referenced by PHPXref 0.7.1