]> BookStack Code Mirror - bookstack/blob - resources/js/components/tabs.ts
Drawings: Added class to extract drawio data from png files
[bookstack] / resources / js / components / tabs.ts
1 import {Component} from './component';
2
3 export interface TabsChangeEvent {
4     showing: string;
5 }
6
7 /**
8  * Tabs
9  * Uses accessible attributes to drive its functionality.
10  * On tab wrapping element:
11  * - role=tablist
12  * On tabs (Should be a button):
13  * - id
14  * - role=tab
15  * - aria-selected=true/false
16  * - aria-controls=<id-of-panel-section>
17  * On panels:
18  * - id
19  * - tabindex=0
20  * - role=tabpanel
21  * - aria-labelledby=<id-of-tab-for-panel>
22  * - hidden (If not shown by default).
23  */
24 export class Tabs extends Component {
25
26     protected container!: HTMLElement;
27     protected tabList!: HTMLElement;
28     protected tabs!: HTMLElement[];
29     protected panels!: HTMLElement[];
30
31     protected activeUnder!: number;
32     protected active: null|boolean = null;
33
34     setup() {
35         this.container = this.$el;
36         this.tabList = this.container.querySelector('[role="tablist"]') as HTMLElement;
37         this.tabs = Array.from(this.tabList.querySelectorAll('[role="tab"]'));
38         this.panels = Array.from(this.container.querySelectorAll(':scope > [role="tabpanel"], :scope > * > [role="tabpanel"]'));
39         this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000;
40
41         this.container.addEventListener('click', event => {
42             const tab = (event.target as HTMLElement).closest('[role="tab"]');
43             if (tab instanceof HTMLElement && this.tabs.includes(tab)) {
44                 this.show(tab.getAttribute('aria-controls') || '');
45             }
46         });
47
48         window.addEventListener('resize', this.updateActiveState.bind(this), {
49             passive: true,
50         });
51         this.updateActiveState();
52     }
53
54     public show(sectionId: string): void {
55         for (const panel of this.panels) {
56             panel.toggleAttribute('hidden', panel.id !== sectionId);
57         }
58
59         for (const tab of this.tabs) {
60             const tabSection = tab.getAttribute('aria-controls');
61             const selected = tabSection === sectionId;
62             tab.setAttribute('aria-selected', selected ? 'true' : 'false');
63         }
64
65         const data: TabsChangeEvent = {showing: sectionId};
66         this.$emit('change', data);
67     }
68
69     protected updateActiveState(): void {
70         const active = window.innerWidth < this.activeUnder;
71         if (active === this.active) {
72             return;
73         }
74
75         if (active) {
76             this.activate();
77         } else {
78             this.deactivate();
79         }
80
81         this.active = active;
82     }
83
84     protected activate(): void {
85         const panelToShow = this.panels.find(p => !p.hasAttribute('hidden')) || this.panels[0];
86         this.show(panelToShow.id);
87         this.tabList.toggleAttribute('hidden', false);
88     }
89
90     protected deactivate(): void {
91         for (const panel of this.panels) {
92             panel.removeAttribute('hidden');
93         }
94         for (const tab of this.tabs) {
95             tab.setAttribute('aria-selected', 'false');
96         }
97         this.tabList.toggleAttribute('hidden', true);
98     }
99
100 }