Toolbar

function
 Toolbar() 

Option name Type Description
el Element
params Object

Toolbar constructor.

var Toolbar = function(el, params) {

  params = params || {};

  if (!el) {
    return;
  }
  this._init(el);
};

Toolbar.prototype = {

_setParams

property
 _setParams 

Include common functionality.

_setParams: Base.setParams,
_unsetParams: Base.unsetParams,
_toggleClass: Base.toggleClass,
_addClass: Base.addClass,
_removeClass: Base.removeClass,
_hasClass: Base.hasClass,
_debounce: Debounce,

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

_whitelistedParams

property
 _whitelistedParams 

Whitelisted parameters which can be set on construction.

_whitelistedParams: [],

toolbarItem

method
 toolbarItem() 

Option name Type Description
parent Element

Reference to parent toolbar

el Element

Node to initalize as toolbarItem

order Number

The original index of the item in list of toolbarItems (used for maintaining order when sorting)

Setup a toolbarItem Instance to track the state of individual toolbar items

toolbarItem: function(parent, el, order) {
  //Setup and cache the values for this item
  var a = {};
  a.parent = parent;
  a.el = el;
  //cache the priority value present on the toolbar element if it is present, else default to 0
  a.priority = a.el.attributes['data-priority'] ? a.el.attributes['data-priority'].value : 0;
  a.order = a.el.attributes['data-order'] ? a.el.attributes['data-order'].value : order;
  a.hasContent = a.el.querySelector('.spark-toolbar__item--content') ? true : false;
  a.helper = a.el.querySelector('.spark-toolbar__item-helper');
  a.label = a.el.attributes.label ? a.el.attributes.label.value : false;
  a.closeOnClick = a.parent._hasClass(a.el, 'spark-toolbar__item--close-more-on-click');
  a.width = a.el.offsetWidth;
  a.height = a.el.offsetHeight;
  a.dropdown = el.querySelector('.spark-toolbar__item--content');
  if (a.dropdown) {
    a.dropdown.sparktoolbardropdown = true;
  }

toggleDropdown

method
 a.toggleDropdown() 

Option name Type Description
open Boolean

Set state to this regardless of current state

Call method to toggle the open state, optional param sets open state to value
Can get current state by referencing a.toggleDropdown.open

a.toggleDropdown = function(open) {
  var o = typeof open !== 'undefined' ? !open : a.toggleDropdown.open;
  if (o) {
    a.toggleDropdown.open = false;
    a.parent._removeClass(a.el, 'animate');
    window.setTimeout(function() {
      a.parent._removeClass(a.el, 'open');
    }, 100);
  }
  else {
    if (a.hasContent) {
      a.toggleDropdown.open = true;
      a.parent._addClass(a.el, 'open');
      a.positionDropdown();
      var e = document.createEvent('Event');
      e.initEvent('spark.visible-children', true, true);
      a.dropdown.dispatchEvent(e);
      window.setTimeout(function() {
        a.parent._addClass(a.el, 'animate');
      }, 0);
    }
    else {
      a.parent._toggleShowMore(false);
    }
  }
};

handleClick

method
 a.handleClick() 

Option name Type Description
open Boolean

Set state to this regardless of current state

Click handler for local element - determines to close element
conditionally based on presence of spark-toolbar__item--close-on-click
closes parent's more dropdown conditionally as well

a.handleClick = function(e) {
  if (!a.toggleDropdown.open) {
    a.toggleDropdown(true);
  }
  else {
    if (e.target === a.el || e.target === a.helper) {
      a.toggleDropdown();
    }
    else {
      var b = e.target;
      while (b !== a.el) {
        if (a.parent._hasClass(b, 'spark-toolbar__item--close-on-click')) {
          a.toggleDropdown(false);
          //close the mode section, as event originated inside a close-on-click area
          a.parent._toggleShowMore(false);
          break;
        }
        b = b.parentElement;
      }
    }
  }
  //e.preventDefault();
};
//perform bounds checking on dropdown open to position dropdown inside visual area
//this is called each time a dropdown is opened, in case the state of the component has
//changed since initialization
a.positionDropdown = function() {
  if (a.dropdown) {
    a.dropdown.style.left = '';
    a.dropdown.style.right = '';
    var pos = a.dropdown.getBoundingClientRect();
    var left = window.pageXOffset;
    var right = window.pageXOffset + document.documentElement.clientWidth;
    if (pos.right > right) {
      a.dropdown.style.left = 'inherit';
      a.dropdown.style.right = 0;
    }
    if (pos.left < left) {
      a.dropdown.style.left = 0;
      a.dropdown.style.right = 'inherit';
    }
  }
};
a.remove = function() {
  if (a.el) {
    delete a.el.sparktoolbar;
  }
  if (a.dropdown) {
    delete a.dropdown.sparktoolbardropdown;
  }
};
a.el.sparktoolbar = a;
return a;
    },

_closeAll

method
 _closeAll() 

Close any open items, and more dropdown

_closeAll: function() {
  this._closeItems();
  this._toggleShowMore(false);
},

_getOpenItems

method
 _getOpenItems() 

Returns array of open toolbarItems

_getOpenItems: function() {
  var a = [];
  for (var i = 0; i < this.items.length; i++) {
    if (this.items[i].toggleDropdown.open) {
      a.push(this.items[i]);
    }
  }
  return a;
},

_closeItems

method
 _closeItems() 

Option name Type Description
a Array

Optional array of toolbarItems to close, defaults to all open items

Close any open items

_closeItems: function(a) {
  a = typeof a === 'undefined' ? this._getOpenItems() : a;
  for (var i = 0; i < a.length; i++) {
    a[i].toggleDropdown(false);
  }
},

_init

method
 _init() 

Option name Type Description
el Element

The node to initalize on

Setup the toolbar element, cache properties, and initalize styling
when complete, show toolbar

_init: function(el) {
  this.el = el;
  //store a reference to this on the node to expedite event handling
  this.el.sparktoolbarcon = this;
  this.visibleContainer = this.el.querySelector('.spark-toolbar__container--visible');
  this.hiddenContainer = this.el.querySelector('.spark-toolbar__container--hidden');
  this.showMoreButton = this.el.querySelector('.spark-toolbar__show-more');
  this.showMoreButton.sparktoolbarshowmore = true;
  this.isOpen = false;
  this.isFocus = false;
  this._setupListeners();
  this.el.style.width = '100%';
  this._initItems();
  this._addClass(this.el, 'measured');
  this._calculateStyles();
  this.tabindex = this.el.attributes.tabindex ? this.el.attributes.tabindex.value : 0;
  this._addClass(this.el, 'ready');
},
_initItems: function() {
  var items = this.el.querySelectorAll('.spark-toolbar__item');
  this.items = [];
  for (var i = 0; i < items.length; i++) {
    this.items[i] = new this.toolbarItem(this, items[i], i);
  }
},

_setupListeners

method
 _setupListeners() 

Setup event listeners for clicks and resize events

_setupListeners: function() {
  this._handleWindowClick = this._handleWindowClickH.bind(this);
  document.addEventListener('click', this._handleWindowClick);
  this._handleResize = this._debounce(this._handleResizeH.bind(this), 100);
  window.addEventListener('resize', this._handleResize);
  this._handleKeyDown = this._handleKeyDownH.bind(this);
  this.el.addEventListener('keydown', this._handleKeyDown);
  this._handleFocus = this._handleFocusH.bind(this);
  document.addEventListener('focus', this._handleFocus, true);
  this._handleBlur = this._handleBlurH.bind(this);
  document.addEventListener('blur', this._handleBlur, true);
  this._handleVisibleChildren = this._handleVisibleChildrenH.bind(this);
  document.addEventListener('spark.visible-children', this._handleVisibleChildren, true);
},

_removeListeners

method
 _removeListeners() 

Remove event listeners for clicks and resize events

_removeListeners: function() {
  document.removeEventListener('click', this._handleWindowClick);
  window.removeEventListener('resize', this._handleResize);
  this.el.removeEventListener('keydown', this._handleKeyDown);
  document.removeEventListener('blur', this._handleBlur, true);
  document.removeEventListener('focus', this._handleFocus, true);
  document.removeEventListener('spark.visible-children', this._handleVisibleChildren, true);
},

remove

method
 remove() 

Option name Type Description
leaveElement Boolean

Leave the element intact.

Remove the element from the DOM and prepare for garbage collection by dereferencing values.

remove: function(leaveElement) {
  this._removeListeners();
  delete this.el.sparktoolbarcon;
  delete this.showMoreButton.sparktoolbarshowmore;
  for(var i = 0; i < this.items.length; i++) {
    this.items[i].remove();
  }
  Base.remove.call(this, leaveElement);
},

_handleBlurH

method
 _handleBlurH() 

Option name Type Description
e Event

The FocusEvent

reset our tab index when user focuses outside of element (gets immediately reset to -1 if focus is placed back inside element)

_handleBlurH: function(e) {
  if (this.el.contains(e.target)) {
    this.el.attributes.tabindex.value = this.tabindex;
  }
},

_handleFocusH

method
 _handleFocusH() 

Option name Type Description
e Event

The FocusEvent

focus handler, works in conjunction with blur handler to set correct tabindex value

_handleFocusH: function(e) {
  //if we're not being focused, reset our tabindex so we are accessible again, and close anything open
  if (!this.el.contains(e.target)) {
    this._closeAll();
    this.el.attributes.tabindex.value = this.tabindex;
  }
  else {
    //set our tabindex to -1 so the user can shift-tab out of our element
    this.el.attributes.tabindex.value = -1;
    if (e.target.sparktoolbarcon) {
      this._focusLast();
      return;
    }
    //handle focusing an item
    if (e.target.sparktoolbar) {
      e.target.sparktoolbar.el.focus();
      return;
    }
    var a = e.target;
    //harder case - look up the tree to find if we're focusing inside content
    while (!a.sparktoolbarcon) {
      if (a.sparktoolbar) {
        break;
      }
      //if we are - give our parent element a tabindex so the user can refocus the menu using shift-tab
      if (a.sparktoolbardropdown) {
        this.el.attributes.tabindex.value = this.tabindex;
        return;
      }
      a = a.parentElement;
    }
  }
},

_focusLast

method
 _focusLast() 

reset our focus to the last menu item that was focused

_focusLast: function() {
  if (!this._lastFocus) {
    var a = this.visibleContainer.querySelector('.spark-toolbar__item') || this.hiddenContainer.querySelector('.spark-toolbar__item');
    this._lastFocus = a.sparktoolbar;
  }
  if (this.hiddenContainer.contains(this._lastFocus.el)) {
    this._toggleShowMore(true);
  }
  this._lastFocus.el.focus();
},

_handleKeyDownH

method
 _handleKeyDownH() 

Option name Type Description
e Event

The KeyDown Event

keydown handler, used for keyboard navigation

_handleKeyDownH: function(e) {
  var a = e.target;
  //find the nearest toolbaritem
  while (!a.sparktoolbarcon) {
    if (a.sparktoolbar) {
      break;
    }
    if (a.sparktoolbardropdown) {
      return;
    }
    a = a.parentElement;
  }
  if (a.sparktoolbar) {
    //handle keys
    switch (e.keyCode) {
      //left arrow
      case 37:
        //up arrow
      case 38:
        if (a.previousSibling && a.previousSibling.sparktoolbar) {
          this._lastFocus = a.previousSibling.sparktoolbar;
          a.previousSibling.focus();
        }
        else {
          if (this.visibleContainer.querySelector('.spark-toolbar__item') !== a.sparktoolbar.el) {
            a = this.visibleContainer.querySelector('.spark-toolbar__item:last-of-type');
            if (a) {
              this._toggleShowMore(false);
              this._lastFocus = a.sparktoolbar;
              a.focus();
            }
          }
        }
        this._closeItems();
        e.preventDefault();
        break;
        //right arrow
      case 39:
        //down arrow
      case 40:
        if (a.nextSibling && a.nextSibling.sparktoolbar) {
          this._lastFocus = a.nextSibling.sparktoolbar;
          a.nextSibling.focus();
        }
        else {
          if (this.hiddenContainer.querySelector('.spark-toolbar__item:last-of-type') !== a.sparktoolbar.el) {
            a = this.hiddenContainer.querySelector('.spark-toolbar__item');
            if (a) {
              this._toggleShowMore(true);
              this._lastFocus = a.sparktoolbar;
              a.focus();
            }
          }
        }
        this._closeItems();
        e.preventDefault();
        break;
        //spacebar
      case 32:
        e.preventDefault();
        //we only want to toggle the toolbar if we are actually focused directly on it;
        if (e.target.sparktoolbar) {
          e.target.sparktoolbar.el.click();
        }
        break;
        //enter
      case 13:
        //we only want to toggle the toolbar if we are actually focused directly on it;
        if (e.target.sparktoolbar) {
          e.target.sparktoolbar.el.click();
        }
        break;
    }
  }
},

_handleVisibleChildrenH

method
 _handleVisibleChildrenH() 

Option name Type Description
e Event

The spark.visible-children event

Hanldes the spark.visible-children event to resize the component when it is made visible.

_handleVisibleChildrenH: function(e) {
  if(e.target.contains(this.el)) {
    window.setTimeout(function() {
      this.change();
    }.bind(this),0);
  }
},

_handleWindowClickH

method
 _handleWindowClickH() 

Option name Type Description
e Event

The click event

Event handler for click events, handles window clicks, control element clicks,
and forwards events to toolbarItem click handlers as needed

_handleWindowClickH: function(e) {
  //Check to see if the click was outside of the toolbar
  if (!this.el.contains(e.target)) {
    this._closeItems();
    this._toggleShowMore(false);
  }
  else {
    var a = e.target;
    //traverse the dom node tree until we find an element that handles the event,
    //or we reach the toolbar root node
    if(a === this.visibleContainer || a === this.el) {
      e.stopPropagation();
      e.preventDefault();
      return;
    }
    while (a !== this.el) {
      if (a.sparktoolbar) {
        var c = this._getOpenItems();
        if (c.indexOf(a.sparktoolbar) >= 0) {
          c.splice(c.indexOf(a.sparktoolbar), 1);
        }
        this._closeItems(c);
        if (!this.hiddenContainer.contains(e.target)) {
          this._toggleShowMore(false);
        }
        return a.sparktoolbar.handleClick(e);
      }
      if (a.sparktoolbarshowmore) {
        this._closeItems();
        this._toggleShowMore();
        return;
      }
      a = a.parentElement;
    }
    this._closeAll();
  }
},

_toggleShowMore

method
 _toggleShowMore() 

Option name Type Description
open Boolean

The new state of the show more dropdown

Toggle the state of the show more dropdown, optional parameter overrides toggle and
sets state to passed value

_toggleShowMore: function(open) {
  var o = typeof open !== 'undefined' ? !open : this.isOpen;
  if (o) {
    this._removeClass(this.el, 'animate');
    window.setTimeout(function() {
      this._removeClass(this.el, 'open');
      this.isOpen = false;
    }.bind(this), 100);
  }
  else {
    this.isOpen = true;
    this._addClass(this.el, 'open');
    this._positionShowMore();
    window.setTimeout(function() {
      this._addClass(this.el, 'animate');
    }.bind(this), 0);
  }
},

_positionShowMore

method
 _positionShowMore() 

Do bounds checking on show-more dropdown when it is opened, and position it accordingly

_positionShowMore: function() {
  this.hiddenContainer.style.right = '0px';
  var pos = this.hiddenContainer.getBoundingClientRect();
  var left = window.pageXOffset;
  var right = window.pageXOffset + document.documentElement.clientWidth;
  if (pos.right > right) {
    this.hiddenContainer.style.right = 'calc(' + (pos.right - right) + 'px + 1rem)';
  }
  if (pos.left < left) {
    this.hiddenContainer.style.right = 'calc(' + (pos.left - left) + 'px - 1rem)';
  }
},

_handleResizeH

method
 _handleResizeH() 

Resize event helper, closes items then triggers recalculation of styles

_handleResizeH: function() {
  this._closeAll();
  this._calculateStyles();
},

_calculateStyles

method
 _calculateStyles() 

Option name Type Description
showMore Boolean

Used to conditionally evaluate styling when showMore area is used

Reevaluates the available area of the toolbar and places toolbarItems into
the hidden container, as necessary. Should not call with any specified value
for showMore (used internally)

_calculateStyles: function(showMore) {
  this.el.style.width = '100%';
  showMore = typeof showMore !== 'undefined' ? showMore : false;
  if (!showMore) {
    this._removeClass(this.el, 'show-more');
  }
  var visible = [];
  var hidden = [];
  var i;
  //sort items by their priority to ensure higher-priority items are always placed
  //into the visible area first
  this.items.sort(this._prioritySort);
  //get container width and start placing items into their containers
  var visibleWidth = this.visibleContainer.clientWidth;
  for (i = 0; i < this.items.length; i++) {
    if (visibleWidth - this.items[i].width >= 0) {
      visible.push(this.items[i]);
      visibleWidth -= this.items[i].width;
    }
    else {
      if (!showMore) {
        this._addClass(this.el, 'show-more');
        return this._calculateStyles(true);
      }
      hidden.push(this.items[i]);
    }
  }
  //sort items back into their original order before inserting them into the document
  visible.sort(this._orderSort);
  hidden.sort(this._orderSort);
  var v = document.createDocumentFragment();
  var h = document.createDocumentFragment();
  for (i = 0; i < visible.length; i++) {
    v.appendChild(visible[i].el);
  }
  for (i = 0; i < hidden.length; i++) {
    h.appendChild(hidden[i].el);
  }
  this.visibleContainer.appendChild(v);
  this.hiddenContainer.appendChild(h);
  this.el.style.width = '';
},

_prioritySort

method
 _prioritySort() 

Sorts toolbar items in descending order based on their priority value

_prioritySort: function(l, r) {
  return r.priority - l.priority;
},

_orderSort

method
 _orderSort() 

Sorts toolbar items in ascending order based on their order value

_orderSort: function(l, r) {
  return l.order - r.order;
},

change

method
 change() 

This function will update cached sizing when an element in the toolbar is changed
or, when toolbar items are added or removed

change: function() {
  this._closeAll();
  this._removeClass(this.el, ['ready', 'show-more', 'measured']);
  var v = document.createDocumentFragment();
  for (var i = 0; i < this.items.length; i++) {
    v.appendChild(this.items[i].el);
  }
  this.visibleContainer.appendChild(v);
  this._initItems();
  this._addClass(this.el, 'measured');
  this._calculateStyles();
  this._addClass(this.el, 'ready');
}
  };

  Base.exportjQuery(Toolbar, 'Toolbar');
  return Toolbar;
}));