Header

function
 Header() 

Option name Type Description
el Element
params Object

Header constructor.

var Header = function(el, params) {

  if (!el) {
    return;
  }

  this._setParams(this.defaults, true);
  this._bindEventListenerCallbacks();
  this._cacheElements(el);
  this._setParams(params || {});
  this._addEventListeners();

  // Determine the initial menu size now and when we get a ready state change
  this._determineInitialSize();
  this._listenForReadyStateChange();
};

Header.prototype = {

_setParams

property
 _setParams 

Include common functionality.

_setParams: Base.setParams,
_toggleClass: Base.toggleClass,
_addClass: Base.addClass,
_hasClass: Base.hasClass,
_removeClass: Base.removeClass,
_getChildIndex: Base.getChildIndex,
_elementHasParent: Base.elementHasParent,
_appendChildren: Base.appendChildren,
_insertBefore: Base.insertBefore,
_getBreakpoint: Base.getBreakpoint,
_getElementMatchingParent: Base.getElementMatchingParent,
_getElementMatchingParents: Base.getElementMatchingParents,
remove: Base.remove,

_whitelistedParams

property
 _whitelistedParams 

Whitelisted parameters which can be set on construction.

_whitelistedParams: ['breakpoints', 'fixed', 'fixedDistance'],

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: {
  el: null,
  fixed: false,
  fixedDistance: 10,
  navEl: null,
  menuEl: null,
  listEl: null,
  listEls: null,
  listMoreEl: null,
  listMoreListEl: null,
  placeholder: null,
  toggleEl: null,
  lastBreakpoint: null,
  currentBreakpoint: null,
  isActive: false,
  isCollapsed: null,
  moreSwapIndex: -1,
  menu: null,
  breakpoints: null,
  _onResizeBound: null,
  _onScrollBound: null,
  _onMoreClickBound: null,
  _onToggleClickBound: null,
  _onNavClickBound: null
},

update

method
 update() 

Update the elements used.

update: function() {

  this._removeEventListeners();
  this._removePlaceholder();
  this._cacheElements(this.el);
  this._addEventListeners();
  this._ensureActiveAtMoreSwapIndex();
  this.checkFixed();

  // Run on the next frame so sizes have updated
  setTimeout(function() {
    this._determineMenuSize();
  }.bind(this), 0);
},

checkFixed

method
 checkFixed() 

Check of we should be fixed.

checkFixed: function() {

  if (!this.fixed) {
    return;
  }

  var scrollTop = window.pageYOffset !== undefined ? window.pageYOffset : window.document.body.scrollTop;
  var isCondensed = scrollTop > this.fixedDistance;
  this._toggleClass(this.el, 'spark-header--condensed', isCondensed);
  this._toggleClass(document.body, 'spark-header-condensed', isCondensed);
},

_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.navEl = this.el.querySelector('.spark-header__nav');
  this.menuEl = this.navEl && this.navEl.querySelector('.spark-header__menu');
  this.listEl = this.menuEl && this.menuEl.querySelector('.spark-header__list, [data-role="list"]'); // @todo: remove data-role in v2.x.x
  this.toggleEl = this.el.querySelector('.spark-header__toggle');

  if (!this.fixed && this._hasClass(this.el, 'spark-header--fixed')) {
    this.fixed = true;
    this.checkFixed();
  }

  // Create a new instance of the menu component
  if (this.menuEl) {
    this.menu = new Menu(this.menuEl, {
      onToggle: this._onToggleClickBound
    });
  }

  // The items in the list need to show/hide based on the width of the container.
  // Cache these items so we can manipulate their display independent of what is
  // currently in the DOM. Also, create the "More" dropdown which will be shown
  // and hidden based on availabile space.
  if (this.listEl && this.listEl.children.length) {
    this.listEls = Array.prototype.slice.call(this.listEl.children, 0);
    this._createListMore();
  }

  // Create a clone of the header which will NOT be affected by changes in breakpoint.
  // This lets us continue to measure how many list elements will fit. Since we go to the
  // "condensed" view when we are at the sm/xs breakpoint OR only one item will fit in the nav,
  // we can't rely on breakpoints alone to determine what to show. Without a cloned placeholder
  // it is impossible to continue to measure the available space once we show the condensed view.
  if (this.listEl) {
    this._createPlaceholder();
  }
},

_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._onResizeBound = this._onResize.bind(this);
  this._onScrollBound = this._onScroll.bind(this);
  this._onMoreClickBound = this._onMoreClick.bind(this);
  this._onToggleClickBound = this._onToggleClick.bind(this);
  this._onNavClickBound = this._onNavClick.bind(this);
},

_addEventListeners

method
 _addEventListeners() 

Add event listeners for DOM events.

_addEventListeners: function() {

  if (!this.listEl) {
    return;
  }

  window.addEventListener('resize', this._onResizeBound);
  window.addEventListener('orientationchange', this._onResizeBound);

  if (this.listMoreEl) {
    this.listMoreListEl.addEventListener('click', this._onMoreClickBound);
  }

  if (this.toggleEl) {
    this.toggleEl.addEventListener('click', this._onToggleClickBound);
  }

  if (this.navEl) {
    this.navEl.addEventListener('click', this._onNavClickBound);
  }

  if (this.fixed) {
    window.addEventListener('scroll', this._onScrollBound);
  }
},

_removeEventListeners

method
 _removeEventListeners() 

Remove event listeners for DOM events..

_removeEventListeners: function() {

  if (!this.listEl) {
    return;
  }

  window.removeEventListener('resize', this._onResizeBound);
  window.removeEventListener('orientationchange', this._onResizeBound);

  if (this.listMoreEl) {
    this.listMoreListEl.removeEventListener('click', this._onMoreClickBound);
  }

  if (this.toggleEl) {
    this.toggleEl.removeEventListener('click', this._onToggleClickBound);
  }

  if (this.navEl) {
    this.navEl.removeEventListener('click', this._onNavClickBound);
  }

  if (this.fixed) {
    window.removeEventListener('scroll', this._onScrollBound);
  }
},

_getCurrentBreakpoint

method
 _getCurrentBreakpoint() 

Get the current breakpoint for the header.

_getCurrentBreakpoint: function() {
  this.lastBreakpoint = this.currentBreakpoint;
  this.currentBreakpoint = this._getBreakpoint(this.el.clientWidth, this.breakpoints);
  this.el.setAttribute('data-breakpoint', this.currentBreakpoint);
},

_createPlaceholder

method
 _createPlaceholder() 

Create a placeholder for the whole header so that we can keep track
of the width of each child element regardless of whether or not we're
condensed. Condensed styles do not apply to instances of the element
with the placeholder class.

_createPlaceholder: function() {

  var div = document.createElement('div');
  div.innerHTML = this.navEl.outerHTML;

  var el = div.children[0];
  el.setAttribute('aria-hidden', true);

  this._addClass(el, 'spark-header__placeholder');

  this._disablePlaceholderLinkTab(el);

  this.el.appendChild(el);

  // Cache the common elements
  this.placeholder = {
    el: el,
    menuEl: el.querySelector('.spark-header__menu'),
    listEl: el.querySelector('.spark-header__list, [data-role="list"]') // @todo: remove data-role in v2.x.x
  };

  // Add a copy of the "more" button to the list so we always know what size it would be
  if (this.listMoreEl) {
    this.placeholder.listEl.innerHTML += this.listMoreEl.outerHTML;
    this.placeholder.listMoreEl = this.placeholder.listEl.querySelector('.spark-header__more');
  }
},

_removePlaceholder

method
 _removePlaceholder() 

Remove the placeholder

_removePlaceholder: function() {

  if (this.placeholder) {
    this.placeholder.el.parentNode.removeChild(this.placeholder.el);
    this.placeholder.menuEl.parentNode.removeChild(this.placeholder.menuEl);
    this.placeholder.listEl.parentNode.removeChild(this.placeholder.listEl);
  }

  if (this.listMoreEl) {
    this.placeholder.listMoreEl.parentNode.removeChild(this.placeholder.listMoreEl);
  }
},

_disablePlaceholderLinkTab

method
 _disablePlaceholderLinkTab() 

Option name Type Description
el Element

Disable tabbing for items in the placeholder.

_disablePlaceholderLinkTab: function(el) {

  // Set a negative tab index on each link in the placeholder
  var links = el.querySelectorAll('.spark-menu__list-link, .spark-menu__list-expand');
  var i = 0;
  var len = links.length;

  for (; i < len; i++) {
    links.item(i).setAttribute('tabindex', -1);
  }
},

_createListMore

method
 _createListMore() 

Create a place to store overflow items of the list.
Also add this item to the placeholder element so we always know
which size it would be.

_createListMore: function() {

  var div = document.createElement('div');
  div.innerHTML = '<li><a class="spark-menu__list-link spark-menu__ignore" title="More Items" tabindex="0"><i class="spark-icon-menu-ellipsis-horizontal spark-icon--fill"></i></a><ul class="spark-menu__list"></ul></li>';

  var li = div.children[0];
  this._addClass(li, 'spark-menu__list-item spark-header__more');

  this.listMoreEl = li;
  this.listMoreListEl = li.querySelector('ul');
},

_determineInitialSize

method
 _determineInitialSize() 

Determine the menu size.

_determineInitialSize: function() {
  this._addClass(this.el, 'spark-header--visible');
  this._ensureActiveAtMoreSwapIndex();
  this._determineMenuSize();
},

_determineMenuSize

method
 _determineMenuSize() 

Option name Type Description
isSwap Boolean

Optional Is this a swapping event? If so, ignore redundancy checks.

Determine how many nav items can fit.

_determineMenuSize: function(isSwap) {

  // Don't do anything w/o primary nav.
  if (!this.listEls || !this.listEls.length) {
    return;
  }

  // If we're at the XS or SM breakpoint, don't worry about this stuff.
  if (this._isMenuBreakpoint(['xs', 'sm'])) {
    this._removeListMore();
    return this._toggleCollapsed(true);
  }

  // Get the items to show and hide
  var items = this._getItemsToShowAndHide();

  // Add a class saying that the size has been determined. This removes the overflow:hidden
  // so that dropdowns will appear.
  this._addClass(this.el, 'spark-header--overflow-checked');

  // If there are less than two elements to show and we have hidden elements, collapse the nav.
  if (items.show.length < 2 && items.hide.length) {
    this._removeListMore();
    return this._toggleCollapsed(true);
  }

  // We aren't at the XS breakpoint and there aren't too few items to show, so disable collapsing
  this._toggleCollapsed(false);

  // If the number of children to hide is the same as those already hidden, stop.
  if (items.hide.length === this.listMoreListEl.children.length && !isSwap) {

    if (!items.hide.length) {
      this._removeListMore();
    }

    return;
  }

  // Add the elements we're supposed to show before the "more element"
  this._insertBefore(this.listEl, this.listMoreEl, items.show);

  // If we have items to hide, append them to the more element
  if (items.hide.length) {
    this._appendChildren(this.listMoreListEl, items.hide);
  }
  // Otherwise, remove the more element
  else {
    this._removeListMore();
  }
},

_listenForReadyStateChange

method
 _listenForReadyStateChange() 

Listen for the ready state change and rerun the menu size determination.

_listenForReadyStateChange: function() {

  // Already loaded
  if (document.readyState === 'complete' || document.readyState === 'loaded') {
    return;
  }

  // Bound listener
  var run = function() {
    if (document.readyState === 'complete' || document.readyState === 'loaded') {
      this._determineMenuSize();
      document.removeEventListener('readystatechange', run);
    }
  }.bind(this);

  // Only run once
  document.addEventListener('readystatechange', run);
},

_isMenuBreakpoint

method
 _isMenuBreakpoint() 

Option name Type Description
name String, Array

A string or array of string names of breakpoints to check for

Check the primary nav breakpoint.

_isMenuBreakpoint: function(name) {
  this._getCurrentBreakpoint();
  return name instanceof Array ? name.indexOf(this.currentBreakpoint) !== -1 : this.currentBreakpoint === 'xs';
},

_getItemsToShowAndHide

method
 _getItemsToShowAndHide() 

Get the items to show and hide.

_getItemsToShowAndHide: function() {

  var width = this.placeholder.listEl.clientWidth;
  var children = this.placeholder.listEl.children;
  var i = 0;
  var len = children.length;
  var hideIndex = -1;

  this._addListMore();

  // Always include the width of the more button.
  var childrenWidth = this.placeholder.listMoreEl.clientWidth || 0;

  // Loop through the children until we hit a point where they don't fit anymore
  for (; i < len && hideIndex === -1; i++) {
    childrenWidth += children[i].clientWidth;
    if (childrenWidth > width) {
      hideIndex = i;
    }
  }

  // Find all the children that fit and don't fit
  var items = {
    show: hideIndex !== -1 ? Array.prototype.slice.call(this.listEls, 0, hideIndex) : this.listEls,
    hide: hideIndex !== -1 ? Array.prototype.slice.call(this.listEls, hideIndex) : []
  };

  // If we have an index to swap for the last "show" element, replace that element
  if (this.moreSwapIndex > -1 && this.moreSwapIndex >= items.show.length) {

    // Remove the last element from the show array
    var toHide = items.show.splice(items.show.length - 1, 1)[0];

    // Get the index to remove from the hide array. Account for the offset.
    var toShowIndex = this.moreSwapIndex - hideIndex;

    // Remove the desired element from the hide array
    var toShow = items.hide.splice(toShowIndex, 1)[0];

    // Add the toShow element to the end of the show array
    items.show.push(toShow);

    // Insert the toHide element into the hide array at the position of
    // the element we just removed from the hide array.
    items.hide.splice(toShowIndex, 0, toHide);
  }

  return items;
},

_ensureActiveAtMoreSwapIndex

method
 _ensureActiveAtMoreSwapIndex() 

Ensure that any active item is set to the more swap index. This ensures
that the active item is always visible on the screen.

_ensureActiveAtMoreSwapIndex: function() {

  if (!this.listEls || !this.listEls.length) {
    return;
  }

  var el = this.el.querySelector('[class*="list-item"].active');
  if (el) {
    var parents = this._getElementMatchingParents(el, '.spark-menu__list-item', this.el);

    if (parents && parents[parents.length - 1]) {
      el = parents[parents.length - 1];
    }

    var index = this._getChildIndex(this.listEls, el);

    if (index !== this.moreSwapIndex) {
      this.moreSwapIndex = index;
    }
  }
},

_addListMore

method
 _addListMore() 

Add a placeholder for overflow items to the list.

_addListMore: function() {
  if (this.listMoreEl.parentNode !== this.listEl) {
    this.listEl.appendChild(this.listMoreEl);
  }
},

_removeListMore

method
 _removeListMore() 

Remove a placeholder for overflow items from the primary nav.

_removeListMore: function() {
  if (this.listMoreEl.parentNode) {
    this.listMoreEl.parentNode.removeChild(this.listMoreEl);
  }
},

_resetMenuChildren

method
 _resetMenuChildren() 

Reset the children of the primary navigation.

_resetMenuChildren: function() {
  this.moreSwapIndex = -1;
  this._removeClass(this.el, 'spark-header--overflow-checked');
  this._appendChildren(this.listEl, this.listEls);
},

_toggleCollapsed

method
 _toggleCollapsed() 

Option name Type Description
enable Boolean

Toggle the collapsed nav style.

_toggleCollapsed: function(enable) {

  // Same collapsed state is already set
  if (enable === this.isCollapsed) {
    return;
  }

  // Reset children and remove a special no-animate class to top-level items when we collapse
  if (enable) {
    this._enableTopLevelToggling();
    this._resetMenuChildren();
  } else {
    if (this.menu) {
      this.menu._removeAllCachedLists();
    }
    this._disableTopLevelToggling();
  }

  this.isCollapsed = enable;
  this._toggleClass(this.el, 'spark-header--collapsed', enable);
  this._toggleClass(this.el, 'spark-header--visible', !enable);
},

_enableTopLevelToggling

method
 _enableTopLevelToggling() 

Enable toggling on top-level items.

_enableTopLevelToggling: function() {

  var i = 0;
  var len = this.listEls.length;

  for (; i < len; i++) {
    this._removeClass(this.listEls[i], 'spark-no-animate');
  }
},

_disableTopLevelToggling

method
 _disableTopLevelToggling() 

Disable toggling on top-level items.

_disableTopLevelToggling: function() {

  var i = 0;
  var len = this.listEls.length;

  for (; i < len; i++) {
    this._addClass(this.listEls[i], 'spark-no-animate');
  }
},

_onResize

method
 _onResize() 

When the window resizes, redetermine the size of the primary nav elements.

_onResize: function() {

  // Ensure that any active item we may have is at the swap index
  this._ensureActiveAtMoreSwapIndex();
  this._determineMenuSize();

  // If we are fixed, do the scroll check
  if (this.fixed) {
    this.checkFixed();
  }
},

_onScroll

method
 _onScroll() 

Option name Type Description
e Object

Check to see if the header should be fixed.

_onScroll: function() {
  this.checkFixed();
},

_onMoreClick

method
 _onMoreClick() 

Option name Type Description
e Object

When a link in the more list is clicked, swap it with the last element in the visible list.

_onMoreClick: function(e) {

  // Don't do any swapping if we're in a collapsed state
  if (this.isCollapsed) {
    return;
  }

  // Get the index of the clicked element
  var li = this._getElementMatchingParent(e.target, 'li', this.listMoreListEl);

  // Save the index of the element to be swapped
  this.moreSwapIndex = this._getChildIndex(this.listEls, li);

  // Redetermine the primary nav size
  this._determineMenuSize(true);
},

_onToggleClick

method
 _onToggleClick() 

Option name Type Description
e Object

When the toggle is clicked, toggle the active state on the nav

_onToggleClick: function(e) {
  e.preventDefault();
  this.isActive = !this.isActive;
  this._toggleClass(this.navEl, 'active', this.isActive);
  this.menu._openActiveParents();
},

_onNavClick

method
 _onNavClick() 

Option name Type Description
e Object

When the nav is clicked, set to inactive.

_onNavClick: function(e) {
  if (e.target === this.navEl && this.isCollapsed) {
    this.isActive = !this.isActive;
    this._removeClass(this.navEl, 'active');
  }
}
  };

  Base.exportjQuery(Header, 'Header');

  return Header;
}));