選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

714 行
18 KiB

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