import type { TabPage } from './tab-page'; import { Prop, toNative } from 'vue-facing-decorator'; import PowerduckState from '../../app/powerduck-state'; import TsxComponent, { Component } from '../../app/vuetsx'; import { slotUnwrapper } from '../../common/slot-unwrapper'; import { isNullOrEmpty } from '../../common/utils/is-null-or-empty'; import { PortalUtils } from '../../common/utils/utils'; import Card from '../card/card'; import CardBody from '../card/card-body'; import CardHeader from '../card/card-header'; import HtmlLiteral from '../html-literal/html-literal'; import './css/tabs.css'; export enum TabsRenderMode { Normal = 0, SidePills = 1, SidePillsExtended = 2, } interface TabsArgs { renderMode?: TabsRenderMode; stickyOnMobile?: boolean; initialTabIndex?: number; allowTabSwitch?: boolean; wrapInCard?: boolean; cardHasShadow?: boolean; cardRenderMode?: 'default' | 'inlined'; renderHiddenTabs?: boolean; onTabChange?: (index: number) => void; } @Component class TabsComponent extends TsxComponent implements TabsArgs { @Prop() renderMode!: TabsRenderMode; @Prop() stickyOnMobile!: boolean; @Prop() initialTabIndex!: number; @Prop() allowTabSwitch!: boolean; @Prop() wrapInCard!: boolean; @Prop() cardHasShadow!: boolean; @Prop() renderHiddenTabs!: boolean; @Prop() cardRenderMode?: 'default' | 'inlined'; @Prop() onTabChange?: (index: number) => void; registeredCount: number = null; activePageIndex: number = null; uuid: string = null; created() { if (this.initialTabIndex != null) { this.setActivePageIndex(this.initialTabIndex, true); } else { this.setActivePageIndex(0, true); } } mounted() { this.ensureStickyTabs(); } updated() { this.ensureStickyTabs(); } beforeUnmount() { if (this.uuid != null) { PowerduckState.unregisterStickyTabs(this.uuid); } } ensureStickyTabs(): void { if (this.uuid == null) { this.uuid = `t-${PortalUtils.randomString(8)}`; } if (this.stickyOnMobile == true) { PowerduckState.registerStickyTabs(this.uuid, { HasSticky: this.stickyOnMobile == true, AllowTabChange: this.allowTabSwitch != false, }); } } getChildTabs(): typeof TabPage.prototype[] { return this.getSlotProperties('TabPage'); } getContentCssClass(): string { if (this.allowTabSwitch != false) { return 'col-md-9 col-lg-10 tab-content'; } else { return 'col-md-12 tab-content'; } } getCardHasShadow(): boolean { if (this.cardHasShadow != null) { return this.cardHasShadow; } if (this.renderMode == TabsRenderMode.SidePillsExtended) { return true; } return false; } getCardRenderMode(): 'default' | 'inlined' { if (!isNullOrEmpty(this.cardRenderMode)) { return this.cardRenderMode; } if (this.renderMode == TabsRenderMode.SidePillsExtended) { return 'inlined'; } return 'default'; } getActivePageIndex(): number { if (this.renderHiddenTabs != true) { if (this.activePageIndex > this.getChildTabs().length - 1) { this.activePageIndex = 0; } return this.activePageIndex || 0; } else { return (this as any)._activeIndex || 0; } } setActivePageIndex(index: number, initial: boolean = false): void { if (this.renderHiddenTabs != true) { this.activePageIndex = index; } else { (this as any)._activeIndex = index; } if (!initial) { this.onTabChange?.(index); } } render(h) { if (this.renderMode == null || this.renderMode == TabsRenderMode.SidePills || this.renderMode == TabsRenderMode.SidePillsExtended) { return this.renderSidePillsMode(h); } else if (this.renderMode == TabsRenderMode.Normal) { return this.renderNormalMode(h); } else { console.error('Render mode not implemented'); return
Render mode not implemented
; } } getTabHref(tab: typeof TabPage.prototype, index: number): string { if (this.renderHiddenTabs != true) { return 'javascript:'; } else { return `#${this.uuid}-tab-${index}`; } } getActiveSlots(): any[] { const slots = slotUnwrapper(this.$slots.default?.(), 'TabPage'); if (this.renderHiddenTabs != true) { return [slots[this.getActivePageIndex()]]; // return [this.$slots.default?.()[this.getActivePageIndex()]]; } else { return slots; // return this.$slots.default?.(); } } renderSidePillsMode(h) { const childTabs = this.getChildTabs(); let activeIndex = this.getActivePageIndex(); if (activeIndex > childTabs.length - 1) { activeIndex = 0; } const activeTab: typeof TabPage.prototype = childTabs[activeIndex] || ({} as any); const fadeOutMode = this.renderHiddenTabs == true; return (
{this.allowTabSwitch != false && ( )}
{this.getActiveSlots().map((slot, i) => { // In navPills/hidden mode every pane renders up-front and Bootstrap toggles visibility // via CSS (no Vue re-render), so each pane must carry its OWN caption (pane i ↔ childTabs[i]). // Using the single shared `activeTab` froze every header at the first tab's caption. // In single-pane mode the only rendered pane is the active tab. const paneTab: typeof TabPage.prototype = (this.renderHiddenTabs == true ? childTabs[i] : activeTab) || activeTab; return (
{this.wrapInCard === false ? slot : ( {slot} )}
); })}
); } renderNormalMode(h) { const childTabs = this.getChildTabs(); const activeIndex = this.getActivePageIndex(); return (
{this.allowTabSwitch != false && ( )}
{this.renderHiddenTabs != true && this.renderActiveTab(h)} {this.renderHiddenTabs == true && this.$slots.default?.().map(tab =>
{tab}
)}
); } renderActiveTab(h) { return
{slotUnwrapper(this.$slots.default?.())?.[this.getActivePageIndex()]}
; } } const Tabs = toNative(TabsComponent); export default Tabs;