Source:site-navigation.directive.js

(function () {
	'use strict';
	// Main Site Navigation
	angular
		.module('mohistory')
		.directive('siteNavigation', siteNavigation);
	siteNavigation.$inject = ['$transitions', '$rootScope'];
	/**
	 * Directive for the main navigation bar. Data to populate menu is passed in from parent.
	 * @memberof mohistory
     * @name collectionLists
     * @ngdoc directive
	 * @param {object} $transitions UI-Router service for creating hooks that are triggered on route changes
	 * @param {object} $rootScope Master scope for any AngularJS application
	 */
	function siteNavigation($transitions, $rootScope) {
		var directive = {
			templateUrl: 'app/components/site-navigation/site-navigation.directive.html',
			controller: siteNavigationCtrl,
			controllerAs: 'siteNav',
			bindToController: true,
			scope: {
				nav: '<',
				alerts: '<',
			},
			link: siteNavigationLink,
			restrict: 'E',
		};
		return directive;
		function siteNavigationCtrl() {
			var vm = this;
			/* ----- Variables ----- */
			vm.menuOpen = false;
			vm.breadcrumbs = [];
			// menuOpenCount is used to apply class when menu is 
			// closed for first time.
			vm.menuOpenCount = 0;
			vm.themeNames = {
				society: 'MHS',
				museum: 'MHM',
				library: 'LRC',
				memorial: 'SM',
			};
			vm.themeLookup = {
				MHS: 'society',
				MHM: 'museum',
				LRC: 'library',
				SM: 'memorial',
			};
			// UI-Router hook triggered when a transition from one state to another is registered.
			// When the menu is open, clicking any link except for the location links at the bottom
			// will close the menu.
			var uiOnStart = $transitions.onStart({}, function ($transition$) {
				var params = $transition$.params();
				var exclude = ['society', 'library', 'memorial', 'museum'];
				if (params.firstID && !params.secondID && exclude.indexOf(params.firstID) > 0) {
					// If the route is '/society', '/library', '/memorial', or '/museum' then do nothing
					return true;
				} else {
					vm.closeMenu();
				}
			});
			// UI-Router hook triggered when a transition from one state to another is registered.
			// Any transition error will trigger the menu to close.
			var uiOnError = $transitions.onError({}, function ($transition$) {
				vm.closeMenu();
			});
			/* ----- Function Bindings ----- */
			vm.$onDestroy = onDestroy;
			vm.toggleMenu = toggleMenu;
			vm.closeMenu = closeMenu;
			vm.toggleSubmenu = toggleSubmenu;
			vm.isSubmenuOpen = isSubmenuOpen;
			/* ----- Function Definitions ----- */
			/**
			 * Clean up the UI-Router hooks when the component is destroyed.
			 * @memberof siteNavigation
			 * @function onDestroy
			 */
			function onDestroy() {
				uiOnStart();
				uiOnError();
			}
			/**
			 * Toggle the menu from closed to open or visa versa.
			 * @memberof siteNavigation
			 * @function toggleMenu
			 */
			function toggleMenu() {
				if (vm.menuOpen) {
					// Close Menu
					vm.menuOpen = false;
					vm.breadcrumbs = [];
				} else {
					// Open Menu
					vm.menuOpen = true;
					vm.menuOpenCount++;
				}
			}
			/**
			 * Completely close the menu.
			 * @memberof siteNavigation
			 * @function closeMenu
			 */
			function closeMenu() {
				vm.menuOpen = false;
				vm.breadcrumbs = [];
			}
			/**
			 * Open or close a submenu depending on the menu's current state.
			 * @memberof siteNavigation
			 * @function toggleSubmenu
			 * @param {String} level The name of the level to open or close. Names 
			 * come from the links that are used to toggle the submenu. 
			 */
			function toggleSubmenu(level) {
				if (level === vm.breadcrumbs[1]) {
					// Go back one level
					vm.breadcrumbs.pop();
				} else if (level === vm.breadcrumbs[0]) {
					// Go Back to Top menu
					vm.breadcrumbs = [];
				} else {
					vm.breadcrumbs.push(level);
				}
			}
			/**
			 * Returns true if any submenu is open, false otherwise.
			 * @memberof siteNavigation
			 * @function isSubmenuOpen
			 * @param {string} level The name of the level to check. Names come from
			 * the links that are used to toggle the submenu. 
			 * @return {boolean} True if submenu is open, false otherwise.
			 */
			function isSubmenuOpen(level) {
				return vm.breadcrumbs[0] === level || vm.breadcrumbs[1] === level;
			}
			/**
			 * Returns true if given submenu is open, false otherwise.
			 * @memberof siteNavigation
			 * @function isSubmenuVisible
			 * @param {string} level The name of the level to check. Names come from
			 * the links that are used to toggle the submenu. 
			 * @return {boolean} True if submenu is open, false otherwise.
			 */
			function isSubmenuVisible(level) {
				return vm.breadcrumbs[vm.breadcrumbs.length - 1] === level;
			}
		}
		/**
		 * Update the DOM and handle keyboard events for site-navigation.
		 * @function siteNavigationLink
		 * @memberof siteNavigation
		 * @param {String} $scope an AngularJS scope object.
		 * @param {String} $element the jqLite-wrapped element that this directive matches.
		 * @param {String} $attrs a hash object with key-value pairs of normalized attribute 
		 * names and their corresponding attribute values.
		 * @param {String} $ctrl the controller associated with the directive
		 */
		function siteNavigationLink($scope, $element, $attrs, $ctrl) {
			/* ----- Variables ----- */
			var body = $('body');
			var html = $('html');
			var header = $element.find('.main-header');
			var hamburger = header.find('#hamburger');
			var mainMenu = header.find('#main-menu');
			var menuOpen = header.find('#main-open');
			var firstFocusMenu = header.find('#menu-close');
			var lastFocusMenu = header.find('#last-link');
			var prevScrollPos = $rootScope.viewport.scrollX;
			/* ----- Scope and DOM Listeners ----- */
			body.on('keyup', closeOnEscape);
			hamburger.on('keydown', toggleOnEnter);
			hamburger.on('click', toggleOnClick);
			hamburger.on('keydown', onShiftTabGoToLast);
			lastFocusMenu.on('keydown', onTabGoToFirst);
			// Watch the boolean value passed in from the site-navigation component
			$scope.$watch(function () {
				return $ctrl.menuOpen;
			}, toggleFocusLock);
			// Watch the scroll position stored on the rootScope
			$scope.$watch(function () {
				return $rootScope.viewport.scrollY;
			}, hideOnScroll);
			// Remove JQuery listeners when directive is destroyed
			$scope.$on('$destroy', function () {
				body.off('keyup', closeOnEscape);
				// hamburger.off('keydown', toggleOnEnter);
				hamburger.off('keydown', onShiftTabGoToLast);
				lastFocusMenu.off('keydown', onTabGoToFirst);
			});
			/* ----- Function Definitions ----- */
			/**
			 * When the menu is opened make sure the close button has focus and scrolling
			 * behind the menu is disabled.
			 * @param {String} value true if the menu is open, false otherwise
			 * @memberof siteNavigation
			 * @function toggleFocusLock
			 */
			function toggleFocusLock(value) {
				value = Boolean(value);
				if (value) {
					hamburger.focus();
					body.addClass('locked');
					html.addClass('locked');
				} else {
					menuOpen.focus();
					body.removeClass('locked');
					html.removeClass('locked');
				}
			}
			/**
			 * Hitting enter on the hamburger button, opens/closes the menu.
			 * @memberof siteNavigation
			 * @param {object} e event object
			 */
			function toggleOnEnter(e) {
				var keyCode = e.keyCode || e.which;
				if (hamburger.is(':focus') && keyCode === 13) {
					e.preventDefault();
					e.stopImmediatePropagation();
					$ctrl.toggleMenu();
					$scope.$apply();
				}
			}
			/**
			 * Clicking the hamburger button opens/closes the menu.
			 * @param {object} e event object
			 */
			function toggleOnClick(e) {
				$ctrl.toggleMenu();
				$scope.$apply();
			}
			/**
			 * Close the menu when the escape key is pressed.
			 * @memberof siteNavigation
			 * @function closeOnEscape
			 * @param {Object} e event Object 
			 */
			function closeOnEscape(e) {
				var keyCode = e.keyCode || e.which;
				if (keyCode === 27) {
					$ctrl.closeMenu();
					$scope.$apply();
				}
			}
			/**
			 * When menu is open pressing shift tab on the hamburger icon will
			 * move focus to the bottom of the menu.
			 * @memberof siteNavigation
			 * @function onShiftTabGoToLast
			 * @param {Object} e event object
			 */
			function onShiftTabGoToLast(e) {
				var keyCode = e.keyCode || e.which;
				if (keyCode === 9 && e.shiftKey && $ctrl.menuOpen) {
					lastFocusMenu.focus();
					e.preventDefault();
				}
			}
			/**
			 * When menu is open pressing tab on the last link in the menu
			 * will move focus to the top of the menu.
			 * @memberof siteNavigation
			 * @function onTabGoToFirst
			 * @param {Object} e event object
			 */
			function onTabGoToFirst(e) {
				var keyCode = e.keyCode || e.which;
				if (keyCode === 9 && !e.shiftKey && $ctrl.menuOpen) {
					hamburger.focus();
					e.preventDefault();
				}
			}
			/**
			 * Hide the menu when scrolling down, but show it again when scrolling back up.
			 * @memberof siteNavigation
			 * @function hideOnScroll
			 */
			function hideOnScroll() {
				if (!$ctrl.menuOpen) {
					var headerHeight = header[0].offsetHeight;
					var currScrollPos = $rootScope.viewport.scrollY;
					// Scrolling down
					if (currScrollPos > prevScrollPos) {
						header.removeClass('peek');
						hamburger.removeClass('peek');
					} 
					// Scrolling back up, but it only activates if we have already scrolled
					// up 50 units. 50 is a arbitrary number, but it's enough to prevent
					// jerky behavior on the thumbnail view for search
					if(prevScrollPos - currScrollPos > 50) {
						header.addClass('peek');
						hamburger.addClass('peek');
					};
					// Close enough to the top of the page, not to hide the
					// header
					if ($rootScope.viewport.scrollY <= headerHeight) {
						header.addClass('peek');
						hamburger.addClass('peek');
					}
					prevScrollPos = currScrollPos;
				}
			}
		}
	}
})();