Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secondary Nav Component - DEC-71 #528

Merged
merged 19 commits into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 377 additions & 0 deletions core/dist/css/decanter.css

Large diffs are not rendered by default.

3,401 changes: 3,322 additions & 79 deletions core/dist/js/decanter.js

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion core/src/js/components/components.js
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
import './main-nav/main-nav.js';
/**
* Primary roll up file for all javascript components.
*/

// The Primary Navigation Component.
import './main-nav/main-nav.js';
// The Secondary Navigation Component.
import './secondary-nav/secondary-nav.js';
106 changes: 106 additions & 0 deletions core/src/js/components/nav/ActivePath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* ActivePath Class
*
* NEEDS DESCRIPTION.
*/
export default class ActivePath {

/**
* [constructor description]
* @param {[type]} element [description]
*/
constructor(element, item, options = {}) {
this.elem = element;
this.item = item;
// Class properties.
this.itemActiveClass = options.itemActiveClass || 'active';
this.itemActiveTrailClass = options.itemActiveTrailClass || 'active-trail';
this.itemExpandedClass = options.itemExpandedClass || 'expanded';
}

/**
* Dynamically add an active path to the menu tree.
*
* Find all links with the current window's url and add the
* options.itemActiveClass class to the LI element container all the way up
* the menu tree back to the root.
*/
setActivePath() {
let path = window.location.pathname;
let anchor = window.location.hash || '';
let query = window.location.search || '';
let currentItem = false;

// Queries to run to find matching active paths in order of unqiueness.
let finders = [
this.elem.querySelector("a[href*='" + anchor + "']"),
this.elem.querySelector("a[href*='" + query + "']"),
this.elem.querySelector("a[href='" + path + query + anchor + "']"),
this.elem.querySelector("a[href*='" + path + query + "']")
];

// Go through the queries and see if we have any results.
finders.forEach(function (val) {
if (!currentItem && val) {
currentItem = val;
}
});

// Can't find anything. End.
if (!currentItem) {
return;
}

// While we have parents go up and add the active class.
while (currentItem) {

// If we are on a LI element we need to add the active class.
if (currentItem.tagName === 'LI') {
currentItem.classList.add(this.itemActiveClass);
break;
}

// Always increment.
currentItem = currentItem.parentNode;
}
}

/**
* Expand all menus in the active path.
*
* After this.setActivePath() has been run or the itemActiveClass has been set
* on all the appropriate menu items go through the nav and expand the
* subNavItems that contain activeClass items.
*/
expandActivePath() {
let actives = this.elem.querySelectorAll('.' + this.itemActiveClass);
if (actives.length) {
actives.forEach(
element => {

// While we have parents go up and add the active class.
while (element) {
// End when we get to the parent nav item stop.
if (element === this.elem) {
// Stop at the top most level.
break;
}

// If we are on a LI element we need to add the active class.
if (element.tagName === 'LI') {
element.classList.add(this.itemExpandedClass);
element.classList.add(this.itemActiveTrailClass);
// "Hook" of sorts.
if (typeof this.item.expandActivePathItem == "function") {
this.item.expandActivePathItem(element);
}
}

// Always increment.
element = element.parentNode;
}
}
);
}
}
}
75 changes: 75 additions & 0 deletions core/src/js/components/nav/ElementFetcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* ActivePath Class
*
* NEEDS DESCRIPTION.
*/
export default class ElementFetcher {

/**
* [constructor description]
* @param {[type]} element [description]
* @param {[type]} what [description]
*/
constructor(element, what) {
this.item = element;
this.what = what;
}

/**
* [fetch description]
* @return {[type]} [description]
*/
fetch() {
try {
switch (this.what) {
case 'first':
return this.item.parentNode.firstElementChild.firstChild;
case 'last':
return this.item.parentNode.lastElementChild.firstChild;
case 'firstElement':
return this.item.parentNode.firstElementChild;
case 'lastElement':
return this.item.parentNode.lastElementChild;
case 'next':
return this.item.nextElementSibling.querySelector('a');
case 'prev':
return this.item.previousElementSibling.querySelector('a');
case 'nextElement':
return this.item.nextElementSibling;
case 'prevElement':
return this.item.previousElementSibling;
case 'parentItem':
var node = this.item.parentNode.parentNode;
if (node.tagName === 'NAV') { return false; }
return node.querySelector('a');
case 'parentButton':
return this.item.parentNode.parentNode.querySelector('button');
case 'parentNav':
return this.item.parentNode.parentNode;
case 'parentNavLast':
return this.item.parentNode.parentNode.parentNode.lastElementChild.querySelector('a');
case 'parentNavFirst':
return this.item.parentNode.parentNode.parentNode.firstElementChild.querySelector('a');
case 'parentNavNext':
return this.item.parentNode.parentNode.nextElementSibling;
case 'parentNavNextItem':
return this.item.parentNode.parentNode.nextElementSibling.querySelector('a');
case 'parentNavPrev':
return this.item.parentNode.parentNode.previousElementSibling;
case 'parentNavPrevItem':
return this.item.parentNode.parentNode.previousElementSibling.querySelector('a');
case 'firstSubnavLink':
return this.item.querySelector(':scope > ul li a');
case 'firstSubnavItem':
return this.item.querySelector(':scope > ul li');
case 'subnav':
return this.item.querySelector(':scope > ul');
default:
return false;
}
}
catch (err) {
return false;
}
}
}
118 changes: 118 additions & 0 deletions core/src/js/components/nav/EventHandlerDispatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { normalizeKey } from '../../utilities/keyboard';

/**
* EventHandlerDispatch Class
*
* NEEDS DESCRIPTION.
*/
export default class EventHandlerDispatch {

/**
* [constructor description]
* @param {[type]} element [description]
*/
constructor(element, handler) {
this.elem = element;
this.handler = handler;
this.createEventListeners();
}

/**
* Create new event listeners.
*/
createEventListeners() {
// What to do when a key is down?
this.elem.addEventListener('keydown', this);

// Listen to the click so we can act on it.
this.elem.addEventListener('click', this);

// Listen to custom events so we can act on it.
this.elem.addEventListener('preOpenSubnav', this);

// Listen to custom events so we can act on it.
this.elem.addEventListener('postOpenSubnav', this);
}

/**
* Handler for all events attached to an instance of this class.
*
* This method must exist when events are bound to an instance of a class
* (vs a function). This method is called for all events bound to an
* instance. It inspects the instance (this) for an appropriate handler
* based on the event type. If found, it dispatches the event to the
* appropriate handler.
*
* @param {Event} event - The triggering event.
*/
handleEvent(event) {
event = event || window.event;

// Create an event signature.
const eventMethod = 'on'
+ event.type.charAt(0).toUpperCase()
+ event.type.slice(1);

// What was clicked.
const target = event.target || event.srcElement;

if (eventMethod == "onKeydown") {
this.onKeydown(event, target);
}
else if (eventMethod == "onClick") {
this.onClick(event, target);
}
else {
this.callEvent(eventMethod, event, target);
}
}

/**
* Handler for keydown events. keydown is bound to all NavItem's.
* Dispatched from this.handleEvent().
*
* @param {KeyboardEvent} event - The keyboard event object.
* @param {HTMLElement} target - The HTML element target.
*/
onKeydown(event, target) {
let theKey = event.key || event.keyCode;
let normalized = normalizeKey(theKey);

// We don't know or need to handle the key that was pressed.
if (!normalized) {
return;
}

// Prepare a dynamic handler.
let eventMethod = 'onKeydown'
+ normalized.charAt(0).toUpperCase()
+ normalized.slice(1);

// Do eet.
this.callEvent(eventMethod, event, target);
}

/**
* [onClick description]
* @param {[type]} event [description]
* @param {[type]} target [description]
* @return {[type]} [description]
*/
onClick(event, target) {
this.callEvent('onClick', event, target);
}

/**
* [callEvent description]
* @param {[type]} event [description]
* @param {[type]} target [description]
* @return {[type]} [description]
*/
callEvent(eventMethod, event, target) {
if (typeof this.handler.eventRegistry[eventMethod] === 'function') {
var eventObj = new this.handler.eventRegistry[eventMethod](this.handler, event, target);
eventObj.init();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import SecondaryNavAbstract from '../common/SecondaryNavAbstract';
import SecondaryNavItem from '../common/SecondaryNavItem';
import SecondarySubNavAccordion from './SecondarySubNavAccordion';

/**
* A secondary menu with toggle buttons.
*/
export default class SecondaryNavAccordion extends SecondaryNavAbstract {
/**
* [constructor description]
* @param {[type]} elem [description]
* @param {Object} [options={}] [description]
*/
constructor(elem, options = {}) {
// Let super do what super does.
super(elem, options);

// Ok do the creation.
this.createSubNavItems();

// Expand the active path.
this.activePath.expandActivePath();
}

/**
* Add the additional state handling after the abstract option has run.
*/
expandActivePathItem(item) {
item.firstElementChild.setAttribute('aria-expanded', 'true');
}

/**
* [newParentItem description]
* @param {[type]} item [description]
* @param {[type]} depth [description]
* @return {[type]} [description]
*/
newParentItem(item, depth, parent) {
var opts = this.options;
opts.depth = depth;

var nav = new SecondarySubNavAccordion(
item,
this,
parent,
opts
);
this.subNavItems.push(nav);
return nav;
}

/**
* [newNavItem description]
* @param {[type]} item [description]
* @param {[type]} depth [description]
* @return {[type]} [description]
*/
newNavItem(item, depth, parent) {
var opts = this.options;
opts.depth = depth;

var nav = new SecondaryNavItem(
item,
this,
parent,
opts
);
this.navItems.push(nav);
return nav;
}
}
Loading