UNPKG

@ribajs/bs5

Version:

Bootstrap 5 module for Riba.js

398 lines (362 loc) 10.3 kB
import { handleizeFormatter, FormatterFn, TemplateFunction, TemplatesComponent, ScopeBase, } from "@ribajs/core"; import templateHorizontal from "./bs5-tabs-horizontal.component.html?raw"; import templateVertical from "./bs5-tabs-vertical.component.html?raw"; import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js"; import { throttle } from "@ribajs/utils/src/control"; const handleize = handleizeFormatter.read as FormatterFn; export interface Tab { title: string; content: string; handle: string; active: boolean; type?: string; index: number; } export interface Scope extends ScopeBase { items: Tab[]; activate: Bs5TabsComponent["activate"]; deactivate: Bs5TabsComponent["activate"]; deactivateAll: Bs5TabsComponent["deactivateAll"]; optionTabsAutoHeight: boolean; optionTabsAngle: "vertical" | "horizontal"; } export class Bs5TabsComponent extends TemplatesComponent { public static tagName = "bs5-tabs"; protected templateAttributes = [ { name: "title", required: true, }, { name: "handle", required: false, }, { name: "type", required: false, }, { name: "active", required: false, }, { name: "index", required: false, }, ]; public scope: Scope = { items: new Array<Tab>(), activate: this.activate, deactivate: this.deactivate, deactivateAll: this.deactivateAll, optionTabsAutoHeight: false, optionTabsAngle: "horizontal", }; protected tabs?: NodeListOf<Element>; protected tabPanes?: NodeListOf<Element>; protected scrollable?: Element | null; static get observedAttributes(): string[] { return [ "option-tabs-auto-height", "option-tabs-angle", "tab-0-title", "tab-0-content", "tab-0-handle", "tab-1-title", "tab-1-content", "tab-1-handle", "tab-2-title", "tab-2-content", "tab-2-handle", "tab-3-title", "tab-3-content", "tab-3-handle", "tab-4-title", "tab-4-content", "tab-4-handle", "tab-5-title", "tab-5-content", "tab-5-handle", "tab-6-title", "tab-6-content", "tab-6-handle", "tab-7-title", "tab-7-content", "tab-7-handle", "tab-8-title", "tab-8-content", "tab-8-handle", "tab-9-title", "tab-9-content", "tab-9-handle", "tab-10-title", "tab-10-content", "tab-10-handle", "tab-11-title", "tab-11-content", "tab-11-handle", "tab-12-title", "tab-12-content", "tab-12-handle", "tab-13-title", "tab-13-content", "tab-13-handle", "tab-14-title", "tab-14-content", "tab-14-handle", "tab-15-title", "tab-15-content", "tab-15-handle", "tab-16-title", "tab-16-content", "tab-16-handle", "tab-17-title", "tab-17-content", "tab-17-handle", "tab-18-title", "tab-18-content", "tab-18-handle", "tab-19-title", "tab-19-content", "tab-19-handle", ]; } constructor() { super(); } protected _onResize() { this.setHeight(); } protected onResize = throttle(this._onResize.bind(this)); /** * Make all tabs panes as height as the highest tab pane */ public setHeight() { if (this.scope.optionTabsAutoHeight) { return; } // Bind static template this.setElements(); let highest = 0; if (!this.tabPanes) { return; } this.tabPanes.forEach((tabPane) => { if (!(tabPane as unknown as HTMLElement).style) { return; } (tabPane as unknown as HTMLElement).style.height = "auto"; (tabPane as unknown as HTMLElement).style.display = "block"; const height = (tabPane as unknown as HTMLElement).offsetHeight || 0; if (height > highest) { highest = height; } }); this.tabPanes.forEach((tabPane) => { if (!(tabPane as unknown as HTMLElement).style) { return; } // Reset display style property (tabPane as unknown as HTMLElement).style.display = ""; if (highest > 0) { (tabPane as unknown as HTMLElement).style.height = highest + "px"; } }); } public deactivateAll() { for (let index = 0; index < this.scope.items.length; index++) { const tab = this.scope.items[index]; this.deactivate(tab); } } public deactivate(tab: Tab) { tab.active = false; const firstTabContentChild = this.getTabContentChildByIndex(tab.index); if (firstTabContentChild) { this.triggerVisibilityChangedForElement(firstTabContentChild, tab.active); } } public activate(tab: Tab) { this.deactivateAll(); tab.active = true; const firstTabContentChild = this.getTabContentChildByIndex(tab.index); if (firstTabContentChild) { this.triggerVisibilityChangedForElement( firstTabContentChild as Element, tab.active, ); } } protected activateFirstTab() { if (this.scope.items.length > 0) { this.activate(this.scope.items[0]); } } protected getTabContentChildByIndex(index: number) { return ( this.querySelector( `.tab-content .tab-pane:nth-child(${index + 1}) > *`, ) || undefined ); } /** * Trigger `visibility-changed` for components that need to update if visibility changes. * E.g. this event is used the bs5-slideshow component * @param element * @param visible */ protected triggerVisibilityChangedForElement( element: Element, visible: boolean, ) { setTimeout(() => { // Use this event to update any custom element when it becomes visible element.dispatchEvent( new CustomEvent("visibility-changed", { detail: { visible } }), ); }, 200); } protected connectedCallback() { super.connectedCallback(); this.initTabs(); this.activateFirstTab(); this.init(Bs5TabsComponent.observedAttributes); } protected disconnectedCallback() { if (this.tabs) { this.tabs.forEach((tab) => { tab.removeEventListener("shown.bs.tab", this.onTabShownEventHandler); }); } window.removeEventListener("resize", this.onResize); } protected setElements() { this.tabs = this.querySelectorAll('[role="tab"]'); this.tabPanes = this.querySelectorAll('[role="tabpanel"]'); this.scrollable = this.querySelector("[scrollable]"); } protected resizeTabsArray(newSize: number) { while (newSize > this.scope.items.length) { this.scope.items.push({ handle: "", title: "", content: "", active: false, index: this.scope.items.length - 1, }); } } protected onTabShownEventHandler(event: Event) { const curTab = (event.target || event.srcElement) as Element | null; if (!curTab) { return; } if (this.scrollable) { const tabScrollPosition = curTab.getBoundingClientRect(); const scrollLeftTo = this.scrollable.scrollLeft || 0 + tabScrollPosition.left; // TODO animate // this.scrollable.animate({ scrollLeft: scrollLeftTo}, 'slow'); this.scrollable.scrollLeft = scrollLeftTo; } } protected initTabs() { // Bind static template this.setElements(); if (this.tabs) { this.tabs.forEach((tab) => { tab.removeEventListener("shown.bs.tab", this.onTabShownEventHandler); tab.addEventListener("shown.bs.tab", this.onTabShownEventHandler); }); } if (this.scope.optionTabsAutoHeight) { window.removeEventListener("resize", this.onResize); window.addEventListener("resize", this.onResize, { passive: true }); this.setHeight(); } } protected addTabByAttribute(attributeName: string, newValue: string) { const index = Number(attributeName.replace(/[^0-9]/g, "")); if (index >= this.scope.items.length) { this.resizeTabsArray(index + 1); } this.scope.items[index].index = index; if (attributeName.endsWith("Content")) { this.scope.items[index].content = newValue; } if (attributeName.endsWith("Title")) { this.scope.items[index].title = newValue; this.scope.items[index].handle = this.scope.items[index].handle || handleize(this.scope.items[index].title); } if (attributeName.endsWith("Handle")) { this.scope.items[index].handle = newValue; } // if is first tab if ( this.scope.items.length > 0 && this.scope.items[0] && this.scope.items[0].content.length > 0 && this.scope.items[0].title.length > 0 && this.scope.items[0].handle.length > 0 ) { this.activateFirstTab(); } } /** * Extends TemplatesComponent.transformTemplateAttributes to set the handle by the title if no handle is set */ protected transformTemplateAttributes(attributes: any, index: number) { attributes = super.transformTemplateAttributes(attributes, index); if (!attributes.handle && attributes.title) { attributes.handle = handleize(attributes.title); } attributes.active = attributes.active || false; return attributes; } protected parsedAttributeChangedCallback( attributeName: string, oldValue: any, newValue: any, namespace: string | null, ) { super.parsedAttributeChangedCallback( attributeName, oldValue, newValue, namespace, ); if (attributeName.startsWith("tab")) { this.addTabByAttribute(attributeName, newValue); this.initTabs(); } } protected async afterBind(): Promise<any> { // Workaround setTimeout(() => { if (this.scope.optionTabsAutoHeight) { this.setHeight(); } }, 500); await super.afterBind(); } protected template(): ReturnType<TemplateFunction> { // Only set the component template if there no childs or the childs are templates if (!hasChildNodesTrim(this) || this.hasOnlyTemplateChilds()) { if (this.scope.optionTabsAngle === "horizontal") { return templateHorizontal; } else { return templateVertical; } } else { return null; } } }