Files
Learn_System/frontend/js/admin/router.js
T
Maxim Dolgolyov 8a7bed487f feat(admin): phase 1 — hash-router
AdminRouter wraps existing switchTab for deep-linking.

- frontend/js/admin/router.js (new, 102L): parse/navigate/current/on/off, recursion guard via _navigating flag

- admin.html: +1 <script> before admin.js

- admin.js: switchTab(btn, opts) + initAdminRouter IIFE for hashchange dispatch

Backward compat: all 21 onclick=switchTab(this) callsites continue working.

F5 / back / forward / deep-link verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:22:20 +03:00

102 lines
3.1 KiB
JavaScript

'use strict';
/* AdminRouter — hash-based router for admin panel.
* Wraps the existing switchTab() flow without replacing it.
*
* Hash format: #<route>[/<param1>[/<param2>...]]
* #stats → { route: 'stats', params: [] }
* #users → { route: 'users', params: [] }
* #users/123 → { route: 'users', params: ['123'] }
* #sessions/456/foo → { route: 'sessions', params: ['456','foo'] }
*
* Public API: window.AdminRouter
* parse(hash) → route object
* current() → route object for location.hash
* navigate(hash, { replace, silent })
* on(event, fn) / off(event, fn) — 'change' event only
*
* Recursion guard: programmatic navigate() sets a flag so the hashchange
* listener does not re-emit 'change' for our own writes.
*/
(function () {
'use strict';
const listeners = { change: new Set() };
let _navigating = false;
function parse(hash) {
const raw = String(hash || '');
const stripped = raw.charAt(0) === '#' ? raw.slice(1) : raw;
const parts = stripped.split('/').filter(Boolean);
const route = parts.length ? decodeURIComponent(parts[0]) : '';
const params = parts.slice(1).map(p => {
try { return decodeURIComponent(p); } catch { return p; }
});
return { route, params, raw: raw || '' };
}
function current() {
return parse(location.hash);
}
function normalizeHash(input) {
const s = String(input || '');
if (!s) return '';
return s.charAt(0) === '#' ? s : '#' + s;
}
function emit(event, payload) {
const set = listeners[event];
if (!set) return;
set.forEach(fn => {
try { fn(payload); } catch (e) { console.error('AdminRouter listener error', e); }
});
}
function navigate(routeOrHash, opts) {
const options = opts || {};
const target = normalizeHash(routeOrHash);
const currentHash = location.hash || '';
// Same hash → no-op (avoids spurious listener fires).
if (target === currentHash) {
if (!options.silent) emit('change', { ...parse(target), silent: false });
return;
}
_navigating = true;
try {
if (options.replace && typeof history !== 'undefined' && history.replaceState) {
// Preserve current path/query, swap hash without adding history entry.
const url = location.pathname + location.search + target;
history.replaceState(history.state, '', url);
} else {
location.hash = target;
}
} finally {
// hashchange fires async; clear flag after dispatch.
setTimeout(() => { _navigating = false; }, 0);
}
if (!options.silent) {
emit('change', { ...parse(target), silent: false });
}
}
function on(event, fn) {
if (!listeners[event] || typeof fn !== 'function') return;
listeners[event].add(fn);
}
function off(event, fn) {
if (!listeners[event]) return;
listeners[event].delete(fn);
}
window.addEventListener('hashchange', () => {
if (_navigating) return;
emit('change', { ...current(), silent: false });
});
window.AdminRouter = { parse, current, navigate, on, off };
})();