Files
idle_clicker/public/js/ui/tabs.js
T

90 lines
3.0 KiB
JavaScript

// The section menu: horizontal tabs on desktop/tablet, a burger menu on phones
// (the markup is the same, the CSS decides). Tabs unlock with progress.
import { isUnlocked } from '../engine/engine.js';
import { has, t } from '../services/i18n.js';
import { $, h } from './dom.js';
import { showToast } from './toast.js';
const labelKey = (id) => (has(`theme.tab.${id}`) ? `theme.tab.${id}` : `tab.${id}`);
/**
* @param tabs data/tabs.json
* @param onSelect (tabId) called when the visible section changes
*/
export function createTabs(tabs, onSelect) {
const list = $('#tabs');
const menu = $('#menu');
const toggle = $('#menu-toggle');
const buttons = new Map();
let active = 'main';
let known = null; // unlocked tabs seen so far (null until the first update)
const unseen = new Set();
const view = (id) => $(`[data-view="${id}"]`);
function select(id) {
active = id;
unseen.delete(id);
for (const tab of tabs) {
const section = view(tab.id);
if (section) section.hidden = tab.id !== id;
buttons.get(tab.id)?.button.setAttribute('aria-current', tab.id === id ? 'page' : 'false');
}
buttons.get(id)?.item.classList.remove('tab--new');
setMenuOpen(false);
onSelect(id);
}
function setMenuOpen(open) {
menu.toggleAttribute('data-open', open);
toggle.setAttribute('aria-expanded', String(open));
}
toggle.addEventListener('click', () => setMenuOpen(!menu.hasAttribute('data-open')));
/** Build the buttons (call again after a language or theme change to relabel). */
function rebuild() {
list.replaceChildren();
buttons.clear();
for (const tab of tabs) {
const button = h('button', {
class: 'tab',
type: 'button',
'aria-current': tab.id === active ? 'page' : 'false',
text: t(labelKey(tab.id)),
onclick: () => select(tab.id),
});
const item = h('li', { class: 'tabs__item' }, button);
item.hidden = known ? !known.has(tab.id) : tab.id !== 'main';
if (unseen.has(tab.id)) item.classList.add('tab--new');
list.append(item);
buttons.set(tab.id, { item, button });
}
}
/** Call every frame: reveals newly unlocked tabs, flags the ones needing attention. */
function update(state) {
const unlocked = new Set(tabs.filter((tab) => isUnlocked(state, tab.unlock)).map((tab) => tab.id));
for (const tab of tabs) {
const entry = buttons.get(tab.id);
const isNew = known && unlocked.has(tab.id) && !known.has(tab.id);
entry.item.hidden = !unlocked.has(tab.id);
if (isNew) {
unseen.add(tab.id);
entry.item.classList.add('tab--new');
showToast(t('toast.tabUnlocked', { name: t(labelKey(tab.id)) }));
}
}
known = unlocked;
// An opportunity is waiting on the contracts tab.
buttons.get('contracts')?.item.classList.toggle('tab--alert', Boolean(state.events.current));
if (!unlocked.has(active)) select('main');
}
return { rebuild, update, select, get active() { return active; } };
}