govlab-styleguide
Version:
A styleguide / lightweight frontend framework for GovLab websites
307 lines (273 loc) • 8.85 kB
JavaScript
/**
* Tabs module.
* @module foundation.tabs
* @requires foundation.util.keyboard
* @requires foundation.util.timerAndImageLoader if tabs contain images
*/
!function($, Foundation) {
'use strict';
/**
* Creates a new instance of tabs.
* @class
* @fires Tabs#init
* @param {jQuery} element - jQuery object to make into tabs.
* @param {Object} options - Overrides to the default plugin settings.
*/
function Tabs(element, options){
this.$element = element;
this.options = $.extend({}, Tabs.defaults, this.$element.data(), options);
this._init();
Foundation.registerPlugin(this, 'Tabs');
Foundation.Keyboard.register('Tabs', {
'ENTER': 'open',
'SPACE': 'open',
'ARROW_RIGHT': 'next',
'ARROW_UP': 'previous',
'ARROW_DOWN': 'next',
'ARROW_LEFT': 'previous'
// 'TAB': 'next',
// 'SHIFT_TAB': 'previous'
});
}
Tabs.defaults = {
// /**
// * Allows the JS to alter the url of the window. Not yet implemented.
// */
// deepLinking: false,
// /**
// * If deepLinking is enabled, allows the window to scroll to content if window is loaded with a hash including a tab-pane id
// */
// scrollToContent: false,
/**
* Allows the window to scroll to content of active pane on load if set to true.
* @option
* @example false
*/
autoFocus: false,
/**
* Allows keyboard input to 'wrap' around the tab links.
* @option
* @example true
*/
wrapOnKeys: true,
/**
* Allows the tab content panes to match heights if set to true.
* @option
* @example false
*/
matchHeight: false,
/**
* Class applied to `li`'s in tab link list.
* @option
* @example 'tabs-title'
*/
linkClass: 'tabs-title',
// contentClass: 'tabs-content',
/**
* Class applied to the content containers.
* @option
* @example 'tabs-panel'
*/
panelClass: 'tabs-panel'
};
/**
* Initializes the tabs by showing and focusing (if autoFocus=true) the preset active tab.
* @private
*/
Tabs.prototype._init = function(){
var _this = this;
this.$tabTitles = this.$element.find('.' + this.options.linkClass);
this.$tabContent = $('[data-tabs-content="' + this.$element[0].id + '"]');
this.$tabTitles.each(function(){
var $elem = $(this),
$link = $elem.find('a'),
isActive = $elem.hasClass('is-active'),
hash = $link.attr('href').slice(1),
linkId = hash + '-label',
$tabContent = $(hash);
$elem.attr({'role': 'presentation'});
$link.attr({
'role': 'tab',
'aria-controls': hash,
'aria-selected': isActive,
'id': linkId
});
$tabContent.attr({
'role': 'tabpanel',
'aria-hidden': !isActive,
'aria-labelledby': linkId
});
if(isActive && _this.options.autoFocus){
$link.focus();
}
});
if(this.options.matchHeight){
var $images = this.$tabContent.find('img');
if($images.length){
Foundation.onImagesLoaded($images, this._setHeight.bind(this));
}else{
this._setHeight();
}
}
this._events();
};
/**
* Adds event handlers for items within the tabs.
* @private
*/
Tabs.prototype._events = function(){
this._addKeyHandler();
this._addClickHandler();
if(this.options.matchHeight){
$(window).on('changed.zf.mediaquery', this._setHeight.bind(this));
}
};
/**
* Adds click handlers for items within the tabs.
* @private
*/
Tabs.prototype._addClickHandler = function(){
var _this = this;
this.$element.off('click.zf.tabs')
.on('click.zf.tabs', '.' + this.options.linkClass, function(e){
e.preventDefault();
e.stopPropagation();
if($(this).hasClass('is-active')){
return;
}
_this._handleTabChange($(this));
});
};
/**
* Adds keyboard event handlers for items within the tabs.
* @private
*/
Tabs.prototype._addKeyHandler = function(){
var _this = this;
var $firstTab = _this.$element.find('li:first-of-type');
var $lastTab = _this.$element.find('li:last-of-type');
this.$tabTitles.off('keydown.zf.tabs').on('keydown.zf.tabs', function(e){
if(e.which === 9) return;
e.stopPropagation();
e.preventDefault();
var $element = $(this),
$elements = $element.parent('ul').children('li'),
$prevElement,
$nextElement;
$elements.each(function(i) {
if ($(this).is($element)) {
if (_this.options.wrapOnKeys) {
$prevElement = i === 0 ? $elements.last() : $elements.eq(i-1);
$nextElement = i === $elements.length -1 ? $elements.first() : $elements.eq(i+1);
} else {
$prevElement = $elements.eq(Math.max(0, i-1));
$nextElement = $elements.eq(Math.min(i+1, $elements.length-1));
}
return;
}
});
// handle keyboard event with keyboard util
Foundation.Keyboard.handleKey(e, 'Tabs', {
open: function() {
$element.find('[role="tab"]').focus();
_this._handleTabChange($element);
},
previous: function() {
$prevElement.find('[role="tab"]').focus();
_this._handleTabChange($prevElement);
},
next: function() {
$nextElement.find('[role="tab"]').focus();
_this._handleTabChange($nextElement);
}
});
});
};
/**
* Opens the tab `$targetContent` defined by `$target`.
* @param {jQuery} $target - Tab to open.
* @fires Tabs#change
* @function
*/
Tabs.prototype._handleTabChange = function($target){
var $tabLink = $target.find('[role="tab"]'),
hash = $tabLink.attr('href'),
$targetContent = $(hash),
$oldTab = this.$element.find('.' + this.options.linkClass + '.is-active')
.removeClass('is-active').find('[role="tab"]')
.attr({'aria-selected': 'false'}).attr('href');
$($oldTab).removeClass('is-active').attr({'aria-hidden': 'true'});
$target.addClass('is-active');
$tabLink.attr({'aria-selected': 'true'});
$targetContent
.addClass('is-active')
.attr({'aria-hidden': 'false'});
/**
* Fires when the plugin has successfully changed tabs.
* @event Tabs#change
*/
this.$element.trigger('change.zf.tabs', [$target]);
// Foundation.reflow(this.$element, 'tabs');
};
/**
* Public method for selecting a content pane to display.
* @param {jQuery | String} elem - jQuery object or string of the id of the pane to display.
* @function
*/
Tabs.prototype.selectTab = function(elem){
var idStr;
if(typeof elem === 'object'){
idStr = elem[0].id;
}else{
idStr = elem;
}
if(idStr.indexOf('#') < 0){
idStr = '#' + idStr;
}
var $target = this.$tabTitles.find('[href="' + idStr + '"]').parent('.' + this.options.linkClass);
this._handleTabChange($target);
};
/**
* Sets the height of each panel to the height of the tallest panel.
* If enabled in options, gets called on media query change.
* If loading content via external source, can be called directly or with _reflow.
* @function
* @private
*/
Tabs.prototype._setHeight = function(){
var max = 0;
this.$tabContent.find('.' + this.options.panelClass)
.css('height', '')
.each(function(){
var panel = $(this),
isActive = panel.hasClass('is-active');
if(!isActive){
panel.css({'visibility': 'hidden', 'display': 'block'});
}
var temp = this.getBoundingClientRect().height;
if(!isActive){
panel.css({'visibility': '', 'display': ''});
}
max = temp > max ? temp : max;
})
.css('height', max + 'px');
};
/**
* Destroys an instance of an tabs.
* @fires Tabs#destroyed
*/
Tabs.prototype.destroy = function() {
this.$element.find('.' + this.options.linkClass)
.off('.zf.tabs').hide().end()
.find('.' + this.options.panelClass)
.hide();
if(this.options.matchHeight){
$(window).off('changed.zf.mediaquery');
}
Foundation.unregisterPlugin(this);
};
Foundation.plugin(Tabs, 'Tabs');
function checkClass($elem){
return $elem.hasClass('is-active');
}
}(jQuery, window.Foundation);