import { aClass, rClass } from '../../DOM/Class'; import { extend } from '../../Helpers/Extend'; import KeyboardHandler from '../../Events/KeyboardHandler'; import Tab from './Tab'; const DEFAULT_OPTIONS = { "tabSelector": "li[aria-selected]", "animations": { "open": function( $TAB, $TAB_PANNEL ) { aClass( [ $TAB, $TAB_PANNEL ], 'on' ); return Promise.resolve(); }, "close": function( $TAB, $TAB_PANNEL ) { rClass( [ $TAB, $TAB_PANNEL ], 'on' ); return Promise.resolve(); }, "destroy": function( $TAB, $TAB_PANNEL ) { rClass( [ $TAB, $TAB_PANNEL ], 'on' ); return Promise.resolve(); } }, "onOpenAtStart": null, "onOpen": null, "onClose": null }; /** * Tabs * * @example * ```ts * new Tabs( document.querySelector( '.tabs' ), { * "tabSelector": ".tab", * "animations": { * "open": function( $tab, $panel ) { * aClass( [ $tab, $panel ], 'on' ); * * return Promise.resolve(); * }, * "close": function( $tab, $panel ) { * rClass( [ $tab, $panel ], 'on' ); * * return Promise.resolve(); * }, * "destroy": function( $tab, $panel ) { * rClass( [ $tab, $panel ], 'on' ); * * return Promise.resolve(); * } * }, * "onOpenAtStart": function( $tab, $panel ) { * console.log( 'open: ', $tab, $panel ); * }, * "onOpen": function( $tab, $panel ) { * console.log( 'open: ', $tab, $panel ); * }, * "onClose": function( $tab, $panel ) { * console.log( 'close: ', $tab, $panel ); * } * } ); * ``` * * HTML: * * ```html *
* *
* [Content 1] *
*
* [Content 2 (displayed at start)] *
*
* [Content 3] *
*
* [Content 4] *
*
* ``` * * Set aria-selected to "true" and tabindex="0" on the tab you want open at start. * If the tabs are displayed vertically, add aria-orientation="vertical" on the role="tablist" element */ export default class Tabs { #options: FLib.Tabs.Options; #$TABS_LIST: HTMLElement; #$TABS: NodeList; #tablist: FLib.Tabs.Tab[]; #status: string; #VERTICAL_MODE: boolean; #lastOpenedTab: FLib.Tabs.Tab | undefined; #keyboard: KeyboardHandler | undefined; #$tabsWrapper: HTMLElement; #STATUS_ON = 'STATUS_ON'; #STATUS_OFF = 'STATUS_OFF'; constructor( $tabsWrapper: HTMLElement, userOptions: Partial ) { this.#$tabsWrapper = $tabsWrapper; this.#options = extend( DEFAULT_OPTIONS, userOptions ); this.#$TABS_LIST = $tabsWrapper.querySelector( '[role="tablist"]' ) as HTMLElement; this.#$TABS = this.#$TABS_LIST.querySelectorAll( this.#options.tabSelector ); this.#tablist = []; this.#status = this.#STATUS_OFF; this.#VERTICAL_MODE = this.#$TABS_LIST.getAttribute( 'aria-orientation' ) === 'vertical'; this.#on(); } #onOpenTab = ( tab: FLib.Tabs.Tab ): void => { if ( this.#lastOpenedTab ) { this.#lastOpenedTab.close( true ); } this.#lastOpenedTab = tab; } #onNext = (): void => { const lastIndex = this.#lastOpenedTab ? this.#lastOpenedTab.index : 0; const indexToOpen = lastIndex + 1 >= this.#tablist.length ? 0 : lastIndex + 1; this.#tablist[ indexToOpen ].open(); } #onPrevious = (): void => { const lastIndex = this.#lastOpenedTab ? this.#lastOpenedTab.index : 0; const indexToOpen = lastIndex - 1 < 0 ? this.#tablist.length - 1 : lastIndex - 1; this.#tablist[ indexToOpen ].open(); } #on = (): void => { let hasAnOpenedTab; if( this.#status === this.#STATUS_ON ) { return; } this.#status = this.#STATUS_ON; this.#$TABS.forEach( ( $tab, index ) => { const tab = new Tab( $tab as HTMLElement, { ...this.#options, index, "onOpenTab": this.#onOpenTab } ); this.#tablist.push( tab ); if ( tab.isOpened ) { hasAnOpenedTab = tab.isOpened; } } ); if ( !hasAnOpenedTab && this.#tablist.length ) { this.#tablist[ 0 ].open( true ); } if ( this.#VERTICAL_MODE ) { this.#keyboard = new KeyboardHandler( this.#$tabsWrapper, { "selector": this.#options.tabSelector, "onUp": this.#onPrevious, "onDown": this.#onNext } ); } else { this.#keyboard = new KeyboardHandler( this.#$tabsWrapper, { "selector": this.#options.tabSelector, "onRight": this.#onNext, "onLeft": this.#onPrevious } ); } } #off = (): void => { if( this.#status === this.#STATUS_OFF ){ return; } this.#status = this.#STATUS_OFF; this.#tablist.forEach( tab => { tab.destroy(); } ); this.#keyboard?.off(); } /** * Remove all events, css class, ... */ destroy(): this { this.#off(); return this; } /** * Restart the module */ update(): this { this.#off(); this.#on(); return this; } }