2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('node-menunav', function(Y) {
11 * <p>The MenuNav Node Plugin makes it easy to transform existing list-based
12 * markup into traditional, drop down navigational menus that are both accessible
13 * and easy to customize, and only require a small set of dependencies.</p>
16 * <p>To use the MenuNav Node Plugin, simply pass a reference to the plugin to a
17 * Node instance's <code>plug</code> method.</p>
21 * <script type="text/javascript"> <br>
23 * // Call the "use" method, passing in "node-menunav". This will <br>
24 * // load the script and CSS for the MenuNav Node Plugin and all of <br>
25 * // the required dependencies. <br>
27 * YUI().use("node-menunav", function(Y) { <br>
29 * // Use the "contentready" event to initialize the menu when <br>
30 * // the subtree of element representing the root menu <br>
31 * // (<div id="menu-1">) is ready to be scripted. <br>
33 * Y.on("contentready", function () { <br>
35 * // The scope of the callback will be a Node instance <br>
36 * // representing the root menu (<div id="menu-1">). <br>
37 * // Therefore, since "this" represents a Node instance, it <br>
38 * // is possible to just call "this.plug" passing in a <br>
39 * // reference to the MenuNav Node Plugin. <br>
41 * this.plug(Y.Plugin.NodeMenuNav); <br>
47 * </script> <br>
51 * <p>The MenuNav Node Plugin has several configuration properties that can be
52 * set via an object literal that is passed as a second argument to a Node
53 * instance's <code>plug</code> method.
58 * <script type="text/javascript"> <br>
60 * // Call the "use" method, passing in "node-menunav". This will <br>
61 * // load the script and CSS for the MenuNav Node Plugin and all of <br>
62 * // the required dependencies. <br>
64 * YUI().use("node-menunav", function(Y) { <br>
66 * // Use the "contentready" event to initialize the menu when <br>
67 * // the subtree of element representing the root menu <br>
68 * // (<div id="menu-1">) is ready to be scripted. <br>
70 * Y.on("contentready", function () { <br>
72 * // The scope of the callback will be a Node instance <br>
73 * // representing the root menu (<div id="menu-1">). <br>
74 * // Therefore, since "this" represents a Node instance, it <br>
75 * // is possible to just call "this.plug" passing in a <br>
76 * // reference to the MenuNav Node Plugin. <br>
78 * this.plug(Y.Plugin.NodeMenuNav, { mouseOutHideDelay: 1000 });
84 * </script> <br>
88 * @module node-menunav
96 getClassName = Y.ClassNameManager.getClassName,
100 // Frequently used strings
103 MENUITEM = "menuitem",
105 PARENT_NODE = "parentNode",
106 CHILDREN = "children",
107 OFFSET_HEIGHT = "offsetHeight",
108 OFFSET_WIDTH = "offsetWidth",
112 HANDLED_MOUSEOUT = "handledMouseOut",
113 HANDLED_MOUSEOVER = "handledMouseOver",
117 MOUSEDOWN = "mousedown",
121 FIRST_OF_TYPE = "first-of-type",
123 PRESENTATION = "presentation",
124 DESCENDANTS = "descendants",
126 ACTIVE_DESCENDANT = "activeDescendant",
127 USE_ARIA = "useARIA",
128 ARIA_HIDDEN = "aria-hidden",
131 ACTIVE_DESCENDANT_CHANGE = ACTIVE_DESCENDANT + "Change",
136 AUTO_SUBMENU_DISPLAY = "autoSubmenuDisplay",
137 MOUSEOUT_HIDE_DELAY = "mouseOutHideDelay",
142 CSS_MENU = getClassName(MENU),
143 CSS_MENU_HIDDEN = getClassName(MENU, HIDDEN),
144 CSS_MENU_HORIZONTAL = getClassName(MENU, "horizontal"),
145 CSS_MENU_LABEL = getClassName(MENU, LABEL),
146 CSS_MENU_LABEL_ACTIVE = getClassName(MENU, LABEL, ACTIVE),
147 CSS_MENU_LABEL_MENUVISIBLE = getClassName(MENU, LABEL, (MENU + "visible")),
148 CSS_MENUITEM = getClassName(MENUITEM),
149 CSS_MENUITEM_ACTIVE = getClassName(MENUITEM, ACTIVE),
154 MENU_SELECTOR = PERIOD + CSS_MENU,
155 MENU_TOGGLE_SELECTOR = (PERIOD + getClassName(MENU, "toggle")),
156 MENU_CONTENT_SELECTOR = PERIOD + getClassName(MENU, CONTENT),
157 MENU_LABEL_SELECTOR = PERIOD + CSS_MENU_LABEL,
159 STANDARD_QUERY = ">" + MENU_CONTENT_SELECTOR + ">ul>li>a",
160 EXTENDED_QUERY = ">" + MENU_CONTENT_SELECTOR + ">ul>li>" + MENU_LABEL_SELECTOR + ">a:first-child";
165 var getPreviousSibling = function (node) {
167 var oPrevious = node.previous(),
171 oChildren = node.get(PARENT_NODE).get(CHILDREN);
172 oPrevious = oChildren.item(oChildren.size() - 1);
180 var getNextSibling = function (node) {
182 var oNext = node.next();
185 oNext = node.get(PARENT_NODE).get(CHILDREN).item(0);
193 var isAnchor = function (node) {
195 var bReturnVal = false;
198 bReturnVal = node.get("nodeName").toLowerCase() === LOWERCASE_A;
206 var isMenuItem = function (node) {
208 return node.hasClass(CSS_MENUITEM);
213 var isMenuLabel = function (node) {
215 return node.hasClass(CSS_MENU_LABEL);
220 var isHorizontalMenu = function (menu) {
222 return menu.hasClass(CSS_MENU_HORIZONTAL);
227 var hasVisibleSubmenu = function (menuLabel) {
229 return menuLabel.hasClass(CSS_MENU_LABEL_MENUVISIBLE);
234 var getItemAnchor = function (node) {
236 return isAnchor(node) ? node : node.one(LOWERCASE_A);
241 var getNodeWithClass = function (node, className, searchAncestors) {
247 if (node.hasClass(className)) {
251 if (!oItem && searchAncestors) {
252 oItem = node.ancestor((PERIOD + className));
262 var getParentMenu = function (node) {
264 return node.ancestor(MENU_SELECTOR);
269 var getMenu = function (node, searchAncestors) {
271 return getNodeWithClass(node, CSS_MENU, searchAncestors);
276 var getMenuItem = function (node, searchAncestors) {
281 oItem = getNodeWithClass(node, CSS_MENUITEM, searchAncestors);
289 var getMenuLabel = function (node, searchAncestors) {
295 if (searchAncestors) {
296 oItem = getNodeWithClass(node, CSS_MENU_LABEL, searchAncestors);
299 oItem = getNodeWithClass(node, CSS_MENU_LABEL) ||
300 node.one((PERIOD + CSS_MENU_LABEL));
310 var getItem = function (node, searchAncestors) {
315 oItem = getMenuItem(node, searchAncestors) ||
316 getMenuLabel(node, searchAncestors);
324 var getFirstItem = function (menu) {
326 return getItem(menu.one("li"));
331 var getActiveClass = function (node) {
333 return isMenuItem(node) ? CSS_MENUITEM_ACTIVE : CSS_MENU_LABEL_ACTIVE;
338 var handleMouseOverForNode = function (node, target) {
340 return node && !node[HANDLED_MOUSEOVER] &&
341 (node.compareTo(target) || node.contains(target));
346 var handleMouseOutForNode = function (node, relatedTarget) {
348 return node && !node[HANDLED_MOUSEOUT] &&
349 (!node.compareTo(relatedTarget) && !node.contains(relatedTarget));
354 * The NodeMenuNav class is a plugin for a Node instance. The class is used via
355 * the <a href="Node.html#method_plug"><code>plug</code></a> method of Node and
356 * should not be instantiated directly.
360 var NodeMenuNav = function () {
362 NodeMenuNav.superclass.constructor.apply(this, arguments);
366 NodeMenuNav.NAME = "nodeMenuNav";
367 NodeMenuNav.NS = "menuNav";
371 * @property NodeMenuNav.SHIM_TEMPLATE_TITLE
372 * @description String representing the value for the <code>title</code>
373 * attribute for the shim used to prevent <code><select></code> elements
374 * from poking through menus in IE 6.
375 * @default "Menu Stacking Shim"
378 NodeMenuNav.SHIM_TEMPLATE_TITLE = "Menu Stacking Shim";
382 * @property NodeMenuNav.SHIM_TEMPLATE
383 * @description String representing the HTML used to create the
384 * <code><iframe></code> shim used to prevent
385 * <code><select></code> elements from poking through menus in IE 6.
386 * @default "<iframe frameborder="0" tabindex="-1"
387 * class="yui-shim" title="Menu Stacking Shim"
388 * src="javascript:false;"></iframe>"
392 // <iframe> shim notes:
394 // 1) Need to set the "frameBorder" property to 0 to suppress the default
395 // <iframe> border in IE. (Setting the CSS "border" property alone doesn't
398 // 2) The "src" attribute of the <iframe> is set to "javascript:false;" so
399 // that it won't load a page inside it, preventing the secure/nonsecure
400 // warning in IE when using HTTPS.
402 // 3) Since the role of the <iframe> shim is completely presentational, its
403 // "tabindex" attribute is set to "-1" and its title attribute is set to
404 // "Menu Stacking Shim". Both strategies help users of screen readers to
405 // avoid mistakenly interacting with the <iframe> shim.
407 NodeMenuNav.SHIM_TEMPLATE = '<iframe frameborder="0" tabindex="-1" class="' +
408 getClassName("shim") +
409 '" title="' + NodeMenuNav.SHIM_TEMPLATE_TITLE +
410 '" src="javascript:false;"></iframe>';
413 NodeMenuNav.ATTRS = {
416 * Boolean indicating if use of the WAI-ARIA Roles and States should be
417 * enabled for the menu.
430 setter: function (value) {
432 var oMenu = this.get(HOST),
440 oMenu.set(ROLE, MENU);
442 oMenu.all("ul,li," + MENU_CONTENT_SELECTOR).set(ROLE, PRESENTATION);
444 oMenu.all((PERIOD + getClassName(MENUITEM, CONTENT))).set(ROLE, MENUITEM);
446 oMenu.all((PERIOD + CSS_MENU_LABEL)).each(function (node) {
449 oMenuToggle = node.one(MENU_TOGGLE_SELECTOR);
452 oMenuToggle.set(ROLE, PRESENTATION);
453 oMenuLabel = oMenuToggle.previous();
456 oMenuLabel.set(ROLE, MENUITEM);
457 oMenuLabel.set("aria-haspopup", true);
459 oSubmenu = node.next();
463 oSubmenu.set(ROLE, MENU);
465 oMenuLabel = oSubmenu.previous();
466 oMenuToggle = oMenuLabel.one(MENU_TOGGLE_SELECTOR);
469 oMenuLabel = oMenuToggle;
472 sID = Y.stamp(oMenuLabel);
474 if (!oMenuLabel.get(ID)) {
475 oMenuLabel.set(ID, sID);
478 oSubmenu.set("aria-labelledby", sID);
479 oSubmenu.set(ARIA_HIDDEN, true);
493 * Boolean indicating if submenus are automatically made visible when the
494 * user mouses over the menu's items.
496 * @attribute autoSubmenuDisplay
502 autoSubmenuDisplay: {
511 * Number indicating the time (in milliseconds) that should expire before a
512 * submenu is made visible when the user mouses over the menu's label.
514 * @attribute submenuShowDelay
529 * Number indicating the time (in milliseconds) that should expire before a
530 * submenu is hidden when the user mouses out of a menu label heading in the
531 * direction of a submenu.
533 * @attribute submenuHideDelay
548 * Number indicating the time (in milliseconds) that should expire before a
549 * submenu is hidden when the user mouses out of it.
551 * @attribute mouseOutHideDelay
567 Y.extend(NodeMenuNav, Y.Plugin.Base, {
569 // Protected properties
572 * @property _rootMenu
573 * @description Node instance representing the root menu in the menu.
582 * @property _activeItem
583 * @description Node instance representing the menu's active descendent:
584 * the menuitem or menu label the user is currently interacting with.
593 * @property _activeMenu
594 * @description Node instance representing the menu that is the parent of
595 * the menu's active descendent.
604 * @property _hasFocus
605 * @description Boolean indicating if the menu has focus.
613 // In gecko-based browsers a mouseover and mouseout event will fire even
614 // if a DOM element moves out from under the mouse without the user
615 // actually moving the mouse. This bug affects NodeMenuNav because the
616 // user can hit the Esc key to hide a menu, and if the mouse is over the
617 // menu when the user presses Esc, the _onMenuMouseOut handler will be
618 // called. To fix this bug the following flag (_blockMouseEvent) is used
619 // to block the code in the _onMenuMouseOut handler from executing.
622 * @property _blockMouseEvent
623 * @description Boolean indicating whether or not to handle the
629 _blockMouseEvent: false,
633 * @property _currentMouseX
634 * @description Number representing the current x coordinate of the mouse
644 * @property _movingToSubmenu
645 * @description Boolean indicating if the mouse is moving from a menu
646 * label to its corresponding submenu.
651 _movingToSubmenu: false,
655 * @property _showSubmenuTimer
656 * @description Timer used to show a submenu.
661 _showSubmenuTimer: null,
665 * @property _hideSubmenuTimer
666 * @description Timer used to hide a submenu.
671 _hideSubmenuTimer: null,
675 * @property _hideAllSubmenusTimer
676 * @description Timer used to hide a all submenus.
681 _hideAllSubmenusTimer: null,
685 * @property _firstItem
686 * @description Node instance representing the first item (menuitem or menu
687 * label) in the root menu of a menu.
698 initializer: function (config) {
701 oRootMenu = this.get(HOST),
708 menuNav._rootMenu = oRootMenu;
710 oRootMenu.all("ul:first-child").addClass(FIRST_OF_TYPE);
712 // Hide all visible submenus
714 oRootMenu.all(MENU_SELECTOR).addClass(CSS_MENU_HIDDEN);
717 // Wire up all event handlers
719 aHandlers.push(oRootMenu.on("mouseover", menuNav._onMouseOver, menuNav));
720 aHandlers.push(oRootMenu.on("mouseout", menuNav._onMouseOut, menuNav));
721 aHandlers.push(oRootMenu.on("mousemove", menuNav._onMouseMove, menuNav));
722 aHandlers.push(oRootMenu.on(MOUSEDOWN, menuNav._toggleSubmenuDisplay, menuNav));
723 aHandlers.push(Y.on("key", menuNav._toggleSubmenuDisplay, oRootMenu, "down:13", menuNav));
724 aHandlers.push(oRootMenu.on(CLICK, menuNav._toggleSubmenuDisplay, menuNav));
725 aHandlers.push(oRootMenu.on("keypress", menuNav._onKeyPress, menuNav));
726 aHandlers.push(oRootMenu.on(KEYDOWN, menuNav._onKeyDown, menuNav));
728 oDoc = oRootMenu.get("ownerDocument");
730 aHandlers.push(oDoc.on(MOUSEDOWN, menuNav._onDocMouseDown, menuNav));
731 aHandlers.push(oDoc.on("focus", menuNav._onDocFocus, menuNav));
733 this._eventHandlers = aHandlers;
735 menuNav._initFocusManager();
742 destructor: function () {
744 var aHandlers = this._eventHandlers;
748 Y.Array.each(aHandlers, function (handle) {
752 this._eventHandlers = null;
756 this.get(HOST).unplug("focusManager");
766 * @description Returns a boolean indicating if the specified menu is the
767 * root menu in the menu.
769 * @param {Node} menu Node instance representing a menu.
770 * @return {Boolean} Boolean indicating if the specified menu is the root
773 _isRoot: function (menu) {
775 return this._rootMenu.compareTo(menu);
781 * @method _getTopmostSubmenu
782 * @description Returns the topmost submenu of a submenu hierarchy.
784 * @param {Node} menu Node instance representing a menu.
785 * @return {Node} Node instance representing a menu.
787 _getTopmostSubmenu: function (menu) {
790 oMenu = getParentMenu(menu),
797 else if (menuNav._isRoot(oMenu)) {
801 returnVal = menuNav._getTopmostSubmenu(oMenu);
810 * @method _clearActiveItem
811 * @description Clears the menu's active descendent.
814 _clearActiveItem: function () {
817 oActiveItem = menuNav._activeItem;
820 oActiveItem.removeClass(getActiveClass(oActiveItem));
823 menuNav._activeItem = null;
829 * @method _setActiveItem
830 * @description Sets the specified menuitem or menu label as the menu's
833 * @param {Node} item Node instance representing a menuitem or menu label.
835 _setActiveItem: function (item) {
841 menuNav._clearActiveItem();
843 item.addClass(getActiveClass(item));
845 menuNav._activeItem = item;
854 * @description Focuses the specified menuitem or menu label.
856 * @param {Node} item Node instance representing a menuitem or menu label.
858 _focusItem: function (item) {
864 if (item && menuNav._hasFocus) {
866 oMenu = getParentMenu(item);
867 oItem = getItemAnchor(item);
869 if (oMenu && !oMenu.compareTo(menuNav._activeMenu)) {
870 menuNav._activeMenu = oMenu;
871 menuNav._initFocusManager();
874 menuNav._focusManager.focus(oItem);
883 * @description Shows the specified menu.
885 * @param {Node} menu Node instance representing a menu.
887 _showMenu: function (menu) {
889 var oParentMenu = getParentMenu(menu),
890 oLI = menu.get(PARENT_NODE),
894 if (this.get(USE_ARIA)) {
895 menu.set(ARIA_HIDDEN, false);
899 if (isHorizontalMenu(oParentMenu)) {
900 aXY[1] = aXY[1] + oLI.get(OFFSET_HEIGHT);
903 aXY[0] = aXY[0] + oLI.get(OFFSET_WIDTH);
910 if (UA.ie === 6 && !menu.hasIFrameShim) {
912 menu.appendChild(Y.Node.create(NodeMenuNav.SHIM_TEMPLATE));
913 menu.hasIFrameShim = true;
917 // Clear previous values for height and width
919 menu.setStyles({ height: EMPTY_STRING, width: EMPTY_STRING });
921 // Set the width and height of the menu's bounding box - this is
922 // necessary for IE 6 so that the CSS for the <iframe> shim can
923 // simply set the <iframe>'s width and height to 100% to ensure
924 // that dimensions of an <iframe> shim are always sync'd to the
925 // that of its parent menu. Specifying a width and height also
926 // helps when positioning decorator elements (for creating effects
927 // like rounded corners) inside a menu's bounding box in IE 7.
930 height: (menu.get(OFFSET_HEIGHT) + PX),
931 width: (menu.get(OFFSET_WIDTH) + PX) });
935 menu.previous().addClass(CSS_MENU_LABEL_MENUVISIBLE);
936 menu.removeClass(CSS_MENU_HIDDEN);
943 * @description Hides the specified menu.
945 * @param {Node} menu Node instance representing a menu.
946 * @param {Boolean} activateAndFocusLabel Boolean indicating if the label
948 * menu should be focused and set as active.
950 _hideMenu: function (menu, activateAndFocusLabel) {
953 oLabel = menu.previous(),
956 oLabel.removeClass(CSS_MENU_LABEL_MENUVISIBLE);
959 if (activateAndFocusLabel) {
960 menuNav._focusItem(oLabel);
961 menuNav._setActiveItem(oLabel);
964 oActiveItem = menu.one((PERIOD + CSS_MENUITEM_ACTIVE));
967 oActiveItem.removeClass(CSS_MENUITEM_ACTIVE);
970 // Clear the values for top and left that were set by the call to
971 // "setXY" when the menu was shown so that the hidden position
972 // specified in the core CSS file will take affect.
974 menu.setStyles({ left: EMPTY_STRING, top: EMPTY_STRING });
976 menu.addClass(CSS_MENU_HIDDEN);
978 if (menuNav.get(USE_ARIA)) {
979 menu.set(ARIA_HIDDEN, true);
986 * @method _hideAllSubmenus
987 * @description Hides all submenus of the specified menu.
989 * @param {Node} menu Node instance representing a menu.
991 _hideAllSubmenus: function (menu) {
995 menu.all(MENU_SELECTOR).each(Y.bind(function (submenuNode) {
997 menuNav._hideMenu(submenuNode);
1005 * @method _cancelShowSubmenuTimer
1006 * @description Cancels the timer used to show a submenu.
1009 _cancelShowSubmenuTimer: function () {
1012 oShowSubmenuTimer = menuNav._showSubmenuTimer;
1014 if (oShowSubmenuTimer) {
1015 oShowSubmenuTimer.cancel();
1016 menuNav._showSubmenuTimer = null;
1023 * @method _cancelHideSubmenuTimer
1024 * @description Cancels the timer used to hide a submenu.
1027 _cancelHideSubmenuTimer: function () {
1030 oHideSubmenuTimer = menuNav._hideSubmenuTimer;
1033 if (oHideSubmenuTimer) {
1034 oHideSubmenuTimer.cancel();
1035 menuNav._hideSubmenuTimer = null;
1042 * @method _initFocusManager
1043 * @description Initializes and updates the Focus Manager so that is is
1044 * always managing descendants of the active menu.
1047 _initFocusManager: function () {
1050 oRootMenu = menuNav._rootMenu,
1051 oMenu = menuNav._activeMenu || oRootMenu,
1053 menuNav._isRoot(oMenu) ? EMPTY_STRING : ("#" + oMenu.get("id")),
1054 oFocusManager = menuNav._focusManager,
1056 sDescendantSelector,
1059 if (isHorizontalMenu(oMenu)) {
1061 sDescendantSelector = sSelectorBase + STANDARD_QUERY + "," +
1062 sSelectorBase + EXTENDED_QUERY;
1064 sKeysVal = { next: "down:39", previous: "down:37" };
1069 sDescendantSelector = sSelectorBase + STANDARD_QUERY;
1070 sKeysVal = { next: "down:40", previous: "down:38" };
1075 if (!oFocusManager) {
1077 oRootMenu.plug(Y.Plugin.NodeFocusManager, {
1078 descendants: sDescendantSelector,
1083 oFocusManager = oRootMenu.focusManager;
1085 sQuery = "#" + oRootMenu.get("id") + MENU_SELECTOR + " a," +
1086 MENU_TOGGLE_SELECTOR;
1088 oRootMenu.all(sQuery).set("tabIndex", -1);
1090 oFocusManager.on(ACTIVE_DESCENDANT_CHANGE,
1091 this._onActiveDescendantChange, oFocusManager, this);
1093 oFocusManager.after(ACTIVE_DESCENDANT_CHANGE,
1094 this._afterActiveDescendantChange, oFocusManager, this);
1096 menuNav._focusManager = oFocusManager;
1101 oFocusManager.set(ACTIVE_DESCENDANT, -1);
1102 oFocusManager.set(DESCENDANTS, sDescendantSelector);
1103 oFocusManager.set("keys", sKeysVal);
1110 // Event handlers for discrete pieces of pieces of the menu
1114 * @method _onActiveDescendantChange
1115 * @description "activeDescendantChange" event handler for menu's
1118 * @param {Object} event Object representing the Attribute change event.
1119 * @param {NodeMenuNav} menuNav Object representing the NodeMenuNav instance.
1121 _onActiveDescendantChange: function (event, menuNav) {
1123 if (event.src === UI && menuNav._activeMenu &&
1124 !menuNav._movingToSubmenu) {
1126 menuNav._hideAllSubmenus(menuNav._activeMenu);
1134 * @method _afterActiveDescendantChange
1135 * @description "activeDescendantChange" event handler for menu's
1138 * @param {Object} event Object representing the Attribute change event.
1139 * @param {NodeMenuNav} menuNav Object representing the NodeMenuNav instance.
1141 _afterActiveDescendantChange: function (event, menuNav) {
1145 if (event.src === UI) {
1146 oItem = getItem(this.get(DESCENDANTS).item(event.newVal), true);
1147 menuNav._setActiveItem(oItem);
1154 * @method _onDocFocus
1155 * @description "focus" event handler for the owner document of the MenuNav.
1157 * @param {Object} event Object representing the DOM event.
1159 _onDocFocus: function (event) {
1162 oActiveItem = menuNav._activeItem,
1163 oTarget = event.target,
1167 if (menuNav._rootMenu.contains(oTarget)) { // The menu has focus
1169 if (menuNav._hasFocus) {
1171 oMenu = getParentMenu(oTarget);
1173 // If the element that was focused is a descendant of the
1174 // root menu, but is in a submenu not currently being
1175 // managed by the Focus Manager, update the Focus Manager so
1176 // that it is now managing the submenu that is the parent of
1177 // the element that was focused.
1179 if (!menuNav._activeMenu.compareTo(oMenu)) {
1181 menuNav._activeMenu = oMenu;
1182 menuNav._initFocusManager();
1183 menuNav._focusManager.set(ACTIVE_DESCENDANT, oTarget);
1184 menuNav._setActiveItem(getItem(oTarget, true));
1189 else { // Initial focus
1191 // First time the menu has been focused, need to setup focused
1192 // state and established active active descendant
1194 menuNav._hasFocus = true;
1196 oActiveItem = getItem(oTarget, true);
1199 menuNav._setActiveItem(oActiveItem);
1205 else { // The menu has lost focus
1207 menuNav._clearActiveItem();
1209 menuNav._cancelShowSubmenuTimer();
1210 menuNav._hideAllSubmenus(menuNav._rootMenu);
1212 menuNav._activeMenu = menuNav._rootMenu;
1213 menuNav._initFocusManager();
1215 menuNav._focusManager.set(ACTIVE_DESCENDANT, 0);
1217 menuNav._hasFocus = false;
1225 * @method _onMenuMouseOver
1226 * @description "mouseover" event handler for a menu.
1228 * @param {Node} menu Node instance representing a menu.
1229 * @param {Object} event Object representing the DOM event.
1231 _onMenuMouseOver: function (menu, event) {
1234 oHideAllSubmenusTimer = menuNav._hideAllSubmenusTimer;
1236 if (oHideAllSubmenusTimer) {
1237 oHideAllSubmenusTimer.cancel();
1238 menuNav._hideAllSubmenusTimer = null;
1241 menuNav._cancelHideSubmenuTimer();
1243 // Need to update the FocusManager in advance of focus a new
1244 // Menu in order to avoid the FocusManager thinking that
1245 // it has lost focus
1247 if (menu && !menu.compareTo(menuNav._activeMenu)) {
1248 menuNav._activeMenu = menu;
1250 if (menuNav._hasFocus) {
1251 menuNav._initFocusManager();
1256 if (menuNav._movingToSubmenu && isHorizontalMenu(menu)) {
1257 menuNav._movingToSubmenu = false;
1264 * @method _hideAndFocusLabel
1265 * @description Hides all of the submenus of the root menu and focuses the
1266 * label of the topmost submenu
1269 _hideAndFocusLabel: function () {
1272 oActiveMenu = menuNav._activeMenu,
1275 menuNav._hideAllSubmenus(menuNav._rootMenu);
1279 // Focus the label element for the topmost submenu
1280 oSubmenu = menuNav._getTopmostSubmenu(oActiveMenu);
1281 menuNav._focusItem(oSubmenu.previous());
1289 * @method _onMenuMouseOut
1290 * @description "mouseout" event handler for a menu.
1292 * @param {Node} menu Node instance representing a menu.
1293 * @param {Object} event Object representing the DOM event.
1295 _onMenuMouseOut: function (menu, event) {
1298 oActiveMenu = menuNav._activeMenu,
1299 oRelatedTarget = event.relatedTarget,
1300 oActiveItem = menuNav._activeItem,
1305 if (oActiveMenu && !oActiveMenu.contains(oRelatedTarget)) {
1307 oParentMenu = getParentMenu(oActiveMenu);
1310 if (oParentMenu && !oParentMenu.contains(oRelatedTarget)) {
1312 if (menuNav.get(MOUSEOUT_HIDE_DELAY) > 0) {
1314 menuNav._cancelShowSubmenuTimer();
1316 menuNav._hideAllSubmenusTimer =
1318 later(menuNav.get(MOUSEOUT_HIDE_DELAY),
1319 menuNav, menuNav._hideAndFocusLabel);
1328 oMenu = getParentMenu(oActiveItem);
1330 if (!menuNav._isRoot(oMenu)) {
1331 menuNav._focusItem(oMenu.previous());
1344 * @method _onMenuLabelMouseOver
1345 * @description "mouseover" event handler for a menu label.
1347 * @param {Node} menuLabel Node instance representing a menu label.
1348 * @param {Object} event Object representing the DOM event.
1350 _onMenuLabelMouseOver: function (menuLabel, event) {
1353 oActiveMenu = menuNav._activeMenu,
1354 bIsRoot = menuNav._isRoot(oActiveMenu),
1355 bUseAutoSubmenuDisplay =
1356 (menuNav.get(AUTO_SUBMENU_DISPLAY) && bIsRoot || !bIsRoot),
1357 submenuShowDelay = menuNav.get("submenuShowDelay"),
1361 var showSubmenu = function (delay) {
1363 menuNav._cancelHideSubmenuTimer();
1364 menuNav._cancelShowSubmenuTimer();
1366 if (!hasVisibleSubmenu(menuLabel)) {
1368 oSubmenu = menuLabel.next();
1371 menuNav._hideAllSubmenus(oActiveMenu);
1372 menuNav._showSubmenuTimer = later(delay, menuNav, menuNav._showMenu, oSubmenu);
1380 menuNav._focusItem(menuLabel);
1381 menuNav._setActiveItem(menuLabel);
1384 if (bUseAutoSubmenuDisplay) {
1386 if (menuNav._movingToSubmenu) {
1388 // If the user is moving diagonally from a submenu to
1389 // another submenu and they then stop and pause on a
1390 // menu label for an amount of time equal to the amount of
1391 // time defined for the display of a submenu then show the
1392 // submenu immediately.
1393 // http://yuilibrary.com/projects/yui3/ticket/2528316
1395 Y.message("Pause path");
1397 menuNav._hoverTimer = later(submenuShowDelay, menuNav, function () {
1403 showSubmenu(submenuShowDelay);
1412 * @method _onMenuLabelMouseOut
1413 * @description "mouseout" event handler for a menu label.
1415 * @param {Node} menuLabel Node instance representing a menu label.
1416 * @param {Object} event Object representing the DOM event.
1418 _onMenuLabelMouseOut: function (menuLabel, event) {
1421 bIsRoot = menuNav._isRoot(menuNav._activeMenu),
1422 bUseAutoSubmenuDisplay =
1423 (menuNav.get(AUTO_SUBMENU_DISPLAY) && bIsRoot || !bIsRoot),
1425 oRelatedTarget = event.relatedTarget,
1426 oSubmenu = menuLabel.next(),
1427 hoverTimer = menuNav._hoverTimer;
1430 hoverTimer.cancel();
1433 menuNav._clearActiveItem();
1435 if (bUseAutoSubmenuDisplay) {
1437 if (menuNav._movingToSubmenu &&
1438 !menuNav._showSubmenuTimer && oSubmenu) {
1440 // If the mouse is moving diagonally toward the submenu and
1441 // another submenu isn't in the process of being displayed
1442 // (via a timer), then hide the submenu via a timer to give
1443 // the user some time to reach the submenu.
1445 menuNav._hideSubmenuTimer =
1446 later(menuNav.get("submenuHideDelay"), menuNav,
1447 menuNav._hideMenu, oSubmenu);
1450 else if (!menuNav._movingToSubmenu && oSubmenu && (!oRelatedTarget ||
1452 !oSubmenu.contains(oRelatedTarget) &&
1453 !oRelatedTarget.compareTo(oSubmenu)))) {
1455 // If the mouse is not moving toward the submenu, cancel any
1456 // submenus that might be in the process of being displayed
1457 // (via a timer) and hide this submenu immediately.
1459 menuNav._cancelShowSubmenuTimer();
1461 menuNav._hideMenu(oSubmenu);
1471 * @method _onMenuItemMouseOver
1472 * @description "mouseover" event handler for a menuitem.
1474 * @param {Node} menuItem Node instance representing a menuitem.
1475 * @param {Object} event Object representing the DOM event.
1477 _onMenuItemMouseOver: function (menuItem, event) {
1480 oActiveMenu = menuNav._activeMenu,
1481 bIsRoot = menuNav._isRoot(oActiveMenu),
1482 bUseAutoSubmenuDisplay =
1483 (menuNav.get(AUTO_SUBMENU_DISPLAY) && bIsRoot || !bIsRoot);
1486 menuNav._focusItem(menuItem);
1487 menuNav._setActiveItem(menuItem);
1490 if (bUseAutoSubmenuDisplay && !menuNav._movingToSubmenu) {
1492 menuNav._hideAllSubmenus(oActiveMenu);
1500 * @method _onMenuItemMouseOut
1501 * @description "mouseout" event handler for a menuitem.
1503 * @param {Node} menuItem Node instance representing a menuitem.
1504 * @param {Object} event Object representing the DOM event.
1506 _onMenuItemMouseOut: function (menuItem, event) {
1508 this._clearActiveItem();
1514 * @method _onVerticalMenuKeyDown
1515 * @description "keydown" event handler for vertical menus.
1517 * @param {Object} event Object representing the DOM event.
1519 _onVerticalMenuKeyDown: function (event) {
1522 oActiveMenu = menuNav._activeMenu,
1523 oRootMenu = menuNav._rootMenu,
1524 oTarget = event.target,
1525 bPreventDefault = false,
1526 nKeyCode = event.keyCode,
1535 case 37: // left arrow
1537 oParentMenu = getParentMenu(oActiveMenu);
1539 if (oParentMenu && isHorizontalMenu(oParentMenu)) {
1541 menuNav._hideMenu(oActiveMenu);
1542 oLI = getPreviousSibling(oActiveMenu.get(PARENT_NODE));
1543 oItem = getItem(oLI);
1547 if (isMenuLabel(oItem)) { // Menu label
1549 oSubmenu = oItem.next();
1554 menuNav._showMenu(oSubmenu);
1555 menuNav._focusItem(getFirstItem(oSubmenu));
1556 menuNav._setActiveItem(getFirstItem(oSubmenu));
1561 menuNav._focusItem(oItem);
1562 menuNav._setActiveItem(oItem);
1569 menuNav._focusItem(oItem);
1570 menuNav._setActiveItem(oItem);
1577 else if (!menuNav._isRoot(oActiveMenu)) {
1578 menuNav._hideMenu(oActiveMenu, true);
1582 bPreventDefault = true;
1586 case 39: // right arrow
1588 if (isMenuLabel(oTarget)) {
1590 oSubmenu = oTarget.next();
1594 menuNav._showMenu(oSubmenu);
1595 menuNav._focusItem(getFirstItem(oSubmenu));
1596 menuNav._setActiveItem(getFirstItem(oSubmenu));
1601 else if (isHorizontalMenu(oRootMenu)) {
1603 oSubmenu = menuNav._getTopmostSubmenu(oActiveMenu);
1604 oLI = getNextSibling(oSubmenu.get(PARENT_NODE));
1605 oItem = getItem(oLI);
1607 menuNav._hideAllSubmenus(oRootMenu);
1611 if (isMenuLabel(oItem)) { // Menu label
1613 oSubmenu = oItem.next();
1617 menuNav._showMenu(oSubmenu);
1618 menuNav._focusItem(getFirstItem(oSubmenu));
1619 menuNav._setActiveItem(getFirstItem(oSubmenu));
1624 menuNav._focusItem(oItem);
1625 menuNav._setActiveItem(oItem);
1632 menuNav._focusItem(oItem);
1633 menuNav._setActiveItem(oItem);
1641 bPreventDefault = true;
1648 if (bPreventDefault) {
1650 // Prevent the browser from scrolling the window
1652 event.preventDefault();
1660 * @method _onHorizontalMenuKeyDown
1661 * @description "keydown" event handler for horizontal menus.
1663 * @param {Object} event Object representing the DOM event.
1665 _onHorizontalMenuKeyDown: function (event) {
1668 oActiveMenu = menuNav._activeMenu,
1669 oTarget = event.target,
1670 oFocusedItem = getItem(oTarget, true),
1671 bPreventDefault = false,
1672 nKeyCode = event.keyCode,
1676 if (nKeyCode === 40) {
1678 menuNav._hideAllSubmenus(oActiveMenu);
1680 if (isMenuLabel(oFocusedItem)) {
1682 oSubmenu = oFocusedItem.next();
1686 menuNav._showMenu(oSubmenu);
1687 menuNav._focusItem(getFirstItem(oSubmenu));
1688 menuNav._setActiveItem(getFirstItem(oSubmenu));
1692 bPreventDefault = true;
1699 if (bPreventDefault) {
1701 // Prevent the browser from scrolling the window
1703 event.preventDefault();
1710 // Generic DOM Event handlers
1714 * @method _onMouseMove
1715 * @description "mousemove" event handler for the menu.
1717 * @param {Object} event Object representing the DOM event.
1719 _onMouseMove: function (event) {
1723 // Using a timer to set the value of the "_currentMouseX" property
1724 // helps improve the reliability of the calculation used to set the
1725 // value of the "_movingToSubmenu" property - especially in Opera.
1727 later(10, menuNav, function () {
1729 menuNav._currentMouseX = event.pageX;
1737 * @method _onMouseOver
1738 * @description "mouseover" event handler for the menu.
1740 * @param {Object} event Object representing the DOM event.
1742 _onMouseOver: function (event) {
1752 if (menuNav._blockMouseEvent) {
1753 menuNav._blockMouseEvent = false;
1757 oTarget = event.target;
1758 oMenu = getMenu(oTarget, true);
1759 oMenuLabel = getMenuLabel(oTarget, true);
1760 oMenuItem = getMenuItem(oTarget, true);
1763 if (handleMouseOverForNode(oMenu, oTarget)) {
1765 menuNav._onMenuMouseOver(oMenu, event);
1767 oMenu[HANDLED_MOUSEOVER] = true;
1768 oMenu[HANDLED_MOUSEOUT] = false;
1770 oParentMenu = getParentMenu(oMenu);
1774 oParentMenu[HANDLED_MOUSEOUT] = true;
1775 oParentMenu[HANDLED_MOUSEOVER] = false;
1781 if (handleMouseOverForNode(oMenuLabel, oTarget)) {
1783 menuNav._onMenuLabelMouseOver(oMenuLabel, event);
1785 oMenuLabel[HANDLED_MOUSEOVER] = true;
1786 oMenuLabel[HANDLED_MOUSEOUT] = false;
1790 if (handleMouseOverForNode(oMenuItem, oTarget)) {
1792 menuNav._onMenuItemMouseOver(oMenuItem, event);
1794 oMenuItem[HANDLED_MOUSEOVER] = true;
1795 oMenuItem[HANDLED_MOUSEOUT] = false;
1805 * @method _onMouseOut
1806 * @description "mouseout" event handler for the menu.
1808 * @param {Object} event Object representing the DOM event.
1810 _onMouseOut: function (event) {
1813 oActiveMenu = menuNav._activeMenu,
1814 bMovingToSubmenu = false,
1823 menuNav._movingToSubmenu =
1824 (oActiveMenu && !isHorizontalMenu(oActiveMenu) &&
1825 ((event.pageX - 5) > menuNav._currentMouseX));
1827 oTarget = event.target;
1828 oRelatedTarget = event.relatedTarget;
1829 oMenu = getMenu(oTarget, true);
1830 oMenuLabel = getMenuLabel(oTarget, true);
1831 oMenuItem = getMenuItem(oTarget, true);
1834 if (handleMouseOutForNode(oMenuLabel, oRelatedTarget)) {
1836 menuNav._onMenuLabelMouseOut(oMenuLabel, event);
1838 oMenuLabel[HANDLED_MOUSEOUT] = true;
1839 oMenuLabel[HANDLED_MOUSEOVER] = false;
1843 if (handleMouseOutForNode(oMenuItem, oRelatedTarget)) {
1845 menuNav._onMenuItemMouseOut(oMenuItem, event);
1847 oMenuItem[HANDLED_MOUSEOUT] = true;
1848 oMenuItem[HANDLED_MOUSEOVER] = false;
1855 oSubmenu = oMenuLabel.next();
1857 if (oSubmenu && oRelatedTarget &&
1858 (oRelatedTarget.compareTo(oSubmenu) ||
1859 oSubmenu.contains(oRelatedTarget))) {
1861 bMovingToSubmenu = true;
1868 if (handleMouseOutForNode(oMenu, oRelatedTarget) || bMovingToSubmenu) {
1870 menuNav._onMenuMouseOut(oMenu, event);
1872 oMenu[HANDLED_MOUSEOUT] = true;
1873 oMenu[HANDLED_MOUSEOVER] = false;
1881 * @method _toggleSubmenuDisplay
1882 * @description "mousedown," "keydown," and "click" event handler for the
1883 * menu used to toggle the display of a submenu.
1885 * @param {Object} event Object representing the DOM event.
1887 _toggleSubmenuDisplay: function (event) {
1890 oTarget = event.target,
1891 oMenuLabel = getMenuLabel(oTarget, true),
1903 oAnchor = isAnchor(oTarget) ? oTarget : oTarget.ancestor(isAnchor);
1908 // Need to pass "2" as a second argument to "getAttribute" for
1909 // IE otherwise IE will return a fully qualified URL for the
1910 // value of the "href" attribute.
1911 // http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx
1913 sHref = oAnchor.getAttribute("href", 2);
1914 nHashPos = sHref.indexOf("#");
1915 nLen = sHref.length;
1917 if (nHashPos === 0 && nLen > 1) {
1919 sId = sHref.substr(1, nLen);
1920 oSubmenu = oMenuLabel.next();
1922 if (oSubmenu && (oSubmenu.get(ID) === sId)) {
1924 if (sType === MOUSEDOWN || sType === KEYDOWN) {
1926 if ((UA.opera || UA.gecko || UA.ie) && sType === KEYDOWN && !menuNav._preventClickHandle) {
1928 // Prevent the browser from following the URL of
1929 // the anchor element
1931 menuNav._preventClickHandle = menuNav._rootMenu.on("click", function (event) {
1933 event.preventDefault();
1935 menuNav._preventClickHandle.detach();
1936 menuNav._preventClickHandle = null;
1942 if (sType == MOUSEDOWN) {
1944 // Prevent the target from getting focused by
1945 // default, since the element to be focused will
1946 // be determined by weather or not the submenu
1948 event.preventDefault();
1950 // FocusManager will attempt to focus any
1951 // descendant that is the target of the mousedown
1952 // event. Since we want to explicitly control
1953 // where focus is going, we need to call
1954 // "stopImmediatePropagation" to stop the
1955 // FocusManager from doing its thing.
1956 event.stopImmediatePropagation();
1958 // The "_focusItem" method relies on the
1959 // "_hasFocus" property being set to true. The
1960 // "_hasFocus" property is normally set via a
1961 // "focus" event listener, but since we've
1962 // blocked focus from happening, we need to set
1963 // this property manually.
1964 menuNav._hasFocus = true;
1969 if (menuNav._isRoot(getParentMenu(oTarget))) { // Event target is a submenu label in the root menu
1971 // Menu label toggle functionality
1973 if (hasVisibleSubmenu(oMenuLabel)) {
1975 menuNav._hideMenu(oSubmenu);
1976 menuNav._focusItem(oMenuLabel);
1977 menuNav._setActiveItem(oMenuLabel);
1982 menuNav._hideAllSubmenus(menuNav._rootMenu);
1983 menuNav._showMenu(oSubmenu);
1985 menuNav._focusItem(getFirstItem(oSubmenu));
1986 menuNav._setActiveItem(getFirstItem(oSubmenu));
1991 else { // Event target is a submenu label within a submenu
1993 if (menuNav._activeItem == oMenuLabel) {
1995 menuNav._showMenu(oSubmenu);
1996 menuNav._focusItem(getFirstItem(oSubmenu));
1997 menuNav._setActiveItem(getFirstItem(oSubmenu));
2002 if (!oMenuLabel._clickHandle) {
2004 oMenuLabel._clickHandle = oMenuLabel.on("click", function () {
2006 menuNav._hideAllSubmenus(menuNav._rootMenu);
2008 menuNav._hasFocus = false;
2009 menuNav._clearActiveItem();
2012 oMenuLabel._clickHandle.detach();
2014 oMenuLabel._clickHandle = null;
2027 if (sType === CLICK) {
2029 // Prevent the browser from following the URL of
2030 // the anchor element
2032 event.preventDefault();
2049 * @method _onKeyPress
2050 * @description "keypress" event handler for the menu.
2052 * @param {Object} event Object representing the DOM event.
2054 _onKeyPress: function (event) {
2056 switch (event.keyCode) {
2058 case 37: // left arrow
2059 case 38: // up arrow
2060 case 39: // right arrow
2061 case 40: // down arrow
2063 // Prevent the browser from scrolling the window
2065 event.preventDefault();
2075 * @method _onKeyDown
2076 * @description "keydown" event handler for the menu.
2078 * @param {Object} event Object representing the DOM event.
2080 _onKeyDown: function (event) {
2083 oActiveItem = menuNav._activeItem,
2084 oTarget = event.target,
2085 oActiveMenu = getParentMenu(oTarget),
2090 menuNav._activeMenu = oActiveMenu;
2092 if (isHorizontalMenu(oActiveMenu)) {
2093 menuNav._onHorizontalMenuKeyDown(event);
2096 menuNav._onVerticalMenuKeyDown(event);
2100 if (event.keyCode === 27) {
2102 if (!menuNav._isRoot(oActiveMenu)) {
2105 later(0, menuNav, function () {
2106 menuNav._hideMenu(oActiveMenu, true);
2110 menuNav._hideMenu(oActiveMenu, true);
2113 event.stopPropagation();
2114 menuNav._blockMouseEvent = UA.gecko ? true : false;
2117 else if (oActiveItem) {
2119 if (isMenuLabel(oActiveItem) &&
2120 hasVisibleSubmenu(oActiveItem)) {
2122 oSubmenu = oActiveItem.next();
2125 menuNav._hideMenu(oSubmenu);
2131 menuNav._focusManager.blur();
2133 // This is necessary for Webkit since blurring the
2134 // active menuitem won't result in the document
2135 // gaining focus, meaning the that _onDocFocus
2136 // listener won't clear the active menuitem.
2138 menuNav._clearActiveItem();
2140 menuNav._hasFocus = false;
2153 * @method _onDocMouseDown
2154 * @description "mousedown" event handler for the owner document of
2157 * @param {Object} event Object representing the DOM event.
2159 _onDocMouseDown: function (event) {
2162 oRoot = menuNav._rootMenu,
2163 oTarget = event.target;
2166 if (!(oRoot.compareTo(oTarget) || oRoot.contains(oTarget))) {
2168 menuNav._hideAllSubmenus(oRoot);
2170 // Document doesn't receive focus in Webkit when the user mouses
2171 // down on it, so the "_hasFocus" property won't get set to the
2172 // correct value. The following line corrects the problem.
2175 menuNav._hasFocus = false;
2176 menuNav._clearActiveItem();
2186 Y.namespace('Plugin');
2188 Y.Plugin.NodeMenuNav = NodeMenuNav;
2191 }, '3.3.0' ,{requires:['node', 'classnamemanager', 'node-focusmanager']});