]> BookStack Code Mirror - bookstack/blob - resources/js/components/tri-layout.ts
Layout: Converted tri-layout component to ts
[bookstack] / resources / js / components / tri-layout.ts
1 import {Component} from './component';
2
3 export class TriLayout extends Component {
4     private container!: HTMLElement;
5     private tabs!: HTMLElement[];
6     private sidebarScrollContainers!: HTMLElement[];
7
8     private lastLayoutType = 'none';
9     private onDestroy: (()=>void)|null = null;
10     private scrollCache: Record<string, number> = {
11         content: 0,
12         info: 0,
13     };
14     private lastTabShown = 'content';
15
16     setup(): void {
17         this.container = this.$refs.container;
18         this.tabs = this.$manyRefs.tab;
19         this.sidebarScrollContainers = this.$manyRefs.sidebarScrollContainer;
20
21         // Bind any listeners
22         this.mobileTabClick = this.mobileTabClick.bind(this);
23
24         // Watch layout changes
25         this.updateLayout();
26         window.addEventListener('resize', () => {
27             this.updateLayout();
28         }, {passive: true});
29
30         this.setupSidebarScrollHandlers();
31     }
32
33     updateLayout(): void {
34         let newLayout = 'tablet';
35         if (window.innerWidth <= 1000) newLayout = 'mobile';
36         if (window.innerWidth > 1400) newLayout = 'desktop';
37         if (newLayout === this.lastLayoutType) return;
38
39         if (this.onDestroy) {
40             this.onDestroy();
41             this.onDestroy = null;
42         }
43
44         if (newLayout === 'desktop') {
45             this.setupDesktop();
46         } else if (newLayout === 'mobile') {
47             this.setupMobile();
48         }
49
50         this.lastLayoutType = newLayout;
51     }
52
53     setupMobile() {
54         for (const tab of this.tabs) {
55             tab.addEventListener('click', this.mobileTabClick);
56         }
57
58         this.onDestroy = () => {
59             for (const tab of this.tabs) {
60                 tab.removeEventListener('click', this.mobileTabClick);
61             }
62         };
63     }
64
65     setupDesktop(): void {
66         //
67     }
68
69     /**
70      * Action to run when the mobile info toggle bar is clicked/tapped
71      */
72     mobileTabClick(event: MouseEvent): void {
73         const tab = (event.target as HTMLElement).dataset.tab || '';
74         this.showTab(tab);
75     }
76
77     /**
78      * Show the content tab.
79      * Used by the page-display component.
80      */
81     showContent(): void {
82         this.showTab('content', false);
83     }
84
85     /**
86      * Show the given tab
87      */
88     showTab(tabName: string, scroll: boolean = true): void {
89         this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop;
90
91         // Set tab status
92         for (const tab of this.tabs) {
93             const isActive = (tab.dataset.tab === tabName);
94             tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
95         }
96
97         // Toggle section
98         const showInfo = (tabName === 'info');
99         this.container.classList.toggle('show-info', showInfo);
100
101         // Set the scroll position from cache
102         if (scroll) {
103             const pageHeader = document.querySelector('header') as HTMLElement;
104             const defaultScrollTop = pageHeader.getBoundingClientRect().bottom;
105             document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop;
106             setTimeout(() => {
107                 document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop;
108             }, 50);
109         }
110
111         this.lastTabShown = tabName;
112     }
113
114     setupSidebarScrollHandlers(): void {
115         for (const sidebar of this.sidebarScrollContainers) {
116             sidebar.addEventListener('scroll', () => this.handleSidebarScroll(sidebar), {
117                 passive: true,
118             });
119             this.handleSidebarScroll(sidebar);
120         }
121
122         window.addEventListener('resize', () => {
123             for (const sidebar of this.sidebarScrollContainers) {
124                 this.handleSidebarScroll(sidebar);
125             }
126         });
127     }
128
129     handleSidebarScroll(sidebar: HTMLElement): void {
130         const scrollable = sidebar.clientHeight !== sidebar.scrollHeight;
131         const atTop = sidebar.scrollTop === 0;
132         const atBottom = (sidebar.scrollTop + sidebar.clientHeight) === sidebar.scrollHeight;
133
134         if (sidebar.parentElement) {
135             sidebar.parentElement.classList.toggle('scroll-away-from-top', !atTop && scrollable);
136             sidebar.parentElement.classList.toggle('scroll-away-from-bottom', !atBottom && scrollable);
137         }
138     }
139
140 }