@wroud/navigation
Version:
A flexible, pattern-matching navigation system for JavaScript applications with built-in routing, browser integration, and navigation state management
172 lines • 4.99 kB
JavaScript
import { NavigationType, } from "./NavigationListener.js";
import { Router } from "./Router.js";
import { LinkedList } from "./sdk/LinkedList.js";
export class Navigation {
get state() {
return this.innerState.history.get(this.innerState.position);
}
get history() {
return this.innerState.history.toArray();
}
get position() {
return this.innerState.position;
}
innerState;
listeners;
router;
constructor(router) {
this.router = router || new Router();
this.listeners = new Set();
this.innerState = {
position: -1,
history: new LinkedList(),
};
this.addListener = this.addListener.bind(this);
this.getState = this.getState.bind(this);
}
/**
* Get the current navigation state
*/
getState() {
return this.state;
}
/**
* Set the navigation state and position
*/
setState(position, state) {
this.innerState = {
position,
history: new LinkedList(state || []),
};
}
/**
* Replace the current state with a new one
*/
async replace(state) {
if (state) {
const route = this.router.getRoute(state.id);
if (!route) {
throw new Error(`Route ${state.id} not found`);
}
}
if (!(await this.canDeactivate(state)) ||
!(await this.canActivate(state))) {
return;
}
const previousState = this.state;
if (state) {
this.innerState.history.set(this.innerState.position, state);
}
else {
this.innerState.history.setHead(null);
}
this.notifyListeners(NavigationType.Replace, previousState, state);
}
/**
* Navigate to a new state
*/
async navigate(state) {
if (state) {
const route = this.router.getRoute(state.id);
if (!route) {
throw new Error(`Route ${state.id} not found`);
}
}
if (!(await this.canDeactivate(state)) ||
!(await this.canActivate(state))) {
return;
}
const previousState = this.state;
if (this.innerState.position < this.innerState.history.size - 1) {
this.innerState.history.removeFrom(this.innerState.position + 1);
}
if (state) {
this.innerState.history.push(state);
}
else {
this.innerState.history.setHead(null);
}
this.innerState.position++;
this.notifyListeners(NavigationType.Navigate, previousState, state);
}
/**
* Navigate back to the previous state
*/
async goBack() {
if (this.position === 0) {
return;
}
const state = this.innerState.history.get(this.innerState.position - 1) || null;
if (!(await this.canDeactivate(state)) ||
!(await this.canActivate(state))) {
return;
}
const previousState = this.state;
this.innerState.position--;
this.notifyListeners(NavigationType.Back, previousState, state);
}
/**
* Add a navigation listener
*/
addListener(listener) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
/**
* Remove a navigation listener
*/
removeListener(listener) {
this.listeners.delete(listener);
}
/**
* Notify all listeners of a navigation event
*/
notifyListeners(type, from, to) {
for (const listener of this.listeners) {
listener(type, from, to);
}
}
/**
* Check if we can deactivate from the current route
*/
async canDeactivate(state) {
const currentState = this.state;
if (!currentState) {
return true;
}
const deactivationTree = this.router
.getRouteTree(currentState.id)
.reverse();
for (const route of deactivationTree) {
if (route.canDeactivate) {
const result = await route.canDeactivate(state, currentState);
if (result === false) {
return false;
}
}
}
return true;
}
/**
* Check if we can activate the target route
*/
async canActivate(state) {
if (!state) {
return true;
}
const currentState = this.state;
const activationTree = this.router.getRouteTree(state.id);
for (const route of activationTree) {
if (route.canActivate) {
const result = await route.canActivate(state, currentState || null);
if (result === false) {
return false;
}
}
}
return true;
}
}
//# sourceMappingURL=Navigation.js.map