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