(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;
}
}
}
}
})();