@ribajs/bs5
Version:
Bootstrap 5 module for Riba.js
398 lines (362 loc) • 10.3 kB
text/typescript
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;
}
}
}