UNPKG

baseframe-js

Version:

Baseframe JS is a comprehensive suite of modular plugins and utilities designed for front-end development

202 lines (201 loc) 7.79 kB
import $be from "base-elem-js"; import { getDataOptions, setParams } from "./util/helpers"; import Store from "./core/Store"; import UrlState from "./core/UrlState"; import loadFromUrl from './util/loadfromUrl'; ; // const { findOne, useTransition } = $be.static; 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; $tabsNav; $tabsNavBtns; tabsNavBtns; tabsBody; $tabsBody; $tabsBodyPanels; prevTabId; initTabId; initDefaultContent; static defaults = DEFAULTS; static version = VERSION; static pluginName = DATA_NAME; #transition = $be.useTransition(); constructor(element, options) { const s = this, dataOptions = getDataOptions(element, EVENT_NAME), p = setParams(Tabs.defaults, options, dataOptions), $element = $be(element); s.$element = $element; s.params = p; s.tabsNav = $be.findOne(`.${p.cssPrefix}__nav`, element); s.tabsBody = $be.findOne(`.${p.cssPrefix}__body`, element); s.$tabsNav = $be(s.tabsNav); s.$tabsBody = $be(s.tabsBody); s.$tabsBodyPanels = s.$tabsBody.find(`.${p.cssPrefix}__panel`, elem => elem.parentElement === s.tabsBody); s.$tabsNavBtns = s.$tabsNav.find('a, button'); s.tabsNavBtns = s.$tabsNavBtns.toArray(); s.prevTabId = null; s.initDefaultContent = s.$tabsBodyPanels.get(p.defaultContent).attr('data-tab-id'); //init s.#setAriaAttrs(); s.#handleEvents(); s.#defaultContent(); s.#loadFromUrl(); s.params.onInit(s.tabsNav, s.tabsBody); return s; } #loadFromUrl() { const s = this; const p = s.params; loadFromUrl(p, (id) => { if (id) { const clickElem = s.#findTabButtonById(id); if (clickElem) s.changeTab(clickElem, id, false); } }); } #defaultContent() { const s = this; const tabId = s.initDefaultContent; const clickElem = s.#findTabButtonById(tabId); s.changeTab(clickElem, tabId, false); } #findTabButtonById(tabId) { const clickElem = this.tabsNavBtns.find((el) => getTabIDFromEl(el) === tabId); if (clickElem) return clickElem; return null; } #setAriaAttrs() { const s = this; const p = s.params; s.$tabsNavBtns.each((elem) => { const tabId = getTabIDFromEl(elem); const $tabBodyItem = s.$tabsBodyPanels.filter(elem => elem.dataset.tabId === tabId); $be(elem).attr({ 'aria-selected': 'false', 'role': 'tab', 'aria-controls': tabId }); $tabBodyItem.attr({ 'aria-label': p.ariaLabel ? elem.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}]`], (ev, elem) => { const clickElem = elem; const tabId = getTabIDFromEl(clickElem); s.changeTab(clickElem, tabId); ev.preventDefault(); }, "a, button"); s.$tabsNav.on(`keydown.${EVENT_NAME}`, function (e) { const target = e.target; const index = s.tabsNavBtns.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.tabsNavBtns[changeIndex]; if (nextBtn) { const tabId = getTabIDFromEl(nextBtn); s.changeTab(nextBtn, tabId); nextBtn.focus(); } e.preventDefault(); } }); $be(window).on(`popstate.${EVENT_NAME} ${EVENT_NAME}`, (e) => { if (s.params.historyType === 'push') { s.#loadFromUrl(); e.preventDefault(); } }); } changeTab(clickElem, tabId, updateUrl = true) { const s = this; if (tabId === s.prevTabId) return; const p = s.params, cssPrefix = `${p.cssPrefix}__panel--`, cssOpen = `${cssPrefix}open`, cssToggling = `${cssPrefix}toggling`, cssOpening = `${cssPrefix}opening`, cssClosing = `${cssPrefix}closing`; let hasTab = false; const start = () => { s.$tabsBodyPanels.each((elem) => { const thisTabId = elem.dataset.tabId; if (thisTabId === tabId) { $be(elem) .addClass([cssToggling, cssOpening]) .attr({ 'aria-hidden': null, tabindex: '0' }); hasTab = true; } if (s.prevTabId && s.prevTabId === thisTabId) { $be(elem) .addClass([cssToggling, cssClosing]) .attr({ 'aria-hidden': 'true', tabindex: '-1' }); } }); if (hasTab) { const cssActive = `${p.cssPrefix}__nav-li--active`; s.params.tabChange(tabId, s.prevTabId, s.tabsNav, s.tabsBody); s.prevTabId = tabId; s.$tabsNavBtns .attr({ 'aria-selected': 'false', tabindex: '-1' }) .find(elem => elem.closest('li')).rmClass(cssActive); $be(clickElem) .attr({ 'aria-selected': 'true', tabindex: '0' }) .find(elem => elem.closest('li')).addClass(cssActive); 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); } } } }, end = () => { s.$tabsBodyPanels.each((elem) => { const isTab = elem.dataset.tabId === tabId; $be(elem) .tgClass(cssOpen, isTab) .rmClass([cssToggling, cssOpening, cssClosing]); }); }; s.#transition(start, end); } static remove(element, plugin) { $be(element).each((elem) => { const s = plugin || Store(elem, DATA_NAME); const params = s.params; s.$tabsNav.find('a, button').attr({ tabndex: null }); s.$tabsNav.off([`${params.tabsEvent}.${EVENT_NAME}`, `[${EVENT_NAME}]`, `keydown.${EVENT_NAME}`]); $be(window).off([`popstate.${EVENT_NAME}`, `[${EVENT_NAME}]`]); Store(elem, DATA_NAME, null); }); } }