402 lines
11 KiB
JavaScript
Executable file
402 lines
11 KiB
JavaScript
Executable file
(function($, anim) {
|
|
'use strict';
|
|
|
|
let _defaults = {
|
|
duration: 300,
|
|
onShow: null,
|
|
swipeable: false,
|
|
responsiveThreshold: Infinity // breakpoint for swipeable
|
|
};
|
|
|
|
/**
|
|
* @class
|
|
*
|
|
*/
|
|
class Tabs extends Component {
|
|
/**
|
|
* Construct Tabs instance
|
|
* @constructor
|
|
* @param {Element} el
|
|
* @param {Object} options
|
|
*/
|
|
constructor(el, options) {
|
|
super(Tabs, el, options);
|
|
|
|
this.el.M_Tabs = this;
|
|
|
|
/**
|
|
* Options for the Tabs
|
|
* @member Tabs#options
|
|
* @prop {Number} duration
|
|
* @prop {Function} onShow
|
|
* @prop {Boolean} swipeable
|
|
* @prop {Number} responsiveThreshold
|
|
*/
|
|
this.options = $.extend({}, Tabs.defaults, options);
|
|
|
|
// Setup
|
|
this.$tabLinks = this.$el.children('li.tab').children('a');
|
|
this.index = 0;
|
|
this._setupActiveTabLink();
|
|
|
|
// Setup tabs content
|
|
if (this.options.swipeable) {
|
|
this._setupSwipeableTabs();
|
|
} else {
|
|
this._setupNormalTabs();
|
|
}
|
|
|
|
// Setup tabs indicator after content to ensure accurate widths
|
|
this._setTabsAndTabWidth();
|
|
this._createIndicator();
|
|
|
|
this._setupEventHandlers();
|
|
}
|
|
|
|
static get defaults() {
|
|
return _defaults;
|
|
}
|
|
|
|
static init(els, options) {
|
|
return super.init(this, els, options);
|
|
}
|
|
|
|
/**
|
|
* Get Instance
|
|
*/
|
|
static getInstance(el) {
|
|
let domElem = !!el.jquery ? el[0] : el;
|
|
return domElem.M_Tabs;
|
|
}
|
|
|
|
/**
|
|
* Teardown component
|
|
*/
|
|
destroy() {
|
|
this._removeEventHandlers();
|
|
this._indicator.parentNode.removeChild(this._indicator);
|
|
|
|
if (this.options.swipeable) {
|
|
this._teardownSwipeableTabs();
|
|
} else {
|
|
this._teardownNormalTabs();
|
|
}
|
|
|
|
this.$el[0].M_Tabs = undefined;
|
|
}
|
|
|
|
/**
|
|
* Setup Event Handlers
|
|
*/
|
|
_setupEventHandlers() {
|
|
this._handleWindowResizeBound = this._handleWindowResize.bind(this);
|
|
window.addEventListener('resize', this._handleWindowResizeBound);
|
|
|
|
this._handleTabClickBound = this._handleTabClick.bind(this);
|
|
this.el.addEventListener('click', this._handleTabClickBound);
|
|
}
|
|
|
|
/**
|
|
* Remove Event Handlers
|
|
*/
|
|
_removeEventHandlers() {
|
|
window.removeEventListener('resize', this._handleWindowResizeBound);
|
|
this.el.removeEventListener('click', this._handleTabClickBound);
|
|
}
|
|
|
|
/**
|
|
* Handle window Resize
|
|
*/
|
|
_handleWindowResize() {
|
|
this._setTabsAndTabWidth();
|
|
|
|
if (this.tabWidth !== 0 && this.tabsWidth !== 0) {
|
|
this._indicator.style.left = this._calcLeftPos(this.$activeTabLink) + 'px';
|
|
this._indicator.style.right = this._calcRightPos(this.$activeTabLink) + 'px';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle tab click
|
|
* @param {Event} e
|
|
*/
|
|
_handleTabClick(e) {
|
|
let tab = $(e.target).closest('li.tab');
|
|
let tabLink = $(e.target).closest('a');
|
|
|
|
// Handle click on tab link only
|
|
if (!tabLink.length || !tabLink.parent().hasClass('tab')) {
|
|
return;
|
|
}
|
|
|
|
if (tab.hasClass('disabled')) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// Act as regular link if target attribute is specified.
|
|
if (!!tabLink.attr('target')) {
|
|
return;
|
|
}
|
|
|
|
// Make the old tab inactive.
|
|
this.$activeTabLink.removeClass('active');
|
|
let $oldContent = this.$content;
|
|
|
|
// Update the variables with the new link and content
|
|
this.$activeTabLink = tabLink;
|
|
this.$content = $(M.escapeHash(tabLink[0].hash));
|
|
this.$tabLinks = this.$el.children('li.tab').children('a');
|
|
|
|
// Make the tab active.
|
|
this.$activeTabLink.addClass('active');
|
|
let prevIndex = this.index;
|
|
this.index = Math.max(this.$tabLinks.index(tabLink), 0);
|
|
|
|
// Swap content
|
|
if (this.options.swipeable) {
|
|
if (this._tabsCarousel) {
|
|
this._tabsCarousel.set(this.index, () => {
|
|
if (typeof this.options.onShow === 'function') {
|
|
this.options.onShow.call(this, this.$content[0]);
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
if (this.$content.length) {
|
|
this.$content[0].style.display = 'block';
|
|
this.$content.addClass('active');
|
|
if (typeof this.options.onShow === 'function') {
|
|
this.options.onShow.call(this, this.$content[0]);
|
|
}
|
|
|
|
if ($oldContent.length && !$oldContent.is(this.$content)) {
|
|
$oldContent[0].style.display = 'none';
|
|
$oldContent.removeClass('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update widths after content is swapped (scrollbar bugfix)
|
|
this._setTabsAndTabWidth();
|
|
|
|
// Update indicator
|
|
this._animateIndicator(prevIndex);
|
|
|
|
// Prevent the anchor's default click action
|
|
e.preventDefault();
|
|
}
|
|
|
|
/**
|
|
* Generate elements for tab indicator.
|
|
*/
|
|
_createIndicator() {
|
|
let indicator = document.createElement('li');
|
|
indicator.classList.add('indicator');
|
|
|
|
this.el.appendChild(indicator);
|
|
this._indicator = indicator;
|
|
|
|
setTimeout(() => {
|
|
this._indicator.style.left = this._calcLeftPos(this.$activeTabLink) + 'px';
|
|
this._indicator.style.right = this._calcRightPos(this.$activeTabLink) + 'px';
|
|
}, 0);
|
|
}
|
|
|
|
/**
|
|
* Setup first active tab link.
|
|
*/
|
|
_setupActiveTabLink() {
|
|
// If the location.hash matches one of the links, use that as the active tab.
|
|
this.$activeTabLink = $(this.$tabLinks.filter('[href="' + location.hash + '"]'));
|
|
|
|
// If no match is found, use the first link or any with class 'active' as the initial active tab.
|
|
if (this.$activeTabLink.length === 0) {
|
|
this.$activeTabLink = this.$el
|
|
.children('li.tab')
|
|
.children('a.active')
|
|
.first();
|
|
}
|
|
if (this.$activeTabLink.length === 0) {
|
|
this.$activeTabLink = this.$el
|
|
.children('li.tab')
|
|
.children('a')
|
|
.first();
|
|
}
|
|
|
|
this.$tabLinks.removeClass('active');
|
|
this.$activeTabLink[0].classList.add('active');
|
|
|
|
this.index = Math.max(this.$tabLinks.index(this.$activeTabLink), 0);
|
|
|
|
if (this.$activeTabLink.length) {
|
|
this.$content = $(M.escapeHash(this.$activeTabLink[0].hash));
|
|
this.$content.addClass('active');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup swipeable tabs
|
|
*/
|
|
_setupSwipeableTabs() {
|
|
// Change swipeable according to responsive threshold
|
|
if (window.innerWidth > this.options.responsiveThreshold) {
|
|
this.options.swipeable = false;
|
|
}
|
|
|
|
let $tabsContent = $();
|
|
this.$tabLinks.each((link) => {
|
|
let $currContent = $(M.escapeHash(link.hash));
|
|
$currContent.addClass('carousel-item');
|
|
$tabsContent = $tabsContent.add($currContent);
|
|
});
|
|
|
|
let $tabsWrapper = $('<div class="tabs-content carousel carousel-slider"></div>');
|
|
$tabsContent.first().before($tabsWrapper);
|
|
$tabsWrapper.append($tabsContent);
|
|
$tabsContent[0].style.display = '';
|
|
|
|
// Keep active tab index to set initial carousel slide
|
|
let activeTabIndex = this.$activeTabLink.closest('.tab').index();
|
|
|
|
this._tabsCarousel = M.Carousel.init($tabsWrapper[0], {
|
|
fullWidth: true,
|
|
noWrap: true,
|
|
onCycleTo: (item) => {
|
|
let prevIndex = this.index;
|
|
this.index = $(item).index();
|
|
this.$activeTabLink.removeClass('active');
|
|
this.$activeTabLink = this.$tabLinks.eq(this.index);
|
|
this.$activeTabLink.addClass('active');
|
|
this._animateIndicator(prevIndex);
|
|
if (typeof this.options.onShow === 'function') {
|
|
this.options.onShow.call(this, this.$content[0]);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Set initial carousel slide to active tab
|
|
this._tabsCarousel.set(activeTabIndex);
|
|
}
|
|
|
|
/**
|
|
* Teardown normal tabs.
|
|
*/
|
|
_teardownSwipeableTabs() {
|
|
let $tabsWrapper = this._tabsCarousel.$el;
|
|
this._tabsCarousel.destroy();
|
|
|
|
// Unwrap
|
|
$tabsWrapper.after($tabsWrapper.children());
|
|
$tabsWrapper.remove();
|
|
}
|
|
|
|
/**
|
|
* Setup normal tabs.
|
|
*/
|
|
_setupNormalTabs() {
|
|
// Hide Tabs Content
|
|
this.$tabLinks.not(this.$activeTabLink).each((link) => {
|
|
if (!!link.hash) {
|
|
let $currContent = $(M.escapeHash(link.hash));
|
|
if ($currContent.length) {
|
|
$currContent[0].style.display = 'none';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Teardown normal tabs.
|
|
*/
|
|
_teardownNormalTabs() {
|
|
// show Tabs Content
|
|
this.$tabLinks.each((link) => {
|
|
if (!!link.hash) {
|
|
let $currContent = $(M.escapeHash(link.hash));
|
|
if ($currContent.length) {
|
|
$currContent[0].style.display = '';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* set tabs and tab width
|
|
*/
|
|
_setTabsAndTabWidth() {
|
|
this.tabsWidth = this.$el.width();
|
|
this.tabWidth = Math.max(this.tabsWidth, this.el.scrollWidth) / this.$tabLinks.length;
|
|
}
|
|
|
|
/**
|
|
* Finds right attribute for indicator based on active tab.
|
|
* @param {cash} el
|
|
*/
|
|
_calcRightPos(el) {
|
|
return Math.ceil(this.tabsWidth - el.position().left - el[0].getBoundingClientRect().width);
|
|
}
|
|
|
|
/**
|
|
* Finds left attribute for indicator based on active tab.
|
|
* @param {cash} el
|
|
*/
|
|
_calcLeftPos(el) {
|
|
return Math.floor(el.position().left);
|
|
}
|
|
|
|
updateTabIndicator() {
|
|
this._setTabsAndTabWidth();
|
|
this._animateIndicator(this.index);
|
|
}
|
|
|
|
/**
|
|
* Animates Indicator to active tab.
|
|
* @param {Number} prevIndex
|
|
*/
|
|
_animateIndicator(prevIndex) {
|
|
let leftDelay = 0,
|
|
rightDelay = 0;
|
|
|
|
if (this.index - prevIndex >= 0) {
|
|
leftDelay = 90;
|
|
} else {
|
|
rightDelay = 90;
|
|
}
|
|
|
|
// Animate
|
|
let animOptions = {
|
|
targets: this._indicator,
|
|
left: {
|
|
value: this._calcLeftPos(this.$activeTabLink),
|
|
delay: leftDelay
|
|
},
|
|
right: {
|
|
value: this._calcRightPos(this.$activeTabLink),
|
|
delay: rightDelay
|
|
},
|
|
duration: this.options.duration,
|
|
easing: 'easeOutQuad'
|
|
};
|
|
anim.remove(this._indicator);
|
|
anim(animOptions);
|
|
}
|
|
|
|
/**
|
|
* Select tab.
|
|
* @param {String} tabId
|
|
*/
|
|
select(tabId) {
|
|
let tab = this.$tabLinks.filter('[href="#' + tabId + '"]');
|
|
if (tab.length) {
|
|
tab.trigger('click');
|
|
}
|
|
}
|
|
}
|
|
|
|
M.Tabs = Tabs;
|
|
|
|
if (M.jQueryLoaded) {
|
|
M.initializeJqueryWrapper(Tabs, 'tabs', 'M_Tabs');
|
|
}
|
|
})(cash, M.anime);
|