2
Copyright (c) 2008, Yahoo! Inc. All rights reserved.
3
Code licensed under the BSD License:
4
http://developer.yahoo.net/yui/license.txt
7
YUI.add('node-menunav', function(Y) {
10
* <p>The MenuNav Node Plugin makes it easy to transform existing list-based markup into traditional,
11
* drop down navigational menus that are both accessible and easy to customize, and only require
12
* a small set of dependencies.</p>
13
* <p>To use the MenuNav Node Plugin, simply pass a reference to the plugin to a Node instance's
14
* <code>plug</code> method.</p>
18
* <script type="text/javascript"><br>
20
* // Call the "use" method, passing in "node-menunav". This will load the <br>
21
* // script and CSS for the MenuNav Node Plugin and all of the required <br>
22
* // dependencies.<br>
24
* YUI().use("node-menunav", function(Y) {<br>
26
* // Use the "contentready" event to initialize the menu when the subtree of <br>
27
* // element representing the root menu (<div id="menu-1">) is ready to <br>
30
* Y.on("contentready", function () {<br>
32
* // The scope of the callback will be a Node instance representing <br>
33
* // the root menu (<div id="menu-1">). Therefore, since "this"<br>
34
* // represents a Node instance, it is possible to just call "this.plug"<br>
35
* // passing in a reference to the MenuNav Node Plugin.<br>
37
* this.plug(Y.plugin.NodeMenuNav);<br>
43
* </script><br>
47
* <p>The MenuNav Node Plugin has several configuration properties that can be set via an
48
* object literal that is passed as a second argument to a Node instance's <code>plug</code> method.
53
* <script type="text/javascript"><br>
55
* // Call the "use" method, passing in "node-menunav". This will load the <br>
56
* // script and CSS for the MenuNav Node Plugin and all of the required <br>
57
* // dependencies.<br>
59
* YUI().use("node-menunav", function(Y) {<br>
61
* // Use the "contentready" event to initialize the menu when the subtree of <br>
62
* // element representing the root menu (<div id="menu-1">) is ready to <br>
65
* Y.on("contentready", function () {<br>
67
* // The scope of the callback will be a Node instance representing <br>
68
* // the root menu (<div id="menu-1">). Therefore, since "this"<br>
69
* // represents a Node instance, it is possible to just call "this.plug"<br>
70
* // passing in a reference to the MenuNav Node Plugin.<br>
72
* this.plug(Y.plugin.NodeMenuNav, { mouseOutHideDelay: 1000 });<br>
78
* </script><br>
82
* <p> The complete list of the MenuNav Node Plugin configuration properties are:</p>
85
* <dd>Boolean indicating if use of the WAI-ARIA Roles and States should be enabled for the
86
* MenuNav. Set to true by default for Firefox 3 and Internet Explorer 8 as currently only
87
* these browsers have support for ARIA, and are supported by several screen readers for
88
* Windows that also offer support for ARIA.</dd>
90
* <dt>autoSubmenuDisplay</dt>
91
* <dd>Boolean indicating if submenus are automatically made visible when the user mouses over
92
* the menu's items. Set to true by default.</dd>
94
* <dt>submenuShowDelay</dt>
95
* <dd>Number indicating the time (in milliseconds) that should expire before a submenu is
96
* made visible when the user mouses over the menu's label. Set to 250 by default.</dd>
98
* <dt>submenuHideDelay</dt>
99
* <dd>Number indicating the time (in milliseconds) that should expire before a submenu is
100
* hidden when the user mouses out of a menu label heading in the direction of a submenu.
101
* Set to 250 by default.</dd>
103
* <dt>mouseOutHideDelay</dt>
104
* <dd>Number indicating the time (in milliseconds) that should expire before a submenu is
105
* hidden when the user mouses out of it. Set to 750 by default.</dd>
108
* @module node-menunav
116
getClassName = Y.ClassNameManager.getClassName,
127
// Frequently used strings
130
MENUITEM = "menuitem",
132
TAB_INDEX = "tabIndex",
133
PARENT_NODE = "parentNode",
134
CHILDREN = "children",
135
OFFSET_HEIGHT = "offsetHeight",
136
OFFSET_WIDTH = "offsetWidth",
140
HANDLED_MOUSEOUT = "handledMouseOut",
141
HANDLED_MOUSEOVER = "handledMouseOver",
145
MOUSEDOWN = "mousedown",
149
FIRST_OF_TYPE = "first-of-type",
154
CSS_MENU = getClassName(MENU),
155
CSS_MENU_HIDDEN = getClassName(MENU, HIDDEN),
156
CSS_MENU_HORIZONTAL = getClassName(MENU, "horizontal"),
157
CSS_MENU_LABEL = getClassName(MENU, LABEL),
158
CSS_MENU_LABEL_ACTIVE = getClassName(MENU, LABEL, ACTIVE),
159
CSS_MENU_LABEL_MENUVISIBLE = getClassName(MENU, LABEL, (MENU + "visible")),
160
CSS_MENUITEM = getClassName(MENUITEM),
161
CSS_MENUITEM_ACTIVE = getClassName(MENUITEM, ACTIVE),
166
MENU_SELECTOR = PERIOD + CSS_MENU;
172
// TO DO: Remove once Node implements circular functionality
173
var getPreviousSibling = function (node) {
175
var oPrevious = node.previous(),
184
oParentNode = node.get(PARENT_NODE);
185
oULs = oParentNode.get(PARENT_NODE).get(CHILDREN);
187
if (oULs.size() > 1) {
189
oUL = oParentNode.previous();
192
oChildren = oUL.get(CHILDREN);
195
oChildren = oULs.item(oULs.size() - 1).get(CHILDREN);
200
oChildren = oParentNode.get(CHILDREN);
203
oPrevious = oChildren.item(oChildren.size() - 1);
212
// TO DO: Remove once Node implements circular functionality
213
var getNextSibling = function (node) {
215
var oNext = node.next(),
224
oParentNode = node.get(PARENT_NODE);
225
oULs = oParentNode.get(PARENT_NODE).get(CHILDREN);
227
if (oULs.size() > 1) {
229
oUL = oParentNode.next();
232
oChildren = oUL.get(CHILDREN);
235
oChildren = oULs.item(0).get(CHILDREN);
240
oChildren = node.get(PARENT_NODE).get(CHILDREN);
243
oNext = oChildren.item(0);
252
var setARIARole = function (node, role) {
254
node.setAttribute("role", role);
259
var setARIAProperty = function (node, property, value) {
261
node.setAttribute(("aria-" + property), value);
266
var setARIAPresentation = function (node) {
268
setARIARole(node, "presentation");
273
var removeFromTabIndex = function (node) {
275
node.set(TAB_INDEX, -1);
280
var placeInDefaultTabIndex = function (node) {
282
node.set(TAB_INDEX, 0);
287
var isAnchor = function (node) {
289
var bReturnVal = FALSE;
292
bReturnVal = node.get("nodeName").toLowerCase() === LOWERCASE_A;
300
var isMenuItem = function (node) {
302
return node.hasClass(CSS_MENUITEM);
307
var isMenuLabel = function (node) {
309
return node.hasClass(CSS_MENU_LABEL);
314
var isHorizontalMenu = function (menu) {
316
return menu.hasClass(CSS_MENU_HORIZONTAL);
321
var hasVisibleSubmenu = function (menuLabel) {
323
return menuLabel.hasClass(CSS_MENU_LABEL_MENUVISIBLE);
328
var getItemAnchor = function (node) {
330
return isAnchor(node) ? node : node.query(LOWERCASE_A);
335
var getNodeWithClass = function (node, className, searchAncestors) {
341
if (node.hasClass(className)) {
345
if (!oItem && searchAncestors) {
346
oItem = node.ancestor((PERIOD + className));
356
var getParentMenu = function (node) {
358
return node.ancestor(MENU_SELECTOR);
363
var getMenu = function (node, searchAncestors) {
365
return getNodeWithClass(node, CSS_MENU, searchAncestors);
370
var getMenuItem = function (node, searchAncestors) {
375
oItem = getNodeWithClass(node, CSS_MENUITEM, searchAncestors);
383
var getMenuLabel = function (node, searchAncestors) {
389
if (searchAncestors) {
390
oItem = getNodeWithClass(node, CSS_MENU_LABEL, searchAncestors);
393
oItem = getNodeWithClass(node, CSS_MENU_LABEL) || node.query((PERIOD + CSS_MENU_LABEL));
403
var getItem = function (node, searchAncestors) {
408
oItem = getMenuItem(node, searchAncestors) || getMenuLabel(node, searchAncestors);
416
var getNextItem = function (item, previous) {
425
oItemLI = isMenuItem(item) ? item : item.get(PARENT_NODE);
427
oNextLI = previous ? getPreviousSibling(oItemLI) : getNextSibling(oItemLI);
429
oNextItem = getItem(oNextLI);
438
var getPreviousItem = function (item) {
440
return getNextItem(item, true);
445
var getFirstItem = function (menu) {
447
return getItem(menu.query("li"));
452
var getActiveClass = function (node) {
454
return isMenuItem(node) ? CSS_MENUITEM_ACTIVE : CSS_MENU_LABEL_ACTIVE;
459
var blurItem = function (item) {
465
oAnchor = getItemAnchor(item);
467
// TO DO: Remove once implemented in Node
478
var focusItem = function (item) {
484
oAnchor = getItemAnchor(item);
486
// TO DO: Remove once implemented in Node
497
var handleMouseOverForNode = function (node, target) {
499
return node && !node[HANDLED_MOUSEOVER] && (node === target || node.contains(target));
504
var handleMouseOutForNode = function (node, relatedTarget) {
506
return node && !node[HANDLED_MOUSEOUT] &&
507
(node !== relatedTarget && !node.contains(relatedTarget));
513
* The NodeMenuNav class is a plugin for a Node instance. The class is used via the
514
* <a href="Node.html#method_plug"><code>plug</code></a> method of Node and should not be
515
* instantiated directly.
519
var MenuNav = function (config) {
522
oRootMenu = config.owner,
534
bUseARIA = config.useARIA;
535
bAutoSubmenuDisplay = config.autoSubmenuDisplay;
536
nMouseOutHideDelay = config.mouseOutHideDelay;
539
// Enable ARIA for Firefox 3 and IE 8 by default since those are the two browsers
540
// that current support ARIA
542
menuNav._useARIA = Lang.isBoolean(bUseARIA) ?
543
bUseARIA : ((UA.gecko && UA.gecko >= 1.9) || (UA.ie && UA.ie >= 8));
546
menuNav._autoSubmenuDisplay =
547
Lang.isBoolean(bAutoSubmenuDisplay) ? bAutoSubmenuDisplay : TRUE;
549
menuNav._submenuShowDelay = config.submenuShowDelay || 250;
550
menuNav._submenuHideDelay = config.submenuHideDelay || 250;
552
menuNav._mouseOutHideDelay = Lang.isNumber(nMouseOutHideDelay) ? nMouseOutHideDelay : 750;
555
// Hide all visible submenus
557
oMenuNodes = oRootMenu.queryAll(MENU_SELECTOR);
560
oMenuNodes.addClass(CSS_MENU_HIDDEN);
564
oULs = oRootMenu.queryAll("ul:" + FIRST_OF_TYPE);
567
oULs.addClass(FIRST_OF_TYPE);
571
// Wire up all event handlers
574
oRootMenu.on("mouseover", menuNav._onMouseOver, menuNav);
575
oRootMenu.on("mouseout", menuNav._onMouseOut, menuNav);
576
oRootMenu.on("mousemove", menuNav._onMouseMove, menuNav);
577
oRootMenu.on(MOUSEDOWN, menuNav._toggleSubmenuDisplay, menuNav);
578
oRootMenu.on(KEYDOWN, menuNav._toggleSubmenuDisplay, menuNav);
579
oRootMenu.on(CLICK, menuNav._toggleSubmenuDisplay, menuNav);
580
oRootMenu.on("keypress", menuNav._onKeyPress, menuNav);
581
oRootMenu.on(KEYDOWN, menuNav._onKeyDown, menuNav);
583
oDocument = oRootMenu.get("ownerDocument");
585
oDocument.on(MOUSEDOWN, menuNav._onDocMouseDown, menuNav);
587
Y.on("focus", Y.bind(menuNav._onDocFocus, menuNav), oDocument);
589
menuNav._rootMenu = oRootMenu;
592
if (menuNav._useARIA) {
594
menuNav._applyARIA(oRootMenu);
596
oFirstItem = getFirstItem(oRootMenu);
600
placeInDefaultTabIndex(getItemAnchor(oFirstItem));
602
menuNav._firstItem = oFirstItem;
613
MenuNav.NS = "MenuNav";
617
* @property NodeMenuNav.SHIM_TEMPLATE_TITLE
618
* @description String representing the value for the <code>title</code> attribute for the shim used
619
* to prevent <code><select></code> elements from poking through menus in IE 6.
620
* @default "Menu Stacking Shim"
623
MenuNav.SHIM_TEMPLATE_TITLE = "Menu Stacking Shim";
627
* @property NodeMenuNav.SHIM_TEMPLATE
628
* @description String representing the HTML used to create the <code><iframe></code> shim
629
* used to prevent <code><select></code> elements from poking through menus in IE 6.
630
* @default "<iframe frameborder="0" tabindex="-1"
631
* class="yui-shim" title="Menu Stacking Shim"
632
* src="javascript:false;"></iframe>"
636
// <iframe> shim notes:
638
// 1) Need to set the "frameBorder" property to 0 to suppress the default <iframe> border in IE.
639
// (Setting the CSS "border" property alone doesn't suppress it.)
641
// 2) The "src" attribute of the <iframe> is set to "javascript:false;" so that it won't load a
642
// page inside it, preventing the secure/nonsecure warning in IE when using HTTPS.
644
// 3) Since the role of the <iframe> shim is completely presentational, its "tabindex" attribute
645
// is set to "-1" and its title attribute is set to "Menu Stacking Shim". Both strategies help
646
// users of screen readers to avoid mistakenly interacting with the <iframe> shim.
648
MenuNav.SHIM_TEMPLATE = '<iframe frameborder="0" tabindex="-1" class="' +
649
getClassName("shim") +
650
'" title="' + MenuNav.SHIM_TEMPLATE_TITLE +
651
'" src="javascript:false;"></iframe>';
654
MenuNav.prototype = {
656
// Protected properties
659
* @property _rootMenu
660
* @description Node instance representing the root menu in the MenuNav.
669
* @property _activeItem
670
* @description Node instance representing the MenuNav's active descendent - the menuitem or
671
* menu label the user is currently interacting with.
680
* @property _activeMenu
681
* @description Node instance representing the menu that is the parent of the MenuNav's
691
* @property _hasFocus
692
* @description Boolean indicating if the MenuNav has focus.
700
// In gecko-based browsers a mouseover and mouseout event will fire even
701
// if a DOM element moves out from under the mouse without the user actually
702
// moving the mouse. This bug affects MenuNav because the user can hit the
703
// Esc key to hide a menu, and if the mouse is over the menu when the
704
// user presses Esc, the _onMenuMouseOut handler will be called. To fix this
705
// bug the following flag (_blockMouseEvent) is used to block the code in the
706
// _onMenuMouseOut handler from executing.
709
* @property _blockMouseEvent
710
* @description Boolean indicating whether or not to handle the "mouseover" event.
715
_blockMouseEvent: FALSE,
719
* @property _currentMouseX
720
* @description Number representing the current x coordinate of the mouse inside the MenuNav.
729
* @property _movingToSubmenu
730
* @description Boolean indicating if the mouse is moving from a menu label to its
731
* corresponding submenu.
736
_movingToSubmenu: FALSE,
740
* @property _showSubmenuTimer
741
* @description Timer used to show a submenu.
746
_showSubmenuTimer: NULL,
750
* @property _hideSubmenuTimer
751
* @description Timer used to hide a submenu.
756
_hideSubmenuTimer: NULL,
760
* @property _hideAllSubmenusTimer
761
* @description Timer used to hide a all submenus.
766
_hideAllSubmenusTimer: NULL,
770
* @property _firstItem
771
* @description Node instance representing the first item (menuitem or menu label) in the root
781
* @property _autoSubmenuDisplay
782
* @description Boolean indicating if submenus are automatically made visible when the user
783
* mouses over the menu's items.
788
_autoSubmenuDisplay: TRUE,
796
* @description Returns a boolean indicating if the specified menu is the root menu in
799
* @param {Node} menu Node instance representing a menu.
800
* @return {Boolean} Boolean indicating if the specified menu is the root menu in the MenuNav.
802
_isRoot: function (menu) {
804
return this._rootMenu.compareTo(menu);
810
* @method _getTopmostSubmenu
811
* @description Returns the topmost submenu of a submenu hierarchy.
813
* @param {Node} menu Node instance representing a menu.
814
* @return {Node} Node instance representing a menu.
816
_getTopmostSubmenu: function (menu) {
819
oMenu = getParentMenu(menu),
826
else if (menuNav._isRoot(oMenu)) {
830
returnVal = menuNav._getTopmostSubmenu(oMenu);
839
* @method _clearActiveItem
840
* @description Clears the MenuNav's active descendent.
843
_clearActiveItem: function () {
846
oActiveItem = menuNav._activeItem;
850
oActiveItem.removeClass(getActiveClass(oActiveItem));
852
if (menuNav._useARIA) {
853
removeFromTabIndex(getItemAnchor(oActiveItem));
858
menuNav._activeItem = NULL;
864
* @method _setActiveItem
865
* @description Sets the specified menuitem or menu label as the MenuNav's active descendent.
867
* @param {Node} item Node instance representing a menuitem or menu label.
869
_setActiveItem: function (item) {
875
menuNav._clearActiveItem();
877
item.addClass(getActiveClass(item));
879
if (menuNav._useARIA) {
880
placeInDefaultTabIndex(getItemAnchor(item));
883
menuNav._activeItem = item;
892
* @description Focuses the specified menuitem or menu label.
894
* @param {Node} item Node instance representing a menuitem or menu label.
896
_focusItem: function (item) {
898
if (item && this._hasFocus) {
900
// Need to focus using a zero-second timeout to get Apple's VoiceOver to
901
// recognize that the focused item has changed
903
later(0, null, focusItem, item);
912
* @description Applies the ARIA Roles, States and Properties to the supplied menu.
914
* @param {Node} menu Node instance representing a menu.
916
_applyARIA: function (menu) {
919
bIsRoot = menuNav._isRoot(menu),
923
oMenuItemContentNodes,
929
setARIARole(menu, (bIsRoot ? "menubar" : MENU));
933
oMenuLabel = menu.previous();
934
oMenuToggle = oMenuLabel.query(PERIOD + getClassName(MENU, "toggle"));
937
oMenuLabel = oMenuToggle;
940
sID = oMenuLabel.get(ID);
944
oMenuLabel.set(ID, sID);
947
setARIAProperty(menu, "labelledby", sID);
948
setARIAProperty(menu, HIDDEN, TRUE);
953
oListNodes = menu.queryAll("ul,li");
957
oListNodes.each(function (node) {
959
setARIAPresentation(node);
966
oMenuItemContentNodes = menu.queryAll((PERIOD + getClassName(MENUITEM, "content")));
968
if (oMenuItemContentNodes) {
970
oMenuItemContentNodes.each(function (node) {
972
removeFromTabIndex(node);
973
setARIARole(node, MENUITEM);
980
oMenuLabelNodes = menu.queryAll((PERIOD + CSS_MENU_LABEL));
982
if (oMenuLabelNodes) {
984
oMenuLabelNodes.each(function (node) {
987
oMenuToggle = node.query((PERIOD + getClassName(MENU, "toggle")));
991
setARIAPresentation(oMenuToggle);
992
removeFromTabIndex(oMenuToggle);
994
oMenuLabel = oMenuToggle.previous();
998
setARIARole(oMenuLabel, MENUITEM);
999
setARIAProperty(oMenuLabel, "haspopup", TRUE);
1000
removeFromTabIndex(oMenuLabel);
1002
oSubmenu = node.next();
1005
menuNav._applyARIA(oSubmenu);
1017
* @description Shows the specified menu.
1019
* @param {Node} menu Node instance representing a menu.
1021
_showMenu: function (menu) {
1024
oParentMenu = getParentMenu(menu),
1025
oLI = menu.get(PARENT_NODE),
1029
if (menuNav._useARIA) {
1030
setARIAProperty(menu, HIDDEN, FALSE);
1033
if (isHorizontalMenu(oParentMenu)) {
1034
aXY[1] = aXY[1] + oLI.get(OFFSET_HEIGHT);
1037
aXY[0] = aXY[0] + oLI.get(OFFSET_WIDTH);
1044
if (UA.ie === 6 && !menu.hasIFrameShim) {
1046
menu.appendChild(Y.Node.create(MenuNav.SHIM_TEMPLATE));
1047
menu.hasIFrameShim = TRUE;
1051
// Clear previous values for height and width
1053
menu.setStyles({ height: EMPTY_STRING, width: EMPTY_STRING });
1055
// Set the width and height of the menu's bounding box - this is necessary for IE 6
1056
// so that the CSS for the <iframe> shim can simply set the <iframe>'s width and height
1057
// to 100% to ensure that dimensions of an <iframe> shim are always sync'd to the
1058
// that of its parent menu. Specifying a width and height also helps when positioning
1059
// decorator elements (for creating effects like rounded corners) inside a menu's
1060
// bounding box in IE 7.
1063
height: (menu.get(OFFSET_HEIGHT) + PX),
1064
width: (menu.get(OFFSET_WIDTH) + PX) });
1068
menu.previous().addClass(CSS_MENU_LABEL_MENUVISIBLE);
1069
menu.removeClass(CSS_MENU_HIDDEN);
1071
oItem = getFirstItem(menu);
1073
menuNav._focusItem(oItem);
1080
* @description Hides the specified menu.
1082
* @param {Node} menu Node instance representing a menu.
1083
* @param {Boolean} activateAndFocusLabel Boolean indicating if the label for the specified
1084
* menu should be focused and set as active.
1086
_hideMenu: function (menu, activateAndFocusLabel) {
1089
oLabel = menu.previous(),
1092
oLabel.removeClass(CSS_MENU_LABEL_MENUVISIBLE);
1095
if (activateAndFocusLabel) {
1096
menuNav._setActiveItem(oLabel);
1097
menuNav._focusItem(oLabel);
1100
oActiveItem = menu.query((PERIOD + CSS_MENUITEM_ACTIVE));
1103
oActiveItem.removeClass(CSS_MENUITEM_ACTIVE);
1106
// Clear the values for top and left that were set by the call to "setXY" when the menu
1107
// was shown so that the hidden position specified in the core CSS file will take affect.
1109
menu.setStyles({ left: EMPTY_STRING, top: EMPTY_STRING });
1111
menu.addClass(CSS_MENU_HIDDEN);
1112
setARIAProperty(menu, HIDDEN, TRUE);
1118
* @method _hideAllSubmenus
1119
* @description Hides all submenus of the specified menu.
1121
* @param {Node} menu Node instance representing a menu.
1123
_hideAllSubmenus: function (menu) {
1126
oSubmenus = menu.queryAll(MENU_SELECTOR);
1130
oSubmenus.each(Y.bind(function (submenuNode) {
1132
menuNav._hideMenu(submenuNode);
1142
* @method _cancelShowSubmenuTimer
1143
* @description Cancels the timer used to show a submenu.
1146
_cancelShowSubmenuTimer: function () {
1149
oShowSubmenuTimer = menuNav._showSubmenuTimer;
1151
if (oShowSubmenuTimer) {
1152
oShowSubmenuTimer.cancel();
1153
menuNav._showSubmenuTimer = NULL;
1160
* @method _cancelHideSubmenuTimer
1161
* @description Cancels the timer used to hide a submenu.
1164
_cancelHideSubmenuTimer: function () {
1167
oHideSubmenuTimer = menuNav._hideSubmenuTimer;
1170
if (oHideSubmenuTimer) {
1171
oHideSubmenuTimer.cancel();
1172
menuNav._hideSubmenuTimer = NULL;
1179
// Event handlers for discrete pieces of pieces of the menu
1183
* @method _onMenuMouseOver
1184
* @description "mouseover" event handler for a menu.
1186
* @param {Node} menu Node instance representing a menu.
1187
* @param {Object} event Object representing the DOM event.
1189
_onMenuMouseOver: function (menu, event) {
1192
oHideAllSubmenusTimer = menuNav._hideAllSubmenusTimer;
1194
if (oHideAllSubmenusTimer) {
1195
oHideAllSubmenusTimer.cancel();
1196
menuNav._hideAllSubmenusTimer = NULL;
1199
menuNav._cancelHideSubmenuTimer();
1201
menuNav._activeMenu = menu;
1204
if (menuNav._movingToSubmenu && isHorizontalMenu(menu)) {
1205
menuNav._movingToSubmenu = FALSE;
1212
* @method _onMenuMouseOut
1213
* @description "mouseout" event handler for a menu.
1215
* @param {Node} menu Node instance representing a menu.
1216
* @param {Object} event Object representing the DOM event.
1218
_onMenuMouseOut: function (menu, event) {
1221
oActiveMenu = menuNav._activeMenu,
1222
oRelatedTarget = event.relatedTarget,
1223
oActiveItem = menuNav._activeItem,
1228
if (oActiveMenu && !oActiveMenu.contains(oRelatedTarget)) {
1230
oParentMenu = getParentMenu(oActiveMenu);
1233
if (oParentMenu && !oParentMenu.contains(oRelatedTarget)) {
1235
if (menuNav._mouseOutHideDelay > 0) {
1237
menuNav._cancelShowSubmenuTimer();
1239
menuNav._hideAllSubmenusTimer =
1241
later(menuNav._mouseOutHideDelay, menuNav, function () {
1245
oActiveMenu = menuNav._activeMenu;
1247
menuNav._hideAllSubmenus(menuNav._rootMenu);
1251
// Focus the label element for the topmost submenu
1252
oSubmenu = menuNav._getTopmostSubmenu(oActiveMenu);
1253
menuNav._focusItem(oSubmenu.previous());
1266
oMenu = getParentMenu(oActiveItem);
1268
if (!menuNav._isRoot(oMenu)) {
1269
menuNav._focusItem(oMenu.previous());
1282
* @method _onMenuLabelMouseOver
1283
* @description "mouseover" event handler for a menu label.
1285
* @param {Node} menuLabel Node instance representing a menu label.
1286
* @param {Object} event Object representing the DOM event.
1288
_onMenuLabelMouseOver: function (menuLabel, event) {
1291
oActiveMenu = menuNav._activeMenu,
1292
bIsRoot = menuNav._isRoot(oActiveMenu),
1293
bUseAutoSubmenuDisplay = (menuNav._autoSubmenuDisplay && bIsRoot || !bIsRoot),
1297
menuNav._setActiveItem(menuLabel);
1298
menuNav._focusItem(menuLabel);
1301
if (bUseAutoSubmenuDisplay && !menuNav._movingToSubmenu) {
1303
menuNav._cancelHideSubmenuTimer();
1304
menuNav._cancelShowSubmenuTimer();
1307
if (!hasVisibleSubmenu(menuLabel)) {
1309
oSubmenu = menuLabel.next();
1314
menuNav._hideAllSubmenus(oActiveMenu);
1316
menuNav._showSubmenuTimer =
1317
later(menuNav._submenuShowDelay, menuNav,
1318
menuNav._showMenu, oSubmenu);
1330
* @method _onMenuLabelMouseOut
1331
* @description "mouseout" event handler for a menu label.
1333
* @param {Node} menuLabel Node instance representing a menu label.
1334
* @param {Object} event Object representing the DOM event.
1336
_onMenuLabelMouseOut: function (menuLabel, event) {
1339
bIsRoot = menuNav._isRoot(menuNav._activeMenu),
1340
bUseAutoSubmenuDisplay = (menuNav._autoSubmenuDisplay && bIsRoot || !bIsRoot),
1341
oRelatedTarget = event.relatedTarget,
1342
oSubmenu = menuLabel.next();
1344
menuNav._clearActiveItem();
1346
if (bUseAutoSubmenuDisplay) {
1348
if (menuNav._movingToSubmenu && !menuNav._showSubmenuTimer && oSubmenu) {
1350
// If the mouse is moving diagonally toward the submenu and another submenu
1351
// isn't in the process of being displayed (via a timer), then hide the submenu
1352
// via a timer to give the user some time to reach the submenu.
1354
menuNav._hideSubmenuTimer = later(menuNav._submenuHideDelay, menuNav,
1355
menuNav._hideMenu, oSubmenu);
1358
else if (!menuNav._movingToSubmenu && oSubmenu &&
1359
!oSubmenu.contains(oRelatedTarget) && oRelatedTarget !== oSubmenu) {
1361
// If the mouse is not moving toward the submenu, cancel any submenus that
1362
// might be in the process of being displayed (via a timer) and hide this
1363
// submenu immediately.
1365
menuNav._cancelShowSubmenuTimer();
1367
menuNav._hideMenu(oSubmenu);
1377
* @method _onMenuItemMouseOver
1378
* @description "mouseover" event handler for a menuitem.
1380
* @param {Node} menuItem Node instance representing a menuitem.
1381
* @param {Object} event Object representing the DOM event.
1383
_onMenuItemMouseOver: function (menuItem, event) {
1386
oActiveMenu = menuNav._activeMenu,
1387
bIsRoot = menuNav._isRoot(oActiveMenu),
1388
bUseAutoSubmenuDisplay = (menuNav._autoSubmenuDisplay && bIsRoot || !bIsRoot);
1391
menuNav._setActiveItem(menuItem);
1392
menuNav._focusItem(menuItem);
1395
if (bUseAutoSubmenuDisplay && !menuNav._movingToSubmenu) {
1397
menuNav._hideAllSubmenus(oActiveMenu);
1405
* @method _onMenuItemMouseOut
1406
* @description "mouseout" event handler for a menuitem.
1408
* @param {Node} menuItem Node instance representing a menuitem.
1409
* @param {Object} event Object representing the DOM event.
1411
_onMenuItemMouseOut: function (menuItem, event) {
1413
this._clearActiveItem();
1419
* @method _onVerticalMenuKeyDown
1420
* @description "keydown" event handler for vertical menus of a MenuNav.
1422
* @param {Object} event Object representing the DOM event.
1424
_onVerticalMenuKeyDown: function (event) {
1427
oActiveMenu = menuNav._activeMenu,
1428
oRootMenu = menuNav._rootMenu,
1429
oTarget = event.target,
1430
oFocusedItem = getItem(oTarget, TRUE),
1431
bPreventDefault = FALSE,
1432
nKeyCode = event.keyCode,
1442
case 37: // left arrow
1444
oParentMenu = getParentMenu(oActiveMenu);
1446
if (oParentMenu && isHorizontalMenu(oParentMenu)) {
1448
menuNav._hideMenu(oActiveMenu);
1449
oLI = getPreviousSibling(oActiveMenu.get(PARENT_NODE));
1450
oItem = getItem(oLI);
1454
if (isMenuLabel(oItem)) { // Menu label
1456
oSubmenu = oItem.next();
1461
menuNav._showMenu(oSubmenu);
1462
menuNav._setActiveItem(getFirstItem(oSubmenu));
1467
menuNav._setActiveItem(oItem);
1475
menuNav._setActiveItem(oItem);
1483
else if (!menuNav._isRoot(oActiveMenu)) {
1484
menuNav._hideMenu(oActiveMenu, TRUE);
1488
bPreventDefault = TRUE;
1492
case 39: // right arrow
1494
if (isMenuLabel(oTarget)) {
1496
oSubmenu = oTarget.next();
1499
menuNav._showMenu(oSubmenu);
1500
menuNav._setActiveItem(getFirstItem(oSubmenu));
1504
else if (isHorizontalMenu(oRootMenu)) {
1506
oSubmenu = menuNav._getTopmostSubmenu(oActiveMenu);
1507
oLI = getNextSibling(oSubmenu.get(PARENT_NODE));
1508
oItem = getItem(oLI);
1510
menuNav._hideAllSubmenus(oRootMenu);
1514
if (isMenuLabel(oItem)) { // Menu label
1516
oSubmenu = oItem.next();
1520
menuNav._showMenu(oSubmenu);
1521
menuNav._setActiveItem(getFirstItem(oSubmenu));
1526
menuNav._setActiveItem(oItem);
1534
menuNav._setActiveItem(oItem);
1543
bPreventDefault = TRUE;
1547
case 38: // up arrow
1548
case 40: // down arrow
1550
menuNav._hideAllSubmenus(oActiveMenu);
1552
oNextItem = nKeyCode === 38 ?
1553
getPreviousItem(oFocusedItem) : getNextItem(oFocusedItem);
1555
menuNav._setActiveItem(oNextItem);
1556
focusItem(oNextItem);
1558
bPreventDefault = TRUE;
1565
if (bPreventDefault) {
1567
// Prevent the browser from scrolling the window
1569
event.preventDefault();
1577
* @method _onHorizontalMenuKeyDown
1578
* @description "keydown" event handler for horizontal menus of a MenuNav.
1580
* @param {Object} event Object representing the DOM event.
1582
_onHorizontalMenuKeyDown: function (event) {
1585
oActiveMenu = menuNav._activeMenu,
1586
oTarget = event.target,
1587
oFocusedItem = getItem(oTarget, TRUE),
1588
bPreventDefault = FALSE,
1589
nKeyCode = event.keyCode,
1595
case 37: // left arrow
1596
case 39: // right arrow
1598
menuNav._hideAllSubmenus(oActiveMenu);
1600
oNextItem = nKeyCode === 37 ?
1601
getPreviousItem(oFocusedItem) : getNextItem(oFocusedItem);
1603
menuNav._setActiveItem(oNextItem);
1604
focusItem(oNextItem);
1606
bPreventDefault = TRUE;
1610
case 40: // down arrow
1612
menuNav._hideAllSubmenus(oActiveMenu);
1614
if (isMenuLabel(oFocusedItem)) {
1616
oSubmenu = oFocusedItem.next();
1619
menuNav._showMenu(oSubmenu);
1620
menuNav._setActiveItem(getFirstItem(oSubmenu));
1623
bPreventDefault = TRUE;
1632
if (bPreventDefault) {
1634
// Prevent the browser from scrolling the window
1636
event.preventDefault();
1643
// Generic DOM Event handlers
1647
* @method _onMouseMove
1648
* @description "mousemove" event handler for the MenuNav.
1650
* @param {Object} event Object representing the DOM event.
1652
_onMouseMove: function (event) {
1656
// Using a timer to set the value of the "_currentMouseX" property helps improve the
1657
// reliability of the calculation used to set the value of the "_movingToSubmenu"
1658
// property - especially in Opera.
1660
later(10, menuNav, function () {
1662
menuNav._currentMouseX = event.pageX;
1670
* @method _onMouseOver
1671
* @description "mouseover" event handler for the MenuNav.
1673
* @param {Object} event Object representing the DOM event.
1675
_onMouseOver: function (event) {
1685
if (menuNav._blockMouseEvent) {
1686
menuNav._blockMouseEvent = FALSE;
1690
oTarget = event.target;
1691
oMenu = getMenu(oTarget, TRUE);
1692
oMenuLabel = getMenuLabel(oTarget, TRUE);
1693
oMenuItem = getMenuItem(oTarget, TRUE);
1696
if (handleMouseOverForNode(oMenu, oTarget)) {
1698
menuNav._onMenuMouseOver(oMenu, event);
1700
oMenu[HANDLED_MOUSEOVER] = TRUE;
1701
oMenu[HANDLED_MOUSEOUT] = FALSE;
1703
oParentMenu = getParentMenu(oMenu);
1707
oParentMenu[HANDLED_MOUSEOUT] = TRUE;
1708
oParentMenu[HANDLED_MOUSEOVER] = FALSE;
1714
if (handleMouseOverForNode(oMenuLabel, oTarget)) {
1716
menuNav._onMenuLabelMouseOver(oMenuLabel, event);
1718
oMenuLabel[HANDLED_MOUSEOVER] = TRUE;
1719
oMenuLabel[HANDLED_MOUSEOUT] = FALSE;
1723
if (handleMouseOverForNode(oMenuItem, oTarget)) {
1725
menuNav._onMenuItemMouseOver(oMenuItem, event);
1727
oMenuItem[HANDLED_MOUSEOVER] = TRUE;
1728
oMenuItem[HANDLED_MOUSEOUT] = FALSE;
1738
* @method _onMouseOut
1739
* @description "mouseout" event handler for the MenuNav.
1741
* @param {Object} event Object representing the DOM event.
1743
_onMouseOut: function (event) {
1746
oActiveMenu = menuNav._activeMenu,
1747
bMovingToSubmenu = FALSE,
1756
menuNav._movingToSubmenu = (oActiveMenu && !isHorizontalMenu(oActiveMenu) &&
1757
((event.pageX - 5) > menuNav._currentMouseX));
1759
oTarget = event.target;
1760
oRelatedTarget = event.relatedTarget;
1761
oMenu = getMenu(oTarget, TRUE);
1762
oMenuLabel = getMenuLabel(oTarget, TRUE);
1763
oMenuItem = getMenuItem(oTarget, TRUE);
1766
if (handleMouseOutForNode(oMenuLabel, oRelatedTarget)) {
1768
menuNav._onMenuLabelMouseOut(oMenuLabel, event);
1770
oMenuLabel[HANDLED_MOUSEOUT] = TRUE;
1771
oMenuLabel[HANDLED_MOUSEOVER] = FALSE;
1775
if (handleMouseOutForNode(oMenuItem, oRelatedTarget)) {
1777
menuNav._onMenuItemMouseOut(oMenuItem, event);
1779
oMenuItem[HANDLED_MOUSEOUT] = TRUE;
1780
oMenuItem[HANDLED_MOUSEOVER] = FALSE;
1787
oSubmenu = oMenuLabel.next();
1789
if (oSubmenu && (oRelatedTarget === oSubmenu || oSubmenu.contains(oRelatedTarget))) {
1791
bMovingToSubmenu = TRUE;
1798
if (handleMouseOutForNode(oMenu, oRelatedTarget) || bMovingToSubmenu) {
1800
menuNav._onMenuMouseOut(oMenu, event);
1802
oMenu[HANDLED_MOUSEOUT] = TRUE;
1803
oMenu[HANDLED_MOUSEOVER] = FALSE;
1811
* @method _toggleSubmenuDisplay
1812
* @description "mousedown," "keydown," and "click" event handler for the MenuNav used to
1813
* toggle the display of a submenu.
1815
* @param {Object} event Object representing the DOM event.
1817
_toggleSubmenuDisplay: function (event) {
1820
oTarget = event.target,
1821
oMenuLabel = getMenuLabel(oTarget, TRUE),
1833
oAnchor = isAnchor(oTarget) ? oTarget : oTarget.ancestor(isAnchor);
1838
// Need to pass "2" as a second argument to "getAttribute" for IE otherwise IE
1839
// will return a fully qualified URL for the value of the "href" attribute.
1840
// http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx
1842
sHref = oAnchor.getAttribute("href", 2);
1843
nHashPos = sHref.indexOf("#");
1844
nLen = sHref.length;
1846
if (nHashPos === 0 && nLen > 1) {
1848
sId = sHref.substr(1, nLen);
1849
oSubmenu = oMenuLabel.next();
1851
if (oSubmenu && (oSubmenu.get(ID) === sId)) {
1854
if (sType === MOUSEDOWN || (sType === KEYDOWN && event.keyCode === 13)) {
1856
// The call to "preventDefault" below results in the element
1857
// serving as the menu's label to not receive focus in Webkit,
1858
// therefore the "_hasFocus" flag never gets set to true, meaning the
1859
// first item in the submenu isn't focused when the submenu is
1860
// displayed. To fix this issue, it is necessary to set the
1861
// "_hasFocus" flag to true.
1863
if (UA.webkit && !menuNav._hasFocus) {
1864
menuNav._hasFocus = TRUE;
1868
if (hasVisibleSubmenu(oMenuLabel)) {
1869
menuNav._hideMenu(oSubmenu);
1870
focusItem(oMenuLabel);
1873
menuNav._hideAllSubmenus(menuNav._rootMenu);
1874
menuNav._showMenu(oSubmenu);
1880
if (sType === CLICK) {
1882
// Prevent the browser from following the URL of the anchor element
1884
event.preventDefault();
1901
* @method _onKeyPress
1902
* @description "keypress" event handler for the MenuNav.
1904
* @param {Object} event Object representing the DOM event.
1906
_onKeyPress: function (event) {
1908
switch (event.keyCode) {
1910
case 37: // left arrow
1911
case 38: // up arrow
1912
case 39: // right arrow
1913
case 40: // down arrow
1915
// Prevent the browser from scrolling the window
1917
event.preventDefault();
1927
* @method _onKeyDown
1928
* @description "keydown" event handler for the MenuNav.
1930
* @param {Object} event Object representing the DOM event.
1932
_onKeyDown: function (event) {
1935
oActiveItem = menuNav._activeItem,
1936
oTarget = event.target,
1937
oActiveMenu = getParentMenu(oTarget),
1942
menuNav._activeMenu = oActiveMenu;
1944
if (isHorizontalMenu(oActiveMenu)) {
1945
menuNav._onHorizontalMenuKeyDown(event);
1948
menuNav._onVerticalMenuKeyDown(event);
1952
if (event.keyCode === 27) {
1954
if (!menuNav._isRoot(oActiveMenu)) {
1956
menuNav._hideMenu(oActiveMenu, TRUE);
1957
event.stopPropagation();
1958
menuNav._blockMouseEvent = UA.gecko ? TRUE : FALSE;
1961
else if (oActiveItem) {
1963
if (isMenuLabel(oActiveItem) && hasVisibleSubmenu(oActiveItem)) {
1965
oSubmenu = oActiveItem.next();
1968
menuNav._hideMenu(oSubmenu);
1975
menuNav._clearActiveItem();
1989
* @method _onDocMouseDown
1990
* @description "mousedown" event handler for the owner document of the MenuNav.
1992
* @param {Object} event Object representing the DOM event.
1994
_onDocMouseDown: function (event) {
1997
oRoot = menuNav._rootMenu,
1998
oTarget = event.target;
2001
if (!oRoot.compareTo(oTarget) && !oRoot.contains(oTarget)) {
2003
menuNav._hideAllSubmenus(oRoot);
2006
// Document doesn't receive focus in Webkit when the user mouses down on it,
2007
// so the "_hasFocus" property won't get set to the correct value. The
2008
// following line corrects the problem.
2011
menuNav._hasFocus = FALSE;
2012
menuNav._clearActiveItem();
2021
* @method _onDocFocus
2022
* @description "focus" event handler for the owner document of the MenuNav.
2024
* @param {Object} event Object representing the DOM event.
2026
_onDocFocus: function (event) {
2029
bUseARIA = menuNav._useARIA,
2030
oFirstItem = menuNav._firstItem,
2031
oActiveItem = menuNav._activeItem,
2032
oTarget = event.target;
2035
if (menuNav._rootMenu.contains(oTarget)) { // The menu has focus
2037
if (!menuNav._hasFocus) { // Initial focus
2039
// First time the menu has been focused, need to setup focused state and
2040
// established active active descendant
2042
menuNav._hasFocus = TRUE;
2044
oActiveItem = getItem(oTarget, TRUE);
2047
menuNav._setActiveItem(oActiveItem);
2053
else { // The menu has lost focus
2055
menuNav._clearActiveItem();
2057
menuNav._hasFocus = FALSE;
2060
if (oFirstItem && bUseARIA) {
2062
placeInDefaultTabIndex(getItemAnchor(oFirstItem));
2073
Y.namespace('plugin');
2075
Y.plugin.NodeMenuNav = MenuNav;
2078
}, '3.0.0pr2' ,{requires:['node', 'classnamemanager']});