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