1/* Educational Networks Main Menu Keyboard Access 2 * 3 * FOCUS STYLING 4 * It is important to add styling for the :focus event as is done for the :hover event. 5 * - Top level items: 6 * 7 * #nav_items_0 li a:focus, #nav_items_0 li.over a { ... top level hover styling } 8 * - in order to maintain top level hover effect the 'over' class is added to the top level li when 9 * a user is within a sub menu. 10 * 11 * - Sub level items: 12 * #nav_items_0 li ul li a:focus { ...sub level item styling } 13 * 14 */ 15 16function MainMenu() { 17 18 var mm = this; 19 var current_focus, current_elem, previous_focus, latest_menu, disabled_id, disabled_item, focus_fired = null; 20 var keys_down = new Array(), 21 open_menus = new Array(); 22 23 var browser = { 24 isChrome : navigator.userAgent.toLowerCase().indexOf('chrome') > -1, 25 isFirefox : navigator.userAgent.toLowerCase().indexOf('firefox') > -1, 26 isMac : navigator.platform.toUpperCase().indexOf('MAC') > -1 27 } 28 29 var keys = { 30 TAB: 9, 31 ENTER: 13, 32 SPACE: 32, 33 ESCAPE: 27, 34 UP: 38, 35 DOWN: 40, 36 LEFT: 37, 37 RIGHT: 39 38 } 39 40 this.setFocus = function( obj, elem ) { current_focus = obj; current_elem = elem; } 41 this.setPrevFocus = function( obj ) { previous_focus = obj; } 42 this.setLatestMenu = function( menu ) { latest_menu = menu; } 43 44 this.getFocus = function() { return current_focus; } 45 this.getElement = function() { return current_elem; } 46 this.getBrowser = function() { return browser; } 47 this.getKeys = function() { return keys; } 48 this.getKeysDown = function() { return keys_down; } 49 this.getOpenMenus = function() { return open_menus; } 50 this.getLatestMenu = function() { return latest_menu; } 51 this.getPrevFocus = function() { return previous_focus; } 52 53 /* init runs the setup function and sets all needed event listeners 54 * assumes id="menu" */ 55 this.init = function() { 56 57 jAria("#menu").delegate( "a", "focusin", function(e) { 58 e.stopImmediatePropagation(); 59 /* - Parent Items With Links 60 * - disables the link by replacing href with javascript:; 61 * - original href is stored in another attribute [ data-href="*" ] 62 * - menu correction detects and closes menus that should have closed 63 * but didn't because the focus left undetected. Voiceover problem. 64 * - 5/6/2020 menus now open on focusin 65 * - Parents With No Link, Normal Links 66 * - does nothing */ 67 mm.unsetParentHref(this); 68 mm.menuCorrection( jAria(this).closest("ul").attr("id") ); 69 mm.setFocus( jAria(this), document.activeElement ); 70 //mm.openMenu(); 71 }); 72 73 jAria("#menu > ul ul a").delegate( "a", "focus focusin", function(e) { 74 /* ensures top level li maintains '.over' class while within sub menu */ 75 e.stopImmediatePropagation(); 76 mm.unsetParentHref(this); 77 jAria( this ).closest('ul').addClass('over'); 78 }); 79 80 jAria("#menu > ul ul a").delegate( "a", "blur", function(e) { 81 /* removes '.over' class top the top level each time a sub link blurs 82 * it will be added again if the next focusable item is within the same menu 83 * maintaining the hover effect for the top level item. */ 84 e.stopImmediatePropagation(); 85 mm.setParentHref(this); 86 jAria(this).closest('ul').removeClass('over'); 87 }); 88 89 jAria("#menu").delegate( "a", "blur", function() { 90 /* - Parent Items With Links 91 * - re-enables the link by replacing href with data-href attribute 92 * - data-href="*" attribute is removed 93 * - Parents With No Link, and Normal Links 94 * - does nothing */ 95 mm.setPrevFocus(jAria(this)); 96 mm.setParentHref(this); 97 }); 98 99 jAria("#menu").delegate( "a", "mouseenter", function() { 100 /* - Parent Items With Links 101 * - sets the *made up* en-mouseover attribute on the item preventing the link from being disabled as we would do for a keyboard user 102 * - Parents With No Link, and Normal Links 103 * - does nothing */ 104 mm.setParentHref(this); 105 jAria(this).attr("en-mouseover", "yes"); 106 }); 107 108 jAria("#menu").delegate( "a", "mouseleave", function() { 109 /* - Parent Items With Links 110 * - clears the *made up* en-mouseover attribute on the item preventing the link from being disabled as we would do for a keyboard user 111 * - Parents With No Link, and Normal Links 112 * - does nothing */ 113 jAria(this).attr("en-mouseover", "no"); 114 }); 115 116 jAria("#menu").delegate( "a", "click", function() { 117 /* - Parent Items: TODO - redo notes 118 * - ignore links on first press, open the sub level 119 * - keep focus on the parent item 120 * -* THIS SHOULD MAYBE CHANGE TO FOCUS THE FIRST ITEM FOR PARENTS WHICH ARE NON LINKS *- 121 * - link will become active, a following press will open the link 122 * - Normal Links: 123 * - activates href */ 124 mm.openMenu(); 125 }); 126 127 jAria("#menu").bind("keyup", function(e) { 128 /* when a key is released, remove it from the keys_down array 129 * keys_down keeps track of any key pressed which allows us 130 * to avoid multiple events from firing if a key is held down. */ 131 var key = e.keyCode ? e.keyCode : e.which; 132 if(key != keys.SPACE && key != keys.ENTER) e.preventDefault(); 133 var key_index = jAria.inArray( key, keys_down ); 134 if(key_index != -1) { 135 keys_down.splice(key_index, 1); 136 } 137 }); 138 139 jAria("#menu").bind('keydown', function(e) { 140 /* when a key is pressed, log it in the keys_down. 141 * keys_down array keeps track of any key pressed which allows us 142 * to avoid multiple events from firing if a key is held down. 143 * if a key is already down return immediately 144 * else continue to the main key switch */ 145 var key = e.keyCode ? e.keyCode : e.which; 146 147 // Allow select elements to handle their own keyboard events 148 if (e.target && e.target.tagName === 'SELECT') { 149 return; 150 } 151 152 if( key == keys.UP || key == keys.DOWN || key == keys.LEFT || key == keys.RIGHT || key == keys.SPACE ) 153 e.preventDefault(); 154 155 // the following prevents multiple event fires when a key is held down 156 if(jAria.inArray( key, keys_down ) > -1) { 157 return; 158 } 159 160 // logs a key to prevent multiple event fires 161 keys_down.push(key); 162 163 var active_elem = document.activeElement; 164 mm.setFocus( jAria(this), active_elem ); 165 166 // main menu switch 167 switch(key) { 168 case keys.SPACE: 169 case keys.ENTER: 170 /* - Parent Items: 171 * - if parent item contains href the value is changed to javascript:; and a new menu item is created at the first position of the menu 172 * - focus moves to the first child menu item 173 * - Normal Links: 174 * - activates href */ 175 if( mm.isParent() ) { 176 e.preventDefault(); 177 mm.openMenu(); 178 } else if (active_elem.onclick) { 179 e.preventDefault(); 180 active_elem.click(); 181 } 182 break; 183
184 case keys.ESCAPE: 185 /* - Top Level: 186 * - Exit menu, land in main content area 187 * - Sub Level: 188 * - close current menu 189 * - focus on the parent item of the menu closed */ 190 mm.close( key ); 191 break; 192 193 case keys.TAB: 194 /* 195 * - Top Level: 196 * - go to next item or previous item if shift is held down 197 * - Sub Level: 198 * - close current menu simulating right press, 199 * - if shift is down simulate left press 200 */ 201 if(open_menus.length > 0) { 202 e.preventDefault(); 203 if(e.shiftKey) { 204 mm.close( keys.LEFT ); 205 } else { 206 mm.close( keys.RIGHT ); 207 } 208 } 209 break; 210 case keys.UP: 211 /* - Top Level: 212 * - does nothing 213 * - Sub Level: 214 * - go to prev item in current list 215 * - if at the bottom item jump back to the top item */ 216 if(open_menus.length < 1) { 217 var top_level_item = jAria(document.activeElement); 218 if(top_level_item.parent().is(':first-child')) { 219 top_level_item = top_level_item.parent().parent().find('> li').last().children('a'); 220 } else { 221 var prevItem = mm.searchPrevElement(top_level_item.parent().get(0)); 222 top_level_item = jAria(prevItem).children('a'); 223 } 224 top_level_item.focus(); 225 if(mm.isParent(top_level_item)) { 226 mm.openMenu(top_level_item); 227 var lastMenu = open_menus[open_menus.length-1]; 228 var lastItem = lastMenu.menu_elem.children('li').last().children('a'); 229 lastItem.focus(); 230 lastMenu.active_item = lastItem; 231 lastMenu.has_focus = true; 232 } 233 } else { 234 mm.closestItem( key ); 235 } 236 break; 237 case keys.DOWN: 238 /* - Top Level: 239 * - if parent, open menu 240 * - else do nothing 241 * - Sub Level: 242 * - go to next item in the current list 243 * - if at the bottom item jump back to the top item */ 244 if(open_menus.length < 1) { 245 mm.openMenu(); 246 } else { 247 mm.closestItem( key ); 248 } 249 break; 250 251 case keys.LEFT: 252 /* - Top Level 253 * - go left to the closest item, parent or not 254 * - Sub Level 255 * - close the current menu and: 256 * - if parent level is top level focus the previous top level item 257 * - else if parent level is a sub menu item focus on that parent item 258 */ 259 if(open_menus.length < 1) { 260 mm.closestItem( key ); 261 } else { 262 //mm.closestMenu( keys.LEFT ); 263 mm.close( key ); 264 } 265 break; 266 267 case keys.RIGHT: 268 /* - Top Level 269 * - go right to the closest item, parent or not 270 * - Sub Level 271 * - if current item is a parent open child menu 272 * - if current item is not a parent close all sub menu levels and focus on the next top level item */ 273 if(open_menus.length < 1) { 274 mm.closestItem( key ); 275 } else { 276 //mm.closestMenu( keys.RIGHT ); 277 if( mm.isParent() ) { 278 mm.openMenu(); 279 } else { 280 mm.close( key ); 281 } 282 } 283 break; 284 285 default: 286 /* - Top Level 287 * - Does nothing 288 * - Sub Level 289 * - increases the size of items beginning with any alphabetical key 290 * - case insensitive 291 * - toggle through items on following key presses of the same alphabetical key 292 * - next item, next menu, and close functions will reset the font size and list of items to toggle */ 293 try { 294 mm.filterAlphabetKeys( key ); 295 } catch (e) { 296 console.debug(e); 297 } 298 299 break; 300 } 301 302 }); 303 } 304 305 this.clearFilterKey = function( current_menu ) { 306 if(current_menu.filter_key != null) { 307 current_menu.filter_key = null; 308 current_menu.menu_items.each(function(){ 309 if(jAria(this).hasClass("at-focus")) { 310 jAria(this).removeClass("at-focus"); 311 } 312 }); 313 } 314 } 315 316 this.isParent = function ( active_elem ) { 317 var active_elem = (typeof active_elem !== typeof undefined) ? active_elem : mm.getElement(); 318 var check = jAria( active_elem ).attr('aria-haspopup'); 319 return typeof check !== typeof undefined && check !== false; 320 } 321 322 this.unsetParentHref = function( active_elem ) { 323 if(jAria(active_elem).attr('en-mouseover') == "yes") return; 324 if(mm.isParent(active_elem)) { 325 jAria(active_elem).attr("data-href", jAria(active_elem).attr("href")); 326 jAria(active_elem).attr("href", "javascript:;"); 327 if (!jAria("#menu").hasClass("en-mobnav-show") || (active_elem && active_elem.href && active_elem.href.contains("http"))) { 328 mm.prependParentLink(active_elem); 329 } 330 } 331 } 332 333 this.setParentHref = function( active_elem ) { 334 if(mm.isParent(active_elem)) { 335 jAria(active_elem).attr("href", jAria(active_elem).attr("data-href")); 336 } 337 } 338 339 this.prependParentLink = function( active_elem ) { 340 if(jAria(active_elem).attr("en-header-appended") !== "yes") { 341 jAria(active_elem).attr("en-header-appended", "yes"); 342 var dataHref = jAria(active_elem).attr("data-href"); 343 var sub_ul = jAria("#" + jAria(active_elem).attr("aria-controls")); 344 var newAnchor = jAria("<a/>", { 345 "href": dataHref, 346 "text": jAria(active_elem).text().replace("\u2192", "") 347 }); 348 jAria("<li/>", { 349 "style":"display:none;", 350 "class": "nav_item_first keyboard-only" 351 }).prepend(newAnchor).prependTo(sub_ul); 352 } 353 } 354 355 this.openMenu = function ( active_elem ) { 356 var active_elem = (typeof active_elem !== typeof undefined) ? active_elem : mm.getElement(); 357 var active_obj = jAria( active_elem ); 358 var is_parent = mm.isParent( active_elem ); 359 var is_link = (active_obj.attr('href') != "javascript:;" && active_obj.attr("href") != "#"); 360 var mouse_over = active_obj.attr("en-mouseover") == "yes"; 361 if ( is_parent ) { 362 open_menus.push({ 363 locked_in : true, 364 has_focus : false, 365 menu_id : jAria("#" + active_obj.attr('aria-controls')).attr("id"), 366 menu_parent : jAria(active_elem), 367 menu_elem : jAria("#" + active_obj.attr('aria-controls')), 368 menu_items : null, 369 filtered_items: [], 370 filter_iter : 0, 371 filter_key : null, 372 active_item : null, 373 active_item_id: null, 374 sub_level: open_menus.length 375 }); 376 377 var menu_index = open_menus.length-1; 378 open_menus[menu_index].menu_items = open_menus[menu_index].menu_elem.children('li').children('a'); 379 open_menus[menu_index].menu_parent.parent().addClass('over'); 380 open_menus[menu_index].menu_parent.attr('aria-expanded', 'true'); 381 open_menus[menu_index].menu_elem.attr('aria-expanded', 'true'); 382 open_menus[menu_index].menu_elem.attr('aria-hidden', 'false'); 383 open_menus[menu_index].menu_elem.css('opacity', '1'); 384 open_menus[menu_index].menu_elem.css('visibility', 'visible'); 385 open_menus[menu_index].menu_elem.css('display', 'block'); 386 387 if(!mouse_over) { 388 jAria(open_menus[menu_index].menu_items[0]).parent().show(); 389 } 390 391 mm.setLatestMenu( open_menus[menu_index].menu_elem ); 392 mm.closestItem( keys.DOWN ); 393 } 394 } 395 396 this.closestItem = function ( key ) { 397 var current_menu = open_menus[open_menus.length-1]; 398 var top_level_item = null; 399 if(current_menu != null) { 400 mm.clearFilterKey( current_menu ); 401 if(key == keys.DOWN ) { 402 if(!current_menu.has_focus) { 403 current_menu.active_item = current_menu.menu_elem.children('li').first().children('a'); 404 current_menu.active_item.focus(); 405 current_menu.has_focus = true; 406 } else { 407 if(current_menu.active_item.parent().is(':last-child')) { 408 // Check if there's a next menu by looking at the parent's next sibling 409 var parentLi = current_menu.menu_parent.parent(); 410 if (!parentLi.is(':last-child')) { 411 mm.close(keys.RIGHT); // Move right only if there's a next menu 412 } 413 } else { 414 current_menu.active_item = current_menu.active_item.parent().next().children('a'); 415 current_menu.active_item.focus(); 416 } 417 } 418 } else if ( key == keys.UP) { 419 if(current_menu.active_item.parent().is(':first-child')) { 420 mm.close(keys.UP); 421 } else { 422 current_menu.active_item = current_menu.active_item.parent().prev().children('a'); 423 current_menu.active_item.focus(); 424 } 425 } 426 } else { 427 top_level_item = jAria(document.activeElement); 428 if( key == keys.RIGHT ) { 429 if(top_level_item.parent().is(':last-child')) { 430 top_level_item = top_level_item.parent().parent().find('> li').first().children('a') 431 } else { 432 var nextItem = mm.searchNextElement(top_level_item.parent().get(0)); 433 top_level_item = nextItem.getElementsByTagName('a')[0]; 434 } 435 top_level_item.focus(); 436 } else if ( key == keys.LEFT ) { 437 if(top_level_item.parent().is(':first-child')) { 438 top_level_item = top_level_item.parent().parent().find('> li').last().children('a'); 439 } else { 440 var prevItem = mm.searchPrevElement(top_level_item.parent().get(0)); 441 top_level_item = prevItem.getElementsByTagName('a')[0]; 442 } 443 top_level_item.focus(); 444 } 445 } 446 } 447 448 this.searchNextElement = function(startingPoint) { 449 var foundNode = false;
450 var nextNode = null; 451 var maxAttempts = 25; 452 var attempts = 0; 453 while(!foundNode) { 454 attempts++; 455 nextNode = (nextNode == null) ? startingPoint.nextSibling : nextNode.nextSibling; 456 try { 457 if(nextNode.hasAttribute("en-replacable")) { 458 continue; 459 } 460 } catch(e) {} 461 if(attempts > maxAttempts) { 462 break; 463 } 464 return nextNode; 465 } 466 } 467 468 this.searchPrevElement = function(startingPoint) { 469 var foundNode = false; 470 var nextNode = null; 471 var maxAttempts = 25; 472 var attempts = 0; 473 while(!foundNode) { 474 attempts++; 475 nextNode = (nextNode == null) ? startingPoint.previousSibling : nextNode.previousSibling; 476 try { 477 if(nextNode.hasAttribute("en-replacable")) { 478 continue 479 } 480 } catch(e) { } 481 if(attempts > maxAttempts) { 482 break; 483 } 484 return nextNode; 485 } 486 } 487 488 this.closestMenu = function ( key ) { 489 var current_menu = open_menus[open_menus.length-1]; 490 mm.clearFilterKey( current_menu ); 491 var item_check = null; 492 var menu_length = current_menu.menu_elem.children('li').length-1; 493 for(var i = 0; i <= menu_length; i++) { 494 if(key == keys.RIGHT) { 495 if(document.activeElement == current_menu.menu_elem.children('li').last().children('a').get(0)) { 496 if(current_menu.active_item.parent().is(':last-child')) { 497 item_check = current_menu.active_item; 498 } 499 } else { 500 item_check = (item_check == null) 501 ? ((!current_menu.has_focus) 502 ? current_menu.menu_elem.children('li').first().children('a') 503 : current_menu.active_item.parent().next().children('a')) 504 : item_check.parent().next().children('a'); 505 } 506 } else if(key == keys.LEFT) { 507 if(document.activeElement == current_menu.menu_elem.children('li').first().children('a').get(0)) { 508 if(current_menu.active_item.parent().is(':first-child')) { 509 item_check = current_menu.active_item; 510 mm.close( keys.LEFT ); 511 break; 512 } 513 } else { 514 item_check = (item_check == null) 515 ? ((!current_menu.has_focus) 516 ? current_menu.menu_elem.children('li').last().children('a') 517 : current_menu.active_item.parent().prev().children('a')) 518 : item_check.parent().prev().children('a'); 519 } 520 } 521 current_menu.has_focus = true; 522 if(item_check.attr('aria-haspopup')) { 523 current_menu.active_item = item_check; 524 current_menu.active_item.focus(); 525 break; 526 } 527 if( item_check.parent().is(':last-child') && key == keys.RIGHT ) { 528 mm.close( key ); 529 break; 530 } else if ( item_check.parent().is(':first-child') && key == keys.LEFT ) { 531 mm.close( key ); 532 break; 533 } 534 } 535 } 536 537 this.closeMenu = function( menu_id ) { 538 var menu = open_menus[ menu_id ]; 539 var parent_li = menu.menu_parent.parent(); 540 parent_li.removeClass('over'); 541 menu.menu_parent.attr('aria-expanded', 'false'); 542 menu.menu_elem.attr('aria-expanded', 'false'); 543 menu.menu_elem.attr('aria-hidden', 'true'); 544 menu.menu_elem.css("visibility", ""); 545 menu.menu_elem.css("display", ""); 546 547 menu.menu_elem.find(".keyboard-only").hide(); 548 mm.setParentHref(menu.menu_parent); 549 if(menu.menu_items != null) { 550 menu.menu_items.each(function() { 551 if(jAria(this).hasClass("at-focus")) { 552 jAria(this).removeClass('at-focus'); 553 } 554 }); 555 } 556 557 open_menus.pop(); 558 } 559 560 this.close = function( key ) { 561 if(open_menus.length < 1) { 562 var next = jAria("#content-shortcut-target"); 563 next.attr("tabindex", "-1").focus().attr("tabindex", "0"); 564 return; 565 } 566 var key = (typeof key === "undefined") ? null : key; 567 var num_to_close = (key == keys.ESCAPE || key == keys.LEFT) ? 1 : open_menus.length; 568 var top_level = open_menus[0].menu_parent; 569 var prev_level = open_menus[open_menus.length - 1].menu_parent; 570 for ( var i = open_menus.length; i > 0; --i ) { 571 mm.closeMenu(i-1); 572 num_to_close--; 573 if( num_to_close < 1 ) break; 574 } 575 if( open_menus.length < 1 ) { 576 if( key == keys.LEFT) { 577 if(prev_level == top_level) { 578 var isFirstTopLevel = top_level.parent().is(':first-child'); 579 var parentElement = top_level.parent().get(0); 580 top_level = (isFirstTopLevel) ? parentElement : mm.searchPrevElement(parentElement); 581 top_level_item = top_level.getElementsByTagName('a')[0]; 582 top_level_item.focus(); 583 } else { 584 prev_level.focus(); 585 } 586 } else if( key == keys.RIGHT ) { 587 var isLastTopLevel = top_level.parent().is(':last-child'); 588 var parentElement = top_level.parent().get(0); 589 top_level = (isLastTopLevel) ? parentElement : mm.searchNextElement(parentElement); 590 top_level_item = top_level.getElementsByTagName('a')[0]; 591 top_level_item.focus(); 592 } else { 593 top_level.focus(); 594 } 595 } else { 596 prev_level.focus(); 597 } 598 } 599 600 this.menuCorrection = function ( menu_id ) { 601 if(open_menus.length < 1) return; 602 var num_to_close = open_menus.length; 603 var corrected = false;
604 for ( var i = open_menus.length; i > 0; --i ) { 605 if(open_menus[i-1].menu_id != menu_id) { 606 mm.closeMenu(i-1); 607 num_to_close--; 608 corrected = true; 609 } else if ( (num_to_close < 1) || (open_menus[i-1].menu_id == menu_id) ) { 610 if(corrected || open_menus[open_menus.length-1].active_item != jAria(document.activeElement)) { 611 open_menus[open_menus.length-1].has_focus = true; 612 open_menus[open_menus.length-1].active_item = jAria(document.activeElement); 613 open_menus[open_menus.length-1].active_item.focus(); 614 } 615 break; 616 } 617 } 618 } 619 620 this.filterAlphabetKeys = function ( key ) { 621 if (( key >= 48 && key <= 57 ) 622 || ( key >= 65 && key <= 90 ) 623 || ( key >= 97 && key <= 122 ) ) { 624 var current_menu = open_menus[open_menus.length-1]; 625 if(current_menu.menu_items != null) { 626 if(current_menu.filter_key != key) { 627 current_menu.filtered_items = []; 628 current_menu.filter_key = key; 629 current_menu.filter_iter = 0; 630 current_menu.menu_items.each(function() { 631 if(jAria(this).text().charCodeAt(0) == key || jAria(this).text().charCodeAt(0) == key+32) { 632 jAria(this).addClass("at-focus"); 633 current_menu.filtered_items.push(jAria(this)); 634 } else if(jAria(this).hasClass("at-focus")) { 635 jAria(this).removeClass("at-focus"); 636 } 637 }); 638 if(menu.filtered_items.length > 0) { 639 current_menu.active_item = menu.filtered_items[0]; 640 current_menu.filtered_items[0].focus(); 641 } else { 642 menu.filter_key = null; 643 } 644 } else { 645 current_menu.filter_iter = (current_menu.filter_iter + 1) >= current_menu.filtered_items.length ? 0 : current_menu.filter_iter + 1; 646 current_menu.filtered_items[current_menu.filter_iter].focus(); 647 current_menu.active_item = current_menu.filtered_items[current_menu.filter_iter]; 648 } 649 } 650 } 651 } 652}
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.