diff options
Diffstat (limited to 'ui/jquery.ui.menu.js')
-rw-r--r-- | ui/jquery.ui.menu.js | 379 |
1 files changed, 212 insertions, 167 deletions
diff --git a/ui/jquery.ui.menu.js b/ui/jquery.ui.menu.js index 27e76d909..bf36a77fe 100644 --- a/ui/jquery.ui.menu.js +++ b/ui/jquery.ui.menu.js @@ -20,14 +20,16 @@ $.widget( "ui.menu", { defaultElement: "<ul>", delay: 150, options: { + items: "ul", position: { my: "left top", at: "right top" - } + }, + trigger: null }, _create: function() { - var self = this; this.activeMenu = this.element; + this.isScrolling = false; this.menuId = this.element.attr( "id" ) || "ui-menu-" + idIncrement++; if ( this.element.find( ".ui-icon" ).length ) { this.element.addClass( "ui-menu-icons" ); @@ -38,13 +40,23 @@ $.widget( "ui.menu", { id: this.menuId, role: "menu" }) + // Prevent focus from sticking to links inside menu after clicking + // them (focus should always stay on UL during navigation). + // If the link is clicked, redirect focus to the menu. + // TODO move to _bind below + .bind( "mousedown.menu", function( event ) { + if ( $( event.target).is( "a" ) ) { + event.preventDefault(); + $( this ).focus( 1 ); + } + }) // need to catch all clicks on disabled menu // not possible through _bind - .bind( "click.menu", function( event ) { - if ( self.options.disabled ) { + .bind( "click.menu", $.proxy( function( event ) { + if ( this.options.disabled ) { event.preventDefault(); } - }); + }, this)); this._bind({ "click .ui-menu-item:has(a)": function( event ) { event.stopImmediatePropagation(); @@ -57,143 +69,172 @@ $.widget( "ui.menu", { }, "mouseover .ui-menu-item": function( event ) { event.stopImmediatePropagation(); - 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 ); + if ( !this.isScrolling ) { + 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 ); + } + this.isScrolling = false; }, + "mouseleave": "collapseAll", + "mouseleave .ui-menu": "collapseAll", "mouseout .ui-menu-item": "blur", "focus": function( event ) { this.focus( event, $( event.target ).children( ".ui-menu-item:first" ) ); }, - "blur": "collapseAll" + blur: function( event ) { + this._delay( function() { + if ( ! $.contains( this.element[0], this.document[0].activeElement ) ) { + this.collapseAll( event ); + } + }, 0); + }, + scroll: function( event ) { + // Keep track of scrolling to prevent mouseover from firing inadvertently when scrolling the menu + this.isScrolling = true; + } }); this.refresh(); - this.element.attr( "tabIndex", 0 ).bind( "keydown.menu", function( event ) { - if ( self.options.disabled ) { - return; - } - switch ( event.keyCode ) { - case $.ui.keyCode.PAGE_UP: - self.previousPage( event ); - event.preventDefault(); - event.stopImmediatePropagation(); - break; - case $.ui.keyCode.PAGE_DOWN: - self.nextPage( event ); - event.preventDefault(); - event.stopImmediatePropagation(); - break; - case $.ui.keyCode.HOME: - self._move( "first", "first", event ); - event.preventDefault(); - event.stopImmediatePropagation(); - break; - case $.ui.keyCode.END: - self._move( "last", "last", event ); - event.preventDefault(); - event.stopImmediatePropagation(); - break; - case $.ui.keyCode.UP: - self.previous( event ); - event.preventDefault(); - event.stopImmediatePropagation(); - break; - case $.ui.keyCode.DOWN: - self.next( event ); - event.preventDefault(); - event.stopImmediatePropagation(); - break; - case $.ui.keyCode.LEFT: - if (self.collapse( event )) { + this.element.attr( "tabIndex", 0 ); + this._bind({ + "keydown": function( event ) { + switch ( event.keyCode ) { + case $.ui.keyCode.PAGE_UP: + this.previousPage( event ); + event.preventDefault(); event.stopImmediatePropagation(); - } - event.preventDefault(); - break; - case $.ui.keyCode.RIGHT: - if (self.expand( event )) { + break; + case $.ui.keyCode.PAGE_DOWN: + this.nextPage( event ); + event.preventDefault(); event.stopImmediatePropagation(); - } - event.preventDefault(); - break; - case $.ui.keyCode.ENTER: - if ( self.active.children( "a[aria-haspopup='true']" ).length ) { - if ( self.expand( event ) ) { - event.stopImmediatePropagation(); - } - } - else { - self.select( event ); + break; + case $.ui.keyCode.HOME: + this._move( "first", "first", event ); + event.preventDefault(); event.stopImmediatePropagation(); - } - event.preventDefault(); - break; - case $.ui.keyCode.ESCAPE: - if ( self.collapse( event ) ) { + break; + case $.ui.keyCode.END: + this._move( "last", "last", event ); + event.preventDefault(); event.stopImmediatePropagation(); - } - event.preventDefault(); - break; - default: - event.stopPropagation(); - clearTimeout( self.filterTimer ); - var match, - prev = self.previousFilter || "", - character = String.fromCharCode( event.keyCode ), - skip = false; - - if (character == prev) { - skip = true; - } else { - character = prev + character; - } - function escape( value ) { - return value.replace( /[-[\]{}()*+?.,\\^$|#\s]/g , "\\$&" ); - } - match = self.activeMenu.children( ".ui-menu-item" ).filter( function() { - return new RegExp("^" + escape(character), "i") - .test( $( this ).children( "a" ).text() ); - }); - match = skip && match.index(self.active.next()) != -1 ? self.active.nextAll(".ui-menu-item") : match; - if ( !match.length ) { - character = String.fromCharCode(event.keyCode); - match = self.activeMenu.children(".ui-menu-item").filter( function() { + break; + case $.ui.keyCode.UP: + this.previous( event ); + event.preventDefault(); + event.stopImmediatePropagation(); + break; + case $.ui.keyCode.DOWN: + this.next( event ); + event.preventDefault(); + event.stopImmediatePropagation(); + break; + case $.ui.keyCode.LEFT: + if (this.collapse( event )) { + event.stopImmediatePropagation(); + } + event.preventDefault(); + break; + case $.ui.keyCode.RIGHT: + if (this.expand( event )) { + event.stopImmediatePropagation(); + } + event.preventDefault(); + break; + case $.ui.keyCode.ENTER: + if ( this.active.children( "a[aria-haspopup='true']" ).length ) { + if ( this.expand( event ) ) { + event.stopImmediatePropagation(); + } + } + else { + this.select( event ); + event.stopImmediatePropagation(); + } + event.preventDefault(); + break; + case $.ui.keyCode.ESCAPE: + if ( this.collapse( event ) ) { + event.stopImmediatePropagation(); + } + event.preventDefault(); + break; + default: + event.stopPropagation(); + clearTimeout( this.filterTimer ); + var match, + prev = this.previousFilter || "", + character = String.fromCharCode( event.keyCode ), + skip = false; + + if (character == prev) { + skip = true; + } else { + character = prev + character; + } + function escape( value ) { + return value.replace( /[-[\]{}()*+?.,\\^$|#\s]/g , "\\$&" ); + } + match = this.activeMenu.children( ".ui-menu-item" ).filter( function() { return new RegExp("^" + escape(character), "i") .test( $( this ).children( "a" ).text() ); }); - } - if ( match.length ) { - self.focus( event, match ); - if (match.length > 1) { - self.previousFilter = character; - self.filterTimer = setTimeout( function() { - delete self.previousFilter; - }, 1000 ); + match = skip && match.index(this.active.next()) != -1 ? this.active.nextAll(".ui-menu-item") : match; + if ( !match.length ) { + character = String.fromCharCode(event.keyCode); + match = this.activeMenu.children(".ui-menu-item").filter( function() { + return new RegExp("^" + escape(character), "i") + .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 self.previousFilter; + delete this.previousFilter; } - } else { - delete self.previousFilter; } } }); - this._bind( document, { + this._bind( this.document, { click: function( event ) { if ( !$( event.target ).closest( ".ui-menu" ).length ) { this.collapseAll( event ); } } }); + + if ( this.options.trigger ) { + this.element.popup({ + trigger: this.options.trigger, + managed: true, + focusPopup: $.proxy( function( event, ui ) { + this.focus( event, this.element.children( ".ui-menu-item" ).first() ); + this.element.focus( 1 ); + }, this) + }); + } }, _destroy: function() { //destroy (sub)menus + if ( this.options.trigger ) { + this.element.popup( "destroy" ); + } this.element .removeAttr( "aria-activedescendant" ) - .find( "ul" ) + .find( ".ui-menu" ) .andSelf() .removeClass( "ui-menu ui-widget ui-widget-content ui-corner-all" ) .removeAttr( "role" ) @@ -219,43 +260,38 @@ $.widget( "ui.menu", { }, refresh: function() { - var self = this, - - // initialize nested menus - submenus = this.element.find( "ul:not(.ui-menu)" ) - .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" ) - .attr( "role", "menu" ) - .hide() - .attr( "aria-hidden", "true" ) - .attr( "aria-expanded", "false" ), + // initialize nested menus + var submenus = this.element.find( this.options.items + ":not( .ui-menu )" ) + .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" ) + .attr( "role", "menu" ) + .hide() + .attr( "aria-hidden", "true" ) + .attr( "aria-expanded", "false" ); // don't refresh list items that are already adapted - items = submenus.add( this.element ).children( "li:not(.ui-menu-item):has(a)" ) - .addClass( "ui-menu-item" ) - .attr( "role", "presentation" ); - - items.children( "a" ) - .addClass( "ui-corner-all" ) - .attr( "tabIndex", -1 ) - .attr( "role", "menuitem" ) - .attr( "id", function( i ) { - return self.element.attr( "id" ) + "-" + i; - }); + var menuId = this.menuId; + submenus.add( this.element ).children( ":not( .ui-menu-item ):has( a )" ) + .addClass( "ui-menu-item" ) + .attr( "role", "presentation" ) + .children( "a" ) + .addClass( "ui-corner-all" ) + .attr( "tabIndex", -1 ) + .attr( "role", "menuitem" ) + .attr( "id", function( i ) { + return menuId + "-" + i; + }); submenus.each( function() { var menu = $( this ), item = menu.prev( "a" ); item.attr( "aria-haspopup", "true" ) - .prepend( '<span class="ui-menu-icon ui-icon ui-icon-carat-1-e"></span>' ); + .prepend( '<span class="ui-menu-icon ui-icon ui-icon-carat-1-e"></span>' ); menu.attr( "aria-labelledby", item.attr( "id" ) ); }); }, focus: function( event, item ) { - var nested, - self = this; - this.blur( event ); if ( this._hasScroll() ) { @@ -277,18 +313,18 @@ $.widget( "ui.menu", { .children( "a" ) .addClass( "ui-state-focus" ) .end(); - self.element.attr( "aria-activedescendant", self.active.children("a").attr("id") ); + this.element.attr( "aria-activedescendant", this.active.children( "a" ).attr( "id" ) ); // highlight active parent menu item, if any - this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"); + this.active.parent().closest( ".ui-menu-item" ).children( "a:first" ).addClass( "ui-state-active" ); - self.timer = setTimeout( function() { - self._close(); - }, self.delay ); + this.timer = this._delay( function() { + this._close(); + }, this.delay ); - nested = $( ">ul", item ); + var nested = $( "> .ui-menu", item ); if ( nested.length && ( /^mouse/.test( event.type ) ) ) { - self._startOpening(nested); + this._startOpening(nested); } this.activeMenu = item.parent(); @@ -317,11 +353,10 @@ $.widget( "ui.menu", { return; } - var self = this; - self.timer = setTimeout( function() { - self._close(); - self._open( submenu ); - }, self.delay ); + this.timer = this._delay( function() { + this._close(); + this._open( submenu ); + }, this.delay ); }, _open: function( submenu ) { @@ -345,23 +380,32 @@ $.widget( "ui.menu", { .position( position ); }, - collapseAll: function( event ) { - this.element - .find( "ul" ) - .hide() - .attr( "aria-hidden", "true" ) - .attr( "aria-expanded", "false" ) - .end() - .find( "a.ui-state-active" ) - .removeClass( "ui-state-active" ); + collapseAll: function( event, all ) { + + // 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 = this.element; + this.activeMenu = currentMenu; }, - _close: function() { - this.active.parent() - .find( "ul" ) + // 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" ) @@ -371,27 +415,23 @@ $.widget( "ui.menu", { }, collapse: function( event ) { - var newItem = this.active && this.active.parents("li:not(.ui-menubar-item)").first(); + var newItem = this.active && this.active.parent().closest( ".ui-menu-item", this.element ); if ( newItem && newItem.length ) { - this.active.parent() - .attr("aria-hidden", "true") - .attr("aria-expanded", "false") - .hide(); + this._close(); this.focus( event, newItem ); return true; } }, expand: function( event ) { - var self = this, - newItem = this.active && this.active.children("ul").children("li").first(); + var newItem = this.active && this.active.children( ".ui-menu " ).children( ".ui-menu-item" ).first(); if ( newItem && newItem.length ) { this._open( newItem.parent() ); //timeout so Firefox will not hide activedescendant change in expanding submenu from AT - setTimeout( function() { - self.focus( event, newItem ); + this._delay( function() { + this.focus( event, newItem ); }, 20 ); return true; } @@ -487,11 +527,16 @@ $.widget( "ui.menu", { }, select: function( event ) { + // save active reference before collapseAll triggers blur var ui = { item: this.active }; - this.collapseAll( event ); + this.collapseAll( event, true ); + if ( this.options.trigger ) { + $( this.options.trigger ).focus( 1 ); + this.element.popup( "close" ); + } this._trigger( "select", event, ui ); } }); |