[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

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


Generated: Mon Nov 30 01:00:03 2020 Cross-referenced by PHPXref 0.7.1