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
*
*
* - Tab 1
* - Tab 2
* - Tab 3
* - Tab 4
*
*
* [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;
}
}