/* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html version: 3.3.0 build: 3167 */ YUI.add('node-menunav', function(Y) { /** *

The MenuNav Node Plugin makes it easy to transform existing list-based * markup into traditional, drop down navigational menus that are both accessible * and easy to customize, and only require a small set of dependencies.

* * *

To use the MenuNav Node Plugin, simply pass a reference to the plugin to a * Node instance's plug method.

* *

* * <script type="text/javascript">
*
* // Call the "use" method, passing in "node-menunav". This will
* // load the script and CSS for the MenuNav Node Plugin and all of
* // the required dependencies.
*
* YUI().use("node-menunav", function(Y) {
*
* // Use the "contentready" event to initialize the menu when
* // the subtree of element representing the root menu
* // (<div id="menu-1">) is ready to be scripted.
*
* Y.on("contentready", function () {
*
* // The scope of the callback will be a Node instance
* // representing the root menu (<div id="menu-1">).
* // Therefore, since "this" represents a Node instance, it
* // is possible to just call "this.plug" passing in a
* // reference to the MenuNav Node Plugin.
*
* this.plug(Y.Plugin.NodeMenuNav);
*
* }, "#menu-1");
*
* });
*
* </script>
*
*

* *

The MenuNav Node Plugin has several configuration properties that can be * set via an object literal that is passed as a second argument to a Node * instance's plug method. *

* *

* * <script type="text/javascript">
*
* // Call the "use" method, passing in "node-menunav". This will
* // load the script and CSS for the MenuNav Node Plugin and all of
* // the required dependencies.
*
* YUI().use("node-menunav", function(Y) {
*
* // Use the "contentready" event to initialize the menu when
* // the subtree of element representing the root menu
* // (<div id="menu-1">) is ready to be scripted.
*
* Y.on("contentready", function () {
*
* // The scope of the callback will be a Node instance
* // representing the root menu (<div id="menu-1">).
* // Therefore, since "this" represents a Node instance, it
* // is possible to just call "this.plug" passing in a
* // reference to the MenuNav Node Plugin.
*
* this.plug(Y.Plugin.NodeMenuNav, { mouseOutHideDelay: 1000 }); *

* }, "#menu-1");
*
* });
*
* </script>
*
*

* * @module node-menunav */ // Util shortcuts var UA = Y.UA, later = Y.later, getClassName = Y.ClassNameManager.getClassName, // Frequently used strings MENU = "menu", MENUITEM = "menuitem", HIDDEN = "hidden", PARENT_NODE = "parentNode", CHILDREN = "children", OFFSET_HEIGHT = "offsetHeight", OFFSET_WIDTH = "offsetWidth", PX = "px", ID = "id", PERIOD = ".", HANDLED_MOUSEOUT = "handledMouseOut", HANDLED_MOUSEOVER = "handledMouseOver", ACTIVE = "active", LABEL = "label", LOWERCASE_A = "a", MOUSEDOWN = "mousedown", KEYDOWN = "keydown", CLICK = "click", EMPTY_STRING = "", FIRST_OF_TYPE = "first-of-type", ROLE = "role", PRESENTATION = "presentation", DESCENDANTS = "descendants", UI = "UI", ACTIVE_DESCENDANT = "activeDescendant", USE_ARIA = "useARIA", ARIA_HIDDEN = "aria-hidden", CONTENT = "content", HOST = "host", ACTIVE_DESCENDANT_CHANGE = ACTIVE_DESCENDANT + "Change", // Attribute keys AUTO_SUBMENU_DISPLAY = "autoSubmenuDisplay", MOUSEOUT_HIDE_DELAY = "mouseOutHideDelay", // CSS class names CSS_MENU = getClassName(MENU), CSS_MENU_HIDDEN = getClassName(MENU, HIDDEN), CSS_MENU_HORIZONTAL = getClassName(MENU, "horizontal"), CSS_MENU_LABEL = getClassName(MENU, LABEL), CSS_MENU_LABEL_ACTIVE = getClassName(MENU, LABEL, ACTIVE), CSS_MENU_LABEL_MENUVISIBLE = getClassName(MENU, LABEL, (MENU + "visible")), CSS_MENUITEM = getClassName(MENUITEM), CSS_MENUITEM_ACTIVE = getClassName(MENUITEM, ACTIVE), // CSS selectors MENU_SELECTOR = PERIOD + CSS_MENU, MENU_TOGGLE_SELECTOR = (PERIOD + getClassName(MENU, "toggle")), MENU_CONTENT_SELECTOR = PERIOD + getClassName(MENU, CONTENT), MENU_LABEL_SELECTOR = PERIOD + CSS_MENU_LABEL, STANDARD_QUERY = ">" + MENU_CONTENT_SELECTOR + ">ul>li>a", EXTENDED_QUERY = ">" + MENU_CONTENT_SELECTOR + ">ul>li>" + MENU_LABEL_SELECTOR + ">a:first-child"; // Utility functions var getPreviousSibling = function (node) { var oPrevious = node.previous(), oChildren; if (!oPrevious) { oChildren = node.get(PARENT_NODE).get(CHILDREN); oPrevious = oChildren.item(oChildren.size() - 1); } return oPrevious; }; var getNextSibling = function (node) { var oNext = node.next(); if (!oNext) { oNext = node.get(PARENT_NODE).get(CHILDREN).item(0); } return oNext; }; var isAnchor = function (node) { var bReturnVal = false; if (node) { bReturnVal = node.get("nodeName").toLowerCase() === LOWERCASE_A; } return bReturnVal; }; var isMenuItem = function (node) { return node.hasClass(CSS_MENUITEM); }; var isMenuLabel = function (node) { return node.hasClass(CSS_MENU_LABEL); }; var isHorizontalMenu = function (menu) { return menu.hasClass(CSS_MENU_HORIZONTAL); }; var hasVisibleSubmenu = function (menuLabel) { return menuLabel.hasClass(CSS_MENU_LABEL_MENUVISIBLE); }; var getItemAnchor = function (node) { return isAnchor(node) ? node : node.one(LOWERCASE_A); }; var getNodeWithClass = function (node, className, searchAncestors) { var oItem; if (node) { if (node.hasClass(className)) { oItem = node; } if (!oItem && searchAncestors) { oItem = node.ancestor((PERIOD + className)); } } return oItem; }; var getParentMenu = function (node) { return node.ancestor(MENU_SELECTOR); }; var getMenu = function (node, searchAncestors) { return getNodeWithClass(node, CSS_MENU, searchAncestors); }; var getMenuItem = function (node, searchAncestors) { var oItem; if (node) { oItem = getNodeWithClass(node, CSS_MENUITEM, searchAncestors); } return oItem; }; var getMenuLabel = function (node, searchAncestors) { var oItem; if (node) { if (searchAncestors) { oItem = getNodeWithClass(node, CSS_MENU_LABEL, searchAncestors); } else { oItem = getNodeWithClass(node, CSS_MENU_LABEL) || node.one((PERIOD + CSS_MENU_LABEL)); } } return oItem; }; var getItem = function (node, searchAncestors) { var oItem; if (node) { oItem = getMenuItem(node, searchAncestors) || getMenuLabel(node, searchAncestors); } return oItem; }; var getFirstItem = function (menu) { return getItem(menu.one("li")); }; var getActiveClass = function (node) { return isMenuItem(node) ? CSS_MENUITEM_ACTIVE : CSS_MENU_LABEL_ACTIVE; }; var handleMouseOverForNode = function (node, target) { return node && !node[HANDLED_MOUSEOVER] && (node.compareTo(target) || node.contains(target)); }; var handleMouseOutForNode = function (node, relatedTarget) { return node && !node[HANDLED_MOUSEOUT] && (!node.compareTo(relatedTarget) && !node.contains(relatedTarget)); }; /** * The NodeMenuNav class is a plugin for a Node instance. The class is used via * the plug method of Node and * should not be instantiated directly. * @namespace plugin * @class NodeMenuNav */ var NodeMenuNav = function () { NodeMenuNav.superclass.constructor.apply(this, arguments); }; NodeMenuNav.NAME = "nodeMenuNav"; NodeMenuNav.NS = "menuNav"; /** * @property NodeMenuNav.SHIM_TEMPLATE_TITLE * @description String representing the value for the title * attribute for the shim used to prevent <select> elements * from poking through menus in IE 6. * @default "Menu Stacking Shim" * @type String */ NodeMenuNav.SHIM_TEMPLATE_TITLE = "Menu Stacking Shim"; /** * @property NodeMenuNav.SHIM_TEMPLATE * @description String representing the HTML used to create the * <iframe> shim used to prevent * <select> elements from poking through menus in IE 6. * @default "<iframe frameborder="0" tabindex="-1" * class="yui-shim" title="Menu Stacking Shim" * src="javascript:false;"></iframe>" * @type String */ // '; NodeMenuNav.ATTRS = { /** * Boolean indicating if use of the WAI-ARIA Roles and States should be * enabled for the menu. * * @attribute useARIA * @readOnly * @writeOnce * @default true * @type boolean */ useARIA: { value: true, writeOnce: true, lazyAdd: false, setter: function (value) { var oMenu = this.get(HOST), oMenuLabel, oMenuToggle, oSubmenu, sID; if (value) { oMenu.set(ROLE, MENU); oMenu.all("ul,li," + MENU_CONTENT_SELECTOR).set(ROLE, PRESENTATION); oMenu.all((PERIOD + getClassName(MENUITEM, CONTENT))).set(ROLE, MENUITEM); oMenu.all((PERIOD + CSS_MENU_LABEL)).each(function (node) { oMenuLabel = node; oMenuToggle = node.one(MENU_TOGGLE_SELECTOR); if (oMenuToggle) { oMenuToggle.set(ROLE, PRESENTATION); oMenuLabel = oMenuToggle.previous(); } oMenuLabel.set(ROLE, MENUITEM); oMenuLabel.set("aria-haspopup", true); oSubmenu = node.next(); if (oSubmenu) { oSubmenu.set(ROLE, MENU); oMenuLabel = oSubmenu.previous(); oMenuToggle = oMenuLabel.one(MENU_TOGGLE_SELECTOR); if (oMenuToggle) { oMenuLabel = oMenuToggle; } sID = Y.stamp(oMenuLabel); if (!oMenuLabel.get(ID)) { oMenuLabel.set(ID, sID); } oSubmenu.set("aria-labelledby", sID); oSubmenu.set(ARIA_HIDDEN, true); } }); } } }, /** * Boolean indicating if submenus are automatically made visible when the * user mouses over the menu's items. * * @attribute autoSubmenuDisplay * @readOnly * @writeOnce * @default true * @type boolean */ autoSubmenuDisplay: { value: true, writeOnce: true }, /** * Number indicating the time (in milliseconds) that should expire before a * submenu is made visible when the user mouses over the menu's label. * * @attribute submenuShowDelay * @readOnly * @writeOnce * @default 250 * @type Number */ submenuShowDelay: { value: 250, writeOnce: true }, /** * Number indicating the time (in milliseconds) that should expire before a * submenu is hidden when the user mouses out of a menu label heading in the * direction of a submenu. * * @attribute submenuHideDelay * @readOnly * @writeOnce * @default 250 * @type Number */ submenuHideDelay: { value: 250, writeOnce: true }, /** * Number indicating the time (in milliseconds) that should expire before a * submenu is hidden when the user mouses out of it. * * @attribute mouseOutHideDelay * @readOnly * @writeOnce * @default 750 * @type Number */ mouseOutHideDelay: { value: 750, writeOnce: true } }; Y.extend(NodeMenuNav, Y.Plugin.Base, { // Protected properties /** * @property _rootMenu * @description Node instance representing the root menu in the menu. * @default null * @protected * @type Node */ _rootMenu: null, /** * @property _activeItem * @description Node instance representing the menu's active descendent: * the menuitem or menu label the user is currently interacting with. * @default null * @protected * @type Node */ _activeItem: null, /** * @property _activeMenu * @description Node instance representing the menu that is the parent of * the menu's active descendent. * @default null * @protected * @type Node */ _activeMenu: null, /** * @property _hasFocus * @description Boolean indicating if the menu has focus. * @default false * @protected * @type Boolean */ _hasFocus: false, // In gecko-based browsers a mouseover and mouseout event will fire even // if a DOM element moves out from under the mouse without the user // actually moving the mouse. This bug affects NodeMenuNav because the // user can hit the Esc key to hide a menu, and if the mouse is over the // menu when the user presses Esc, the _onMenuMouseOut handler will be // called. To fix this bug the following flag (_blockMouseEvent) is used // to block the code in the _onMenuMouseOut handler from executing. /** * @property _blockMouseEvent * @description Boolean indicating whether or not to handle the * "mouseover" event. * @default false * @protected * @type Boolean */ _blockMouseEvent: false, /** * @property _currentMouseX * @description Number representing the current x coordinate of the mouse * inside the menu. * @default 0 * @protected * @type Number */ _currentMouseX: 0, /** * @property _movingToSubmenu * @description Boolean indicating if the mouse is moving from a menu * label to its corresponding submenu. * @default false * @protected * @type Boolean */ _movingToSubmenu: false, /** * @property _showSubmenuTimer * @description Timer used to show a submenu. * @default null * @protected * @type Object */ _showSubmenuTimer: null, /** * @property _hideSubmenuTimer * @description Timer used to hide a submenu. * @default null * @protected * @type Object */ _hideSubmenuTimer: null, /** * @property _hideAllSubmenusTimer * @description Timer used to hide a all submenus. * @default null * @protected * @type Object */ _hideAllSubmenusTimer: null, /** * @property _firstItem * @description Node instance representing the first item (menuitem or menu * label) in the root menu of a menu. * @default null * @protected * @type Node */ _firstItem: null, // Public methods initializer: function (config) { var menuNav = this, oRootMenu = this.get(HOST), aHandlers = [], oDoc; if (oRootMenu) { menuNav._rootMenu = oRootMenu; oRootMenu.all("ul:first-child").addClass(FIRST_OF_TYPE); // Hide all visible submenus oRootMenu.all(MENU_SELECTOR).addClass(CSS_MENU_HIDDEN); // Wire up all event handlers aHandlers.push(oRootMenu.on("mouseover", menuNav._onMouseOver, menuNav)); aHandlers.push(oRootMenu.on("mouseout", menuNav._onMouseOut, menuNav)); aHandlers.push(oRootMenu.on("mousemove", menuNav._onMouseMove, menuNav)); aHandlers.push(oRootMenu.on(MOUSEDOWN, menuNav._toggleSubmenuDisplay, menuNav)); aHandlers.push(Y.on("key", menuNav._toggleSubmenuDisplay, oRootMenu, "down:13", menuNav)); aHandlers.push(oRootMenu.on(CLICK, menuNav._toggleSubmenuDisplay, menuNav)); aHandlers.push(oRootMenu.on("keypress", menuNav._onKeyPress, menuNav)); aHandlers.push(oRootMenu.on(KEYDOWN, menuNav._onKeyDown, menuNav)); oDoc = oRootMenu.get("ownerDocument"); aHandlers.push(oDoc.on(MOUSEDOWN, menuNav._onDocMouseDown, menuNav)); aHandlers.push(oDoc.on("focus", menuNav._onDocFocus, menuNav)); this._eventHandlers = aHandlers; menuNav._initFocusManager(); } }, destructor: function () { var aHandlers = this._eventHandlers; if (aHandlers) { Y.Array.each(aHandlers, function (handle) { handle.detach(); }); this._eventHandlers = null; } this.get(HOST).unplug("focusManager"); }, // Protected methods /** * @method _isRoot * @description Returns a boolean indicating if the specified menu is the * root menu in the menu. * @protected * @param {Node} menu Node instance representing a menu. * @return {Boolean} Boolean indicating if the specified menu is the root * menu in the menu. */ _isRoot: function (menu) { return this._rootMenu.compareTo(menu); }, /** * @method _getTopmostSubmenu * @description Returns the topmost submenu of a submenu hierarchy. * @protected * @param {Node} menu Node instance representing a menu. * @return {Node} Node instance representing a menu. */ _getTopmostSubmenu: function (menu) { var menuNav = this, oMenu = getParentMenu(menu), returnVal; if (!oMenu) { returnVal = menu; } else if (menuNav._isRoot(oMenu)) { returnVal = menu; } else { returnVal = menuNav._getTopmostSubmenu(oMenu); } return returnVal; }, /** * @method _clearActiveItem * @description Clears the menu's active descendent. * @protected */ _clearActiveItem: function () { var menuNav = this, oActiveItem = menuNav._activeItem; if (oActiveItem) { oActiveItem.removeClass(getActiveClass(oActiveItem)); } menuNav._activeItem = null; }, /** * @method _setActiveItem * @description Sets the specified menuitem or menu label as the menu's * active descendent. * @protected * @param {Node} item Node instance representing a menuitem or menu label. */ _setActiveItem: function (item) { var menuNav = this; if (item) { menuNav._clearActiveItem(); item.addClass(getActiveClass(item)); menuNav._activeItem = item; } }, /** * @method _focusItem * @description Focuses the specified menuitem or menu label. * @protected * @param {Node} item Node instance representing a menuitem or menu label. */ _focusItem: function (item) { var menuNav = this, oMenu, oItem; if (item && menuNav._hasFocus) { oMenu = getParentMenu(item); oItem = getItemAnchor(item); if (oMenu && !oMenu.compareTo(menuNav._activeMenu)) { menuNav._activeMenu = oMenu; menuNav._initFocusManager(); } menuNav._focusManager.focus(oItem); } }, /** * @method _showMenu * @description Shows the specified menu. * @protected * @param {Node} menu Node instance representing a menu. */ _showMenu: function (menu) { var oParentMenu = getParentMenu(menu), oLI = menu.get(PARENT_NODE), aXY = oLI.getXY(); if (this.get(USE_ARIA)) { menu.set(ARIA_HIDDEN, false); } if (isHorizontalMenu(oParentMenu)) { aXY[1] = aXY[1] + oLI.get(OFFSET_HEIGHT); } else { aXY[0] = aXY[0] + oLI.get(OFFSET_WIDTH); } menu.setXY(aXY); if (UA.ie < 8) { if (UA.ie === 6 && !menu.hasIFrameShim) { menu.appendChild(Y.Node.create(NodeMenuNav.SHIM_TEMPLATE)); menu.hasIFrameShim = true; } // Clear previous values for height and width menu.setStyles({ height: EMPTY_STRING, width: EMPTY_STRING }); // Set the width and height of the menu's bounding box - this is // necessary for IE 6 so that the CSS for the