103 lines
2.8 KiB
JavaScript
103 lines
2.8 KiB
JavaScript
// Hash-based client-side router
|
|
// Usage: navigate('#/templates') or window.location.hash = '#/templates'
|
|
|
|
import { escHtml } from './utils.js';
|
|
|
|
const _routes = {};
|
|
let _current = null;
|
|
|
|
// Register a route: router.register('#/templates', loadFn)
|
|
export function register(hash, loadFn) {
|
|
_routes[hash] = loadFn;
|
|
}
|
|
|
|
// Navigate to a hash route
|
|
export function navigate(hash) {
|
|
window.location.hash = hash;
|
|
}
|
|
|
|
// Navigate and pass data to the view (stored temporarily)
|
|
let _routeData = null;
|
|
export function navigateWith(hash, data) {
|
|
_routeData = data;
|
|
navigate(hash);
|
|
}
|
|
|
|
export function getRouteData() {
|
|
const d = _routeData;
|
|
_routeData = null;
|
|
return d;
|
|
}
|
|
|
|
// Parse route: '#/templates/abc123' → { base: '#/templates', param: 'abc123' }
|
|
function parseHash(hash) {
|
|
const clean = hash || '#/templates';
|
|
const parts = clean.split('/');
|
|
if (parts.length >= 3) {
|
|
return { base: parts.slice(0, 2).join('/'), param: parts.slice(2).join('/') };
|
|
}
|
|
return { base: clean, param: null };
|
|
}
|
|
|
|
// Route to the current hash
|
|
async function route() {
|
|
const { base, param } = parseHash(window.location.hash);
|
|
const key = param ? base : (window.location.hash || '#/templates');
|
|
const baseKey = base;
|
|
|
|
const loader = _routes[key] || _routes[baseKey] || _routes['#/templates'];
|
|
if (!loader) return;
|
|
|
|
_current = key;
|
|
updateActiveNav(baseKey);
|
|
|
|
const outlet = document.getElementById('router-outlet');
|
|
if (outlet) outlet.classList.remove('view-enter');
|
|
|
|
try {
|
|
await loader(param);
|
|
} catch (err) {
|
|
console.error('Router error:', err);
|
|
const outlet = document.getElementById('router-outlet');
|
|
if (outlet) outlet.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">⚠️</div>
|
|
<div class="empty-state-title">Failed to load view</div>
|
|
<div class="empty-state-sub">${escHtml(err.message)}</div>
|
|
</div>`;
|
|
}
|
|
|
|
if (outlet) {
|
|
outlet.classList.add('view-enter');
|
|
// Remove class after animation to allow re-trigger
|
|
setTimeout(() => outlet.classList.remove('view-enter'), 200);
|
|
}
|
|
}
|
|
|
|
// Highlight active nav item
|
|
function updateActiveNav(hash) {
|
|
document.querySelectorAll('.nav-item').forEach(el => {
|
|
el.classList.toggle('active', el.dataset.route === hash);
|
|
});
|
|
|
|
// Update breadcrumb
|
|
const label = document.querySelector(`.nav-item[data-route="${hash}"] .nav-label`);
|
|
const breadcrumbCurrent = document.getElementById('breadcrumb-current');
|
|
if (breadcrumbCurrent && label) {
|
|
breadcrumbCurrent.textContent = label.textContent.trim();
|
|
}
|
|
}
|
|
|
|
// Init: listen for hash changes and route on load
|
|
export function init() {
|
|
window.addEventListener('hashchange', route);
|
|
// Route immediately
|
|
if (!window.location.hash || window.location.hash === '#') {
|
|
window.location.hash = '#/templates';
|
|
} else {
|
|
route();
|
|
}
|
|
}
|
|
|
|
export function current() { return _current; }
|