mirror of
https://github.com/n30nex/CartoLite-Server.git
synced 2026-10-20 17:21:57 +00:00
71 lines
5.4 KiB
TypeScript
71 lines
5.4 KiB
TypeScript
import { graphTemplate } from './cameraControls';
|
|
import type { NetgraphRenderer } from './renderer';
|
|
|
|
export function mountRegionControls(graph: NetgraphRenderer, host: HTMLElement, changed: () => void, closePanels: () => void) {
|
|
const stage = document.getElementById('netgraph-stage')!;
|
|
const navigate = () => stage.dispatchEvent(new Event('graphnavigate'));
|
|
const menu = graphTemplate<HTMLDetailsElement>('graph-regions-ui');
|
|
const summary = menu.querySelector('summary')!, search = menu.querySelector('input')!, list = menu.querySelector<HTMLElement>('.region-checks')!;
|
|
const routes = document.createElement('button'); routes.type = 'button'; routes.id = 'graph-routes-button'; routes.textContent = 'Routes';
|
|
routes.setAttribute('aria-label', 'Show historical routes'); routes.title = 'Hide standing lines; live packets still light their routes briefly';
|
|
host.append(menu, routes);
|
|
const layout = graphTemplate<HTMLDetailsElement>('graph-layout-ui');
|
|
document.getElementById('display-panel')!.append(layout);
|
|
const spacing = layout.querySelector('input')!, output = layout.querySelector('output')!, move = layout.querySelector('select')!;
|
|
let lastAreas: ReturnType<NetgraphRenderer['regions']> | undefined, lastRegions: string[] | null | undefined, signature = '';
|
|
const applyFilter = (values: string[] | null) => { navigate(); graph.setRegions(values); sync(); changed(); };
|
|
menu.querySelector('[data-all]')!.addEventListener('click', () => applyFilter(null));
|
|
menu.querySelector('[data-none]')!.addEventListener('click', () => applyFilter([]));
|
|
list.addEventListener('change', event => {
|
|
const input = event.target as HTMLInputElement, code = input.dataset.region!;
|
|
const selected = new Set(graph.regionLayout().regions ?? graph.regions().map(a => a.code));
|
|
if (input.checked) selected.add(code); else selected.delete(code);
|
|
applyFilter([...selected]);
|
|
});
|
|
const filterList = () => { const query = search.value.trim().toLocaleLowerCase();
|
|
for (const label of list.children) (label as HTMLElement).hidden = !label.textContent!.toLocaleLowerCase().includes(query); };
|
|
search.addEventListener('input', filterList);
|
|
menu.addEventListener('toggle', () => { menu.querySelector<HTMLElement>('.region-options')!.hidden = !menu.open; if (menu.open) closePanels(); });
|
|
menu.addEventListener('keydown', event => { if (event.key === 'Escape' && menu.open) { event.preventDefault(); event.stopPropagation(); menu.open = false; summary.focus(); } });
|
|
const dismiss = (event: PointerEvent) => { if (!menu.contains(event.target as Node)) menu.open = false; };
|
|
document.addEventListener('pointerdown', dismiss);
|
|
routes.addEventListener('click', () => { graph.setRoutesVisible(!graph.regionLayout().routes); sync(); });
|
|
spacing.addEventListener('input', () => { navigate(); graph.setRegionSpacing(Number(spacing.value)); sync(); });
|
|
layout.querySelector('[data-reset]')!.addEventListener('click', () => { navigate(); graph.resetRegionLayout(); sync(); });
|
|
for (const button of layout.querySelectorAll<HTMLButtonElement>('[data-move]')) button.addEventListener('click', () => {
|
|
const [x, y] = button.dataset.move!.split(',').map(Number); navigate(); graph.moveRegion(move.value, x!, y!);
|
|
});
|
|
function sync(): void {
|
|
const prefs = graph.regionLayout(), areas = graph.regions();
|
|
spacing.value = String(prefs.spacing); output.value = `${Math.round(prefs.spacing * 100)}%`;
|
|
routes.setAttribute('aria-pressed', String(prefs.routes)); routes.classList.toggle('selected', prefs.routes);
|
|
summary.textContent = `Regions · ${prefs.regions === null ? 'All' : prefs.regions.length}`;
|
|
layout.querySelector('small')!.textContent = stage.dataset.layoutSaved === 'false'
|
|
? 'Drag region labels to arrange. Storage unavailable; changes last for this visit.'
|
|
: 'Drag a region label to move its whole bubble. Saved on this device.';
|
|
if (areas !== lastAreas || prefs.regions !== lastRegions) {
|
|
lastAreas = areas; lastRegions = prefs.regions;
|
|
const entries = new Map(areas.map(a => [a.code, a.name]));
|
|
for (const code of prefs.regions ?? []) if (!entries.has(code)) entries.set(code, 'No current activity');
|
|
const next = JSON.stringify([...entries]);
|
|
if (next !== signature) {
|
|
signature = next;
|
|
const focused = (document.activeElement as HTMLElement | null)?.dataset.region, scroll = list.scrollTop, moving = move.value;
|
|
list.replaceChildren(); move.replaceChildren();
|
|
for (const [code, name] of [...entries].sort(([a], [b]) => a.localeCompare(b))) {
|
|
const label = document.createElement('label'), input = document.createElement('input'), text = document.createElement('span');
|
|
input.type = 'checkbox'; input.dataset.region = code; text.textContent = `${code} · ${name}`;
|
|
label.append(input, text); list.append(label);
|
|
if (areas.some(a => a.code === code)) move.add(new Option(text.textContent!, code));
|
|
if (focused === code) input.focus({ preventScroll: true });
|
|
}
|
|
if ([...move.options].some(o => o.value === moving)) move.value = moving;
|
|
list.scrollTop = scroll; filterList();
|
|
}
|
|
for (const input of list.querySelectorAll<HTMLInputElement>('input')) input.checked = prefs.regions === null || prefs.regions.includes(input.dataset.region!);
|
|
}
|
|
}
|
|
sync();
|
|
return { sync, destroy() { document.removeEventListener('pointerdown', dismiss); menu.remove(); routes.remove(); layout.remove(); } };
|
|
}
|