| 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 |
|
| 34 |
function init() { |
| 35 |
const tabBar = document.getElementById('mobile-tab-bar'); |
| 36 |
if (!tabBar) return; |
| 37 |
|
| 38 |
|
| 39 |
tabBar.addEventListener('click', (e) => { |
| 40 |
const tab = e.target.closest('.mobile-tab'); |
| 41 |
if (!tab) return; |
| 42 |
|
| 43 |
|
| 44 |
if (tab.id === 'mobile-create-btn') { |
| 45 |
BB.feeds.openAddFeed(); |
| 46 |
return; |
| 47 |
} |
| 48 |
|
| 49 |
|
| 50 |
if (tab.id === 'mobile-more-btn') { |
| 51 |
toggleMorePopover(); |
| 52 |
return; |
| 53 |
} |
| 54 |
|
| 55 |
|
| 56 |
const tabName = tab.dataset.tab; |
| 57 |
if (tabName) switchTab(tabName); |
| 58 |
}); |
| 59 |
|
| 60 |
|
| 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 |
|
| 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 |
|
| 80 |
const desktopSearch = document.getElementById('search-input'); |
| 81 |
if (desktopSearch) desktopSearch.value = mobileSearch.value; |
| 82 |
}, 300)); |
| 83 |
} |
| 84 |
|
| 85 |
|
| 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 |
|
| 93 |
const desktopSort = document.getElementById('sort-select'); |
| 94 |
if (desktopSort) desktopSort.value = e.target.value; |
| 95 |
}); |
| 96 |
} |
| 97 |
|
| 98 |
|
| 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 |
|
| 118 |
closeMobileDetail(); |
| 119 |
|
| 120 |
|
| 121 |
sidebar.classList.remove('mobile-visible'); |
| 122 |
itemsPanel.classList.remove('mobile-hidden'); |
| 123 |
|
| 124 |
switch (tab) { |
| 125 |
case 'feed': |
| 126 |
|
| 127 |
|
| 128 |
if (BB.state.currentSource === '__saved__') { |
| 129 |
BB.sources.select(''); |
| 130 |
} |
| 131 |
break; |
| 132 |
|
| 133 |
case 'saved': |
| 134 |
|
| 135 |
BB.sources.select('__saved__'); |
| 136 |
break; |
| 137 |
|
| 138 |
case 'sources': |
| 139 |
|
| 140 |
sidebar.classList.add('mobile-visible'); |
| 141 |
itemsPanel.classList.add('mobile-hidden'); |
| 142 |
break; |
| 143 |
} |
| 144 |
|
| 145 |
updateTabBar(); |
| 146 |
} |
| 147 |
|
| 148 |
|
| 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 |
|
| 159 |
function openMobileDetail() { |
| 160 |
if (!isMobile()) return; |
| 161 |
document.body.classList.add('mobile-detail-open'); |
| 162 |
|
| 163 |
|
| 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 |
|
| 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; |
| 189 |
switchTab('feed'); |
| 190 |
} |
| 191 |
|
| 192 |
|
| 193 |
function toggleMorePopover() { |
| 194 |
const pop = document.getElementById('mobile-more-popover'); |
| 195 |
if (pop) pop.classList.toggle('visible'); |
| 196 |
} |
| 197 |
|
| 198 |
|
| 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 |
|