1NavTreeItem = (function ($) { 2 3 function NavTreeItem(navTree, parentItem, linkElem, isVertical, expandOnHover, isMobileDevice) { 4 this._initialize(navTree, parentItem, linkElem, isVertical, expandOnHover, isMobileDevice); 5 // After item is initialized, add item to parent's list of children 6 if (this.hasParent()) { 7 this.getParentItem().addChild(this); 8 } 9 } 10 11 // Constants 12 // ---------- 13 14 NavTreeItem.keyCodes = Object.freeze({ 15 ARROW_DOWN: 40, 16 ARROW_LEFT: 37, 17 ARROW_RIGHT: 39, 18 ARROW_UP: 38, 19 END: 35, 20 HOME: 36 21 }); 22 23 NavTreeItem.actions = Object.freeze({ 24 MOVE_NEXT: 1, 25 MOVE_PREV: 2, 26 STEP_INTO: 3, 27 STEP_OUT: 4, 28 TO_END: 5, 29 TO_START: 6 30 }); 31 32 NavTreeItem.prototype = { 33 34 // Properties 35 // ---------- 36 37 _children: [], 38 _hasChildren: false, 39 _hasParent: false, 40 _isCurrentPath: false, 41 _isVertical: false, 42 _isVisible: false, 43 _navLevel: 0, 44 _navTree: null, 45 _parentItem: null, 46 _$linkElem: null, 47 48 49 // Initialization 50 // -------------- 51 52 _initialize: function (navTree, parentItem, linkElem, isVertical, expandTreeOnHover, isMobileDevice) { 53 var hasExpanderIcon = false, 54 $parentListItem; 55 56 // Initalize settings 57 this._navTree = navTree; 58 this._parentItem = parentItem; 59 this._$linkElem = $(linkElem); 60 this._isVertical = (isVertical === true); 61 62 if (this._parentItem) { 63 this._hasParent = true; 64 } 65 this.setTabIndex(-1); 66 67 // Check the class on the parent list item to determine the nav level 68 $parentListItem = this._$linkElem.parent(); 69 if ($parentListItem.hasClass('parent')) { 70 this._navLevel = 1; 71 } else if ($parentListItem.hasClass('child')) { 72 this._navLevel = 2; 73 } else { 74 this._navLevel = 3; 75 } 76 77 // Check the class on the link element to determine if this nav item is on the current page path 78 if (this._$linkElem.hasClass('current')) { 79 this._isCurrentPath = true; 80 } 81 82 // If hover expansion is disabled and this list item has a child list, add an expander element 83 if (!expandTreeOnHover) { 84 hasExpanderIcon = ($parentListItem.children('ul').length > 0); 85 if (hasExpanderIcon) { 86 $parentListItem.prepend('<span class="expander"></span>'); 87 } else { 88 $parentListItem.addClass('no-children'); 89 } 90 } 91 92 // Set up event handlers 93 this._$linkElem.on('keydown', $.proxy(this._handleLinkKeyDown, this)); 94 if (expandTreeOnHover && !isMobileDevice) { 95 this._$linkElem.on('mouseenter', $.proxy(this._handleLinkMouseEnter, this)); 96 } 97 if (hasExpanderIcon) { 98 this._$linkElem.on('click', $.proxy(this._handleLinkClick, this)); 99 $parentListItem.children('.expander').on('click', $.proxy(this._handleExpanderClick, this)); 100 } 101 }, 102 103 104 // Public Methods 105 // --------------- 106 107 addChild: function (childItem) { 108 if (childItem) { 109 this._children.push(childItem); 110 this._hasChildren = true; 111 } 112 }, 113 114 blur: function () { 115 this._$linkElem.blur(); 116 }, 117 118 collapse: function () { 119 this._$linkElem.attr('aria-expanded', 'false'); 120 this._$linkElem.next().css('display', 'none'); 121 this._$linkElem.parent().removeClass('active'); 122 }, 123 124 collapseChildren: function () { 125 var length; 126 if (this._hasChildren) { 127 length = this._children.length; 128 for (var i = 0; i < length; i += 1) { 129 this._children[i].collapse(); 130 } 131 } 132 }, 133 134 expand: function () { 135 this._$linkElem.attr('aria-expanded', 'true'); 136 this._$linkElem.next('ul').css('display', 'block'); 137 this._$linkElem.parent().addClass('active'); 138 }, 139 140 focus: function () { 141 this._$linkElem.focus(); 142 }, 143 144 getNavLevel: function () { 145 return this._navLevel; 146 }, 147 148 getParentItem: function () { 149 return this._parentItem; 150 }, 151 152 hasParent: function () { 153 return this._hasParent; 154 }, 155 156 isCurrentPath: function () { 157 return this._isCurrentPath; 158 }, 159 160 isExpandable: function () { 161 return this._hasChildren; 162 }, 163 164 isExpanded: function () { 165 return (this.isExpandable() && this._$linkElem.attr('aria-expanded') === 'true'); 166 }, 167 168 isVisible: function () { 169 return this._isVisible; 170 }, 171 172 setTabIndex: function (tabIndex) { 173 this._$linkElem.attr('tabIndex', tabIndex.toString()); 174 }, 175 176 updateVisibilityFromParent: function () { 177 var parent; 178 if (this.hasParent()) { 179 // Item is visible if its parent is both visible and expanded 180 parent = this.getParentItem(); 181 this._isVisible = (parent.isVisible() && parent.isExpanded()); 182 } else { 183 // Top level items are always visible 184 this._isVisible = true; 185 } 186 }, 187 188 189 // Private Methods 190 // --------------- 191 192 _executeAction: function (actionCode) { 193 var actionWasHandled = false; 194 195 switch (actionCode) { 196 197 case NavTreeItem.actions.MOVE_NEXT: 198 this._navTree.focusNextItem(this); 199 actionWasHandled = true; 200 break; 201 202 case NavTreeItem.actions.MOVE_PREV: 203 this._navTree.focusPreviousItem(this); 204 actionWasHandled = true; 205 break; 206 207 case NavTreeItem.actions.STEP_INTO: 208 if (this.isExpandable()) { 209 if (this.isExpanded()) { 210 this._navTree.focusNextItem(this); 211 } else { 212 this._navTree.expandItem(this); 213 } 214 } 215 actionWasHandled = true; 216 break; 217 218 case NavTreeItem.actions.STEP_OUT: 219 if (this.isExpandable() && this.isExpanded()) { 220 this._navTree.collapseItem(this); 221 actionWasHandled = true; 222 } else if (this.hasParent()) { 223 this._navTree.focusParentItem(this); 224 actionWasHandled = true; 225 } 226 break; 227 228 case NavTreeItem.actions.TO_START: 229 this._navTree.focusFirstItem(); 230 actionWasHandled = true; 231 break; 232 233 case NavTreeItem.actions.TO_END: 234 this._navTree.focusLastItem(); 235 actionWasHandled = true; 236 break; 237 } 238 return actionWasHandled; 239 }, 240 241 _executeKeyAction: function (keyCode) { 242 var actionCode = null, 243 keyWasHandled = false; 244 245 switch (keyCode) { 246 247 case NavTreeItem.keyCodes.ARROW_DOWN: 248 if (this._isVertical) { 249 actionCode = NavTreeItem.actions.MOVE_NEXT; 250 } else { 251 actionCode = NavTreeItem.actions.STEP_INTO; 252 } 253 break; 254 255 case NavTreeItem.keyCodes.ARROW_LEFT: 256 if (this._isVertical) { 257 actionCode = NavTreeItem.actions.STEP_OUT; 258 } else { 259 actionCode = NavTreeItem.actions.MOVE_PREV; 260 } 261 break; 262 263 case NavTreeItem.keyCodes.ARROW_RIGHT: 264 if (this._isVertical) { 265 actionCode = NavTreeItem.actions.STEP_INTO; 266 } else { 267 actionCode = NavTreeItem.actions.MOVE_NEXT; 268 } 269 break; 270 271 case NavTreeItem.keyCodes.ARROW_UP: 272 if (this._isVertical) { 273 actionCode = NavTreeItem.actions.MOVE_PREV; 274 } else { 275 actionCode = NavTreeItem.actions.STEP_OUT; 276 } 277 break; 278 279 case NavTreeItem.keyCodes.END: 280 actionCode = NavTreeItem.actions.TO_END;
281 break; 282 283 case NavTreeItem.keyCodes.HOME: 284 actionCode = NavTreeItem.actions.TO_START; 285 break; 286 } 287 288 // If the key is mapped to an action, execute the action 289 if (actionCode) { 290 keyWasHandled = this._executeAction(actionCode); 291 } 292 293 // Return whether the action was actually handled 294 return keyWasHandled; 295 }, 296 297 // Event Handlers 298 // -------------- 299 300 _handleExpanderClick: function (e) { 301 if (this.isExpanded()) { 302 this._navTree.collapseItem(this); 303 } else if (this.isExpandable()) { 304 this._navTree.expandItem(this); 305 } 306 }, 307 308 _handleLinkClick: function (e) { 309 if (this.isExpandable() && !this.isExpanded()) { 310 e.preventDefault(); 311 this._navTree.expandItem(this); 312 } 313 }, 314 315 _handleLinkKeyDown: function (e) { 316 if (e.altKey || e.ctrlKey || e.metaKey) { 317 return; 318 } 319 if (this._executeKeyAction(e.keyCode)) { 320 e.stopPropagation(); 321 e.preventDefault(); 322 } 323 }, 324 325 _handleLinkMouseEnter: function (e) { 326 this._navTree.unfocusNavItems(this._navLevel); 327 if (this.isExpandable() && !this.isExpanded()) { 328 this._navTree.collapseSiblings(this); 329 this._navTree.expandItem(this); 330 } 331 e.stopPropagation(); 332 e.preventDefault(); 333 } 334 }; 335 336 NavTreeItem.prototype.constructor = NavTreeItem; 337 NavTreeItem.prototype.parent = Object.prototype; 338 339 return NavTreeItem; 340})(jQuery); 341 342NavTree = (function ($) { 343 344 function NavTree() { 345 this._initialize(); 346 } 347 348 NavTree.prototype = { 349 350 // Properties 351 // ---------- 352 353 _allowMultiOpenSiblings: false, 354 _currentPathItems: [], 355 _expandTreeOnHover: true, 356 _firstVisibleItem: null, 357 _isMobile: false, 358 _isMobileDevice: false, 359 _isVertical: false, 360 _lastVisibleItem: null, 361 _navItems: [], 362 _numNavItems: 0, 363 _openToInitialPage: true, 364 _topLevelItems: [], 365 _$container: null, 366 _$navElement: null, 367 _$navMainList: null, 368 _$navParentItems: null, 369 370 371 // Initialization 372 // -------------- 373 374 _initialize: function () { 375 var isMobile = (navigator.userAgent.match(/(iPhone|iPod|Android|BlackBerry)/) !== null), 376 isiPad = (navigator.userAgent.match(/(iPad)/) !== null), 377 isAppleDevice = (navigator.userAgent.match(/(iPhone|iPod|iPad)/) !== null), 378 isAndroidDevice = (navigator.userAgent.match(/(Android)/) !== null), 379 isMobileDevice = (isMobile || isiPad || isAppleDevice || isAndroidDevice); 380 // Init settings 381 this._isMobile = isMobile === true || isiPad === true; 382 this._isMobileDevice = isMobileDevice 383 this._isVertical = this._isMobile; 384 this._allowMultiOpenSiblings = this._isMobile; 385 this._openToInitialPage = !this._isMobile; 386 this._expandTreeOnHover = !this._isMobile; 387 388 // Cache DOM elements 389 this._$container = $('.header-nav'); 390 this._$navElement = this._isMobile ? this._$container.find('#menu-mobile') : this._$container.find('#menu-desktop'); 391 this._$navMainList = this._$navElement.children('ul[role="menu"]'); 392 this._$navParentItems = this._$navMainList.find('li.parent'); 393 394 if (!this._$navMainList.length) { 395 return false; 396 } 397 398 // Set the ARIA Orientation 399 if (this._isVertical) {
400 this._$navMainList.attr('aria-orientation', 'vertical'); 401 } else { 402 this._$navMainList.attr('aria-orientation', 'horizontal'); 403 } 404 405 // Initialize the tree structure 406 this._loadNavTreeItems(this, null, this._$navMainList[0]); 407 this._updateNavItemsVisibility(); 408 this._firstVisibleItem.setTabIndex(0); 409 410 if (this._openToInitialPage) { 411 // Open the navigation to show the current page 412 this._expandNavToCurrentPage(); 413 } 414 }, 415 416 417 // Public Methods 418 // --------------- 419 420 collapseItem: function (focusedItem) { 421 var navLevel; 422 if (focusedItem.isExpanded()) { 423 // Collapse the currently focused item 424 navLevel = focusedItem.getNavLevel(); 425 focusedItem.collapse(); 426 this._animateContainerHeight(navLevel); 427 this._updateNavItemsVisibility(); 428 } 429 }, 430 431 collapseSiblings: function (focusedItem) { 432 if (focusedItem.hasParent()) { 433 var p = focusedItem.getParentItem(); 434 focusedItem.getParentItem().collapseChildren(); 435 } else { 436 for (var i = 0; i < this._topLevelItems.length; i += 1) { 437 this._topLevelItems[i].collapse(); 438 } 439 } 440 }, 441 442 expandItem: function (focusedItem) { 443 var navLevel, 444 currentPathLeaf; 445 if (focusedItem.isExpandable()) { 446 if (!this._allowMultiOpenSiblings) { 447 // First collapse any open sibling items 448 this.collapseSiblings(focusedItem); 449 } 450 if(navLevel === undefined) { 451 navLevel = (focusedItem.getNavLevel() + 1); 452 } 453 454 this._animateContainerHeight(navLevel, $.proxy(function () { 455 focusedItem.expand(); 456 this._updateNavItemsVisibility(); 457 }, this)); 458 } 459 }, 460 461 focusFirstItem: function () { 462 this._focusItem(this._firstVisibleItem); 463 }, 464 465 focusLastItem: function () { 466 this._focusItem(this._lastVisibleItem); 467 }, 468 469 focusNextItem: function (focusedItem) { 470 var item = null, 471 itemToFocus = null; 472 473 // Loop backwards through visible items up until the current focused item is reached 474 for (var i = (this._numNavItems - 1) ; i >= 0; i-=1) { 475 item = this._navItems[i]; 476 if (item === focusedItem) { 477 break; 478 } 479 if (item.isVisible()) { 480 itemToFocus = item; 481 } 482 } 483 // If a next item was found, focus it 484 if (itemToFocus) { 485 this._focusItem(itemToFocus); 486 } 487 }, 488 489 focusParentItem: function (focusedItem) { 490 if (focusedItem.hasParent()) { 491 this._focusItem(focusedItem.getParentItem()); 492 } 493 }, 494 495 focusPreviousItem: function (focusedItem) { 496 var item = null, 497 itemToFocus = null; 498 499 // Loop foward through visible items up until the current focused item is reached 500 for (var i = 0; i < this._numNavItems; i += 1) { 501 item = this._navItems[i]; 502 if (item === focusedItem) { 503 break; 504 } 505 if (item.isVisible()) { 506 itemToFocus = item; 507 } 508 } 509 // If a previous item was found, focus it 510 if (itemToFocus) { 511 this._focusItem(itemToFocus); 512 } 513 }, 514 515 unfocusNavItems: function (navLevel) { 516 var item; 517 for (var i = 0; i < this._numNavItems; i += 1) { 518 item = this._navItems[i]; 519 if (item._navLevel >= navLevel) { 520 this.collapseItem(item); 521 } 522 this._navItems[i].blur(); 523 } 524 }, 525 526 527 // Private Methods 528 // --------------- 529 530 _animateContainerHeight: function (navLevel, animateCompleteCallbackFunc) { 531 var duration = 250, 532 height = this._getNavPixelHeight(navLevel); 533 534 // Stop any current animations 535 this._$container.stop(); 536 // If the height already matches the desired height, make the duration shorter 537 // Note: duration should not be set to zero because the brief flash between displaying helps visually with the transition 538 if (this._$container.height() === height) { 539 duration = 50; 540 } 541 // Do the animation 542 this._$container.animate({ 'height': height.toString() }, duration, animateCompleteCallbackFunc); 543 }, 544 545 _expandNavToCurrentPage: function (collapseAllFirst) { 546 var item, 547 navLevel = 1; 548 549 // Collapse all nav items on init 550 if (collapseAllFirst) { 551 for (var i = 0; i < this._numNavItems; i += 1) { 552 item = this._navItems[i]; 553 if (item.isExpanded()) { 554 item.collapse(); 555 } 556 } 557 } 558 559 if (this._isMobile) { 560 this._animateContainerHeight(navLevel, $.proxy(function () { 561 for (var i = 0; i < this._numNavItems; i += 1) { 562 // This item is the page we're on 563 item = this._navItems[i]; 564 if (item.isExpandable()) { 565 item.expand(); 566 } else { 567 // focus should be on current page not first item in the tree 568 this._firstVisibleItem.setTabIndex(-1); 569 item.setTabIndex(0); 570 } 571 } 572 this._updateNavItemsVisibility(); 573 }, this)); 574 } else { 575 // Animate the nav and then show the items upon completion 576 this._animateContainerHeight(navLevel, $.proxy(function () { 577 this._firstVisibleItem.setTabIndex(-1); 578 item.setTabIndex(0); 579 this._updateNavItemsVisibility(); 580 }, this)); 581 } 582 }, 583 584 _focusItem: function (navItem) { 585 var item; 586 for (var i = 0; i < this._numNavItems; i += 1) { 587 item = this._navItems[i]; 588 if (item === navItem) { 589 item.setTabIndex(0); 590 item.focus(); 591 } else { 592 item.setTabIndex(-1); 593 } 594 } 595 }, 596 597 _getCurrentPathLeaf: function (navItem) { 598 // navItems are stored in a flat structure 599 for (var i = this._numNavItems - 1; i >= 0; i--) { 600 if (this._navItems[i].isCurrentPath()) { 601 return this._navItems[i]; 602 } 603 } 604 605 return false; 606 }, 607 608 _getNavPixelHeight: function (navDisplayLevel) { 609 if (navDisplayLevel === 1) { return 57; } 610 if (navDisplayLevel === 2) { return 111; } 611 return 169; 612 }, 613 614 _loadNavTreeItems: function (navTree, parentItem, listElem) { 615 var childListElems, 616 listItemElem, 617 navItem; 618 619 // Iterate through the list items in the current list 620 listItemElem = listElem.firstElementChild; 621 while (listItemElem) { 622 // Create a NavTreeItem based on the link tag inside the current list item
623 navItem = new NavTreeItem(navTree, parentItem, listItemElem.firstElementChild, this._isVertical, this._expandTreeOnHover, this._isMobileDevice); 624 this._navItems.push(navItem); 625 if (!navItem.hasParent()) { 626 this._topLevelItems.push(navItem); 627 } 628 if (navItem.isCurrentPath()) { 629 this._currentPathItems.push(navItem); 630 } 631 // If the list item contains a child list, recursively load its items 632 childListElems = listItemElem.getElementsByTagName('ul'); 633 if (childListElems.length) { 634 this._loadNavTreeItems(navTree, navItem, childListElems[0]); 635 } 636 // Move to the next sibling in the list 637 listItemElem = listItemElem.nextElementSibling; 638 } 639 this._numNavItems = this._navItems.length; 640 }, 641 642 _toggleMobileNavItem: function($elem, active) { 643 if (active) { 644 $elem.addClass('active'); 645 $elem.siblings().addClass('inactive'); 646 } else { 647 $elem.removeClass('active'); 648 $elem.siblings().removeClass('inactive'); 649 } 650 }, 651 652 _updateNavItemsVisibility: function () { 653 this._firstVisibleItem = this._navItems[0]; 654 655 for (var i = 0; i < this._numNavItems; i += 1) { 656 var item = this._navItems[i]; 657 item.updateVisibilityFromParent(); 658 if (item.isVisible()) { 659 this._lastVisibleItem = item; 660 } 661 } 662 }, 663 664 665 // Event Handlers 666 // -------------- 667 668 _handleMobileNavExpanderClick: function (e) { 669 var $expander = $(e.currentTarget); 670 if ($expander.parent().hasClass('active')) { 671 this._toggleMobileNavItem($expander.parent(), false); 672 } else { 673 this._toggleMobileNavItem($expander.parent(), true); 674 } 675 }, 676 677 _handleMobileNavLinkClick: function (e) { 678 var $link = $(e.currentTarget); 679 if (!$link.parent().hasClass('active')) { 680 e.preventDefault(); 681 this._toggleMobileNavItem($link.parent(), true); 682 } 683 }, 684 685 _handleNavContainerMouseLeave: function () { 686 this._expandNavToCurrentPage(true); 687 } 688 }; 689 690 NavTree.prototype.constructor = NavTree; 691 NavTree.prototype.parent = Object.prototype; 692 693 return NavTree; 694})(jQuery);
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.