/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* File: menubar-navigation.js
*
* Desc: Creates a menubar of hierarchical set of links
*/
'use strict';
class NavigationContentGenerator {
constructor(siteURL, siteName) {
this.siteName = siteName;
this.siteURL = siteURL;
this.fillerTextSentences = [];
this.fillerTextSentences.push(
'The content on this page is associated with the $linkName link for $siteName.'
);
}
renderParagraph(linkURL, linkName) {
var content = '';
this.fillerTextSentences.forEach(
(s) =>
(content += s
.replace('$siteName', this.siteName)
.replace('$siteURL', this.siteURL)
.replace('$linkName', linkName)
.replace('$linkURL', linkURL))
);
return content;
}
}
class MenubarNavigation {
constructor(domNode) {
var linkURL, linkTitle;
this.domNode = domNode;
this.menuitems = [];
this.popups = [];
this.menuitemGroups = {};
this.menuOrientation = {};
this.isPopup = {};
this.isPopout = {};
this.openPopups = false;
this.firstChars = {}; // see Menubar init method
this.firstMenuitem = {}; // see Menubar init method
this.lastMenuitem = {}; // see Menubar init method
this.initMenu(domNode, 0);
domNode.addEventListener('focusin', this.onMenubarFocusin.bind(this));
domNode.addEventListener('focusout', this.onMenubarFocusout.bind(this));
window.addEventListener(
'pointerdown',
this.onBackgroundPointerdown.bind(this),
true
);
domNode.querySelector('[role=menuitem]').tabIndex = 0;
// Initial content for page
if (location.href.split('#').length > 1) {
linkURL = location.href;
linkTitle = getLinkNameFromURL(location.href);
} else {
linkURL = location.href + '#home';
linkTitle = 'Home';
}
this.contentGenerator = new NavigationContentGenerator(
'#home',
'Mythical University'
);
this.updateContent(linkURL, linkTitle, false);
function getLinkNameFromURL(url) {
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
var name = url.split('#')[1];
if (typeof name === 'string') {
name = name.split('-').map(capitalize).join(' ');
} else {
name = 'Home';
}
return name;
}
}
getParentMenuitem(menuitem) {
var node = menuitem.parentNode;
if (node) {
node = node.parentNode;
if (node) {
node = node.previousElementSibling;
if (node) {
if (node.getAttribute('role') === 'menuitem') {
return node;
}
}
}
}
return false;
}
updateContent(linkURL, linkName, moveFocus) {
var h1Node, paraNodes, pathNode;
if (typeof moveFocus !== 'boolean') {
moveFocus = true;
}
// Update content area
h1Node = document.querySelector('.page .main h1');
if (h1Node) {
h1Node.textContent = linkName;
h1Node.tabIndex = -1;
if (moveFocus) {
h1Node.focus();
}
}
paraNodes = document.querySelectorAll('.page .main p');
paraNodes.forEach(
(p) =>
(p.innerHTML = this.contentGenerator.renderParagraph(linkURL, linkName))
);
// Update aria-current
this.menuitems.forEach((item) => {
item.removeAttribute('aria-current');
item.classList.remove('aria-current-path');
item.title = '';
});
this.menuitems.forEach((item) => {
if (item.href === linkURL) {
item.setAttribute('aria-current', 'page');
pathNode = this.getParentMenuitem(item);
while (pathNode) {
pathNode.classList.add('aria-current-path');
pathNode.title = 'Contains current page link';
pathNode = this.getParentMenuitem(pathNode);
}
}
});
}
getMenuitems(domNode, depth) {
var nodes = [];
var initMenu = this.initMenu.bind(this);
var popups = this.popups;
function findMenuitems(node) {
var role, flag;
while (node) {
flag = true;
role = node.getAttribute('role');
if (role) {
role = role.trim().toLowerCase();
}
switch (role) {
case 'menu':
node.tabIndex = -1;
initMenu(node, depth + 1);
flag = false;
break;
case 'menuitem':
if (node.getAttribute('aria-haspopup') === 'true') {
popups.push(node);
}
nodes.push(node);
break;
default:
break;
}
if (
flag &&
node.firstElementChild &&
node.firstElementChild.tagName !== 'svg'
) {
findMenuitems(node.firstElementChild);
}
node = node.nextElementSibling;
}
}
findMenuitems(domNode.firstElementChild);
return nodes;
}
initMenu(menu, depth) {
var menuitems, menuitem, role;
var menuId = this.getMenuId(menu);
menuitems = this.getMenuitems(menu, depth);
this.menuOrientation[menuId] = this.getMenuOrientation(menu);
this.isPopup[menuId] = menu.getAttribute('role') === 'menu' && depth === 1;
this.isPopout[menuId] = menu.getAttribute('role') === 'menu' && depth > 1;
this.menuitemGroups[menuId] = [];
this.firstChars[menuId] = [];
this.firstMenuitem[menuId] = null;
this.lastMenuitem[menuId] = null;
for (var i = 0; i < menuitems.length; i++) {
menuitem = menuitems[i];
role = menuitem.getAttribute('role');
if (role.indexOf('menuitem') < 0) {
continue;
}
menuitem.tabIndex = -1;
this.menuitems.push(menuitem);
this.menuitemGroups[menuId].push(menuitem);
this.firstChars[menuId].push(
menuitem.textContent.trim().toLowerCase()[0]
);
menuitem.addEventListener('keydown', this.onKeydown.bind(this));
menuitem.addEventListener('click', this.onMenuitemClick.bind(this), {
capture: true,
});
menuitem.addEventListener(
'pointerover',
this.onMenuitemPointerover.bind(this)
);
if (!this.firstMenuitem[menuId]) {
if (this.hasPopup(menuitem)) {
menuitem.tabIndex = 0;
}
this.firstMenuitem[menuId] = menuitem;
}
this.lastMenuitem[menuId] = menuitem;
}
}
setFocusToMenuitem(menuId, newMenuitem) {
this.closePopupAll(newMenuitem);
if (this.menuitemGroups[menuId]) {
this.menuitemGroups[menuId].forEach(function (item) {
if (item === newMenuitem) {
item.tabIndex = 0;
newMenuitem.focus();
} else {
item.tabIndex = -1;
}
});
}
}
setFocusToFirstMenuitem(menuId) {
this.setFocusToMenuitem(menuId, this.firstMenuitem[menuId]);
}
setFocusToLastMenuitem(menuId) {
this.setFocusToMenuitem(menuId, this.lastMenuitem[menuId]);
}
setFocusToPreviousMenuitem(menuId, currentMenuitem) {
var newMenuitem, index;
if (currentMenuitem === this.firstMenuitem[menuId]) {
newMenuitem = this.lastMenuitem[menuId];
} else {
index = this.menuitemGroups[menuId].indexOf(currentMenuitem);
newMenuitem = this.menuitemGroups[menuId][index - 1];
}
this.setFocusToMenuitem(menuId, newMenuitem);
return newMenuitem;
}
setFocusToNextMenuitem(menuId, currentMenuitem) {
var newMenuitem, index;
if (currentMenuitem === this.lastMenuitem[menuId]) {
newMenuitem = this.firstMenuitem[menuId];
} else {
index = this.menuitemGroups[menuId].indexOf(currentMenuitem);
newMenuitem = this.menuitemGroups[menuId][index + 1];
}
this.setFocusToMenuitem(menuId, newMenuitem);
return newMenuitem;
}
setFocusByFirstCharacter(menuId, currentMenuitem, char) {
var start, index;
char = char.toLowerCase();
// Get start index for search based on position of currentItem
start = this.menuitemGroups[menuId].indexOf(currentMenuitem) + 1;
if (start >= this.menuitemGroups[menuId].length) {
start = 0;
}
// Check remaining slots in the menu
index = this.getIndexFirstChars(menuId, start, char);
// If not found in remaining slots, check from beginning
if (index === -1) {
index = this.getIndexFirstChars(menuId, 0, char);
}
// If match was found...
if (index > -1) {
this.setFocusToMenuitem(menuId, this.menuitemGroups[menuId][index]);
}
}
// Utilities
getIndexFirstChars(menuId, startIndex, char) {
for (var i = startIndex; i < this.firstChars[menuId].length; i++) {
if (char === this.firstChars[menuId][i]) {
return i;
}
}
return -1;
}
isPrintableCharacter(str) {
return str.length === 1 && str.match(/\S/);
}
getIdFromAriaLabel(node) {
var id = node.getAttribute('aria-label');
if (id) {
id = id.trim().toLowerCase().replace(' ', '-').replace('/', '-');
}
return id;
}
getMenuOrientation(node) {
var orientation = node.getAttribute('aria-orientation');
if (!orientation) {
var role = node.getAttribute('role');
switch (role) {
case 'menubar':
orientation = 'horizontal';
break;
case 'menu':
orientation = 'vertical';
break;
default:
break;
}
}
return orientation;
}
getMenuId(node) {
var id = false;
var role = node.getAttribute('role');
while (node && role !== 'menu' && role !== 'menubar') {
node = node.parentNode;
if (node) {
role = node.getAttribute('role');
}
}
if (node) {
id = role + '-' + this.getIdFromAriaLabel(node);
}
return id;
}
getMenu(menuitem) {
var menu = menuitem;
var role = menuitem.getAttribute('role');
while (menu && role !== 'menu' && role !== 'menubar') {
menu = menu.parentNode;
if (menu) {
role = menu.getAttribute('role');
}
}
return menu;
}
// Popup menu methods
isAnyPopupOpen() {
for (var i = 0; i < this.popups.length; i++) {
if (this.popups[i].getAttribute('aria-expanded') === 'true') {
return true;
}
}
return false;
}
setMenubarDataExpanded(value) {
this.domNode.setAttribute('data-menubar-item-expanded', value);
}
isMenubarDataExpandedTrue() {
return this.domNode.getAttribute('data-menubar-item-expanded') === 'true';
}
openPopup(menuId, menuitem) {
// set aria-expanded attribute
var popupMenu = menuitem.nextElementSibling;
if (popupMenu) {
var rect = menuitem.getBoundingClientRect();
// Set CSS properties
if (this.isPopup[menuId]) {
popupMenu.parentNode.style.position = 'relative';
popupMenu.style.display = 'block';
popupMenu.style.position = 'absolute';
popupMenu.style.left = rect.width + 10 + 'px';
popupMenu.style.top = '0px';
popupMenu.style.zIndex = 100;
} else {
popupMenu.style.display = 'block';
popupMenu.style.position = 'absolute';
popupMenu.style.left = '0px';
popupMenu.style.top = rect.height + 8 + 'px';
popupMenu.style.zIndex = 100;
}
menuitem.setAttribute('aria-expanded', 'true');
this.setMenubarDataExpanded('true');
return this.getMenuId(popupMenu);
}
return false;
}
closePopout(menuitem) {
var menu,
menuId = this.getMenuId(menuitem),
cmi = menuitem;
while (this.isPopup[menuId] || this.isPopout[menuId]) {
menu = this.getMenu(cmi);
cmi = menu.previousElementSibling;
menuId = this.getMenuId(cmi);
menu.style.display = 'none';
}
cmi.focus();
return cmi;
}
closePopup(menuitem) {
var menu,
menuId = this.getMenuId(menuitem),
cmi = menuitem;
if (this.isMenubar(menuId)) {
if (this.isOpen(menuitem)) {
menuitem.setAttribute('aria-expanded', 'false');
menuitem.nextElementSibling.style.display = 'none';
}
} else {
menu = this.getMenu(menuitem);
cmi = menu.previousElementSibling;
cmi.setAttribute('aria-expanded', 'false');
cmi.focus();
menu.style.display = 'none';
}
return cmi;
}
doesNotContain(popup, menuitem) {
if (menuitem) {
return !popup.nextElementSibling.contains(menuitem);
}
return true;
}
closePopupAll(menuitem) {
if (typeof menuitem !== 'object') {
menuitem = false;
}
for (var i = 0; i < this.popups.length; i++) {
var popup = this.popups[i];
if (this.doesNotContain(popup, menuitem) && this.isOpen(popup)) {
var cmi = popup.nextElementSibling;
if (cmi) {
popup.setAttribute('aria-expanded', 'false');
cmi.style.display = 'none';
}
}
}
}
hasPopup(menuitem) {
return menuitem.getAttribute('aria-haspopup') === 'true';
}
isOpen(menuitem) {
return menuitem.getAttribute('aria-expanded') === 'true';
}
isMenubar(menuId) {
return !this.isPopup[menuId] && !this.isPopout[menuId];
}
isMenuHorizontal(menuitem) {
return this.menuOrientation[menuitem] === 'horizontal';
}
hasFocus() {
return this.domNode.classList.contains('focus');
}
// Menu event handlers
onMenubarFocusin() {
// if the menubar or any of its menus has focus, add styling hook for hover
this.domNode.classList.add('focus');
}
onMenubarFocusout() {
// remove styling hook for hover on menubar item
this.domNode.classList.remove('focus');
}
onKeydown(event) {
var tgt = event.currentTarget,
key = event.key,
flag = false,
menuId = this.getMenuId(tgt),
id,
popupMenuId,
mi;
switch (key) {
case ' ':
case 'Enter':
if (this.hasPopup(tgt)) {
this.openPopups = true;
popupMenuId = this.openPopup(menuId, tgt);
this.setFocusToFirstMenuitem(popupMenuId);
} else {
if (tgt.href !== '#') {
this.closePopupAll();
this.updateContent(tgt.href, tgt.textContent.trim());
this.setMenubarDataExpanded('false');
}
}
flag = true;
break;
case 'Esc':
case 'Escape':
this.openPopups = false;
mi = this.closePopup(tgt);
id = this.getMenuId(mi);
this.setMenubarDataExpanded('false');
flag = true;
break;
case 'Up':
case 'ArrowUp':
if (this.isMenuHorizontal(menuId)) {
if (this.hasPopup(tgt)) {
this.openPopups = true;
popupMenuId = this.openPopup(menuId, tgt);
this.setFocusToLastMenuitem(popupMenuId);
}
} else {
this.setFocusToPreviousMenuitem(menuId, tgt);
}
flag = true;
break;
case 'ArrowDown':
case 'Down':
if (this.isMenuHorizontal(menuId)) {
if (this.hasPopup(tgt)) {
this.openPopups = true;
popupMenuId = this.openPopup(menuId, tgt);
this.setFocusToFirstMenuitem(popupMenuId);
}
} else {
this.setFocusToNextMenuitem(menuId, tgt);
}
flag = true;
break;
case 'Left':
case 'ArrowLeft':
if (this.isMenuHorizontal(menuId)) {
mi = this.setFocusToPreviousMenuitem(menuId, tgt);
if (this.isAnyPopupOpen() || this.isMenubarDataExpandedTrue()) {
this.openPopup(menuId, mi);
}
} else {
if (this.isPopout[menuId]) {
mi = this.closePopup(tgt);
id = this.getMenuId(mi);
mi = this.setFocusToMenuitem(id, mi);
} else {
mi = this.closePopup(tgt);
id = this.getMenuId(mi);
mi = this.setFocusToPreviousMenuitem(id, mi);
this.openPopup(id, mi);
}
}
flag = true;
break;
case 'Right':
case 'ArrowRight':
if (this.isMenuHorizontal(menuId)) {
mi = this.setFocusToNextMenuitem(menuId, tgt);
if (this.isAnyPopupOpen() || this.isMenubarDataExpandedTrue()) {
this.openPopup(menuId, mi);
}
} else {
if (this.hasPopup(tgt)) {
popupMenuId = this.openPopup(menuId, tgt);
this.setFocusToFirstMenuitem(popupMenuId);
} else {
mi = this.closePopout(tgt);
id = this.getMenuId(mi);
mi = this.setFocusToNextMenuitem(id, mi);
this.openPopup(id, mi);
}
}
flag = true;
break;
case 'Home':
case 'PageUp':
this.setFocusToFirstMenuitem(menuId, tgt);
flag = true;
break;
case 'End':
case 'PageDown':
this.setFocusToLastMenuitem(menuId, tgt);
flag = true;
break;
case 'Tab':
this.openPopups = false;
this.setMenubarDataExpanded('false');
this.closePopup(tgt);
break;
default:
if (this.isPrintableCharacter(key)) {
this.setFocusByFirstCharacter(menuId, tgt, key);
flag = true;
}
break;
}
if (flag) {
event.stopPropagation();
event.preventDefault();
}
}
onMenuitemClick(event) {
var tgt = event.currentTarget;
var menuId = this.getMenuId(tgt);
if (this.hasPopup(tgt)) {
if (this.isOpen(tgt)) {
this.closePopup(tgt);
} else {
this.closePopupAll(tgt);
this.openPopup(menuId, tgt);
}
} else {
this.updateContent(tgt.href, tgt.textContent.trim());
this.closePopupAll();
}
event.stopPropagation();
event.preventDefault();
}
onMenuitemPointerover(event) {
var tgt = event.currentTarget;
var menuId = this.getMenuId(tgt);
if (this.hasFocus()) {
this.setFocusToMenuitem(menuId, tgt);
}
if (this.isAnyPopupOpen() || this.hasFocus()) {
this.closePopupAll(tgt);
if (this.hasPopup(tgt)) {
this.openPopup(menuId, tgt);
}
}
}
onBackgroundPointerdown(event) {
if (!this.domNode.contains(event.target)) {
this.closePopupAll();
}
}
}
// Initialize menubar editor
window.addEventListener('load', function () {
var menubarNavs = document.querySelectorAll('.menubar-navigation');
for (var i = 0; i < menubarNavs.length; i++) {
new MenubarNavigation(menubarNavs[i]);
}
});