1/* 2Copyright © 2013 Adobe Systems Incorporated. 3 4Licensed under the Apache License, Version 2.0 (the âLicenseâ); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an âAS ISâ BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17/** 18 * See <a href="http://jquery.com">http://jquery.com</a>. 19 * @name jquery 20 * @class 21 * See the jQuery Library (<a href="http://jquery.com">http://jquery.com</a>) for full details. This just 22 * documents the function and classes that are added to jQuery by this plug-in. 23 */ 24 25/** 26 * See <a href="http://jquery.com">http://jquery.com</a> 27 * @name fn 28 * @class 29 * See the jQuery Library (<a href="http://jquery.com">http://jquery.com</a>) for full details. This just 30 * documents the function and classes that are added to jQuery by this plug-in. 31 * @memberOf jquery 32 */ 33 34/** 35 * @fileOverview accessibleMegaMenu plugin 36 * 37 *<p>Licensed under the Apache License, Version 2.0 (the âLicenseâ) 38 *<br />Copyright © 2013 Adobe Systems Incorporated. 39 *<br />Project page <a href="https://github.com/adobe-accessibility/Accessible-Mega-Menu">https://github.com/adobe-accessibility/Accessible-Mega-Menu</a> 40 * @version 0.1 41 * @author Michael Jordan 42 * @requires jquery 43 */ 44 45/*jslint browser: true, devel: true, plusplus: true, nomen: true */ 46/*global jQuery, window, document */ 47(function ($, window, document) { 48 "use strict"; 49 var pluginName = "accessibleMegaMenu", 50 defaults = { 51 uuidPrefix: "accessible-megamenu", // unique ID's are required to indicate aria-owns, aria-controls and aria-labelledby 52 menuClass: "accessible-megamenu", // default css class used to define the megamenu styling 53 topNavItemClass: "accessible-megamenu-top-nav-item", // default css class for a top-level navigation item in the megamenu 54 panelClass: "accessible-megamenu-panel", // default css class for a megamenu panel 55 panelGroupClass: "accessible-megamenu-panel-group", // default css class for a group of items within a megamenu panel 56 hoverClass: "hover", // default css class for the hover state 57 focusClass: "focus", // default css class for the focus state 58 openClass: "open", // default css class for the open state, 59 toggleButtonClass: "accessible-megamenu-toggle", // default css class responsive toggle button 60 openDelay: 0, // default open delay when opening menu via mouseover 61 closeDelay: 250, // default open delay when opening menu via mouseover 62 openOnMouseover: false // default setting for whether menu should open on mouseover 63 }, 64 Keyboard = { 65 BACKSPACE: 8, 66 COMMA: 188, 67 DELETE: 46, 68 DOWN: 40, 69 END: 35, 70 ENTER: 13, 71 ESCAPE: 27, 72 HOME: 36, 73 LEFT: 37, 74 PAGE_DOWN: 34, 75 PAGE_UP: 33, 76 PERIOD: 190, 77 RIGHT: 39, 78 SPACE: 32, 79 TAB: 9, 80 UP: 38, 81 keyMap: { 82 48: "0", 83 49: "1", 84 50: "2", 85 51: "3", 86 52: "4", 87 53: "5", 88 54: "6", 89 55: "7", 90 56: "8", 91 57: "9", 92 59: ";", 93 65: "a", 94 66: "b", 95 67: "c", 96 68: "d", 97 69: "e", 98 70: "f", 99 71: "g", 100 72: "h", 101 73: "i", 102 74: "j", 103 75: "k", 104 76: "l", 105 77: "m", 106 78: "n", 107 79: "o", 108 80: "p", 109 81: "q", 110 82: "r", 111 83: "s", 112 84: "t", 113 85: "u", 114 86: "v", 115 87: "w", 116 88: "x", 117 89: "y", 118 90: "z", 119 96: "0", 120 97: "1", 121 98: "2", 122 99: "3", 123 100: "4", 124 101: "5", 125 102: "6", 126 103: "7", 127 104: "8", 128 105: "9", 129 190: "." 130 } 131 }, 132 clearTimeout = window.clearTimeout, 133 setTimeout = window.setTimeout, 134 isOpera = window.opera && window.opera.toString() === '[object Opera]'; 135 /**
136 * @desc Creates a new accessible mega menu instance. 137 * @param {jquery} element 138 * @param {object} [options] Mega Menu options 139 * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby 140 * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling 141 * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu 142 * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel 143 * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel 144 * @param {string} [options.hoverClass=hover] - CSS class for the hover state 145 * @param {string} [options.focusClass=focus] - CSS class for the focus state 146 * @param {string} [options.openClass=open] - CSS class for the open state 147 * @constructor 148 */ 149 function AccessibleMegaMenu(element, options) { 150 this.element = element; 151 152 // merge optional settings and defaults into settings 153 this.settings = $.extend({}, defaults, options); 154 155 this._defaults = defaults; 156 this._name = pluginName; 157 158 this.mouseTimeoutID = null; 159 this.focusTimeoutID = null; 160 this.mouseFocused = false; 161 this.justFocused = false; 162 163 this.init(); 164 } 165 166 AccessibleMegaMenu.prototype = (function () { 167 168 /* private attributes and methods ------------------------ */ 169 var uuid = 0, 170 keydownTimeoutDuration = 1000, 171 keydownSearchString = "", 172 isTouch = 'ontouchstart' in window || window.navigator.msMaxTouchPoints, 173 _getPlugin, 174 _addUniqueId, 175 _togglePanel, 176 _clickHandler, 177 _touchmoveHandler, 178 _clickOutsideHandler, 179 _DOMAttrModifiedHandler, 180 _focusInHandler, 181 _focusOutHandler, 182 _keyDownHandler, 183 _mouseDownHandler, 184 _mouseOverHandler, 185 _mouseOutHandler, 186 _clickToggleHandler, 187 _toggleExpandedEventHandlers, 188 _addEventHandlers, 189 _removeEventHandlers; 190 191 /** 192 * @name jQuery.fn.accessibleMegaMenu~_getPlugin 193 * @desc Returns the parent accessibleMegaMenu instance for a given element 194 * @param {jQuery} element 195 * @memberof jQuery.fn.accessibleMegaMenu 196 * @inner 197 * @private 198 */ 199 _getPlugin = function (element) { 200 return $(element).closest(':data(plugin_' + pluginName + ')').data("plugin_" + pluginName); 201 }; 202 203 /** 204 * @name jQuery.fn.accessibleMegaMenu~_addUniqueId 205 * @desc Adds a unique id and element. 206 * The id string starts with the 207 * string defined in settings.uuidPrefix. 208 * @param {jQuery} element 209 * @memberof jQuery.fn.accessibleMegaMenu 210 * @inner 211 * @private 212 */ 213 _addUniqueId = function (element) { 214 element = $(element); 215 var settings = this.settings; 216 if (!element.attr("id")) { 217 element.attr("id", settings.uuidPrefix + "-" + new Date().getTime() + "-" + (++uuid)); 218 } 219 }; 220 221 /** 222 * @name jQuery.fn.accessibleMegaMenu~_togglePanel 223 * @desc Toggle the display of mega menu panels in response to an event. 224 * The optional boolean value 'hide' forces all panels to hide. 225 * @param {event} event 226 * @param {Boolean} [hide] Hide all mega menu panels when true 227 * @memberof jQuery.fn.accessibleMegaMenu 228 * @inner 229 * @private 230 */ 231 _togglePanel = function (event, hide) { 232 var target = $(event.target), 233 that = this, 234 settings = this.settings, 235 menu = this.menu, 236 topli = target.closest('.' + settings.topNavItemClass), 237 panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass), 238 newfocus; 239 240 _toggleExpandedEventHandlers.call(this, true); 241 242 // Hide all panels. 243 if (hide) { 244 // Get the first top level menu item. 245 topli = menu.find('.' + settings.topNavItemClass + ' .' + settings.openClass + ':first').closest('.' + settings.topNavItemClass); 246 // Validate event. 247 if (!(topli.is(event.relatedTarget) || topli.has(event.relatedTarget).length > 0)) { 248 if ((event.type === 'mouseout' || event.type === 'focusout') && topli.has(document.activeElement).length > 0) { 249 return; 250 } 251 252 // Close top level link. 253 topli.find('[aria-expanded]') 254 .attr('aria-expanded', 'false') 255 .removeClass(settings.openClass); 256 // Close panel. 257 topli.find('.' + settings.panelClass) 258 .removeClass(settings.openClass) 259 .attr('aria-hidden', 'true'); 260 261 if ((event.type === 'keydown' && event.keyCode === Keyboard.ESCAPE) || event.type === 'DOMAttrModified') { 262 newfocus = topli.find(':tabbable:first'); 263 setTimeout(function () { 264 menu.find('[aria-expanded].' + that.settings.panelClass).off('DOMAttrModified.accessible-megamenu'); 265 newfocus.focus(); 266 that.justFocused = false;
267 }, 99); 268 } 269 } else if (topli.length === 0) { 270 menu.find('[aria-expanded=true]') 271 .attr('aria-expanded', 'false') 272 .removeClass(settings.openClass) 273 .closest('.' + settings.topNavItemClass) 274 .find('.' + settings.panelClass) 275 .removeClass(settings.openClass) 276 .attr('aria-hidden', 'true'); 277 } 278 } 279 // Toggle panels. 280 else { 281 clearTimeout(that.focusTimeoutID); 282 // Close previously open top level link and its panel. 283 var openli = menu.find('[aria-expanded=true]').closest('.' + settings.topNavItemClass); 284 if (!openli.is(topli)) { 285 openli.find('[aria-expanded]') 286 .attr('aria-expanded', 'false') 287 .removeClass(settings.openClass); 288 openli.find('.' + settings.panelClass) 289 .removeClass(settings.openClass) 290 .attr('aria-hidden', 'true'); 291 } 292 // Open current top level link and its panel. 293 topli.find('[aria-expanded]') 294 .attr('aria-expanded', 'true') 295 .addClass(settings.openClass); 296 topli.find('.' + settings.panelClass) 297 .addClass(settings.openClass) 298 .attr('aria-hidden', 'false'); 299 300 if (topli.length) { 301 var pageScrollPosition = $('html')[0].scrollTop; 302 var openPanelTopPosition = topli.offset().top; 303 304 if (pageScrollPosition > openPanelTopPosition) { 305 $('html')[0].scrollTop = openPanelTopPosition; 306 } 307 } 308 309 if (event.type === 'mouseover' && target.is(':tabbable') && topli.length === 1 && panel.length === 0 && menu.has(document.activeElement).length > 0) { 310 target.focus(); 311 that.justFocused = false; 312 } 313 314 _toggleExpandedEventHandlers.call(that); 315 } 316 }; 317 318 /** 319 * @name jQuery.fn.accessibleMegaMenu~_clickHandler 320 * @desc Handle click event on mega menu item 321 * @param {event} Event object 322 * @memberof jQuery.fn.accessibleMegaMenu 323 * @inner 324 * @private 325 */ 326 _clickHandler = function (event) { 327 var target = $(event.target).closest(':tabbable'), 328 topli = target.closest('.' + this.settings.topNavItemClass), 329 panel = target.closest('.' + this.settings.panelClass);
330 // With panel. 331 if (topli.length === 1 && panel.length === 0 && topli.find('.' + this.settings.panelClass).length === 1) { 332 // Handle click event. 333 if (!target.hasClass(this.settings.openClass)) { 334 event.preventDefault(); 335 event.stopPropagation(); 336 _togglePanel.call(this, event); 337 this.justFocused = false; 338 } 339 else { 340 // Handle focus event. 341 if (this.justFocused) { 342 event.preventDefault(); 343 event.stopPropagation(); 344 this.justFocused = false; 345 } 346 // Handle touch/click event. 347 else if (isTouch || !isTouch && !this.settings.openOnMouseover) { 348 event.preventDefault(); 349 event.stopPropagation(); 350 _togglePanel.call(this, event, target.hasClass(this.settings.openClass)); 351 } 352 } 353 }
354 // Without panel on enter event. 355 else if (topli.length === 1 && panel.length === 0 && event.type == "keydown" && target.hasAttribute("href")) { 356 window.location.href = target.attr("href"); 357 } 358 }; 359 360 /** 361 * @name jQuery.fn.accessibleMegaMenu~_touchmoveHandler 362 * @desc Handle touch move event on menu 363 * @memberof jQuery.fn.accessibleMegaMenu 364 * @inner 365 * @private 366 */ 367 _touchmoveHandler = function () { 368 this.justMoved = true; 369 }; 370 371 /** 372 * @name jQuery.fn.accessibleMegaMenu~_clickOutsideHandler 373 * @desc Handle click event outside of a the megamenu 374 * @param {event} Event object 375 * @memberof jQuery.fn.accessibleMegaMenu 376 * @inner 377 * @private 378 */ 379 _clickOutsideHandler = function (event) { 380 if ($(event.target).closest(this.menu).length === 0) { 381 event.preventDefault(); 382 event.stopPropagation(); 383 _togglePanel.call(this, event, true); 384 } 385 }; 386 387 /** 388 * @name jQuery.fn.accessibleMegaMenu~_DOMAttrModifiedHandler 389 * @desc Handle DOMAttrModified event on panel to respond to Windows 8 Narrator ExpandCollapse pattern 390 * @param {event} Event object 391 * @memberof jQuery.fn.accessibleMegaMenu 392 * @inner 393 * @private 394 */ 395 _DOMAttrModifiedHandler = function (event) { 396 if (event.originalEvent.attrName === 'aria-expanded' 397 && event.originalEvent.newValue === 'false' 398 && $(event.target).hasClass(this.settings.openClass)) { 399 event.preventDefault(); 400 event.stopPropagation(); 401 _togglePanel.call(this, event, true); 402 } 403 }; 404 405 /** 406 * @name jQuery.fn.accessibleMegaMenu~_focusInHandler 407 * @desc Handle focusin event on mega menu item. 408 * @param {event} Event object 409 * @memberof jQuery.fn.accessibleMegaMenu 410 * @inner 411 * @private 412 */ 413 _focusInHandler = function (event) { 414 clearTimeout(this.focusTimeoutID); 415 var target = $(event.target), 416 panel = target.closest('.' + this.settings.panelClass); 417 target 418 .addClass(this.settings.focusClass); 419 this.justFocused = !this.mouseFocused || (!this.settings.openOnMouseover && this.mouseFocused); 420 this.mouseFocused = false; 421 if (this.justFocused && this.panels.not(panel).filter('.' + this.settings.openClass).length) { 422 _togglePanel.call(this, event); 423 } 424 }; 425 426 /** 427 * @name jQuery.fn.accessibleMegaMenu~_focusOutHandler 428 * @desc Handle focusout event on mega menu item. 429 * @param {event} Event object 430 * @memberof jQuery.fn.accessibleMegaMenu 431 * @inner 432 * @private 433 */ 434 _focusOutHandler = function (event) { 435 this.justFocused = false; 436 var that = this, 437 target = $(event.target), 438 topli = target.closest('.' + this.settings.topNavItemClass); 439 target 440 .removeClass(this.settings.focusClass); 441 442 if (typeof window.cvox === 'object' && typeof window.cvox.Api !== 'object') { 443 // If ChromeVox is running... 444 that.focusTimeoutID = setTimeout(function () { 445 window.cvox.Api.getCurrentNode(function (node) { 446 if (topli.has(node).length) { 447 // and the current node being voiced is in 448 // the mega menu, clearTimeout, 449 // so the panel stays open. 450 clearTimeout(that.focusTimeoutID); 451 } else { 452 that.focusTimeoutID = setTimeout(function (scope, event, hide) { 453 _togglePanel.call(scope, event, hide); 454 }, 275, that, event, true); 455 } 456 }); 457 }, 25); 458 } else { 459 that.focusTimeoutID = setTimeout(function () { 460 if (that.mouseFocused && event.relatedTarget === null) { 461 return; 462 } 463 _togglePanel.call(that, event, true); 464 }, 300); 465 } 466 }; 467 468 /** 469 * @name jQuery.fn.accessibleMegaMenu~_keyDownHandler
470 * @desc Handle keydown event on mega menu. 471 * @param {event} Event object 472 * @memberof jQuery.fn.accessibleMegaMenu 473 * @inner 474 * @private 475 */ 476 _keyDownHandler = function (event) { 477 var that = (this.constructor === AccessibleMegaMenu) ? this : _getPlugin(this), // determine the AccessibleMegaMenu plugin instance 478 settings = that.settings, 479 target = $($(this).is('.' + settings.hoverClass + ':tabbable') ? this : event.target), // if the element is hovered the target is this, otherwise, its the focused element 480 menu = that.menu, 481 topnavitems = that.topnavitems, 482 topli = target.closest('.' + settings.topNavItemClass), 483 tabbables = menu.find(':tabbable'), 484 panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass), 485 panelGroups = panel.find('.' + settings.panelGroupClass), 486 currentPanelGroup = target.closest('.' + settings.panelGroupClass), 487 next, 488 keycode = event.keyCode || event.which, 489 start, 490 i, 491 o, 492 label, 493 found = false, 494 newString = Keyboard.keyMap[event.keyCode] || '', 495 regex, 496 isTopNavItem = (topli.length === 1 && panel.length === 0); 497
498 if (target.is("input:focus, select:focus, textarea:focus, button:focus")) { 499 // if the event target is a form element we should handle keydown normally 500 return; 501 } 502 503 if (target.is('.' + settings.hoverClass + ':tabbable')) { 504 $('html').off('keydown.accessible-megamenu'); 505 } 506 507 switch (keycode) { 508 case Keyboard.ESCAPE: 509 this.mouseFocused = false; 510 _togglePanel.call(that, event, true); 511 break; 512 case Keyboard.DOWN: 513 event.preventDefault(); 514 this.mouseFocused = false; 515 if (isTopNavItem) { 516 _togglePanel.call(that, event); 517 found = (topli.find('.' + settings.panelClass + ' :tabbable:first').focus().length === 1); 518 } else { 519 found = (tabbables.filter(':gt(' + tabbables.index(target) + '):first').focus().length === 1); 520 } 521 522 if (!found && isOpera && (event.ctrlKey || event.metaKey)) { 523 tabbables = $(':tabbable'); 524 i = tabbables.index(target); 525 found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 526 } 527 break; 528 case Keyboard.UP: 529 event.preventDefault(); 530 this.mouseFocused = false; 531 if (isTopNavItem && target.hasClass(settings.openClass)) { 532 _togglePanel.call(that, event, true); 533 next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last'); 534 if (next.children('.' + settings.panelClass).length) { 535 next.find('[aria-expanded]') 536 .attr('aria-expanded', 'true') 537 .addClass(settings.openClass); 538 found = (next.children('.' + settings.panelClass) 539 .addClass(settings.openClass) 540 .attr('aria-hidden', 'false') 541 .find(':tabbable:last') 542 .focus() === 1); 543 } 544 } else if (!isTopNavItem) { 545 found = (tabbables.filter(':lt(' + tabbables.index(target) + '):last').focus().length === 1); 546 } 547 548 if (!found && isOpera && (event.ctrlKey || event.metaKey)) { 549 tabbables = $(':tabbable'); 550 i = tabbables.index(target); 551 found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 552 } 553 break; 554 case Keyboard.RIGHT: 555 event.preventDefault(); 556 this.mouseFocused = false; 557 if (isTopNavItem) { 558 found = (topnavitems.filter(':gt(' + topnavitems.index(topli) + '):first').find(':tabbable:first').focus().length === 1); 559 } else { 560 if (panelGroups.length && currentPanelGroup.length) { 561 // if the current panel contains panel groups, and we are able to focus the first tabbable element of the next panel group 562 found = (panelGroups.filter(':gt(' + panelGroups.index(currentPanelGroup) + '):first').find(':tabbable:first').focus().length === 1); 563 } 564 565 if (!found) { 566 found = (topli.find(':tabbable:first').focus().length === 1); 567 } 568 } 569 break; 570 case Keyboard.LEFT: 571 event.preventDefault(); 572 this.mouseFocused = false; 573 if (isTopNavItem) { 574 found = (topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last').find(':tabbable:first').focus().length === 1); 575 } else { 576 if (panelGroups.length && currentPanelGroup.length) { 577 // if the current panel contains panel groups, and we are able to focus the first tabbable element of the previous panel group 578 found = (panelGroups.filter(':lt(' + panelGroups.index(currentPanelGroup) + '):last').find(':tabbable:first').focus().length === 1); 579 } 580 581 if (!found) { 582 found = (topli.find(':tabbable:first').focus().length === 1); 583 } 584 } 585 break; 586 case Keyboard.TAB: 587 this.mouseFocused = false;
588 i = tabbables.index(target); 589 if (event.shiftKey && isTopNavItem && target.hasClass(settings.openClass)) { 590 _togglePanel.call(that, event, true); 591 next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last'); 592 if (next.children('.' + settings.panelClass).length) { 593 found = next.children() 594 .attr('aria-expanded', 'true') 595 .addClass(settings.openClass) 596 .filter('.' + settings.panelClass) 597 .attr('aria-hidden', 'false') 598 .find(':tabbable:last') 599 .focus(); 600 } 601 } else if (event.shiftKey && i > 0) { 602 found = (tabbables.filter(':lt(' + i + '):last').focus().length === 1); 603 } else if (!event.shiftKey && i < tabbables.length - 1) { 604 found = (tabbables.filter(':gt(' + i + '):first').focus().length === 1); 605 } else if (isOpera) { 606 tabbables = $(':tabbable'); 607 i = tabbables.index(target); 608 if (event.shiftKey) { 609 found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):last').focus().length === 1); 610 } else { 611 found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 612 } 613 } 614 615 if (found) { 616 event.preventDefault(); 617 } 618 break; 619 case Keyboard.SPACE: 620 case Keyboard.ENTER: 621 // Top level links. 622 if (isTopNavItem) { 623 event.preventDefault(); 624 // Handle enter event on open top level link as escape event. 625 if (target.hasClass("open")) { 626 this.mouseFocused = false; 627 _togglePanel.call(that, event, true); 628 } 629 // Handle enter event on top level link as a click. 630 else { 631 _clickHandler.call(that, event); 632 } 633 } 634 // Sub level links. 635 else { 636 return true; 637 } 638 break; 639 default: 640 // alphanumeric filter 641 clearTimeout(this.keydownTimeoutID); 642 643 keydownSearchString += newString !== keydownSearchString ? newString : ''; 644 645 if (keydownSearchString.length === 0) { 646 return; 647 } 648 649 this.keydownTimeoutID = setTimeout(function () { 650 keydownSearchString = ''; 651 }, keydownTimeoutDuration); 652 653 if (isTopNavItem && !target.hasClass(settings.openClass)) { 654 tabbables = tabbables.filter(':not(.' + settings.panelClass + ' :tabbable)'); 655 } else { 656 tabbables = topli.find(':tabbable'); 657 } 658 659 if (event.shiftKey) { 660 tabbables = $(tabbables.get() 661 .reverse()); 662 } 663 664 for (i = 0; i < tabbables.length; i++) { 665 o = tabbables.eq(i); 666 if (o.is(target)) { 667 start = (keydownSearchString.length === 1) ? i + 1 : i; 668 break; 669 } 670 } 671 672 regex = new RegExp('^' + keydownSearchString.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'), 'i'); 673 674 for (i = start; i < tabbables.length; i++) { 675 o = tabbables.eq(i); 676 label = $.trim(o.text()); 677 if (regex.test(label)) { 678 found = true; 679 o.focus(); 680 break; 681 } 682 } 683 if (!found) { 684 for (i = 0; i < start; i++) { 685 o = tabbables.eq(i); 686 label = $.trim(o.text()); 687 if (regex.test(label)) { 688 o.focus(); 689 break; 690 } 691 } 692 } 693 break; 694 } 695 that.justFocused = false;
696 }; 697 698 /** 699 * @name jQuery.fn.accessibleMegaMenu~_mouseDownHandler 700 * @desc Handle mousedown event on mega menu. 701 * @param {event} Event object 702 * @memberof accessibleMegaMenu 703 * @inner 704 * @private 705 */ 706 _mouseDownHandler = function (event) { 707 if ($(event.target).closest(this.settings.panelClass) || $(event.target).closest(":focusable").length) { 708 this.mouseFocused = true; 709 if ($(event.target).closest(this.settings.menuClass)) { 710 $('html').on('keydown.accessible-megamenu', $.proxy(_keyDownHandler, event.target)); 711 } 712 } 713 clearTimeout(this.mouseTimeoutID); 714 this.mouseTimeoutID = setTimeout(function () { 715 clearTimeout(this.focusTimeoutID); 716 }, 1); 717 }; 718 719 /** 720 * @name jQuery.fn.accessibleMegaMenu~_mouseOverHandler 721 * @desc Handle mouseover event on mega menu. 722 * @param {event} Event object 723 * @memberof jQuery.fn.accessibleMegaMenu 724 * @inner 725 * @private 726 */ 727 _mouseOverHandler = function (event) { 728 clearTimeout(this.mouseTimeoutID); 729 var that = this; 730 731 if (!that.settings.openOnMouseover) { 732 return; 733 } 734 735 this.mouseTimeoutID = setTimeout(function () { 736 $(event.target).addClass(that.settings.hoverClass); 737 _togglePanel.call(that, event); 738 if ($(event.target).closest(that.settings.menuClass)) { 739 $('html').on('keydown.accessible-megamenu', $.proxy(_keyDownHandler, event.target)); 740 } 741 }, this.settings.openDelay); 742 }; 743 744 /** 745 * @name jQuery.fn.accessibleMegaMenu~_mouseOutHandler 746 * @desc Handle mouseout event on mega menu. 747 * @param {event} Event object 748 * @memberof jQuery.fn.accessibleMegaMenu 749 * @inner 750 * @private 751 */ 752 _mouseOutHandler = function (event) { 753 clearTimeout(this.mouseTimeoutID); 754 var that = this; 755 756 if (!that.settings.openOnMouseover) { 757 return; 758 } 759 760 $(event.target) 761 .removeClass(that.settings.hoverClass); 762 763 that.mouseTimeoutID = setTimeout(function () { 764 _togglePanel.call(that, event, true); 765 }, this.settings.closeDelay); 766 if ($(event.target).is(':tabbable')) { 767 $('html').off('keydown.accessible-megamenu'); 768 } 769 }; 770 771 /** 772 * @name jQuery.fn.accessibleMegaMenu~_clickToggleHandler 773 * @desc Handle click event on menu toggle button. 774 * @memberof jQuery.fn.accessibleMegaMenu 775 * @inner 776 * @private 777 */ 778 _clickToggleHandler = function () { 779 var isExpanded = this.toggleButton.attr('aria-expanded') === 'true'; 780 this.toggleButton.attr({'aria-expanded': !isExpanded}); 781 }; 782 783 _toggleExpandedEventHandlers = function (hide) { 784 var menu = this.menu; 785 if (hide) { 786 $('html').off('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu'); 787 788 menu.find('[aria-expanded].' + this.settings.panelClass).off('DOMAttrModified.accessible-megamenu'); 789 } else { 790 $('html').on('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu', $.proxy(_clickOutsideHandler, this)); 791 792 /* Narrator in Windows 8 automatically toggles the aria-expanded property on double tap or click. 793 To respond to the change to collapse the panel, we must add a listener for a DOMAttrModified event. */ 794 menu.find('[aria-expanded=true].' + this.settings.panelClass).on('DOMAttrModified.accessible-megamenu', $.proxy(_DOMAttrModifiedHandler, this)); 795 } 796 }; 797 798 _addEventHandlers = function() { 799 var menu = this.menu, 800 toggleButton = this.toggleButton; 801 menu.on("focusin.accessible-megamenu", ":focusable, ." + this.settings.panelClass, $.proxy(_focusInHandler, this)) 802 .on("focusout.accessible-megamenu", ":focusable, ." + this.settings.panelClass, $.proxy(_focusOutHandler, this))
803 .on("keydown.accessible-megamenu", $.proxy(_keyDownHandler, this)) 804 .on("mouseover.accessible-megamenu", $.proxy(_mouseOverHandler, this)) 805 .on("mouseout.accessible-megamenu", $.proxy(_mouseOutHandler, this)) 806 .on("mousedown.accessible-megamenu", $.proxy(_mouseDownHandler, this)) 807 .on("click.accessible-megamenu", $.proxy(_clickHandler, this)); 808 809 toggleButton.on('click.accessible-megamenu', $.proxy(_clickToggleHandler, this)); 810 811 if (isTouch) { 812 menu.on("touchmove.accessible-megamenu", $.proxy(_touchmoveHandler, this)); 813 } 814 815 if ($(document.activeElement).closest(menu).length) { 816 $(document.activeElement).trigger("focusin.accessible-megamenu"); 817 } 818 }; 819 820 _removeEventHandlers = function () { 821 var menu = this.menu, 822 toggleButton = this.toggleButton; 823 menu.off('.accessible-megamenu'); 824 if (menu.find('[aria-expanded=true].' + this.settings.panelClass).length) { 825 _toggleExpandedEventHandlers.call(this, true); 826 } 827 828 toggleButton.off('.accessible-megamenu'); 829 }; 830 831 /* public attributes and methods ------------------------- */ 832 return { 833 constructor: AccessibleMegaMenu, 834 835 /** 836 * @lends jQuery.fn.accessibleMegaMenu 837 * @desc Initializes an instance of the accessibleMegaMenu plugins 838 * @memberof jQuery.fn.accessibleMegaMenu 839 * @instance 840 */ 841 init: function () { 842 var settings = this.settings, 843 nav = $(this.element), 844 menu = nav.children('ol,ul').first(), 845 topnavitems = menu.children(), 846 toggleButton = nav.children('button').first(); 847 this.start(settings, nav, menu, topnavitems, toggleButton); 848 }, 849 850 start: function(settings, nav, menu, topnavitems, toggleButton) { 851 var that = this; 852 this.settings = settings; 853 this.menu = menu; 854 this.topnavitems = topnavitems; 855 this.toggleButton = toggleButton; 856 857 nav.attr("role", "navigation"); 858 _addUniqueId.call(that, menu); 859 menu.addClass(settings.menuClass); 860 menu.addClass(['js', settings.menuClass].join('-')); 861 topnavitems.each(function (i, topnavitem) { 862 var topnavitemlink, topnavitempanel; 863 topnavitem = $(topnavitem); 864 topnavitem.addClass(settings.topNavItemClass); 865 topnavitemlink = topnavitem.find(":tabbable:first"); 866 topnavitempanel = topnavitem.children(":not(:tabbable):last"); 867 _addUniqueId.call(that, topnavitemlink); 868 // When sub nav exists. 869 if (topnavitempanel.length) { 870 _addUniqueId.call(that, topnavitempanel); 871 // Add attributes to top level link. 872 topnavitemlink.attr({ 873 "role": "button", 874 "aria-controls": topnavitempanel.attr("id"), 875 "aria-expanded": false, 876 "tabindex": 0 877 }); 878 // Add attributes to sub nav. 879 topnavitempanel.attr({ 880 "role": "region", 881 "aria-hidden": true 882 }) 883 .addClass(settings.panelClass) 884 .not("[aria-labelledby]") 885 .attr("aria-labelledby", topnavitemlink.attr("id")); 886 } 887 }); 888 889 this.panels = menu.find("." + settings.panelClass); 890 891 menu.find("hr").attr("role", "separator"); 892 893 toggleButton.addClass(settings.toggleButtonClass); 894 toggleButton.attr({'aria-expanded': false, 'aria-controls': menu.attr('id')}); 895 896 _addEventHandlers.call(this); 897 }, 898 899 /**
900 * @desc Removes maga menu javascript behavior 901 * @example $(selector).accessibleMegaMenu("destroy"); 902 * @return {object} 903 * @memberof jQuery.fn.accessibleMegaMenu 904 * @instance 905 */ 906 destroy: function () { 907 this.menu.removeClass(['js', this.settings.menuClass].join('-')); 908 _removeEventHandlers.call(this, true); 909 }, 910 911 /** 912 * @desc Get default values 913 * @example $(selector).accessibleMegaMenu("getDefaults"); 914 * @return {object} 915 * @memberof jQuery.fn.accessibleMegaMenu 916 * @instance 917 */ 918 getDefaults: function () { 919 return this._defaults; 920 }, 921 922 /** 923 * @desc Get any option set to plugin using its name (as string) 924 * @example $(selector).accessibleMegaMenu("getOption", some_option); 925 * @param {string} opt 926 * @return {string} 927 * @memberof jQuery.fn.accessibleMegaMenu 928 * @instance 929 */ 930 getOption: function (opt) { 931 return this.settings[opt]; 932 }, 933 934 /** 935 * @desc Get all options 936 * @example $(selector).accessibleMegaMenu("getAllOptions"); 937 * @return {object} 938 * @memberof jQuery.fn.accessibleMegaMenu 939 * @instance 940 */ 941 getAllOptions: function () { 942 return this.settings; 943 }, 944 945 /** 946 * @desc Set option 947 * @example $(selector).accessibleMegaMenu("setOption", "option_name", "option_value", reinitialize); 948 * @param {string} opt - Option name 949 * @param {string} val - Option value 950 * @param {boolean} [reinitialize] - boolean to re-initialize the menu. 951 * @memberof jQuery.fn.accessibleMegaMenu 952 * @instance 953 */ 954 setOption: function (opt, value, reinitialize) { 955 this.settings[opt] = value; 956 if (reinitialize) { 957 this.init(); 958 } 959 } 960 }; 961 }()); 962 963 /* lightweight plugin wrapper around the constructor, 964 to prevent against multiple instantiations */ 965 966 /** 967 * @class accessibleMegaMenu 968 * @memberOf jQuery.fn 969 * @classdesc Implements an accessible mega menu as a jQuery plugin. 970 * <p>The mega-menu It is modeled after the mega menu on {@link http://adobe.com|adobe.com} but has been simplified for use by others. A brief description of the interaction design choices can be found in a blog post at {@link http://blogs.adobe.com/accessibility/2013/05/adobe-com.html|Mega menu accessibility on adobe.com}.</p> 971 * <h3>Keyboard Accessibility</h3> 972 * <p>The accessible mega menu supports keyboard interaction modeled after the behavior described in the {@link http://www.w3.org/TR/wai-aria-practices/#menu|WAI-ARIA Menu or Menu bar (widget) design pattern}, however we also try to respect users' general expectations for the behavior of links in a global navigation. To this end, the accessible mega menu implementation permits tab focus on each of the six top-level menu items. When one of the menu items has focus, pressing the Enter key, Spacebar or Down arrow will open the submenu panel, and pressing the Left or Right arrow key will shift focus to the adjacent menu item. Links within the submenu panels are included in the tab order when the panel is open. They can also be navigated with the arrow keys or by typing the first character in the link name, which speeds up keyboard navigation considerably. Pressing the Escape key closes the submenu and restores focus to the parent menu item.</p> 973 * <h3>Screen Reader Accessibility</h3> 974 * <p>The accessible mega menu models its use of WAI-ARIA Roles, States, and Properties after those described in the {@link http://www.w3.org/TR/wai-aria-practices/#menu|WAI-ARIA Menu or Menu bar (widget) design pattern} with some notable exceptions, so that it behaves better with screen reader user expectations for global navigation. We don't use <code class="prettyprint prettyprinted" style=""><span class="pln">role</span><span class="pun">=</span><span class="str">"menu"</span></code> for the menu container and <code class="prettyprint prettyprinted" style=""><span class="pln">role</span><span class="pun">=</span><span class="str">"menuitem"</span></code> for each of the links therein, because if we do, assistive technology will no longer interpret the links as links, but instead, as menu items, and the links in our global navigation will no longer show up when a screen reader user executes a shortcut command to bring up a list of links in the page.</p> 975 * @example <h4>HTML</h4><hr/> 976<nav> 977 <ul class="nav-menu"> 978 <li class="nav-item"> 979 <a href="?movie">Movies</a> 980 <div class="sub-nav"> 981 <ul class="sub-nav-group"> 982 <li><a href="?movie&genre=0">Action &amp; Adventure</a></li> 983 <li><a href="?movie&genre=2">Children &amp; Family</a></li> 984 <li>&#8230;</li> 985 </ul> 986 <ul class="sub-nav-group"> 987 <li><a href="?movie&genre=7">Dramas</a></li> 988 <li><a href="?movie&genre=9">Foreign</a></li> 989 <li>&#8230;</li> 990 </ul> 991 <ul class="sub-nav-group"> 992 <li><a href="?movie&genre=14">Musicals</a></li> 993 <li><a href="?movie&genre=15">Romance</a></li> 994 <li>&#8230;</li> 995 </ul> 996 </div> 997 </li> 998 <li class="nav-item"> 999 <a href="?tv">TV Shows</a> 1000 <div class="sub-nav"> 1001 <ul class="sub-nav-group"> 1002 <li><a href="?tv&genre=20">Classic TV</a></li> 1003 <li><a href="?tv&genre=21">Crime TV</a></li> 1004 <li>&#8230;</li> 1005 </ul> 1006 <ul class="sub-nav-group"> 1007 <li><a href="?tv&genre=27">Reality TV</a></li> 1008 <li><a href="?tv&genre=30">TV Action</a></li> 1009 <li>&#8230;</li> 1010 </ul> 1011 <ul class="sub-nav-group"> 1012 <li><a href="?tv&genre=33">TV Dramas</a></li> 1013 <li><a href="?tv&genre=34">TV Horror</a></li> 1014 <li>&#8230;</li> 1015 </ul> 1016 </div> 1017 </li> 1018 </ul> 1019</nav> 1020 * @example <h4>CSS</h4><hr/> 1021/* Rudimentary mega menu CSS for demonstration */ 1022 1023/* mega menu list */ 1024.nav-menu { 1025 display: block; 1026 position: relative; 1027 list-style: none; 1028 margin: 0; 1029 padding: 0; 1030 z-index: 15; 1031} 1032 1033/* a top level navigation item in the mega menu */ 1034.nav-item { 1035 list-style: none; 1036 display: inline-block; 1037 padding: 0; 1038 margin: 0; 1039} 1040 1041/* first descendant link within a top level navigation item */ 1042.nav-item > a { 1043 position: relative; 1044 display: inline-block; 1045 padding: 0.5em 1em; 1046 margin: 0 0 -1px 0; 1047 border: 1px solid transparent; 1048} 1049 1050/* focus/open states of first descendant link within a top level 1051 navigation item */ 1052.nav-item > a:focus, 1053.nav-item > a.open { 1054 border: 1px solid #dedede; 1055} 1056 1057/* open state of first descendant link within a top level 1058 navigation item */ 1059.nav-item > a.open { 1060 background-color: #fff; 1061 border-bottom: none; 1062 z-index: 1; 1063} 1064 1065/* sub-navigation panel */ 1066.sub-nav { 1067 position: absolute; 1068 display: none; 1069 top: 2.2em;
1070 margin-top: -1px; 1071 padding: 0.5em 1em; 1072 border: 1px solid #dedede; 1073 background-color: #fff; 1074} 1075 1076/* sub-navigation panel open state */ 1077.sub-nav.open { 1078 display: block; 1079} 1080 1081/* list of items within sub-navigation panel */ 1082.sub-nav ul { 1083 display: inline-block; 1084 vertical-align: top; 1085 margin: 0 1em 0 0; 1086 padding: 0; 1087} 1088 1089/* list item within sub-navigation panel */ 1090.sub-nav li { 1091 display: block; 1092 list-style-type: none; 1093 margin: 0; 1094 padding: 0; 1095} 1096 * @example <h4>JavaScript</h4><hr/> 1097<!-- include jquery --> 1098<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script> 1099 1100<!-- include the jquery-accessibleMegaMenu plugin script --> 1101<script src="js/jquery-accessibleMegaMenu.js"></script> 1102 1103<!-- initialize a selector as an accessibleMegaMenu --> 1104<script> 1105 $("nav:first").accessibleMegaMenu({ 1106 /* prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby */ 1107 uuidPrefix: "accessible-megamenu", 1108 1109 /* css class used to define the megamenu styling */ 1110 menuClass: "nav-menu", 1111 1112 /* css class for a top-level navigation item in the megamenu */ 1113 topNavItemClass: "nav-item", 1114 1115 /* css class for a megamenu panel */ 1116 panelClass: "sub-nav", 1117 1118 /* css class for a group of items within a megamenu panel */ 1119 panelGroupClass: "sub-nav-group", 1120 1121 /* css class for the hover state */ 1122 hoverClass: "hover", 1123 1124 /* css class for the focus state */ 1125 focusClass: "focus", 1126 1127 /* css class for the open state */ 1128 openClass: "open" 1129 }); 1130</script> 1131 * @param {object} [options] Mega Menu options 1132 * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby 1133 * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling 1134 * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu 1135 * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel 1136 * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel 1137 * @param {string} [options.hoverClass=hover] - CSS class for the hover state 1138 * @param {string} [options.focusClass=focus] - CSS class for the focus state 1139 * @param {string} [options.openClass=open] - CSS class for the open state 1140 * @param {string} [options.openDelay=0] - Open delay when opening menu via mouseover 1141 * @param {string} [options.closeDelay=250] - Open delay when opening menu via mouseover 1142 * @param {boolean} [options.openOnMouseover=false] - Should menu open on mouseover 1143 */ 1144 $.fn[pluginName] = function (options) { 1145 return this.each(function () { 1146 var pluginInstance = $.data(this, "plugin_" + pluginName); 1147 if (!pluginInstance) { 1148 $.data(this, "plugin_" + pluginName, new $.fn[pluginName].Acce
1148ssibleMegaMenu(this, options)); 1149 } else if (typeof pluginInstance[options] === 'function') { 1150 pluginInstance[options].apply(pluginInstance, Array.prototype.slice.call(arguments, 1)); 1151 } 1152 }); 1153 }; 1154 1155 $.fn[pluginName].AccessibleMegaMenu = AccessibleMegaMenu; 1156 1157 /* :focusable and :tabbable selectors from 1158 https://raw.github.com/jquery/jquery-ui/master/ui/jquery.ui.core.js */ 1159 1160 /** 1161 * @private 1162 */ 1163 function visible(element) { 1164 return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function () { 1165 return $.css(this, "visibility") === "hidden"; 1166 }).length; 1167 } 1168 1169 /** 1170 * @private 1171 */ 1172 function focusable(element, isTabIndexNotNaN) { 1173 var map, mapName, img, 1174 nodeName = element.nodeName.toLowerCase(); 1175 if ("area" === nodeName) { 1176 map = element.parentNode; 1177 mapName = map.name; 1178 if (!element.href || !mapName || map.nodeName.toLowerCase() !== "map") { 1179 return false; 1180 } 1181 img = $("img[usemap=#" + mapName + "]")[0]; 1182 return !!img && visible(img); 1183 } 1184 return (/input|select|textarea|button|object/.test(nodeName) ? !element.disabled : 1185 "a" === nodeName ? 1186 element.href || isTabIndexNotNaN : 1187 isTabIndexNotNaN) && 1188 // the element and all of its ancestors must be visible 1189 visible(element); 1190 } 1191 1192 $.extend($.expr[":"], { 1193 data: $.expr.createPseudo ? $.expr.createPseudo(function (dataName) { 1194 return function (elem) { 1195 return !!$.data(elem, dataName); 1196 }; 1197 }) : // support: jQuery <1.8 1198 function (elem, i, match) { 1199 return !!$.data(elem, match[3]); 1200 }, 1201 1202 focusable: function (element) { 1203 return focusable(element, !isNaN($.attr(element, "tabindex"))); 1204 }, 1205 1206 tabbable: function (element) { 1207 var tabIndex = $.attr(element, "tabindex"), 1208 isTabIndexNaN = isNaN(tabIndex); 1209 return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); 1210 } 1211 }); 1212}(jQuery, window, document));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.