Skip to main content

max / balanced_breakfast

8.0 KB · 240 lines History Blame Raw
1 /**
2 * @fileoverview Mobile tab bar navigation and panel switching.
3 *
4 * On mobile (<= 768px), replaces the three-column layout with a bottom tab bar.
5 * Tabs: Feed (items list), Saved (starred items), Sources (sidebar).
6 * The + button opens the add-feed modal; More opens a popover with settings/sync/OPML.
7 *
8 * All functions are no-ops on desktop (>768px) — class additions/removals are harmless
9 * when the tab bar is hidden and panels are laid out by CSS grid/flex.
10 */
11 (function() {
12 'use strict';
13
14 let currentTab = 'feed';
15
16 /**
17 * Check if we're in mobile layout by testing tab bar visibility.
18 * @returns {boolean} True if the mobile tab bar is visible.
19 */
20 function isMobile() {
21 const tabBar = document.getElementById('mobile-tab-bar');
22 return tabBar && getComputedStyle(tabBar).display !== 'none';
23 }
24
25 /**
26 * Get the current active tab name.
27 * @returns {string} Current tab: 'feed', 'saved', or 'sources'.
28 */
29 function getCurrentTab() {
30 return currentTab;
31 }
32
33 /** Initialize tab bar event listeners. */
34 function init() {
35 const tabBar = document.getElementById('mobile-tab-bar');
36 if (!tabBar) return;
37
38 // Tab buttons
39 tabBar.addEventListener('click', (e) => {
40 const tab = e.target.closest('.mobile-tab');
41 if (!tab) return;
42
43 // Create button
44 if (tab.id === 'mobile-create-btn') {
45 BB.feeds.openAddFeed();
46 return;
47 }
48
49 // More button
50 if (tab.id === 'mobile-more-btn') {
51 toggleMorePopover();
52 return;
53 }
54
55 // Regular tab
56 const tabName = tab.dataset.tab;
57 if (tabName) switchTab(tabName);
58 });
59
60 // More popover actions
61 const popover = document.getElementById('mobile-more-popover');
62 if (popover) {
63 popover.addEventListener('click', (e) => {
64 const btn = e.target.closest('[data-action]');
65 if (btn) {
66 handleMoreAction(btn.dataset.action);
67 closeMorePopover();
68 }
69 });
70 }
71
72 // Mobile search input — debounced, mirrors desktop search behavior
73 const mobileSearch = document.getElementById('mobile-search-input');
74 if (mobileSearch) {
75 mobileSearch.addEventListener('input', BB.utils.debounce(() => {
76 BB.state.set('currentSearch', mobileSearch.value);
77 BB.state.resetPagination();
78 BB.items.load();
79 // Sync desktop search
80 const desktopSearch = document.getElementById('search-input');
81 if (desktopSearch) desktopSearch.value = mobileSearch.value;
82 }, 300));
83 }
84
85 // Mobile sort select
86 const mobileSort = document.getElementById('mobile-sort-select');
87 if (mobileSort) {
88 mobileSort.addEventListener('change', (e) => {
89 BB.state.set('currentOrder', e.target.value);
90 BB.state.resetPagination();
91 BB.items.load();
92 // Sync desktop sort
93 const desktopSort = document.getElementById('sort-select');
94 if (desktopSort) desktopSort.value = e.target.value;
95 });
96 }
97
98 // Close more popover on outside click
99 document.addEventListener('click', (e) => {
100 const pop = document.getElementById('mobile-more-popover');
101 if (!pop || !pop.classList.contains('visible')) return;
102 if (!pop.contains(e.target) && e.target.id !== 'mobile-more-btn') {
103 closeMorePopover();
104 }
105 });
106 }
107
108 /**
109 * Switch the active tab and manage panel visibility.
110 * @param {string} tab - Tab name: 'feed', 'saved', or 'sources'.
111 */
112 function switchTab(tab) {
113 currentTab = tab;
114 const sidebar = document.querySelector('.sidebar');
115 const itemsPanel = document.querySelector('.items-panel');
116
117 // Close detail panel when switching tabs
118 closeMobileDetail();
119
120 // Reset panel visibility classes
121 sidebar.classList.remove('mobile-visible');
122 itemsPanel.classList.remove('mobile-hidden');
123
124 switch (tab) {
125 case 'feed':
126 // Show items, hide sidebar
127 // If currently viewing saved, switch back to all
128 if (BB.state.currentSource === '__saved__') {
129 BB.sources.select('');
130 }
131 break;
132
133 case 'saved':
134 // Show items (filtered to saved), hide sidebar
135 BB.sources.select('__saved__');
136 break;
137
138 case 'sources':
139 // Show sidebar full-width, hide items
140 sidebar.classList.add('mobile-visible');
141 itemsPanel.classList.add('mobile-hidden');
142 break;
143 }
144
145 updateTabBar();
146 }
147
148 /** Update tab bar active states to reflect current tab. */
149 function updateTabBar() {
150 const tabs = document.querySelectorAll('.mobile-tab[data-tab]');
151 tabs.forEach(t => {
152 const isActive = t.dataset.tab === currentTab;
153 t.classList.toggle('active', isActive);
154 t.setAttribute('aria-selected', isActive ? 'true' : 'false');
155 });
156 }
157
158 /** Show mobile detail overlay: hide tab bar, add back button. */
159 function openMobileDetail() {
160 if (!isMobile()) return;
161 document.body.classList.add('mobile-detail-open');
162
163 // Add back button to detail header if not present
164 const header = document.querySelector('.detail-header');
165 if (header && !header.querySelector('.mobile-back-btn')) {
166 const backBtn = document.createElement('button');
167 backBtn.className = 'btn btn-small mobile-back-btn';
168 backBtn.textContent = '\u2190 Back';
169 backBtn.addEventListener('click', () => BB.detail.close());
170 header.prepend(backBtn);
171 }
172 }
173
174 /** Close mobile detail overlay: show tab bar, remove back button. */
175 function closeMobileDetail() {
176 document.body.classList.remove('mobile-detail-open');
177 const backBtn = document.querySelector('.mobile-back-btn');
178 if (backBtn) backBtn.remove();
179 }
180
181 /**
182 * Called from sources.js after a source is selected.
183 * On mobile, switches to Feed tab to show filtered items.
184 * @param {string} sourceId - The selected source ID.
185 */
186 function onSourceSelected(sourceId) {
187 if (!isMobile()) return;
188 if (sourceId === '__saved__') return; // Saved tab handles this
189 switchTab('feed');
190 }
191
192 /** Toggle the more popover visibility. */
193 function toggleMorePopover() {
194 const pop = document.getElementById('mobile-more-popover');
195 if (pop) pop.classList.toggle('visible');
196 }
197
198 /** Close the more popover. */
199 function closeMorePopover() {
200 const pop = document.getElementById('mobile-more-popover');
201 if (pop) pop.classList.remove('visible');
202 }
203
204 /**
205 * Dispatch a more popover action to the appropriate handler.
206 * @param {string} action - Action name from data-action attribute.
207 */
208 function handleMoreAction(action) {
209 switch (action) {
210 case 'settings':
211 BB.app.showSettings();
212 break;
213 case 'sync':
214 BB.sync.openSettings();
215 break;
216 case 'import':
217 BB.feeds.importOpml();
218 break;
219 case 'export':
220 BB.feeds.exportOpml();
221 break;
222 case 'shortcuts':
223 BB.app.showHelp();
224 break;
225 }
226 }
227
228 BB.navigation = {
229 init,
230 isMobile,
231 switchTab,
232 updateTabBar,
233 openMobileDetail,
234 closeMobileDetail,
235 onSourceSelected,
236 closeMorePopover,
237 getCurrentTab,
238 };
239 })();
240