wiki.techinc.nl/resources/lib/jquery.ui/jquery.ui.menu.js
Timo Tijhof 3cb2ccd878 Upgrade jQuery UI from 1.8.24 to 1.9.2
jQuery UI 1.10 has already been released, and 1.11 is on the
horizon (1.9 is marked "legacy" as of writing). 1.10 drops the
compatibility layer for 1.8 API. So, users should definitely be
encouraged to upgrade their code to move away from the deprecated
1.8 APIs. However, to take it one step at a time, this commit
upgrades us to 1.9 first. That way we're at least not using an
unsupported code base anymore, while at the same time not rolling
out breaking changes.

jQuery UI has redesigned many of its APIs[1], but compatibility
with the jQuery UI 1.8 API is maintained throughout 1.9.x.

* Base theme is now called "Smoothness".

* Sorting the entries in Resources.php alphabetically to make it
  easier to ensure all files are listed when comparing them
  side-by-side.

* Update dependencies to match the "Depends:" header inside
  the javascript files.

* In jQuery UI 1.9, effect files were renamed to match the
  jquery.ui.*.js naming pattern.

* New modules:
  - jquery.ui.menu
  - jquery.ui.spinner
  - jquery.ui.tooltip

* Release notes:
  - http://blog.jqueryui.com/2012/10/jquery-ui-1-9-0/
    - http://jqueryui.com/upgrade-guide/1.9/
    - http://jqueryui.com/changelog/1.9.0/
  - http://blog.jqueryui.com/2012/10/jquery-ui-1-9-1/
    - http://jqueryui.com/changelog/1.9.1/
  - http://blog.jqueryui.com/2012/11/jquery-ui-1-9-2/

[1] http://blog.jqueryui.com/2011/03/api-redesigns-the-past-present-and-future/

Bug: 47076
Change-Id: I0e10b42fb7c25b9d4704719f21c44c08f36ddfa7
2014-06-05 00:29:23 +02:00

610 lines
16 KiB
JavaScript

/*!
* jQuery UI Menu 1.9.2
* http://jqueryui.com
*
* Copyright 2012 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/menu/
*
* Depends:
* jquery.ui.core.js
* jquery.ui.widget.js
* jquery.ui.position.js
*/
(function( $, undefined ) {
var mouseHandled = false;
$.widget( "ui.menu", {
version: "1.9.2",
defaultElement: "<ul>",
delay: 300,
options: {
icons: {
submenu: "ui-icon-carat-1-e"
},
menus: "ul",
position: {
my: "left top",
at: "right top"
},
role: "menu",
// callbacks
blur: null,
focus: null,
select: null
},
_create: function() {
this.activeMenu = this.element;
this.element
.uniqueId()
.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length )
.attr({
role: this.options.role,
tabIndex: 0
})
// need to catch all clicks on disabled menu
// not possible through _on
.bind( "click" + this.eventNamespace, $.proxy(function( event ) {
if ( this.options.disabled ) {
event.preventDefault();
}
}, this ));
if ( this.options.disabled ) {
this.element
.addClass( "ui-state-disabled" )
.attr( "aria-disabled", "true" );
}
this._on({
// Prevent focus from sticking to links inside menu after clicking
// them (focus should always stay on UL during navigation).
"mousedown .ui-menu-item > a": function( event ) {
event.preventDefault();
},
"click .ui-state-disabled > a": function( event ) {
event.preventDefault();
},
"click .ui-menu-item:has(a)": function( event ) {
var target = $( event.target ).closest( ".ui-menu-item" );
if ( !mouseHandled && target.not( ".ui-state-disabled" ).length ) {
mouseHandled = true;
this.select( event );
// Open submenu on click
if ( target.has( ".ui-menu" ).length ) {
this.expand( event );
} else if ( !this.element.is( ":focus" ) ) {
// Redirect focus to the menu
this.element.trigger( "focus", [ true ] );
// If the active item is on the top level, let it stay active.
// Otherwise, blur the active item since it is no longer visible.
if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) {
clearTimeout( this.timer );
}
}
}
},
"mouseenter .ui-menu-item": function( event ) {
var target = $( event.currentTarget );
// Remove ui-state-active class from siblings of the newly focused menu item
// to avoid a jump caused by adjacent elements both having a class with a border
target.siblings().children( ".ui-state-active" ).removeClass( "ui-state-active" );
this.focus( event, target );
},
mouseleave: "collapseAll",
"mouseleave .ui-menu": "collapseAll",
focus: function( event, keepActiveItem ) {
// If there's already an active item, keep it active
// If not, activate the first item
var item = this.active || this.element.children( ".ui-menu-item" ).eq( 0 );
if ( !keepActiveItem ) {
this.focus( event, item );
}
},
blur: function( event ) {
this._delay(function() {
if ( !$.contains( this.element[0], this.document[0].activeElement ) ) {
this.collapseAll( event );
}
});
},
keydown: "_keydown"
});
this.refresh();
// Clicks outside of a menu collapse any open menus
this._on( this.document, {
click: function( event ) {
if ( !$( event.target ).closest( ".ui-menu" ).length ) {
this.collapseAll( event );
}
// Reset the mouseHandled flag
mouseHandled = false;
}
});
},
_destroy: function() {
// Destroy (sub)menus
this.element
.removeAttr( "aria-activedescendant" )
.find( ".ui-menu" ).andSelf()
.removeClass( "ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons" )
.removeAttr( "role" )
.removeAttr( "tabIndex" )
.removeAttr( "aria-labelledby" )
.removeAttr( "aria-expanded" )
.removeAttr( "aria-hidden" )
.removeAttr( "aria-disabled" )
.removeUniqueId()
.show();
// Destroy menu items
this.element.find( ".ui-menu-item" )
.removeClass( "ui-menu-item" )
.removeAttr( "role" )
.removeAttr( "aria-disabled" )
.children( "a" )
.removeUniqueId()
.removeClass( "ui-corner-all ui-state-hover" )
.removeAttr( "tabIndex" )
.removeAttr( "role" )
.removeAttr( "aria-haspopup" )
.children().each( function() {
var elem = $( this );
if ( elem.data( "ui-menu-submenu-carat" ) ) {
elem.remove();
}
});
// Destroy menu dividers
this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" );
},
_keydown: function( event ) {
var match, prev, character, skip, regex,
preventDefault = true;
function escape( value ) {
return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
}
switch ( event.keyCode ) {
case $.ui.keyCode.PAGE_UP:
this.previousPage( event );
break;
case $.ui.keyCode.PAGE_DOWN:
this.nextPage( event );
break;
case $.ui.keyCode.HOME:
this._move( "first", "first", event );
break;
case $.ui.keyCode.END:
this._move( "last", "last", event );
break;
case $.ui.keyCode.UP:
this.previous( event );
break;
case $.ui.keyCode.DOWN:
this.next( event );
break;
case $.ui.keyCode.LEFT:
this.collapse( event );
break;
case $.ui.keyCode.RIGHT:
if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
this.expand( event );
}
break;
case $.ui.keyCode.ENTER:
case $.ui.keyCode.SPACE:
this._activate( event );
break;
case $.ui.keyCode.ESCAPE:
this.collapse( event );
break;
default:
preventDefault = false;
prev = this.previousFilter || "";
character = String.fromCharCode( event.keyCode );
skip = false;
clearTimeout( this.filterTimer );
if ( character === prev ) {
skip = true;
} else {
character = prev + character;
}
regex = new RegExp( "^" + escape( character ), "i" );
match = this.activeMenu.children( ".ui-menu-item" ).filter(function() {
return regex.test( $( this ).children( "a" ).text() );
});
match = skip && match.index( this.active.next() ) !== -1 ?
this.active.nextAll( ".ui-menu-item" ) :
match;
// If no matches on the current filter, reset to the last character pressed
// to move down the menu to the first item that starts with that character
if ( !match.length ) {
character = String.fromCharCode( event.keyCode );
regex = new RegExp( "^" + escape( character ), "i" );
match = this.activeMenu.children( ".ui-menu-item" ).filter(function() {
return regex.test( $( this ).children( "a" ).text() );
});
}
if ( match.length ) {
this.focus( event, match );
if ( match.length > 1 ) {
this.previousFilter = character;
this.filterTimer = this._delay(function() {
delete this.previousFilter;
}, 1000 );
} else {
delete this.previousFilter;
}
} else {
delete this.previousFilter;
}
}
if ( preventDefault ) {
event.preventDefault();
}
},
_activate: function( event ) {
if ( !this.active.is( ".ui-state-disabled" ) ) {
if ( this.active.children( "a[aria-haspopup='true']" ).length ) {
this.expand( event );
} else {
this.select( event );
}
}
},
refresh: function() {
var menus,
icon = this.options.icons.submenu,
submenus = this.element.find( this.options.menus );
// Initialize nested menus
submenus.filter( ":not(.ui-menu)" )
.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
.hide()
.attr({
role: this.options.role,
"aria-hidden": "true",
"aria-expanded": "false"
})
.each(function() {
var menu = $( this ),
item = menu.prev( "a" ),
submenuCarat = $( "<span>" )
.addClass( "ui-menu-icon ui-icon " + icon )
.data( "ui-menu-submenu-carat", true );
item
.attr( "aria-haspopup", "true" )
.prepend( submenuCarat );
menu.attr( "aria-labelledby", item.attr( "id" ) );
});
menus = submenus.add( this.element );
// Don't refresh list items that are already adapted
menus.children( ":not(.ui-menu-item):has(a)" )
.addClass( "ui-menu-item" )
.attr( "role", "presentation" )
.children( "a" )
.uniqueId()
.addClass( "ui-corner-all" )
.attr({
tabIndex: -1,
role: this._itemRole()
});
// Initialize unlinked menu-items containing spaces and/or dashes only as dividers
menus.children( ":not(.ui-menu-item)" ).each(function() {
var item = $( this );
// hyphen, em dash, en dash
if ( !/[^\-—–\s]/.test( item.text() ) ) {
item.addClass( "ui-widget-content ui-menu-divider" );
}
});
// Add aria-disabled attribute to any disabled menu item
menus.children( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
// If the active item has been removed, blur the menu
if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
this.blur();
}
},
_itemRole: function() {
return {
menu: "menuitem",
listbox: "option"
}[ this.options.role ];
},
focus: function( event, item ) {
var nested, focused;
this.blur( event, event && event.type === "focus" );
this._scrollIntoView( item );
this.active = item.first();
focused = this.active.children( "a" ).addClass( "ui-state-focus" );
// Only update aria-activedescendant if there's a role
// otherwise we assume focus is managed elsewhere
if ( this.options.role ) {
this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
}
// Highlight active parent menu item, if any
this.active
.parent()
.closest( ".ui-menu-item" )
.children( "a:first" )
.addClass( "ui-state-active" );
if ( event && event.type === "keydown" ) {
this._close();
} else {
this.timer = this._delay(function() {
this._close();
}, this.delay );
}
nested = item.children( ".ui-menu" );
if ( nested.length && ( /^mouse/.test( event.type ) ) ) {
this._startOpening(nested);
}
this.activeMenu = item.parent();
this._trigger( "focus", event, { item: item } );
},
_scrollIntoView: function( item ) {
var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
if ( this._hasScroll() ) {
borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0;
paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0;
offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
scroll = this.activeMenu.scrollTop();
elementHeight = this.activeMenu.height();
itemHeight = item.height();
if ( offset < 0 ) {
this.activeMenu.scrollTop( scroll + offset );
} else if ( offset + itemHeight > elementHeight ) {
this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
}
}
},
blur: function( event, fromFocus ) {
if ( !fromFocus ) {
clearTimeout( this.timer );
}
if ( !this.active ) {
return;
}
this.active.children( "a" ).removeClass( "ui-state-focus" );
this.active = null;
this._trigger( "blur", event, { item: this.active } );
},
_startOpening: function( submenu ) {
clearTimeout( this.timer );
// Don't open if already open fixes a Firefox bug that caused a .5 pixel
// shift in the submenu position when mousing over the carat icon
if ( submenu.attr( "aria-hidden" ) !== "true" ) {
return;
}
this.timer = this._delay(function() {
this._close();
this._open( submenu );
}, this.delay );
},
_open: function( submenu ) {
var position = $.extend({
of: this.active
}, this.options.position );
clearTimeout( this.timer );
this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) )
.hide()
.attr( "aria-hidden", "true" );
submenu
.show()
.removeAttr( "aria-hidden" )
.attr( "aria-expanded", "true" )
.position( position );
},
collapseAll: function( event, all ) {
clearTimeout( this.timer );
this.timer = this._delay(function() {
// If we were passed an event, look for the submenu that contains the event
var currentMenu = all ? this.element :
$( event && event.target ).closest( this.element.find( ".ui-menu" ) );
// If we found no valid submenu ancestor, use the main menu to close all sub menus anyway
if ( !currentMenu.length ) {
currentMenu = this.element;
}
this._close( currentMenu );
this.blur( event );
this.activeMenu = currentMenu;
}, this.delay );
},
// With no arguments, closes the currently active menu - if nothing is active
// it closes all menus. If passed an argument, it will search for menus BELOW
_close: function( startMenu ) {
if ( !startMenu ) {
startMenu = this.active ? this.active.parent() : this.element;
}
startMenu
.find( ".ui-menu" )
.hide()
.attr( "aria-hidden", "true" )
.attr( "aria-expanded", "false" )
.end()
.find( "a.ui-state-active" )
.removeClass( "ui-state-active" );
},
collapse: function( event ) {
var newItem = this.active &&
this.active.parent().closest( ".ui-menu-item", this.element );
if ( newItem && newItem.length ) {
this._close();
this.focus( event, newItem );
}
},
expand: function( event ) {
var newItem = this.active &&
this.active
.children( ".ui-menu " )
.children( ".ui-menu-item" )
.first();
if ( newItem && newItem.length ) {
this._open( newItem.parent() );
// Delay so Firefox will not hide activedescendant change in expanding submenu from AT
this._delay(function() {
this.focus( event, newItem );
});
}
},
next: function( event ) {
this._move( "next", "first", event );
},
previous: function( event ) {
this._move( "prev", "last", event );
},
isFirstItem: function() {
return this.active && !this.active.prevAll( ".ui-menu-item" ).length;
},
isLastItem: function() {
return this.active && !this.active.nextAll( ".ui-menu-item" ).length;
},
_move: function( direction, filter, event ) {
var next;
if ( this.active ) {
if ( direction === "first" || direction === "last" ) {
next = this.active
[ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" )
.eq( -1 );
} else {
next = this.active
[ direction + "All" ]( ".ui-menu-item" )
.eq( 0 );
}
}
if ( !next || !next.length || !this.active ) {
next = this.activeMenu.children( ".ui-menu-item" )[ filter ]();
}
this.focus( event, next );
},
nextPage: function( event ) {
var item, base, height;
if ( !this.active ) {
this.next( event );
return;
}
if ( this.isLastItem() ) {
return;
}
if ( this._hasScroll() ) {
base = this.active.offset().top;
height = this.element.height();
this.active.nextAll( ".ui-menu-item" ).each(function() {
item = $( this );
return item.offset().top - base - height < 0;
});
this.focus( event, item );
} else {
this.focus( event, this.activeMenu.children( ".ui-menu-item" )
[ !this.active ? "first" : "last" ]() );
}
},
previousPage: function( event ) {
var item, base, height;
if ( !this.active ) {
this.next( event );
return;
}
if ( this.isFirstItem() ) {
return;
}
if ( this._hasScroll() ) {
base = this.active.offset().top;
height = this.element.height();
this.active.prevAll( ".ui-menu-item" ).each(function() {
item = $( this );
return item.offset().top - base + height > 0;
});
this.focus( event, item );
} else {
this.focus( event, this.activeMenu.children( ".ui-menu-item" ).first() );
}
},
_hasScroll: function() {
return this.element.outerHeight() < this.element.prop( "scrollHeight" );
},
select: function( event ) {
// TODO: It should never be possible to not have an active item at this
// point, but the tests don't trigger mouseenter before click.
this.active = this.active || $( event.target ).closest( ".ui-menu-item" );
var ui = { item: this.active };
if ( !this.active.has( ".ui-menu" ).length ) {
this.collapseAll( event, true );
}
this._trigger( "select", event, ui );
}
});
}( jQuery ));