Menu

function
 Menu() 

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 = {

_setParams

property
 _setParams 

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,

_whitelistedParams

property
 _whitelistedParams 

Whitelisted parameters which can be set on construction.

_whitelistedParams: ['onToggle'],

defaults

property
 defaults 

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() {}
},

_cacheElements

method
 _cacheElements() 

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');
},

_bindEventListenerCallbacks

method
 _bindEventListenerCallbacks() 

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

_addEventListeners

method
 _addEventListeners() 

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

_removeEventListeners

method
 _removeEventListeners() 

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

_toggleItem

method
 _toggleItem() 

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

_checkAnimation

method
 _checkAnimation() 

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

_createMenuAnimationWrapper

method
 _createMenuAnimationWrapper() 

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;
},

_animateListChange

method
 _animateListChange() 

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

_appendList

method
 _appendList() 

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

_removeLastList

method
 _removeLastList() 

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

_removeAllCachedLists

method
 _removeAllCachedLists() 

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

_getNextList

method
 _getNextList() 

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;
},

_openItem

method
 _openItem() 

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');
},

_closeItem

method
 _closeItem() 

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');
},

_activateItem

method
 _activateItem() 

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

_activateItemParents

method
 _activateItemParents() 

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');
  }
},

_deactivateItems

method
 _deactivateItems() 

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');
  }
},

_deactivateItemSiblings

method
 _deactivateItemSiblings() 

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');
  }
},

_openActiveParents

method
 _openActiveParents() 

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');
      }
    }
  }
},

_cachedListContainsID

method
 _cachedListContainsID() 

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;
},

_onClick

method
 _onClick() 

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

_onFocus

method
 _onFocus() 

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;
  }
},

_onBlur

method
 _onBlur() 

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