Sabre Spark

Menu

Option name Type Description
module components/menu.js
Example
new Menu(el, {
  // Optional. Callback method for when the menu toggles.
  onToggle: function(){}
});

Menu constructor.

Option name Type Description
el Element
params Object
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.

Option name Type Description
_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.

Option name Type Description
defaults: {
  cachedList: null,
  el: null,
  toggleEl: null,
  wrapperEl: null,
  _onClickBound: null,
  _onFocusBound: null,
  _onBlurBound: null,
  onToggle: function() {}
},

_cacheElements

method
 _cacheElements() 

Store a reference to the tabs list, each tab and each panel.
Set which tab is active, or use the first.

Option name Type Description
el Element
_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() 

Toggle the open state of an item.

Option name Type Description
item Element
_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() 

Animate the position of the animation wrapper. Optionally, do
so immediately without waiting for an animation.

Option name Type Description
noAnimate Boolean
_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() 

Append list to menu element

Option name Type Description
list Element
noAnimate Boolean
_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() 

Finds and returns the next nested list

Option name Type Description
item Object
return Object
_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() 

Open an item by animating it.

Option name Type Description
item Object
_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() 

Close an item by animating it shut.

Option name Type Description
item Object
_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() 

Make an item active.

Option name Type Description
item Element
_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() 

Activate parent items.

Option name Type Description
el Element
limitEl Element
_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() 

Deactivate items.

Option name Type Description
el Element
_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() 

Deactivate siblings items.

Option name Type Description
el Element
_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() 

Check if the cached list contains a certain ID

Option name Type Description
id String
return Boolean
_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() 

When an item is clicked, make it active. Determine if the click was on an expand
button and open the list if so.

Option name Type Description
e Object
_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() 

Keep track of when items have focus.

Option name Type Description
e Object
_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() 

Keep track of when items lose focus.

Option name Type Description
e Object
_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;
}));