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 = {
Include common functionality.
_setParams: Base.setParams,
_unsetParams: Base.unsetParams,
_toggleClass: Base.toggleClass,
_addClass: Base.addClass,
_removeClass: Base.removeClass,
_hasClass: Base.hasClass,
_debounce: Debounce,
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
},
Whitelisted parameters which can be set on construction.
_whitelistedParams: [],
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;
}
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);
}
}
};
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;
},
Close any open items, and more dropdown
_closeAll: function() {
this._closeItems();
this._toggleShowMore(false);
},
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;
},
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);
}
},
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);
}
},
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);
},
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);
},
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);
},
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;
}
},
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;
}
}
},
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();
},
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;
}
}
},
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);
}
},
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();
}
},
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);
}
},
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)';
}
},
Resize event helper, closes items then triggers recalculation of styles
_handleResizeH: function() {
this._closeAll();
this._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 = '';
},
Sorts toolbar items in descending order based on their priority value
_prioritySort: function(l, r) {
return r.priority - l.priority;
},
Sorts toolbar items in ascending order based on their order value
_orderSort: function(l, r) {
return l.order - r.order;
},
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;
}));