!function(factory) {
if(typeof define === 'function' && define.amd) define(['index', 'jquery', 'jquery.ba-resize'], factory);
else TabsAccordion = factory(Index, jQuery);
}
(function(Index, $) {
var count = 0,
namespace = 'tabsaccordion',
$window = $(window),
$html = $(document.documentElement).addClass('js'),
$body = $(document.body);
$.resize.throttleWindow = false;
$.fn.TabsAccordion = function(options) {
function T(element, options) {
var idNamespace = namespace + '-' + count++,
$element = $(element),
$panels,
$tabs,
$tablist,
$content,
self = {
version: '1.2.0',
type: ($element.hasClass('accordion') && 'accordion') || ($element.hasClass('tabs') && 'tabs'),
create: function() {
$panels = $element.children();
$tabs = $panels.children(':first-child');
if(self.index) var prev = self.index.curr;
(self.index = Index($panels.length - 1)).loop = true; // <- index looping for keyboard accessibility
if(prev) self.index.set(prev);
if(self.type === 'tabs') $element.prepend(($tabs = self.tabsCreateTablist($tabs).children()).end());
$tablist = (self.type === 'tabs' ? $tabs.parent() : $element).attr('role', 'tablist');
$tabs.attr({
'id': function(index) {
return this.id || idNamespace + '-tab-' + index;
},
'role': 'tab'
});
($content = $panels.map(function(index) {
return $(this)
.attr({
'aria-labelledby': $tabs[index].id,
'id': this.id || idNamespace + '-panel-' + index,
'role': 'tabpanel'
})
.children()
.slice(1)
.wrapAll('
')
.parent()
.parent()
.get();
}))
.each(self.collapse);
$element
.attr({
'id': element.id || idNamespace,
'tabindex': 0
})
.on('click.' + idNamespace, self.type === 'accordion' && '> * > :first-child' || '> :first-child > *', function(event) {
self.goTo($tabs.index($(event.target).closest($tabs)));
})
.on('keydown.' + idNamespace, function(event) {
// event.target should be the element and not a descendant
if(event.target !== element) return;
var match = {
37: 'prev',
38: 'prev',
39: 'next',
40: 'next'
}[event.keyCode];
if(match) {
event.preventDefault();
self.goTo(self.index[match]);
}
})
.on('resize.' + idNamespace, self.resize)
.trigger('create');
if(options.saveState) self.extensions.saveState(options.saveState);
if(options.responsiveSwitch) self.extensions.responsiveSwitch(options.responsiveSwitch);
if(options.hashWatch) self.extensions.hashWatch();
if(options.pauseMedia) self.extensions.pauseMedia();
if(typeof self.index.curr !== 'number') self.index.set(0);
setTimeout(function() {
$element.addClass('transition');
})
return self.expand(self.index.curr);
},
destroy: function(keepData) {
if(self.type === 'tabs') {
$element.height('auto');
$tablist.remove();
}
else {
$tabs
.removeAttr('role')
.filter('[id^="' + idNamespace + '"]').removeAttr('id');
$tablist.removeAttr('role');
}
$panels
.removeAttr('aria-expanded aria-labelledby role')
.filter('[id^="' + idNamespace + '"]').removeAttr('id');
$content
.children()
.children()
.unwrap()
.unwrap();
if(!keepData)
$element
.removeData(namespace)
.removeData('responsiveBreakpoint.' + idNamespace);
$element
.add([window, document.body])
.off('.' + idNamespace)
.end()
.removeAttr('aria-activedescendant tabindex')
.removeClass(self.type)
.filter('[id^="' + idNamespace + '"]').removeAttr('id')
.end()
.trigger('destroy');
return self;
},
resize: function() {
if(self.type === 'tabs') $element.height($tablist.outerHeight() + $panels.eq(self.index.curr).outerHeight());
else if(self.type === 'accordion' && $panels[self.index.curr].ariaExpanded)
$content
.eq(self.index.curr)
.height($content.eq(self.index.curr).children().outerHeight());
return self;
},
expand: function(index) {
var $panel = $panels.eq(index).attr('aria-expanded', $panels[index].ariaExpanded = true);
if(self.resize().type === 'tabs') $tabs.eq(index).addClass('current');
$element
.attr('aria-activedescendant', $panels[self.index.curr].id)
.trigger('expand', [index, $panel]);
return self;
},
collapse: function(index) {
var $panel = $panels.eq(index).attr('aria-expanded', $panels[index].ariaExpanded = false);
if(self.type === 'tabs') $tabs.eq(index).removeClass('current');
else $content.eq(index).height(0);
$element.trigger('collapse', [index, $panel]);
return self;
},
goTo: function(index) {
if(self.index.curr !== index && typeof self.index.curr === 'number') self.collapse(self.index.curr);
self.index.set(index);
return self[self.type === 'accordion' && $panels.eq(index).prop('ariaExpanded') ? 'collapse' : 'expand'](self.index.curr);
},
tabsCreateTablist: options.tabsCreateTablist || function(titles) {
for(var i = 0, li = ''; i < titles.length; i++) li += '' + titles[i].innerHTML + '';
return $('');
},
extensions: {
hashWatch: function() {
var that = {
changeHash: function(hash, $target) {
var id = $target[0].id;
$target[0].id = '';
location.hash = hash;
$target[0].id = id;
return that;
},
expand: function(hash, event) {
var $target = $element.find(hash);
if($target.length) {
var $panel = $target.closest($panels);
if($panel.length) {
if(event) event.preventDefault();
self.goTo($panels.index($panel));
that.changeHash(hash, $target);
if(event) {
setTimeout(function() {
$html
.add($body)
.animate({
scrollTop: $target.offset().top
});
},250);
}
}
}
return that;
}
};
$body
// hash anchor activation
.on('click.' + idNamespace, 'a[href^="#"]:not([href="#"])', function(event) {
that.expand($(event.target).attr('href'), event);
})
// navigation e.g. back button
.on('hashchange.' + idNamespace, function() {
that.expand(location.hash);
});
return that.expand(location.hash);
},
saveStateLoaded: false,
saveState: function(storage) {
if(typeof storage !== 'object') return;
var state = {
remove: function() {
storage.removeItem(idNamespace);
},
load: function() {
var item = storage.getItem(idNamespace),
data = JSON.parse(item);
if(data && data.current) self.index.set(data.current);
self.extensions.saveStateLoaded = true;
},
save: function() {
storage.setItem(idNamespace, JSON.stringify({current: self.index.curr, expanded: $panels[self.index.curr].ariaExpanded}));
}
};
// load only once per instance per page load
if(!self.extensions.saveStateLoaded) state.load();
$window.on('unload.' + idNamespace, state.save);
return state;
},
responsiveSwitch: function(breakpoint) {
if(breakpoint === 'tablist') {
if(self.type === 'tabs') $element.data('responsiveBreakpoint.' + idNamespace, breakpoint = getTablistWidth());
else breakpoint = $element.data('responsiveBreakpoint.' + idNamespace);
}
function getTablistWidth() {
// measure combined width of all tabs instead of single width of tablist, because tabs are floated and can jump to the next line
for(var i = 0, width = 0; i < $tabs.length; i++) width += $tabs.eq(i).outerWidth(true);
return width;
}
function switchTo(type) {
var current = self.index.curr,
expanded = $panels[current].ariaExpanded;
self.destroy(true);
$element.addClass(self.type = type);
self.index.set(current);
self.create();
$element.trigger('typechange', type);
}
function checkBreakpoint() {
var type = $element.outerWidth() <= breakpoint ? 'accordion' : 'tabs';
if(self.type !== type) switchTo(type);
}
$element.on('resize.' + idNamespace, checkBreakpoint);
},
pauseMedia: function() {
if(typeof Modernizr === 'undefined' || !Modernizr.audio || !Modernizr.video || !$element.find('audio, video').length) return;
$element.on('collapse.' + idNamespace, function(event, index, $panel) {
$panel.find('audio, video').each(function() {
this.pause();
});
});
}
}
};
return self.create();
}
var options = options || {},
args = Array.prototype.slice.call(arguments, 1);
return this.each(function(index) {
var $this = $(this);
// method call : instantiation
return $this.data(namespace) ? $this.data(namespace)[options].apply(this, args) : $this.data(namespace, T(this, options));
});
}
});