137 lines
4.3 KiB
JavaScript
137 lines
4.3 KiB
JavaScript
/**
|
|
* router.js — Graph-based routing for el-ui.
|
|
*
|
|
* Routes are nodes in the graph. Navigation activates the target route node,
|
|
* which spreads through the graph to any components subscribed to routing.
|
|
* This means route changes propagate with the same activation semantics as
|
|
* state changes — route-dependent components update automatically.
|
|
*
|
|
* Route nodes have:
|
|
* type: 'route'
|
|
* name: the path string
|
|
* content: the path string
|
|
* importance: starts at 0.5, boosted on activation (recently visited routes
|
|
* become more salient — models browser history behavior)
|
|
*/
|
|
|
|
export class Router {
|
|
/**
|
|
* @param {import('./graph.js').Graph} graph
|
|
* @param {Record<string, any>} routes path → Component class
|
|
*/
|
|
constructor(graph, routes) {
|
|
this.graph = graph;
|
|
this.routes = routes;
|
|
this.currentPath = window.location.pathname;
|
|
this._routeNodes = {};
|
|
this._subscribers = [];
|
|
|
|
// Seed each route as a node in the graph
|
|
for (const [path] of Object.entries(routes)) {
|
|
const id = graph.seed({ type: 'route', name: path, content: path, importance: 0.3 });
|
|
this._routeNodes[path] = id;
|
|
}
|
|
|
|
// Connect route nodes in path order (parent → child activation chain)
|
|
// e.g., / → /about → /about/team
|
|
const paths = Object.keys(routes).sort();
|
|
for (let i = 0; i < paths.length - 1; i++) {
|
|
const parent = paths[i];
|
|
const child = paths[i + 1];
|
|
if (child.startsWith(parent) && parent !== child) {
|
|
graph.connect(this._routeNodes[parent], this._routeNodes[child], {
|
|
weight: 0.8,
|
|
relation: 'subroute',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Listen for browser back/forward
|
|
window.addEventListener('popstate', () => {
|
|
this._activate(window.location.pathname);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Navigate to a path, update history, and activate the route graph node.
|
|
* @param {string} path
|
|
* @param {boolean} [replace=false] use replaceState instead of pushState
|
|
*/
|
|
navigate(path, replace = false) {
|
|
if (path === this.currentPath) return;
|
|
|
|
if (replace) {
|
|
history.replaceState({}, '', path);
|
|
} else {
|
|
history.pushState({}, '', path);
|
|
}
|
|
|
|
this._activate(path);
|
|
}
|
|
|
|
/**
|
|
* Get the Component class for the current path.
|
|
* Falls back to the '*' wildcard route if present.
|
|
* @returns {any}
|
|
*/
|
|
currentComponent() {
|
|
return this.routes[this.currentPath] ?? this.routes['*'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Subscribe to route changes.
|
|
* @param {function(string): void} callback called with the new path
|
|
* @returns {function} unsubscribe
|
|
*/
|
|
subscribe(callback) {
|
|
this._subscribers.push(callback);
|
|
return () => {
|
|
const idx = this._subscribers.indexOf(callback);
|
|
if (idx >= 0) this._subscribers.splice(idx, 1);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate an href-compatible link string.
|
|
* @param {string} path
|
|
* @returns {string}
|
|
*/
|
|
href(path) {
|
|
return path;
|
|
}
|
|
|
|
/** @private */
|
|
_activate(path) {
|
|
const prevPath = this.currentPath;
|
|
this.currentPath = path;
|
|
|
|
// Find the best matching route
|
|
const matchedPath = this._matchPath(path);
|
|
if (matchedPath && this._routeNodes[matchedPath]) {
|
|
// Spreading activation from the new route node
|
|
this.graph.activate(this._routeNodes[matchedPath]);
|
|
// Boost importance of the newly visited route
|
|
const node = this.graph.get(this._routeNodes[matchedPath]);
|
|
if (node) node.importance = Math.min(1.0, node.importance + 0.2);
|
|
}
|
|
|
|
// Notify route subscribers
|
|
if (path !== prevPath) {
|
|
this._subscribers.forEach(cb => cb(path));
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
_matchPath(path) {
|
|
// Exact match first
|
|
if (this.routes[path]) return path;
|
|
// Prefix match (longest prefix wins)
|
|
const candidates = Object.keys(this.routes)
|
|
.filter(r => r !== '*' && path.startsWith(r))
|
|
.sort((a, b) => b.length - a.length);
|
|
if (candidates.length > 0) return candidates[0];
|
|
// Wildcard
|
|
return '*';
|
|
}
|
|
}
|