Option name | Type | Description |
---|---|---|
el | Element | |
params | Object |
Menu constructor.
var Menu = function(el, params) {
if (!el) {
return;
}
this._setParams(this.defaults, true);
this._cacheElements(el);
this._setParams(params || {});
this._bindEventListenerCallbacks();
this._addEventListeners();
this._checkAnimation();
};
Menu.prototype = {
Include common functionality.
_setParams: Base.setParams,
_toggleClass: Base.toggleClass,
_addClass: Base.addClass,
_removeClass: Base.removeClass,
_hasClass: Base.hasClass,
_appendChildren: Base.appendChildren,
_elementHasParent: Base.elementHasParent,
_getElementMatchingParent: Base.getElementMatchingParent,
_getElementMatchingParents: Base.getElementMatchingParents,
_getMatchingChild: Base.getMatchingChild,
_wrapElement: Base.wrapElement,
remove: Base.remove,
Whitelisted parameters which can be set on construction.
_whitelistedParams: ['onToggle'],
Default values for internal properties we will be setting.
These are set on each construction so we don't leak properties
into the prototype chain.
defaults: {
cachedList: null,
el: null,
toggleEl: null,
wrapperEl: null,
_onClickBound: null,
_onFocusBound: null,
_onBlurBound: null,
onToggle: function() {}
},
Option name | Type | Description |
---|---|---|
el | Element |
Store a reference to the tabs list, each tab and each panel.
Set which tab is active, or use the first.
_cacheElements: function(el) {
this.el = el;
this.toggleEl = this.el.querySelector('.spark-menu__toggle');
},
Create bound versions of event listener callbacks and store them.
Otherwise we can't unbind from these events later because the
function signatures won't match.
_bindEventListenerCallbacks: function() {
this._onClickBound = this._onClick.bind(this);
this._onFocusBound = this._onFocus.bind(this);
this._onBlurBound = this._onBlur.bind(this);
},
Add event listeners for DOM events.
_addEventListeners: function() {
this.el.addEventListener('click', this._onClickBound);
this.el.addEventListener('focus', this._onFocusBound, true);
this.el.addEventListener('blur', this._onBlurBound, true);
},
Remove event listeners for DOM events..
_removeEventListeners: function() {
this.el.removeEventListener('click', this._onClickBound);
this.el.removeEventListener('focus', this._onFocusBound);
this.el.removeEventListener('blur', this._onBlurBound);
},
Option name | Type | Description |
---|---|---|
item | Element |
Toggle the open state of an item.
_toggleItem: function(item) {
if (this._hasClass(item, 'open')) {
this._closeItem(item);
} else {
this._openItem(item);
}
},
Check for a nested list and create the wrappers needed
for animating the lists
_checkAnimation: function() {
if (this.el.querySelector('.spark-menu__list-next')) {
this.cachedList = this.cachedList || [];
this._createMenuAnimationWrapper();
this._animateListChange();
}
},
Create wrapper class to help with animation of sliding lists
_createMenuAnimationWrapper: function() {
if (this.wrapperEl) {
return;
}
var wrapperEl = document.createElement('div');
this._addClass(wrapperEl, 'spark-menu__animation-wrapper');
this._wrapElement(this.el.querySelector('.spark-menu__list'), wrapperEl);
this.wrapperEl = wrapperEl;
},
Option name | Type | Description |
---|---|---|
noAnimate | Boolean |
Animate the position of the animation wrapper. Optionally, do
so immediately without waiting for an animation.
_animateListChange: function(noAnimate) {
if (noAnimate) {
this._addClass(this.wrapperEl, 'no-animate');
}
this.wrapperEl.setAttribute('style', transform('translateX', '-' + (this.cachedList.length * 100) + '%'));
if (noAnimate) {
setTimeout(function() {
this._removeClass(this.wrapperEl, 'no-animate');
}.bind(this), 1);
}
},
Option name | Type | Description |
---|---|---|
list | Element | |
noAnimate | Boolean |
Append list to menu element
_appendList: function(item, noAnimate) {
// Create wrapper
this._createMenuAnimationWrapper();
var newList = item.cloneNode(true);
this._addClass(newList, 'nestedList');
newList.setAttribute('data-nested-list-id', newList.getAttribute('id'));
newList.removeAttribute('id');
if (this.wrapperEl) {
// Add child node to wrapper
this.wrapperEl.appendChild(newList);
// Add to cached Array to keep track of all added lists
this.cachedList.push(newList);
// Slide navigation
this._animateListChange(noAnimate);
}
},
Remove list to nav
_removeLastList: function() {
// If there are any items to remove
if (this.cachedList.length) {
// Retrieve last item from list
var removeElement = this.cachedList.pop();
if (this.wrapperEl) {
// Slide navigation
this._animateListChange();
}
window.setTimeout(function() {
// Remove itself from DOM
removeElement.parentNode.removeChild(removeElement);
}, 250);
}
},
Remove all lists from panel menu
_removeAllCachedLists: function() {
if (this.cachedList) {
// If there are any items to remove
while (this.cachedList.length) {
// While there are still items, remove them
this._removeLastList();
}
}
},
Option name | Type | Description |
---|---|---|
item | Object | |
return | Object |
Finds and returns the next nested list
_getNextList: function(item) {
return item.querySelector('.spark-menu__list-next') ? document.querySelector(item.querySelector('.spark-menu__list-next').getAttribute('data-menu')) : null;
},
Option name | Type | Description |
---|---|---|
item | Object |
Open an item by animating it.
_openItem: function(item) {
// Item is already open
if (this._hasClass(item, 'open')) {
return;
}
animateHeight({
el: item,
toggleEl: '.spark-menu__list'
});
this._addClass(item, 'open');
},
Option name | Type | Description |
---|---|---|
item | Object |
Close an item by animating it shut.
_closeItem: function(item) {
// Item is already closed
if (!this._hasClass(item, 'open')) {
return;
}
animateHeight({
el: item,
toggleEl: '.spark-menu__list',
toggleValue: 'none',
action: 'collapse'
});
this._removeClass(item, 'open');
},
Option name | Type | Description |
---|---|---|
item | Element |
Make an item active.
_activateItem: function(item) {
// Item is already active
if (this._hasClass(item, 'active')) {
return;
}
// Deactivate any active items
var parents = this._getElementMatchingParents(item, '.spark-menu__list', this.el);
this._deactivateItems(parents[parents.length - 1]);
this._deactivateItemSiblings(item);
// Add the active class
this._addClass(item, 'active');
// If there is a parent that is also a list item, open it.
this._activateItemParents(item, this.el);
},
Option name | Type | Description |
---|---|---|
el | Element | |
limitEl | Element |
Activate parent items.
_activateItemParents: function(el, limitEl) {
var parents = this._getElementMatchingParents(el.parentNode, '[class*="list-item"]', limitEl);
var i = 0;
var len = parents.length;
// Add the active class
for (; i < len; i++) {
this._openItem(parents[i]);
this._addClass(parents[i], 'child-active');
}
},
Option name | Type | Description |
---|---|---|
el | Element |
Deactivate items.
_deactivateItems: function(el) {
var actives = el.querySelectorAll('[class*="list-item"].active');
var i = 0;
var len = actives.length;
// Remove the active class
for (; i < len; i++) {
this._removeClass(actives.item(i), 'active');
}
},
Option name | Type | Description |
---|---|---|
el | Element |
Deactivate siblings items.
_deactivateItemSiblings: function(el) {
var actives = el.parentNode.querySelectorAll('[class*="list-item"].child-active');
var i = 0;
var len = actives.length;
// Remove the active class
for (; i < len; i++) {
this._removeClass(actives[i], 'child-active');
this._removeClass(actives[i], 'open');
}
},
Open the parents of the active item.
_openActiveParents: function() {
var activeItem = this.el.querySelector('.active');
if (activeItem) {
var parentItems = this._getElementMatchingParents(activeItem, '.spark-menu__list-item', this.el);
var itemLinks;
var nextList;
for (var i = parentItems.length - 1; i >= 0; i--) {
itemLinks = this._getMatchingChild(parentItems[i], '.spark-menu__list-links');
if (itemLinks && itemLinks.querySelector('.spark-menu__list-next')) {
nextList = this._getNextList(parentItems[i]);
if (nextList && !this._cachedListContainsID(nextList.getAttribute('id'))) {
this._appendList(nextList, true);
}
} else {
this._addClass(parentItems[i], 'open');
}
}
}
},
Option name | Type | Description |
---|---|---|
id | String | |
return | Boolean |
Check if the cached list contains a certain ID
_cachedListContainsID: function contains(id) {
var i = this.cachedList.length;
while (i--) {
if (this.cachedList[i].getAttribute('data-nested-list-id') === id) {
return true;
}
}
return false;
},
Option name | Type | Description |
---|---|---|
e | Object |
When an item is clicked, make it active. Determine if the click was on an expand
button and open the list if so.
_onClick: function(e) {
// Don't make forms active
if (this._getElementMatchingParent(e.target, 'form', this.el)) {
return;
}
// Toggle the visibility of the menu?
var toggle = e.target === this.toggleEl || this._elementHasParent(e.target, this.toggleEl);
if (toggle) {
return this.onToggle(e, this);
}
// Is there a parent to open and an item?
var open = this._getElementMatchingParent(e.target, '.spark-menu__list-expand', this.el);
var item = this._getElementMatchingParent(e.target, '.spark-menu__list-item', this.el);
// If we have no item or have been told to ignore the item
if (!item || this._getElementMatchingParent(e.target, '.spark-menu__ignore', this.el)) {
return;
}
if (open) {
return this._toggleItem(item);
}
// Check if we have a valid item and we aren't inside the expanded header
if (item && !this._elementHasParent(e.target, document.querySelector('.spark-header--visible'))) {
var next = this._getNextList(item);
if (next && this._hasClass(e.target, 'spark-menu__list-next')) {
// Active item
this._activateItem(item);
this._appendList(next);
return;
}
var back = this._getElementMatchingParent(e.target, '.spark-menu__list-back', item);
if (back && this._hasClass(e.target, 'spark-menu__list-back')) {
this._removeLastList();
return;
}
}
// Active item
this._activateItem(item);
},
Option name | Type | Description |
---|---|---|
e | Object |
Keep track of when items have focus.
_onFocus: function(e) {
var parent = e.target;
var lastParent = parent;
while(parent) {
parent = this._getElementMatchingParent(lastParent.parentNode, '.spark-menu__list-item', this.el);
if (!parent || parent === lastParent) break;
this._addClass(parent, 'has-focus');
lastParent = parent;
}
},
Option name | Type | Description |
---|---|---|
e | Object |
Keep track of when items lose focus.
_onBlur: function(e) {
var parent = e.target;
var lastParent = parent;
while(parent) {
parent = this._getElementMatchingParent(lastParent.parentNode, '.spark-menu__list-item', this.el);
if (!parent || parent === lastParent) break;
this._removeClass(parent, 'has-focus');
lastParent = parent;
}
}
};
Base.exportjQuery(Menu, 'Menu');
return Menu;
}));