mapdash/frontend/src/viewmode.ts

104 lines
3.2 KiB
TypeScript

// 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 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');
}
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();
return {
getMode: () => mode,
setMode,
};
}