Jake Vanderwerf
2026-05-12 c32ed859f4abd1591c882f4f2a6ee16b1ec275e2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
/**
 * Handles the tabs functionality, given the html requirements are met
 */
class TabsContainer{
    constructor(container, callbacks = {}, parent = null){
        this.tabs = container.querySelector('.tabs');
        this.a11y = window.jvbA11y;
 
        this.updateURL = true;
 
        this.parent = parent;
        this.childTabs = new Map();
        if ('updateURL' in callbacks && callbacks.updateURL === false) {
            this.updateURL = false;
        }
        this.callbacks = callbacks;
 
 
        this.activeTab = (this.updateURL) ? this.getInitialTabFromHash() :
            container.querySelector('button.tab.active')?.dataset.tab;
 
        this.container = container;
 
        if (this.tabs) {
            this.tabs.addEventListener('click', e => {
                const tab = e.target.closest('[data-tab]');
                if (tab) {
                    let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true;
                    this.switchTab(tab.dataset.tab, update);
                }
            });
        }
 
        // Check for and initialize child tab containers
        this.initializeChildTabs();
 
        // Find and set up select dropdown listeners
        this.selectDropdown = document.querySelector('select.tab-list');
        if (this.selectDropdown) {
            this.selectDropdown.addEventListener('change', e => {
 
                let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true;
                this.switchTab(e.target.value, update);
            });
        }
 
        let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true;
 
        if (!this.activeTab){
            this.activeTab = document.querySelector('button.tab')?.dataset.tab;
        }
 
        this.switchTab(this.activeTab, update);
    }
 
    /**
     * Auto-detect and initialize child tab containers
     */
    initializeChildTabs() {
 
        this.tabs.querySelectorAll('button').forEach(tab => {
            let hasChildren = this.container.querySelector(`.tab-content[data-tab="${tab.dataset['tab']}"]`);
 
            if(hasChildren && hasChildren.querySelector('.tabs')){
                let container = this.container.querySelector(`.tab-content[data-tab="${tab.dataset['tab']}"]`);
                let tabs = new window.jvbTabs(container, {updateURL: false}, this);
                this.childTabs.set(tab.dataset.tab, tabs);
            }
        });
        // Find all tab content panels in this container
        // const tabPanels = this.container.querySelectorAll('.tab-content');
        // tabPanels.forEach(panel => {
        //     const tabId = panel.dataset.tab;
        //     if (!tabId) return;
        //
        //     // Look for nested tab containers within this panel
        //     const nestedTabContainers = panel.querySelectorAll('.tabs, [data-tabs]');
        //
        //     nestedTabContainers.forEach(nestedContainer => {
        //         // Create a new TabsContainer for this nested container
        //         const childContainer = new TabsContainer(nestedContainer, {}, this);
        //
        //         // Store the child container
        //         this.childTabs.set(tabId, childContainer);
        //     });
        // });
    }
 
    /**
     * Get initial tab from URL hash
     * @returns {string|false} Tab identifier from hash or null if not present
     */
    getInitialTabFromHash() {
        if (!window.location.hash) return false;
 
        const hash = window.location.hash.substring(1); // Remove the # character
        const pathParts = hash.split('/');
 
        // If this is a root container, check first path part
        if (!this.parent) {
            const rootTabId = pathParts[0];
            // Check if this hash corresponds to a valid tab
            const matchingTab = this.tabs.querySelector(`[data-tab="${rootTabId}"]`);
            if (matchingTab) {
                return rootTabId;
            }
        }
        // If this is a child container, check if our parent's active tab matches the path
        else if (this.parent && pathParts.length > 1) {
            const parentIdx = this.getParentDepth();
            if (parentIdx < pathParts.length) {
                const childTabId = pathParts[parentIdx];
                const matchingTab = this.tabs.querySelector(`[data-tab="${childTabId}"]`);
                if (matchingTab) {
                    return childTabId;
                }
            }
        }
 
        return null;
    }
 
    /**
     * Calculate the depth of this tabs container in the hierarchy
     * @returns {number} The depth (0 for root, 1 for first level child, etc.)
     */
    getParentDepth() {
        let depth = 0;
        let parent = this.parent;
 
        while (parent) {
            depth++;
            parent = parent.parent;
        }
 
        return depth;
    }
 
    /**
     * Build the full path for this tab including all parent tabs
     * @param {string} tabId - The current tab ID
     * @returns {string} The full path including parent tabs
     */
    getFullTabPath(tabId) {
        if (!this.parent) {
            return tabId;
        }
 
        // Get parent's active tab path and append this tab
        const parentPath = this.parent.getFullTabPath(this.parent.activeTab);
        return `${parentPath}|${tabId}`;
    }
 
    /**
     * Switch between tabs
     * @param {string} tab - Tab to switch to ('items' or 'lists')
     * @param {boolean} updateHistory - Whether to push the state to the url
     */
    switchTab(tab, updateHistory = false) {
        document.activeElement?.blur();
 
        // Update tab buttons
        this.tabs.querySelectorAll('[data-tab]').forEach(tabBtn => {
            tabBtn.classList.toggle('active', tabBtn.dataset.tab === tab);
            tabBtn.setAttribute('aria-selected', tabBtn.dataset.tab === tab);
        });
 
        // Update tab panels
        this.container.querySelectorAll('.tab-content').forEach(content => {
            content.classList.toggle('active', content.dataset.tab === tab);
            content.setAttribute('aria-hidden', content.dataset.tab !== tab);
            content.hidden = content.dataset.tab !== tab;
        });
 
        // Update state
        this.activeTab = tab;
        if (this.callbacks[tab]) {
            this.callbacks[tab]();
        }
 
        // Activate first child tab if this tab has children
        const childContainer = this.childTabs.get(tab);
        if (childContainer) {
            const firstTab = childContainer.container.querySelector('button.tab')?.dataset.tab;
            if (firstTab) {
                childContainer.switchTab(firstTab, false);
            }
        }
 
        // Update URL hash with full path (only from root container)
        if (updateHistory) {
            if (!this.parent) {
                window.history.pushState({ tab: tab }, '', `#${tab}`);
            } else {
                // This is a child container, notify parent to update URL
                this.parent.updateUrlFromChild();
            }
        }
 
        // Update select dropdown if it exists
        if (this.selectDropdown && this.selectDropdown.querySelector(`option[value="${tab}"]`)) {
            this.selectDropdown.value = tab;
        }
 
        //TODO: Initialize notification system
        // this.notify('tab-switched', {
        //  current: this.activeTab
        // });
 
        // Announce to screen readers
        this.a11y.announce(`Switched to ${tab} tab`);
    }
 
    /**
     * Update URL when a child tab changes
     */
    updateUrlFromChild() {
        console.log('Updating URL');
        if (('updateURL' in this.callbacks) ? this.callbacks.updateURL : true) {
            if (!this.parent) {
                // Only the root container should update the URL
                const fullPath = this.getFullTabPath(this.activeTab);
                window.history.pushState({ tab: fullPath }, '', `#${fullPath}`);
            } else {
                // Propagate up to the root
                this.parent.updateUrlFromChild();
            }
        }
    }
}
 
window.jvbTabs = TabsContainer;