/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* File: treeview-navigation.js
* Desc: Tree item object for representing the state and user interactions for a
* tree widget for navigational 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.'
);
// this.fillerTextSentences.push('The text content in this paragraph is filler text providing a detectable change of content when the $linkName link is selected from the menu. ');
// this.fillerTextSentences.push('$siteName doesn\'t really exist, but the use of an organizational name is useful to provide context for the $linkName link. ');
// this.fillerTextSentences.push('Since $siteName doesn\'t exist there really is no real content associated with the $linkName link.');
}
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 TreeViewNavigation {
constructor(node) {
var linkURL, linkTitle;
// Check whether node is a DOM element
if (typeof node !== 'object') {
return;
}
document.body.addEventListener('focusin', this.onBodyFocusin.bind(this));
document.body.addEventListener('mousedown', this.onBodyFocusin.bind(this));
this.treeNode = node;
this.navNode = node.parentElement;
this.treeitems = this.treeNode.querySelectorAll('[role="treeitem"]');
for (let i = 0; i < this.treeitems.length; i++) {
let ti = this.treeitems[i];
ti.addEventListener('keydown', this.onKeydown.bind(this));
ti.addEventListener('click', this.onLinkClick.bind(this));
// first tree item is in tab sequence of page
if (i == 0) {
ti.tabIndex = 0;
} else {
ti.tabIndex = -1;
}
var groupNode = this.getGroupNode(ti);
if (groupNode) {
var span = ti.querySelector('span.icon');
span.addEventListener('click', this.onIconClick.bind(this));
}
}
// 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;
}
}
updateContent(linkURL, linkName, moveFocus) {
var h1Node, paraNodes;
if (typeof moveFocus !== 'boolean') {
moveFocus = true;
}
// Update content area
h1Node = document.querySelector('.page .main h1');
if (h1Node) {
h1Node.textContent = linkName;
}
paraNodes = document.querySelectorAll('.page .main p');
paraNodes.forEach(
(p) =>
(p.innerHTML = this.contentGenerator.renderParagraph(linkURL, linkName))
);
// move focus to the content region
if (moveFocus && h1Node) {
h1Node.tabIndex = -1;
h1Node.focus();
}
// Update aria-current
this.updateAriaCurrent(linkURL);
}
getAriaCurrentURL() {
let url = false;
let node = this.treeNode.querySelector('[aria-current]');
if (node) {
url = node.href;
}
return url;
}
updateAriaCurrent(url) {
if (typeof url !== 'string') {
url = this.getAriaCurrentURL();
}
this.treeitems.forEach((item) => {
if (item.href === url) {
item.setAttribute('aria-current', 'page');
// Make sure link is visible
this.showTreeitem(item);
this.setTabIndex(item);
} else {
item.removeAttribute('aria-current');
}
});
}
showTreeitem(treeitem) {
var parentNode = this.getParentTreeitem(treeitem);
while (parentNode) {
parentNode.setAttribute('aria-expanded', 'true');
parentNode = this.getParentTreeitem(parentNode);
}
}
setTabIndex(treeitem) {
this.treeitems.forEach((item) => (item.tabIndex = -1));
treeitem.tabIndex = 0;
}
getParentTreeitem(treeitem) {
var node = treeitem.parentNode;
if (node) {
node = node.parentNode;
if (node) {
node = node.previousElementSibling;
if (node && node.getAttribute('role') === 'treeitem') {
return node;
}
}
}
return false;
}
isVisible(treeitem) {
var flag = true;
if (this.isInSubtree(treeitem)) {
treeitem = this.getParentTreeitem(treeitem);
if (!treeitem || treeitem.getAttribute('aria-expanded') === 'false') {
return false;
}
}
return flag;
}
isInSubtree(treeitem) {
if (treeitem.parentNode && treeitem.parentNode.parentNode) {
return treeitem.parentNode.parentNode.getAttribute('role') === 'group';
}
return false;
}
isExpandable(treeitem) {
return treeitem.hasAttribute('aria-expanded');
}
isExpanded(treeitem) {
return treeitem.getAttribute('aria-expanded') === 'true';
}
getGroupNode(treeitem) {
var groupNode = false;
var id = treeitem.getAttribute('aria-owns');
if (id) {
groupNode = document.getElementById(id);
}
return groupNode;
}
getVisibleTreeitems() {
var items = [];
for (var i = 0; i < this.treeitems.length; i++) {
var ti = this.treeitems[i];
if (this.isVisible(ti)) {
items.push(ti);
}
}
return items;
}
collapseTreeitem(treeitem) {
if (treeitem.getAttribute('aria-owns')) {
var groupNode = document.getElementById(
treeitem.getAttribute('aria-owns')
);
if (groupNode) {
treeitem.setAttribute('aria-expanded', 'false');
}
}
}
expandTreeitem(treeitem) {
if (treeitem.getAttribute('aria-owns')) {
var groupNode = document.getElementById(
treeitem.getAttribute('aria-owns')
);
if (groupNode) {
treeitem.setAttribute('aria-expanded', 'true');
}
}
}
expandAllSiblingTreeitems(treeitem) {
var parentNode = treeitem.parentNode.parentNode;
if (parentNode) {
var siblingTreeitemNodes = parentNode.querySelectorAll(
':scope > li > a[aria-expanded]'
);
for (var i = 0; i < siblingTreeitemNodes.length; i++) {
siblingTreeitemNodes[i].setAttribute('aria-expanded', 'true');
}
}
}
setFocusToTreeitem(treeitem) {
treeitem.focus();
}
setFocusToNextTreeitem(treeitem) {
var visibleTreeitems = this.getVisibleTreeitems();
var nextItem = false;
for (var i = visibleTreeitems.length - 1; i >= 0; i--) {
var ti = visibleTreeitems[i];
if (ti === treeitem) {
break;
}
nextItem = ti;
}
if (nextItem) {
this.setFocusToTreeitem(nextItem);
}
}
setFocusToPreviousTreeitem(treeitem) {
var visibleTreeitems = this.getVisibleTreeitems();
var prevItem = false;
for (var i = 0; i < visibleTreeitems.length; i++) {
var ti = visibleTreeitems[i];
if (ti === treeitem) {
break;
}
prevItem = ti;
}
if (prevItem) {
this.setFocusToTreeitem(prevItem);
}
}
setFocusToParentTreeitem(treeitem) {
if (this.isInSubtree(treeitem)) {
var ti = treeitem.parentNode.parentNode.previousElementSibling;
this.setFocusToTreeitem(ti);
}
}
setFocusByFirstCharacter(treeitem, char) {
var start,
i,
ti,
index = -1;
var visibleTreeitems = this.getVisibleTreeitems();
char = char.toLowerCase();
// Get start index for search based on position of treeitem
start = visibleTreeitems.indexOf(treeitem) + 1;
if (start >= visibleTreeitems.length) {
start = 0;
}
// Check remaining items in the tree
for (i = start; i < visibleTreeitems.length; i++) {
ti = visibleTreeitems[i];
if (char === ti.textContent.trim()[0].toLowerCase()) {
index = i;
break;
}
}
// If not found in remaining slots, check from beginning
if (index === -1) {
for (i = 0; i < start; i++) {
ti = visibleTreeitems[i];
if (char === ti.textContent.trim()[0].toLowerCase()) {
index = i;
break;
}
}
}
// If match was found...
if (index > -1) {
this.setFocusToTreeitem(visibleTreeitems[index]);
}
}
// Event handlers
onBodyFocusin(event) {
var tgt = event.target;
if (this.treeNode.contains(tgt)) {
this.navNode.classList.add('focus');
} else {
this.navNode.classList.remove('focus');
this.updateAriaCurrent();
}
}
onIconClick(event) {
var tgt = event.currentTarget;
if (this.isExpanded(tgt.parentNode.parentNode)) {
this.collapseTreeitem(tgt.parentNode.parentNode);
} else {
this.expandTreeitem(tgt.parentNode.parentNode);
}
event.preventDefault();
event.stopPropagation();
}
onLinkClick(event) {
var tgt = event.currentTarget;
this.updateContent(tgt.href, tgt.textContent.trim());
event.preventDefault();
event.stopPropagation();
}
onKeydown(event) {
var tgt = event.currentTarget,
flag = false,
key = event.key;
function isPrintableCharacter(str) {
return str.length === 1 && str.match(/\S/);
}
if (event.altKey || event.ctrlKey || event.metaKey) {
return;
}
if (event.shift) {
if (
event.keyCode == this.keyCode.SPACE ||
event.keyCode == this.keyCode.RETURN
) {
event.stopPropagation();
} else {
if (isPrintableCharacter(key)) {
if (key == '*') {
this.expandAllSiblingTreeitems(tgt);
flag = true;
} else {
this.setFocusByFirstCharacter(tgt, key);
}
}
}
} else {
switch (key) {
// NOTE: Return key is supported through the click event
case ' ':
this.updateContent(tgt.href, tgt.textContent.trim());
flag = true;
break;
case 'Up':
case 'ArrowUp':
this.setFocusToPreviousTreeitem(tgt);
flag = true;
break;
case 'Down':
case 'ArrowDown':
this.setFocusToNextTreeitem(tgt);
flag = true;
break;
case 'Right':
case 'ArrowRight':
if (this.isExpandable(tgt)) {
if (this.isExpanded(tgt)) {
this.setFocusToNextTreeitem(tgt);
} else {
this.expandTreeitem(tgt);
}
}
flag = true;
break;
case 'Left':
case 'ArrowLeft':
if (this.isExpandable(tgt) && this.isExpanded(tgt)) {
this.collapseTreeitem(tgt);
flag = true;
} else {
if (this.isInSubtree(tgt)) {
this.setFocusToParentTreeitem(tgt);
flag = true;
}
}
break;
case 'Home':
this.setFocusToTreeitem(this.treeitems[0]);
flag = true;
break;
case 'End':
var visibleTreeitems = this.getVisibleTreeitems();
this.setFocusToTreeitem(
visibleTreeitems[visibleTreeitems.length - 1]
);
flag = true;
break;
default:
if (isPrintableCharacter(key)) {
if (key == '*') {
this.expandAllSiblingTreeitems(tgt);
flag = true;
} else {
this.setFocusByFirstCharacter(tgt, key);
}
}
break;
}
}
if (flag) {
event.stopPropagation();
event.preventDefault();
}
}
}
/**
* ARIA Treeview example
*
* @function onload
* @description after page has loaded initialize all treeitems based on the role=treeitem
*/
window.addEventListener('load', function () {
var trees = document.querySelectorAll('nav [role="tree"]');
for (let i = 0; i < trees.length; i++) {
new TreeViewNavigation(trees[i]);
}
});