baseframe-js
Version:
A suite of useful Javascript plugins and functions to help with Front-end Development on websites
202 lines (201 loc) • 7.99 kB
JavaScript
import $ from 'cash-dom';
import { getDataOptions } from "./util/helpers";
import Store from "./core/Store";
import UrlState from "./core/UrlState";
import transition from "./fn/transition";
const VERSION = "1.5.0";
const DATA_NAME = 'Tabs';
const EVENT_NAME = 'tabs';
const DEFAULTS = {
tabsEvent: 'click',
cssPrefix: 'tabs',
locationFilter: null,
urlFilterType: 'hash',
historyType: 'push',
loadLocation: true,
addIDtoPanel: true,
ariaLabel: true,
defaultContent: 0,
tabChange: () => { },
onInit: () => { }
};
const getTabIDFromEl = (el) => {
return (el instanceof HTMLButtonElement ? el.dataset.href : el.hash)?.replace(/^\#/, '') || '';
};
export default class Tabs {
$element;
params;
$tabsNav;
$tabsNavClickElems;
tabsNavClickElems;
$tabsBody;
$tabsBodyPanels;
prevTabId;
initTabId;
initDefaultContent;
static defaults = DEFAULTS;
static version = VERSION;
static pluginName = DATA_NAME;
#transition = transition();
constructor(element, options) {
const s = this;
const dataOptions = getDataOptions(element, EVENT_NAME);
s.$element = $(element);
s.params = $.extend({}, Tabs.defaults, options, dataOptions);
const p = s.params;
s.$tabsNav = s.$element.find(`.${p.cssPrefix}__nav`).first();
s.$tabsBody = s.$element.find(`.${p.cssPrefix}__body`).first();
const tabsBody = s.$tabsBody[0];
s.$tabsBodyPanels = s.$tabsBody.find(`.${p.cssPrefix}__panel`)
// ensure they're the children of the body
// must be direct child
.filter((i, el) => el.parentElement.isSameNode(tabsBody));
s.$tabsNavClickElems = s.$tabsNav.find('a, button');
s.tabsNavClickElems = [...s.$tabsNavClickElems];
s.prevTabId = null;
s.initDefaultContent = s.$tabsBodyPanels.eq(p.defaultContent).data('tab-id');
//init
s.setAriaAttrs();
s.handleEvents();
s.loadDefaultContent();
s.loadFromUrl();
s.params.onInit(s.$tabsNav, s.$tabsBody);
return s;
}
loadDefaultContent() {
const s = this;
const tabId = s.initDefaultContent;
const clickElem = s.getClickElemFromTabId(tabId);
s.changeTabElements(clickElem, tabId, false);
}
loadFromUrl() {
const s = this;
const p = s.params;
if (p.locationFilter !== null && p.loadLocation && p.urlFilterType !== 'none') {
const tabId = UrlState.get(p.urlFilterType, p.locationFilter);
if (tabId) {
const clickElem = s.getClickElemFromTabId(tabId);
clickElem && s.changeTabElements(clickElem, tabId, false);
}
}
}
getClickElemFromTabId(tabId) {
const $clickElem = this.$tabsNavClickElems.filter((i, el) => getTabIDFromEl(el) === tabId);
if ($clickElem.length) {
return $clickElem[0];
}
return null;
}
setAriaAttrs() {
const s = this;
const p = s.params;
s.$tabsNavClickElems.each(function () {
const tabId = getTabIDFromEl(this);
const $tabBodyItem = s.$tabsBodyPanels.filter((i, el) => el.dataset.tabId === tabId);
$(this).attr({
'aria-selected': 'false',
'role': 'tab',
'aria-controls': tabId
});
$tabBodyItem.attr({
'aria-label': p.ariaLabel ? this.textContent : null,
'role': 'tabpanel',
tabindex: '-1'
});
if (p.addIDtoPanel) {
$tabBodyItem.attr({ 'id': tabId });
}
});
}
handleEvents() {
const s = this;
s.$tabsNav.on(`${s.params.tabsEvent}.${EVENT_NAME} ${EVENT_NAME}`, "a, button", function (e) {
const clickElem = this;
const tabId = getTabIDFromEl(clickElem);
s.changeTabElements(clickElem, tabId);
e.preventDefault();
});
s.$tabsNav.on('keydown.' + EVENT_NAME, function (e) {
const target = e.target;
const index = s.tabsNavClickElems.findIndex(el => el.isSameNode(target));
const next = e.key === 'ArrowRight' || e.key === 'ArrowDown';
const prev = e.key === 'ArrowLeft' || e.key === 'ArrowUp';
if (index !== -1 && (next || prev)) {
const changeIndex = next ? index + 1 : index - 1;
const nextBtn = s.tabsNavClickElems[changeIndex];
if (nextBtn) {
// $(target).attr({'tabindex': '-1'});
const tabId = getTabIDFromEl(nextBtn);
s.changeTabElements(nextBtn, tabId);
$(nextBtn)[0].focus();
}
e.preventDefault();
}
});
$(window).on(`popstate.${EVENT_NAME} ${EVENT_NAME}`, (e) => {
if (s.params.historyType === 'push') {
s.loadFromUrl();
e.preventDefault();
}
});
}
changeTabElements(clickElem, tabId, updateUrl = true) {
const s = this;
const p = s.params;
let hasTab = false;
const cssOpen = `${p.cssPrefix}__panel--open`, cssToggling = `${p.cssPrefix}__panel--toggling`, cssOpening = `${p.cssPrefix}__panel--opening`, cssClosing = `${p.cssPrefix}__panel--closing`;
s.#transition(() => {
s.$tabsBodyPanels.each(function () {
const thisTabId = this.dataset.tabId;
if (thisTabId === tabId) {
$(this)
.addClass(`${cssToggling} ${cssOpening}`)
.attr({ 'aria-hidden': null, tabindex: '0' });
hasTab = true;
}
if (s.prevTabId && s.prevTabId === thisTabId) {
$(this)
.addClass(`${cssToggling} ${cssClosing}`)
.attr({ 'aria-hidden': 'true', tabindex: '-1' });
}
});
if (hasTab) {
s.params.tabChange(tabId, s.prevTabId, s.$tabsNav, s.$tabsBody);
s.prevTabId = tabId;
s.$tabsNavClickElems
.attr({ 'aria-selected': 'false', tabindex: '-1' })
.parent('li').removeClass(`${p.cssPrefix}__nav-li--active`);
$(clickElem)
.attr({ 'aria-selected': 'true', tabindex: '0' })
.parent('li').addClass(`${p.cssPrefix}__nav-li--active`);
if (updateUrl && p.urlFilterType !== 'none') {
const paramVal = s.initDefaultContent === tabId ? null : tabId;
if (p.urlFilterType === 'hashVal') {
UrlState.setHashVal(paramVal, p.historyType);
}
else if (p.urlFilterType === 'hash' || p.urlFilterType === 'search') {
UrlState.set(p.urlFilterType, p.locationFilter, paramVal, p.historyType);
}
}
}
}, () => {
s.$tabsBodyPanels.each(function () {
const isTab = this.dataset.tabId === tabId;
$(this)
.toggleClass(cssOpen, isTab)
.removeClass(`${cssToggling} ${cssOpening} ${cssClosing}`);
});
});
}
static remove(element, plugin) {
$(element).each(function () {
const s = plugin || Store(this, DATA_NAME);
const params = s.params;
s.$tabsNav.off(`${params.tabsEvent}.${EVENT_NAME} ${EVENT_NAME}`);
s.$tabsNav.off('keydown.' + EVENT_NAME);
s.$tabsNav.find('a, button').attr({ tabndex: null });
$(window).off(`popstate.${EVENT_NAME} ${EVENT_NAME}`);
Store(this, DATA_NAME, null);
});
}
}