// Header segmented control: "График" / "Карта" / "Карта и график" plus the // draggable vertical splitter used in the combined mode. State (mode + split // ratio) persists in localStorage. export type ViewMode = 'timeline' | 'map' | 'split'; const LS_MODE = 'mapdash.viewmode'; const LS_SPLIT = 'mapdash.splitLeft'; const MIN_PCT = 20; const MAX_PCT = 80; export interface ViewModeElements { view: HTMLElement; splitResizer: HTMLElement; btnTimeline: HTMLElement; btnMap: HTMLElement; btnSplit: HTMLElement; } export interface ViewModeController { getMode(): ViewMode; setMode(mode: ViewMode): void; } export interface ViewModeCallbacks { // Called after the mode changes (layout already applied). mapVisible/timelineVisible // tell the caller what to (re)initialise or resize. onChange(mode: ViewMode): void; // Called continuously while the splitter is dragged (rAF-throttled). onResize(): void; } export function createViewModes(els: ViewModeElements, cb: ViewModeCallbacks): ViewModeController { let mode: ViewMode = 'timeline'; const savedSplit = parseFloat(localStorage.getItem(LS_SPLIT) || ''); if (savedSplit >= MIN_PCT && savedSplit <= MAX_PCT) { els.view.style.setProperty('--split-left', savedSplit + '%'); } function positionIndicator(): void { const active = mode === 'timeline' ? els.btnTimeline : mode === 'map' ? els.btnMap : els.btnSplit; const ind = els.btnTimeline.parentElement?.querySelector('.mode-ind'); if (ind && active.offsetHeight) { ind.style.top = active.offsetTop + 'px'; ind.style.height = active.offsetHeight + 'px'; } } function apply(): void { els.view.classList.remove('mode-timeline', 'mode-map', 'mode-split'); els.view.classList.add('mode-' + mode); els.btnTimeline.classList.toggle('active', mode === 'timeline'); els.btnMap.classList.toggle('active', mode === 'map'); els.btnSplit.classList.toggle('active', mode === 'split'); positionIndicator(); } window.addEventListener('resize', positionIndicator); function setMode(next: ViewMode): void { mode = next; localStorage.setItem(LS_MODE, mode); apply(); cb.onChange(mode); } els.btnTimeline.addEventListener('click', () => setMode('timeline')); els.btnMap.addEventListener('click', () => setMode('map')); els.btnSplit.addEventListener('click', () => setMode('split')); // ---- splitter drag ---- let dragging = false; let rafPending = false; let lastPct = savedSplit >= MIN_PCT ? savedSplit : 50; function applyDrag(): void { rafPending = false; els.view.style.setProperty('--split-left', lastPct + '%'); cb.onResize(); } els.splitResizer.addEventListener('mousedown', (e) => { if (mode !== 'split') return; dragging = true; document.body.style.userSelect = 'none'; document.body.style.cursor = 'col-resize'; e.preventDefault(); }); window.addEventListener('mousemove', (e) => { if (!dragging) return; const rect = els.view.getBoundingClientRect(); let pct = ((e.clientX - rect.left) / rect.width) * 100; pct = Math.max(MIN_PCT, Math.min(MAX_PCT, pct)); lastPct = pct; if (rafPending) return; rafPending = true; requestAnimationFrame(applyDrag); }); window.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; document.body.style.userSelect = ''; document.body.style.cursor = ''; localStorage.setItem(LS_SPLIT, String(lastPct)); cb.onResize(); }); // initial mode from storage const saved = localStorage.getItem(LS_MODE) as ViewMode | null; mode = saved === 'map' || saved === 'split' ? saved : 'timeline'; apply(); requestAnimationFrame(positionIndicator); // header is laid out by the next frame return { getMode: () => mode, setMode, }; }