2 Copyright (c) 2009, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.net/yui/license.txt
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",
133 STANDARD_QUERY = ">.yui-menu-content>ul>li>a",
134 EXTENDED_QUERY = ">.yui-menu-content>ul>li>.yui-menu-label>a:first-child",
139 AUTO_SUBMENU_DISPLAY = "autoSubmenuDisplay",
140 MOUSEOUT_HIDE_DELAY = "mouseOutHideDelay",
145 CSS_MENU = getClassName(MENU),
146 CSS_MENU_HIDDEN = getClassName(MENU, HIDDEN),
147 CSS_MENU_HORIZONTAL = getClassName(MENU, "horizontal"),
148 CSS_MENU_LABEL = getClassName(MENU, LABEL),
149 CSS_MENU_LABEL_ACTIVE = getClassName(MENU, LABEL, ACTIVE),
150 CSS_MENU_LABEL_MENUVISIBLE = getClassName(MENU, LABEL, (MENU + "visible")),
151 CSS_MENUITEM = getClassName(MENUITEM),
152 CSS_MENUITEM_ACTIVE = getClassName(MENUITEM, ACTIVE),
157 MENU_SELECTOR = PERIOD + CSS_MENU,
158 MENU_TOGGLE_SELECTOR = (PERIOD + getClassName(MENU, "toggle"));
164 var getPreviousSibling = function (node) {
166 var oPrevious = node.previous(),
170 oChildren = node.get(PARENT_NODE).get(CHILDREN);
171 oPrevious = oChildren.item(oChildren.size() - 1);
179 var getNextSibling = function (node) {
181 var oNext = node.next();
184 oNext = node.get(PARENT_NODE).get(CHILDREN).item(0);
192 var isAnchor = function (node) {
194 var bReturnVal = false;
197 bReturnVal = node.get("nodeName").toLowerCase() === LOWERCASE_A;
205 var isMenuItem = function (node) {
207 return node.hasClass(CSS_MENUITEM);
212 var isMenuLabel = function (node) {
214 return node.hasClass(CSS_MENU_LABEL);
219 var isHorizontalMenu = function (menu) {
221 return menu.hasClass(CSS_MENU_HORIZONTAL);
226 var hasVisibleSubmenu = function (menuLabel) {
228 return menuLabel.hasClass(CSS_MENU_LABEL_MENUVISIBLE);
233 var getItemAnchor = function (node) {
235 return isAnchor(node) ? node : node.one(LOWERCASE_A);
240 var getNodeWithClass = function (node, className, searchAncestors) {
246 if (node.hasClass(className)) {
250 if (!oItem && searchAncestors) {
251 oItem = node.ancestor((PERIOD + className));
261 var getParentMenu = function (node) {
263 return node.ancestor(MENU_SELECTOR);
268 var getMenu = function (node, searchAncestors) {
270 return getNodeWithClass(node, CSS_MENU, searchAncestors);
275 var getMenuItem = function (node, searchAncestors) {
280 oItem = getNodeWithClass(node, CSS_MENUITEM, searchAncestors);
288 var getMenuLabel = function (node, searchAncestors) {
294 if (searchAncestors) {
295 oItem = getNodeWithClass(node, CSS_MENU_LABEL, searchAncestors);
298 oItem = getNodeWithClass(node, CSS_MENU_LABEL) ||
299 node.one((PERIOD + CSS_MENU_LABEL));
309 var getItem = function (node, searchAncestors) {
314 oItem = getMenuItem(node, searchAncestors) ||
315 getMenuLabel(node, searchAncestors);
323 var getFirstItem = function (menu) {
325 return getItem(menu.one("li"));
330 var getActiveClass = function (node) {
332 return isMenuItem(node) ? CSS_MENUITEM_ACTIVE : CSS_MENU_LABEL_ACTIVE;
337 var handleMouseOverForNode = function (node, target) {
339 return node && !node[HANDLED_MOUSEOVER] &&
340 (node.compareTo(target) || node.contains(target));
345 var handleMouseOutForNode = function (node, relatedTarget) {
347 return node && !node[HANDLED_MOUSEOUT] &&
348 (!node.compareTo(relatedTarget) && !node.contains(relatedTarget));
353 * The NodeMenuNav class is a plugin for a Node instance. The class is used via
354 * the <a href="Node.html#method_plug"><code>plug</code></a> method of Node and
355 * should not be instantiated directly.
359 var NodeMenuNav = function () {
361 NodeMenuNav.superclass.constructor.apply(this, arguments);
365 NodeMenuNav.NAME = "nodeMenuNav";
366 NodeMenuNav.NS = "menuNav";
370 * @property NodeMenuNav.SHIM_TEMPLATE_TITLE
371 * @description String representing the value for the <code>title</code>
372 * attribute for the shim used to prevent <code><select></code> elements
373 * from poking through menus in IE 6.
374 * @default "Menu Stacking Shim"
377 NodeMenuNav.SHIM_TEMPLATE_TITLE = "Menu Stacking Shim";
381 * @property NodeMenuNav.SHIM_TEMPLATE
382 * @description String representing the HTML used to create the
383 * <code><iframe></code> shim used to prevent
384 * <code><select></code> elements from poking through menus in IE 6.
385 * @default "<iframe frameborder="0" tabindex="-1"
386 * class="yui-shim" title="Menu Stacking Shim"
387 * src="javascript:false;"></iframe>"
391 // <iframe> shim notes:
393 // 1) Need to set the "frameBorder" property to 0 to suppress the default
394 // <iframe> border in IE. (Setting the CSS "border" property alone doesn't
397 // 2) The "src" attribute of the <iframe> is set to "javascript:false;" so
398 // that it won't load a page inside it, preventing the secure/nonsecure
399 // warning in IE when using HTTPS.
401 // 3) Since the role of the <iframe> shim is completely presentational, its
402 // "tabindex" attribute is set to "-1" and its title attribute is set to
403 // "Menu Stacking Shim". Both strategies help users of screen readers to
404 // avoid mistakenly interacting with the <iframe> shim.
406 NodeMenuNav.SHIM_TEMPLATE = '<iframe frameborder="0" tabindex="-1" class="' +
407 getClassName("shim") +
408 '" title="' + NodeMenuNav.SHIM_TEMPLATE_TITLE +
409 '" src="javascript:false;"></iframe>';
412 NodeMenuNav.ATTRS = {
415 * Boolean indicating if use of the WAI-ARIA Roles and States should be
416 * enabled for the menu.
429 setter: function (value) {
431 var oMenu = this.get(HOST),
439 oMenu.set(ROLE, MENU);
441 oMenu.all("ul,li,." + getClassName(MENU, CONTENT)).set(ROLE, PRESENTATION);
443 oMenu.all((PERIOD + getClassName(MENUITEM, CONTENT))).set(ROLE, MENUITEM);
445 oMenu.all((PERIOD + CSS_MENU_LABEL)).each(function (node) {
448 oMenuToggle = node.one(MENU_TOGGLE_SELECTOR);
451 oMenuToggle.set(ROLE, PRESENTATION);
452 oMenuLabel = oMenuToggle.previous();
455 oMenuLabel.set(ROLE, MENUITEM);
456 oMenuLabel.set("aria-haspopup", true);
458 oSubmenu = node.next();
462 oSubmenu.set(ROLE, MENU);
464 oMenuLabel = oSubmenu.previous();
465 oMenuToggle = oMenuLabel.one(MENU_TOGGLE_SELECTOR);
468 oMenuLabel = oMenuToggle;
471 sID = Y.stamp(oMenuLabel);
473 if (!oMenuLabel.get(ID)) {
474 oMenuLabel.set(ID, sID);
477 oSubmenu.set("aria-labelledby", sID);
478 oSubmenu.set(ARIA_HIDDEN, true);
492 * Boolean indicating if submenus are automatically made visible when the
493 * user mouses over the menu's items.
495 * @attribute autoSubmenuDisplay
501 autoSubmenuDisplay: {
510 * Number indicating the time (in milliseconds) that should expire before a
511 * submenu is made visible when the user mouses over the menu's label.
513 * @attribute submenuShowDelay
528 * Number indicating the time (in milliseconds) that should expire before a
529 * submenu is hidden when the user mouses out of a menu label heading in the
530 * direction of a submenu.
532 * @attribute submenuHideDelay
547 * Number indicating the time (in milliseconds) that should expire before a
548 * submenu is hidden when the user mouses out of it.
550 * @attribute mouseOutHideDelay
566 Y.extend(NodeMenuNav, Y.Plugin.Base, {
568 // Protected properties
571 * @property _rootMenu
572 * @description Node instance representing the root menu in the menu.
581 * @property _activeItem
582 * @description Node instance representing the menu's active descendent:
583 * the menuitem or menu label the user is currently interacting with.
592 * @property _activeMenu
593 * @description Node instance representing the menu that is the parent of
594 * the menu's active descendent.
603 * @property _hasFocus
604 * @description Boolean indicating if the menu has focus.
612 // In gecko-based browsers a mouseover and mouseout event will fire even
613 // if a DOM element moves out from under the mouse without the user
614 // actually moving the mouse. This bug affects NodeMenuNav because the
615 // user can hit the Esc key to hide a menu, and if the mouse is over the
616 // menu when the user presses Esc, the _onMenuMouseOut handler will be
617 // called. To fix this bug the following flag (_blockMouseEvent) is used
618 // to block the code in the _onMenuMouseOut handler from executing.
621 * @property _blockMouseEvent
622 * @description Boolean indicating whether or not to handle the
628 _blockMouseEvent: false,
632 * @property _currentMouseX
633 * @description Number representing the current x coordinate of the mouse
643 * @property _movingToSubmenu
644 * @description Boolean indicating if the mouse is moving from a menu
645 * label to its corresponding submenu.
650 _movingToSubmenu: false,
654 * @property _showSubmenuTimer
655 * @description Timer used to show a submenu.
660 _showSubmenuTimer: null,
664 * @property _hideSubmenuTimer
665 * @description Timer used to hide a submenu.
670 _hideSubmenuTimer: null,
674 * @property _hideAllSubmenusTimer
675 * @description Timer used to hide a all submenus.
680 _hideAllSubmenusTimer: null,
684 * @property _firstItem
685 * @description Node instance representing the first item (menuitem or menu
686 * label) in the root menu of a menu.
697 initializer: function (config) {
700 oRootMenu = this.get(HOST),
707 menuNav._rootMenu = oRootMenu;
709 oRootMenu.all("ul:first-child").addClass(FIRST_OF_TYPE);
711 // Hide all visible submenus
713 oRootMenu.all(MENU_SELECTOR).addClass(CSS_MENU_HIDDEN);
716 // Wire up all event handlers
718 aHandlers.push(oRootMenu.on("mouseover", menuNav._onMouseOver, menuNav));
719 aHandlers.push(oRootMenu.on("mouseout", menuNav._onMouseOut, menuNav));
720 aHandlers.push(oRootMenu.on("mousemove", menuNav._onMouseMove, menuNav));
721 aHandlers.push(oRootMenu.on(MOUSEDOWN, menuNav._toggleSubmenuDisplay, menuNav));
722 aHandlers.push(Y.on("key", menuNav._toggleSubmenuDisplay, oRootMenu, "down:13", menuNav));
723 aHandlers.push(oRootMenu.on(CLICK, menuNav._toggleSubmenuDisplay, menuNav));
724 aHandlers.push(oRootMenu.on("keypress", menuNav._onKeyPress, menuNav));
725 aHandlers.push(oRootMenu.on(KEYDOWN, menuNav._onKeyDown, menuNav));
727 oDoc = oRootMenu.get("ownerDocument");
729 aHandlers.push(oDoc.on(MOUSEDOWN, menuNav._onDocMouseDown, menuNav));
730 aHandlers.push(oDoc.on("focus", menuNav._onDocFocus, menuNav));
732 this._eventHandlers = aHandlers;
734 menuNav._initFocusManager();
741 destructor: function () {
743 var aHandlers = this._eventHandlers;
747 Y.Array.each(aHandlers, function (handle) {
751 this._eventHandlers = null;
755 this.get(HOST).unplug("focusManager");
765 * @description Returns a boolean indicating if the specified menu is the
766 * root menu in the menu.
768 * @param {Node} menu Node instance representing a menu.
769 * @return {Boolean} Boolean indicating if the specified menu is the root
772 _isRoot: function (menu) {
774 return this._rootMenu.compareTo(menu);
780 * @method _getTopmostSubmenu
781 * @description Returns the topmost submenu of a submenu hierarchy.
783 * @param {Node} menu Node instance representing a menu.
784 * @return {Node} Node instance representing a menu.
786 _getTopmostSubmenu: function (menu) {
789 oMenu = getParentMenu(menu),
796 else if (menuNav._isRoot(oMenu)) {
800 returnVal = menuNav._getTopmostSubmenu(oMenu);
809 * @method _clearActiveItem
810 * @description Clears the menu's active descendent.
813 _clearActiveItem: function () {
816 oActiveItem = menuNav._activeItem;
819 oActiveItem.removeClass(getActiveClass(oActiveItem));
822 menuNav._activeItem = null;
828 * @method _setActiveItem
829 * @description Sets the specified menuitem or menu label as the menu's
832 * @param {Node} item Node instance representing a menuitem or menu label.
834 _setActiveItem: function (item) {
840 menuNav._clearActiveItem();
842 item.addClass(getActiveClass(item));
844 menuNav._activeItem = item;
853 * @description Focuses the specified menuitem or menu label.
855 * @param {Node} item Node instance representing a menuitem or menu label.
857 _focusItem: function (item) {
863 if (item && menuNav._hasFocus) {
865 oMenu = getParentMenu(item);
866 oItem = getItemAnchor(item);
868 if (oMenu && !oMenu.compareTo(menuNav._activeMenu)) {
869 menuNav._activeMenu = oMenu;
870 menuNav._initFocusManager();
873 menuNav._focusManager.focus(oItem);
882 * @description Shows the specified menu.
884 * @param {Node} menu Node instance representing a menu.
886 _showMenu: function (menu) {
888 var oParentMenu = getParentMenu(menu),
889 oLI = menu.get(PARENT_NODE),
893 if (this.get(USE_ARIA)) {
894 menu.set(ARIA_HIDDEN, false);
898 if (isHorizontalMenu(oParentMenu)) {
899 aXY[1] = aXY[1] + oLI.get(OFFSET_HEIGHT);
902 aXY[0] = aXY[0] + oLI.get(OFFSET_WIDTH);
909 if (UA.ie === 6 && !menu.hasIFrameShim) {
911 menu.appendChild(Y.Node.create(NodeMenuNav.SHIM_TEMPLATE));
912 menu.hasIFrameShim = true;
916 // Clear previous values for height and width
918 menu.setStyles({ height: EMPTY_STRING, width: EMPTY_STRING });
920 // Set the width and height of the menu's bounding box - this is
921 // necessary for IE 6 so that the CSS for the <iframe> shim can
922 // simply set the <iframe>'s width and height to 100% to ensure
923 // that dimensions of an <iframe> shim are always sync'd to the
924 // that of its parent menu. Specifying a width and height also
925 // helps when positioning decorator elements (for creating effects
926 // like rounded corners) inside a menu's bounding box in IE 7.
929 height: (menu.get(OFFSET_HEIGHT) + PX),
930 width: (menu.get(OFFSET_WIDTH) + PX) });
934 menu.previous().addClass(CSS_MENU_LABEL_MENUVISIBLE);
935 menu.removeClass(CSS_MENU_HIDDEN);
942 * @description Hides the specified menu.
944 * @param {Node} menu Node instance representing a menu.
945 * @param {Boolean} activateAndFocusLabel Boolean indicating if the label
947 * menu should be focused and set as active.
949 _hideMenu: function (menu, activateAndFocusLabel) {
952 oLabel = menu.previous(),
955 oLabel.removeClass(CSS_MENU_LABEL_MENUVISIBLE);
958 if (activateAndFocusLabel) {
959 menuNav._focusItem(oLabel);
960 menuNav._setActiveItem(oLabel);
963 oActiveItem = menu.one((PERIOD + CSS_MENUITEM_ACTIVE));
966 oActiveItem.removeClass(CSS_MENUITEM_ACTIVE);
969 // Clear the values for top and left that were set by the call to
970 // "setXY" when the menu was shown so that the hidden position
971 // specified in the core CSS file will take affect.
973 menu.setStyles({ left: EMPTY_STRING, top: EMPTY_STRING });
975 menu.addClass(CSS_MENU_HIDDEN);
977 if (menuNav.get(USE_ARIA)) {
978 menu.set(ARIA_HIDDEN, true);
985 * @method _hideAllSubmenus
986 * @description Hides all submenus of the specified menu.
988 * @param {Node} menu Node instance representing a menu.
990 _hideAllSubmenus: function (menu) {
994 menu.all(MENU_SELECTOR).each(Y.bind(function (submenuNode) {
996 menuNav._hideMenu(submenuNode);
1004 * @method _cancelShowSubmenuTimer
1005 * @description Cancels the timer used to show a submenu.
1008 _cancelShowSubmenuTimer: function () {
1011 oShowSubmenuTimer = menuNav._showSubmenuTimer;
1013 if (oShowSubmenuTimer) {
1014 oShowSubmenuTimer.cancel();
1015 menuNav._showSubmenuTimer = null;
1022 * @method _cancelHideSubmenuTimer
1023 * @description Cancels the timer used to hide a submenu.
1026 _cancelHideSubmenuTimer: function () {
1029 oHideSubmenuTimer = menuNav._hideSubmenuTimer;
1032 if (oHideSubmenuTimer) {
1033 oHideSubmenuTimer.cancel();
1034 menuNav._hideSubmenuTimer = null;
1041 * @method _initFocusManager
1042 * @description Initializes and updates the Focus Manager so that is is
1043 * always managing descendants of the active menu.
1046 _initFocusManager: function () {
1049 oRootMenu = menuNav._rootMenu,
1050 oMenu = menuNav._activeMenu || oRootMenu,
1052 menuNav._isRoot(oMenu) ? EMPTY_STRING : ("#" + oMenu.get("id")),
1053 oFocusManager = menuNav._focusManager,
1055 sDescendantSelector,
1058 if (isHorizontalMenu(oMenu)) {
1060 sDescendantSelector = sSelectorBase + STANDARD_QUERY + "," +
1061 sSelectorBase + EXTENDED_QUERY;
1063 sKeysVal = { next: "down:39", previous: "down:37" };
1068 sDescendantSelector = sSelectorBase + STANDARD_QUERY;
1069 sKeysVal = { next: "down:40", previous: "down:38" };
1074 if (!oFocusManager) {
1076 oRootMenu.plug(Y.Plugin.NodeFocusManager, {
1077 descendants: sDescendantSelector,
1082 oFocusManager = oRootMenu.focusManager;
1084 sQuery = "#" + oRootMenu.get("id") + " .yui-menu a," +
1085 MENU_TOGGLE_SELECTOR;
1087 oRootMenu.all(sQuery).set("tabIndex", -1);
1089 oFocusManager.on(ACTIVE_DESCENDANT_CHANGE,
1090 this._onActiveDescendantChange, oFocusManager, this);
1092 oFocusManager.after(ACTIVE_DESCENDANT_CHANGE,
1093 this._afterActiveDescendantChange, oFocusManager, this);
1095 menuNav._focusManager = oFocusManager;
1100 oFocusManager.set(ACTIVE_DESCENDANT, -1);
1101 oFocusManager.set(DESCENDANTS, sDescendantSelector);
1102 oFocusManager.set("keys", sKeysVal);
1109 // Event handlers for discrete pieces of pieces of the menu
1113 * @method _onActiveDescendantChange
1114 * @description "activeDescendantChange" event handler for menu's
1117 * @param {Object} event Object representing the Attribute change event.
1118 * @param {NodeMenuNav} menuNav Object representing the NodeMenuNav instance.
1120 _onActiveDescendantChange: function (event, menuNav) {
1122 if (event.src === UI && menuNav._activeMenu &&
1123 !menuNav._movingToSubmenu) {
1125 menuNav._hideAllSubmenus(menuNav._activeMenu);
1133 * @method _afterActiveDescendantChange
1134 * @description "activeDescendantChange" event handler for menu's
1137 * @param {Object} event Object representing the Attribute change event.
1138 * @param {NodeMenuNav} menuNav Object representing the NodeMenuNav instance.
1140 _afterActiveDescendantChange: function (event, menuNav) {
1144 if (event.src === UI) {
1145 oItem = getItem(this.get(DESCENDANTS).item(event.newVal), true);
1146 menuNav._setActiveItem(oItem);
1153 * @method _onDocFocus
1154 * @description "focus" event handler for the owner document of the MenuNav.
1156 * @param {Object} event Object representing the DOM event.
1158 _onDocFocus: function (event) {
1161 oActiveItem = menuNav._activeItem,
1162 oTarget = event.target,
1166 if (menuNav._rootMenu.contains(oTarget)) { // The menu has focus
1168 if (menuNav._hasFocus) {
1170 oMenu = getParentMenu(oTarget);
1172 // If the element that was focused is a descendant of the
1173 // root menu, but is in a submenu not currently being
1174 // managed by the Focus Manager, update the Focus Manager so
1175 // that it is now managing the submenu that is the parent of
1176 // the element that was focused.
1178 if (!menuNav._activeMenu.compareTo(oMenu)) {
1180 menuNav._activeMenu = oMenu;
1181 menuNav._initFocusManager();
1182 menuNav._focusManager.set(ACTIVE_DESCENDANT, oTarget);
1183 menuNav._setActiveItem(getItem(oTarget, true));
1188 else { // Initial focus
1190 // First time the menu has been focused, need to setup focused
1191 // state and established active active descendant
1193 menuNav._hasFocus = true;
1195 oActiveItem = getItem(oTarget, true);
1198 menuNav._setActiveItem(oActiveItem);
1204 else { // The menu has lost focus
1206 menuNav._clearActiveItem();
1208 menuNav._cancelShowSubmenuTimer();
1209 menuNav._hideAllSubmenus(menuNav._rootMenu);
1211 menuNav._activeMenu = menuNav._rootMenu;
1212 menuNav._initFocusManager();
1214 menuNav._focusManager.set(ACTIVE_DESCENDANT, 0);
1216 menuNav._hasFocus = false;
1224 * @method _onMenuMouseOver
1225 * @description "mouseover" event handler for a menu.
1227 * @param {Node} menu Node instance representing a menu.
1228 * @param {Object} event Object representing the DOM event.
1230 _onMenuMouseOver: function (menu, event) {
1233 oHideAllSubmenusTimer = menuNav._hideAllSubmenusTimer;
1235 if (oHideAllSubmenusTimer) {
1236 oHideAllSubmenusTimer.cancel();
1237 menuNav._hideAllSubmenusTimer = null;
1240 menuNav._cancelHideSubmenuTimer();
1242 // Need to update the FocusManager in advance of focus a new
1243 // Menu in order to avoid the FocusManager thinking that
1244 // it has lost focus
1246 if (menu && !menu.compareTo(menuNav._activeMenu)) {
1247 menuNav._activeMenu = menu;
1249 if (menuNav._hasFocus) {
1250 menuNav._initFocusManager();
1255 if (menuNav._movingToSubmenu && isHorizontalMenu(menu)) {
1256 menuNav._movingToSubmenu = false;
1263 * @method _hideAndFocusLabel
1264 * @description Hides all of the submenus of the root menu and focuses the
1265 * label of the topmost submenu
1268 _hideAndFocusLabel: function () {
1271 oActiveMenu = menuNav._activeMenu,
1274 menuNav._hideAllSubmenus(menuNav._rootMenu);
1278 // Focus the label element for the topmost submenu
1279 oSubmenu = menuNav._getTopmostSubmenu(oActiveMenu);
1280 menuNav._focusItem(oSubmenu.previous());
1288 * @method _onMenuMouseOut
1289 * @description "mouseout" event handler for a menu.
1291 * @param {Node} menu Node instance representing a menu.
1292 * @param {Object} event Object representing the DOM event.
1294 _onMenuMouseOut: function (menu, event) {
1297 oActiveMenu = menuNav._activeMenu,
1298 oRelatedTarget = event.relatedTarget,
1299 oActiveItem = menuNav._activeItem,
1304 if (oActiveMenu && !oActiveMenu.contains(oRelatedTarget)) {
1306 oParentMenu = getParentMenu(oActiveMenu);
1309 if (oParentMenu && !oParentMenu.contains(oRelatedTarget)) {
1311 if (menuNav.get(MOUSEOUT_HIDE_DELAY) > 0) {
1313 menuNav._cancelShowSubmenuTimer();
1315 menuNav._hideAllSubmenusTimer =
1317 later(menuNav.get(MOUSEOUT_HIDE_DELAY),
1318 menuNav, menuNav._hideAndFocusLabel);
1327 oMenu = getParentMenu(oActiveItem);
1329 if (!menuNav._isRoot(oMenu)) {
1330 menuNav._focusItem(oMenu.previous());
1343 * @method _onMenuLabelMouseOver
1344 * @description "mouseover" event handler for a menu label.
1346 * @param {Node} menuLabel Node instance representing a menu label.
1347 * @param {Object} event Object representing the DOM event.
1349 _onMenuLabelMouseOver: function (menuLabel, event) {
1352 oActiveMenu = menuNav._activeMenu,
1353 bIsRoot = menuNav._isRoot(oActiveMenu),
1354 bUseAutoSubmenuDisplay =
1355 (menuNav.get(AUTO_SUBMENU_DISPLAY) && bIsRoot || !bIsRoot),
1359 menuNav._focusItem(menuLabel);
1360 menuNav._setActiveItem(menuLabel);
1363 if (bUseAutoSubmenuDisplay && !menuNav._movingToSubmenu) {
1365 menuNav._cancelHideSubmenuTimer();
1366 menuNav._cancelShowSubmenuTimer();
1369 if (!hasVisibleSubmenu(menuLabel)) {
1371 oSubmenu = menuLabel.next();
1376 menuNav._hideAllSubmenus(oActiveMenu);
1378 menuNav._showSubmenuTimer =
1379 later(menuNav.get("submenuShowDelay"), menuNav,
1380 menuNav._showMenu, oSubmenu);
1392 * @method _onMenuLabelMouseOut
1393 * @description "mouseout" event handler for a menu label.
1395 * @param {Node} menuLabel Node instance representing a menu label.
1396 * @param {Object} event Object representing the DOM event.
1398 _onMenuLabelMouseOut: function (menuLabel, event) {
1401 bIsRoot = menuNav._isRoot(menuNav._activeMenu),
1402 bUseAutoSubmenuDisplay =
1403 (menuNav.get(AUTO_SUBMENU_DISPLAY) && bIsRoot || !bIsRoot),
1405 oRelatedTarget = event.relatedTarget,
1406 oSubmenu = menuLabel.next();
1408 menuNav._clearActiveItem();
1410 if (bUseAutoSubmenuDisplay) {
1412 if (menuNav._movingToSubmenu &&
1413 !menuNav._showSubmenuTimer && oSubmenu) {
1415 // If the mouse is moving diagonally toward the submenu and
1416 // another submenu isn't in the process of being displayed
1417 // (via a timer), then hide the submenu via a timer to give
1418 // the user some time to reach the submenu.
1420 menuNav._hideSubmenuTimer =
1421 later(menuNav.get("submenuHideDelay"), menuNav,
1422 menuNav._hideMenu, oSubmenu);
1425 else if (!menuNav._movingToSubmenu && oSubmenu &&
1426 !oSubmenu.contains(oRelatedTarget) &&
1427 !oRelatedTarget.compareTo(oSubmenu)) {
1429 // If the mouse is not moving toward the submenu, cancel any
1430 // submenus that might be in the process of being displayed
1431 // (via a timer) and hide this submenu immediately.
1433 menuNav._cancelShowSubmenuTimer();
1435 menuNav._hideMenu(oSubmenu);
1445 * @method _onMenuItemMouseOver
1446 * @description "mouseover" event handler for a menuitem.
1448 * @param {Node} menuItem Node instance representing a menuitem.
1449 * @param {Object} event Object representing the DOM event.
1451 _onMenuItemMouseOver: function (menuItem, event) {
1454 oActiveMenu = menuNav._activeMenu,
1455 bIsRoot = menuNav._isRoot(oActiveMenu),
1456 bUseAutoSubmenuDisplay =
1457 (menuNav.get(AUTO_SUBMENU_DISPLAY) && bIsRoot || !bIsRoot);
1460 menuNav._focusItem(menuItem);
1461 menuNav._setActiveItem(menuItem);
1464 if (bUseAutoSubmenuDisplay && !menuNav._movingToSubmenu) {
1466 menuNav._hideAllSubmenus(oActiveMenu);
1474 * @method _onMenuItemMouseOut
1475 * @description "mouseout" event handler for a menuitem.
1477 * @param {Node} menuItem Node instance representing a menuitem.
1478 * @param {Object} event Object representing the DOM event.
1480 _onMenuItemMouseOut: function (menuItem, event) {
1482 this._clearActiveItem();
1488 * @method _onVerticalMenuKeyDown
1489 * @description "keydown" event handler for vertical menus.
1491 * @param {Object} event Object representing the DOM event.
1493 _onVerticalMenuKeyDown: function (event) {
1496 oActiveMenu = menuNav._activeMenu,
1497 oRootMenu = menuNav._rootMenu,
1498 oTarget = event.target,
1499 bPreventDefault = false,
1500 nKeyCode = event.keyCode,
1509 case 37: // left arrow
1511 oParentMenu = getParentMenu(oActiveMenu);
1513 if (oParentMenu && isHorizontalMenu(oParentMenu)) {
1515 menuNav._hideMenu(oActiveMenu);
1516 oLI = getPreviousSibling(oActiveMenu.get(PARENT_NODE));
1517 oItem = getItem(oLI);
1521 if (isMenuLabel(oItem)) { // Menu label
1523 oSubmenu = oItem.next();
1528 menuNav._showMenu(oSubmenu);
1529 menuNav._focusItem(getFirstItem(oSubmenu));
1530 menuNav._setActiveItem(getFirstItem(oSubmenu));
1535 menuNav._focusItem(oItem);
1536 menuNav._setActiveItem(oItem);
1543 menuNav._focusItem(oItem);
1544 menuNav._setActiveItem(oItem);
1551 else if (!menuNav._isRoot(oActiveMenu)) {
1552 menuNav._hideMenu(oActiveMenu, true);
1556 bPreventDefault = true;
1560 case 39: // right arrow
1562 if (isMenuLabel(oTarget)) {
1564 oSubmenu = oTarget.next();
1568 menuNav._showMenu(oSubmenu);
1569 menuNav._focusItem(getFirstItem(oSubmenu));
1570 menuNav._setActiveItem(getFirstItem(oSubmenu));
1575 else if (isHorizontalMenu(oRootMenu)) {
1577 oSubmenu = menuNav._getTopmostSubmenu(oActiveMenu);
1578 oLI = getNextSibling(oSubmenu.get(PARENT_NODE));
1579 oItem = getItem(oLI);
1581 menuNav._hideAllSubmenus(oRootMenu);
1585 if (isMenuLabel(oItem)) { // Menu label
1587 oSubmenu = oItem.next();
1591 menuNav._showMenu(oSubmenu);
1592 menuNav._focusItem(getFirstItem(oSubmenu));
1593 menuNav._setActiveItem(getFirstItem(oSubmenu));
1598 menuNav._focusItem(oItem);
1599 menuNav._setActiveItem(oItem);
1606 menuNav._focusItem(oItem);
1607 menuNav._setActiveItem(oItem);
1615 bPreventDefault = true;
1622 if (bPreventDefault) {
1624 // Prevent the browser from scrolling the window
1626 event.preventDefault();
1634 * @method _onHorizontalMenuKeyDown
1635 * @description "keydown" event handler for horizontal menus.
1637 * @param {Object} event Object representing the DOM event.
1639 _onHorizontalMenuKeyDown: function (event) {
1642 oActiveMenu = menuNav._activeMenu,
1643 oTarget = event.target,
1644 oFocusedItem = getItem(oTarget, true),
1645 bPreventDefault = false,
1646 nKeyCode = event.keyCode,
1650 if (nKeyCode === 40) {
1652 menuNav._hideAllSubmenus(oActiveMenu);
1654 if (isMenuLabel(oFocusedItem)) {
1656 oSubmenu = oFocusedItem.next();
1660 menuNav._showMenu(oSubmenu);
1661 menuNav._focusItem(getFirstItem(oSubmenu));
1662 menuNav._setActiveItem(getFirstItem(oSubmenu));
1666 bPreventDefault = true;
1673 if (bPreventDefault) {
1675 // Prevent the browser from scrolling the window
1677 event.preventDefault();
1684 // Generic DOM Event handlers
1688 * @method _onMouseMove
1689 * @description "mousemove" event handler for the menu.
1691 * @param {Object} event Object representing the DOM event.
1693 _onMouseMove: function (event) {
1697 // Using a timer to set the value of the "_currentMouseX" property
1698 // helps improve the reliability of the calculation used to set the
1699 // value of the "_movingToSubmenu" property - especially in Opera.
1701 later(10, menuNav, function () {
1703 menuNav._currentMouseX = event.pageX;
1711 * @method _onMouseOver
1712 * @description "mouseover" event handler for the menu.
1714 * @param {Object} event Object representing the DOM event.
1716 _onMouseOver: function (event) {
1726 if (menuNav._blockMouseEvent) {
1727 menuNav._blockMouseEvent = false;
1731 oTarget = event.target;
1732 oMenu = getMenu(oTarget, true);
1733 oMenuLabel = getMenuLabel(oTarget, true);
1734 oMenuItem = getMenuItem(oTarget, true);
1737 if (handleMouseOverForNode(oMenu, oTarget)) {
1739 menuNav._onMenuMouseOver(oMenu, event);
1741 oMenu[HANDLED_MOUSEOVER] = true;
1742 oMenu[HANDLED_MOUSEOUT] = false;
1744 oParentMenu = getParentMenu(oMenu);
1748 oParentMenu[HANDLED_MOUSEOUT] = true;
1749 oParentMenu[HANDLED_MOUSEOVER] = false;
1755 if (handleMouseOverForNode(oMenuLabel, oTarget)) {
1757 menuNav._onMenuLabelMouseOver(oMenuLabel, event);
1759 oMenuLabel[HANDLED_MOUSEOVER] = true;
1760 oMenuLabel[HANDLED_MOUSEOUT] = false;
1764 if (handleMouseOverForNode(oMenuItem, oTarget)) {
1766 menuNav._onMenuItemMouseOver(oMenuItem, event);
1768 oMenuItem[HANDLED_MOUSEOVER] = true;
1769 oMenuItem[HANDLED_MOUSEOUT] = false;
1779 * @method _onMouseOut
1780 * @description "mouseout" event handler for the menu.
1782 * @param {Object} event Object representing the DOM event.
1784 _onMouseOut: function (event) {
1787 oActiveMenu = menuNav._activeMenu,
1788 bMovingToSubmenu = false,
1797 menuNav._movingToSubmenu =
1798 (oActiveMenu && !isHorizontalMenu(oActiveMenu) &&
1799 ((event.pageX - 5) > menuNav._currentMouseX));
1801 oTarget = event.target;
1802 oRelatedTarget = event.relatedTarget;
1803 oMenu = getMenu(oTarget, true);
1804 oMenuLabel = getMenuLabel(oTarget, true);
1805 oMenuItem = getMenuItem(oTarget, true);
1808 if (handleMouseOutForNode(oMenuLabel, oRelatedTarget)) {
1810 menuNav._onMenuLabelMouseOut(oMenuLabel, event);
1812 oMenuLabel[HANDLED_MOUSEOUT] = true;
1813 oMenuLabel[HANDLED_MOUSEOVER] = false;
1817 if (handleMouseOutForNode(oMenuItem, oRelatedTarget)) {
1819 menuNav._onMenuItemMouseOut(oMenuItem, event);
1821 oMenuItem[HANDLED_MOUSEOUT] = true;
1822 oMenuItem[HANDLED_MOUSEOVER] = false;
1829 oSubmenu = oMenuLabel.next();
1832 (oRelatedTarget.compareTo(oSubmenu) ||
1833 oSubmenu.contains(oRelatedTarget))) {
1835 bMovingToSubmenu = true;
1842 if (handleMouseOutForNode(oMenu, oRelatedTarget) || bMovingToSubmenu) {
1844 menuNav._onMenuMouseOut(oMenu, event);
1846 oMenu[HANDLED_MOUSEOUT] = true;
1847 oMenu[HANDLED_MOUSEOVER] = false;
1855 * @method _toggleSubmenuDisplay
1856 * @description "mousedown," "keydown," and "click" event handler for the
1857 * menu used to toggle the display of a submenu.
1859 * @param {Object} event Object representing the DOM event.
1861 _toggleSubmenuDisplay: function (event) {
1864 oTarget = event.target,
1865 oMenuLabel = getMenuLabel(oTarget, true),
1877 oAnchor = isAnchor(oTarget) ? oTarget : oTarget.ancestor(isAnchor);
1882 // Need to pass "2" as a second argument to "getAttribute" for
1883 // IE otherwise IE will return a fully qualified URL for the
1884 // value of the "href" attribute.
1885 // http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx
1887 sHref = oAnchor.getAttribute("href", 2);
1888 nHashPos = sHref.indexOf("#");
1889 nLen = sHref.length;
1891 if (nHashPos === 0 && nLen > 1) {
1893 sId = sHref.substr(1, nLen);
1894 oSubmenu = oMenuLabel.next();
1896 if (oSubmenu && (oSubmenu.get(ID) === sId)) {
1898 if (sType === MOUSEDOWN || sType === KEYDOWN) {
1900 if ((UA.opera || UA.gecko || UA.ie) && sType === KEYDOWN && !menuNav._preventClickHandle) {
1902 // Prevent the browser from following the URL of
1903 // the anchor element
1905 menuNav._preventClickHandle = menuNav._rootMenu.on("click", function (event) {
1907 event.preventDefault();
1909 menuNav._preventClickHandle.detach();
1910 menuNav._preventClickHandle = null;
1916 if (sType == MOUSEDOWN) {
1918 // Prevent the target from getting focused by
1919 // default, since the element to be focused will
1920 // be determined by weather or not the submenu
1922 event.preventDefault();
1924 // FocusManager will attempt to focus any
1925 // descendant that is the target of the mousedown
1926 // event. Since we want to explicitly control
1927 // where focus is going, we need to call
1928 // "stopImmediatePropagation" to stop the
1929 // FocusManager from doing its thing.
1930 event.stopImmediatePropagation();
1932 // The "_focusItem" method relies on the
1933 // "_hasFocus" property being set to true. The
1934 // "_hasFocus" property is normally set via a
1935 // "focus" event listener, but since we've
1936 // blocked focus from happening, we need to set
1937 // this property manually.
1938 menuNav._hasFocus = true;
1943 if (menuNav._isRoot(getParentMenu(oTarget))) { // Event target is a submenu label in the root menu
1945 // Menu label toggle functionality
1947 if (hasVisibleSubmenu(oMenuLabel)) {
1949 menuNav._hideMenu(oSubmenu);
1950 menuNav._focusItem(oMenuLabel);
1951 menuNav._setActiveItem(oMenuLabel);
1956 menuNav._hideAllSubmenus(menuNav._rootMenu);
1957 menuNav._showMenu(oSubmenu);
1959 menuNav._focusItem(getFirstItem(oSubmenu));
1960 menuNav._setActiveItem(getFirstItem(oSubmenu));
1965 else { // Event target is a submenu label within a submenu
1967 if (menuNav._activeItem == oMenuLabel) {
1969 menuNav._showMenu(oSubmenu);
1970 menuNav._focusItem(getFirstItem(oSubmenu));
1971 menuNav._setActiveItem(getFirstItem(oSubmenu));
1976 if (!oMenuLabel._clickHandle) {
1978 oMenuLabel._clickHandle = oMenuLabel.on("click", function () {
1980 menuNav._hideAllSubmenus(menuNav._rootMenu);
1982 menuNav._hasFocus = false;
1983 menuNav._clearActiveItem();
1986 oMenuLabel._clickHandle.detach();
1988 oMenuLabel._clickHandle = null;
2001 if (sType === CLICK) {
2003 // Prevent the browser from following the URL of
2004 // the anchor element
2006 event.preventDefault();
2023 * @method _onKeyPress
2024 * @description "keypress" event handler for the menu.
2026 * @param {Object} event Object representing the DOM event.
2028 _onKeyPress: function (event) {
2030 switch (event.keyCode) {
2032 case 37: // left arrow
2033 case 38: // up arrow
2034 case 39: // right arrow
2035 case 40: // down arrow
2037 // Prevent the browser from scrolling the window
2039 event.preventDefault();
2049 * @method _onKeyDown
2050 * @description "keydown" event handler for the menu.
2052 * @param {Object} event Object representing the DOM event.
2054 _onKeyDown: function (event) {
2057 oActiveItem = menuNav._activeItem,
2058 oTarget = event.target,
2059 oActiveMenu = getParentMenu(oTarget),
2064 menuNav._activeMenu = oActiveMenu;
2066 if (isHorizontalMenu(oActiveMenu)) {
2067 menuNav._onHorizontalMenuKeyDown(event);
2070 menuNav._onVerticalMenuKeyDown(event);
2074 if (event.keyCode === 27) {
2076 if (!menuNav._isRoot(oActiveMenu)) {
2079 later(0, menuNav, function () {
2080 menuNav._hideMenu(oActiveMenu, true);
2084 menuNav._hideMenu(oActiveMenu, true);
2087 event.stopPropagation();
2088 menuNav._blockMouseEvent = UA.gecko ? true : false;
2091 else if (oActiveItem) {
2093 if (isMenuLabel(oActiveItem) &&
2094 hasVisibleSubmenu(oActiveItem)) {
2096 oSubmenu = oActiveItem.next();
2099 menuNav._hideMenu(oSubmenu);
2105 menuNav._focusManager.blur();
2107 // This is necessary for Webkit since blurring the
2108 // active menuitem won't result in the document
2109 // gaining focus, meaning the that _onDocFocus
2110 // listener won't clear the active menuitem.
2112 menuNav._clearActiveItem();
2114 menuNav._hasFocus = false;
2127 * @method _onDocMouseDown
2128 * @description "mousedown" event handler for the owner document of
2131 * @param {Object} event Object representing the DOM event.
2133 _onDocMouseDown: function (event) {
2136 oRoot = menuNav._rootMenu,
2137 oTarget = event.target;
2140 if (!(oRoot.compareTo(oTarget) || oRoot.contains(oTarget))) {
2142 menuNav._hideAllSubmenus(oRoot);
2144 // Document doesn't receive focus in Webkit when the user mouses
2145 // down on it, so the "_hasFocus" property won't get set to the
2146 // correct value. The following line corrects the problem.
2149 menuNav._hasFocus = false;
2150 menuNav._clearActiveItem();
2160 Y.namespace('Plugin');
2162 Y.Plugin.NodeMenuNav = NodeMenuNav;
2165 }, '3.0.0' ,{requires:['node', 'classnamemanager', 'node-focusmanager']});