126 lines
3.9 KiB
JavaScript
Raw Normal View History

2025-11-11 01:05:13 +01:00
export default class Router {
constructor(options = {}) {
this.routes = new Map();
this.currentRoute = null;
this.basePath = options.basePath || '/';
this.transitionEnabled = true;
this.logger = options.logger || null;
this.appState = options.appState || null;
this._setupListeners();
}
register(path, handler, metadata = {}) {
this.routes.set(path, { handler, metadata });
return this;
}
async navigate(path, state = {}, skipTransition = false) {
try {
const route = this._matchRoute(path);
if (!route) {
this.logger?.warn(`No route found for: ${path}`);
return;
}
if (this.currentRoute?.path === path) return;
this.appState?.setState({ isLoading: true });
const performTransition = () => {
if (this.transitionEnabled && !skipTransition && document.startViewTransition) {
document.startViewTransition(() => {
this._executeRoute(path, route, state);
}).finished.then(() => {
this.appState?.setState({ isLoading: false });
}).catch(err => {
this.logger?.error('View transition failed', err);
this.appState?.setState({ isLoading: false });
});
} else {
this._executeRoute(path, route, state);
this.appState?.setState({ isLoading: false });
}
};
window.history.pushState(state, '', `${this.basePath}${path}`);
performTransition();
} catch (error) {
this.logger?.error(`Navigation error: ${error.message}`);
this.appState?.setState({ isLoading: false });
}
}
_executeRoute(path, route, state) {
const root = document.getElementById('app-root');
if (!root) return;
root.innerHTML = '';
this.currentRoute = { path, ...route };
try {
const result = route.handler(state);
if (result instanceof HTMLElement) {
root.appendChild(result);
} else if (typeof result === 'string') {
root.innerHTML = result;
}
this.appState?.setState({ currentPage: path });
window.dispatchEvent(new CustomEvent('route-changed', { detail: { path, state } }));
} catch (error) {
this.logger?.error(`Route handler failed for ${path}`, error);
root.innerHTML = '<h1>Error loading page</h1>';
}
}
_matchRoute(path) {
if (this.routes.has(path)) {
return this.routes.get(path);
}
for (const [routePath, route] of this.routes.entries()) {
const regex = this._pathToRegex(routePath);
if (regex.test(path)) {
return route;
}
}
return null;
}
_pathToRegex(path) {
const pattern = path
.replace(/\//g, '\\/')
.replace(/:(\w+)/g, '(?<$1>[^/]+)')
.replace(/\*/g, '.*');
return new RegExp(`^${pattern}$`);
}
_setupListeners() {
window.addEventListener('popstate', (event) => {
const path = window.location.pathname.replace(this.basePath, '') || '/';
this.navigate(path, event.state, true);
});
document.addEventListener('click', (event) => {
const link = event.target.closest('a[data-nav]');
if (link) {
event.preventDefault();
const path = link.getAttribute('href') || link.getAttribute('data-nav');
this.navigate(path);
}
});
}
back() {
window.history.back();
}
forward() {
window.history.forward();
}
}