save
This commit is contained in:
parent
1111fc5e66
commit
ea36cef397
4
index.ts
4
index.ts
@ -10,11 +10,13 @@ declare module 'elexis' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object.assign($, {
|
Object.assign($, {
|
||||||
routers: new Set<Router>,
|
routers: Router.routers,
|
||||||
open(path: string | URL | undefined) { return Router.open(path) },
|
open(path: string | URL | undefined) { return Router.open(path) },
|
||||||
replace(path: string | URL | undefined) { return Router.replace(path) },
|
replace(path: string | URL | undefined) { return Router.replace(path) },
|
||||||
back() { return Router.back() }
|
back() { return Router.back() }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addEventListener('popstate', Router.popstate); // Start listening
|
||||||
|
|
||||||
export * from './lib/Route';
|
export * from './lib/Route';
|
||||||
export * from './lib/Router';
|
export * from './lib/Router';
|
@ -17,7 +17,7 @@ type PathParams<Path> = Path extends `${infer Segment}/${infer Rest}`
|
|||||||
export type PathResolverFn = (path: string) => undefined | string;
|
export type PathResolverFn = (path: string) => undefined | string;
|
||||||
|
|
||||||
type PathParamResolver<P extends PathResolverFn | string> = P extends PathResolverFn
|
type PathParamResolver<P extends PathResolverFn | string> = P extends PathResolverFn
|
||||||
? undefined : PathParams<P>
|
? undefined : PathParams<P>
|
||||||
|
|
||||||
// type PathResolverRecord<P extends PathResolverFn> = {
|
// type PathResolverRecord<P extends PathResolverFn> = {
|
||||||
// [key in keyof ReturnType<P>]: ReturnType<P>[key]
|
// [key in keyof ReturnType<P>]: ReturnType<P>[key]
|
||||||
|
@ -3,15 +3,18 @@ import { $Util } from "elexis/lib/$Util";
|
|||||||
import { PathResolverFn, Route, RouteRecord } from "./Route";
|
import { PathResolverFn, Route, RouteRecord } from "./Route";
|
||||||
export interface Router extends $EventMethod<RouterEventMap> {};
|
export interface Router extends $EventMethod<RouterEventMap> {};
|
||||||
export class Router {
|
export class Router {
|
||||||
|
static routers = new Set<Router>();
|
||||||
|
static index: number = 0;
|
||||||
|
static events = new $EventManager<RouterGlobalEventMap>().register('pathchange', 'notfound', 'load');
|
||||||
|
static currentPath: URL = new URL(location.href);
|
||||||
|
static navigationDirection: NavigationDirection;
|
||||||
|
static readonly SCROLL_HISTORY_KEY = '$router_scroll_history';
|
||||||
|
static preventSetScrollHistory = false;
|
||||||
routeMap = new Map<string | PathResolverFn, Route<any>>();
|
routeMap = new Map<string | PathResolverFn, Route<any>>();
|
||||||
recordMap = new Map<string, RouteRecord>();
|
recordMap = new Map<string, RouteRecord>();
|
||||||
$view: $View;
|
$view: $View;
|
||||||
static index: number = 0;
|
|
||||||
static events = new $EventManager<RouterGlobalEventMap>().register('pathchange', 'notfound', 'load');
|
|
||||||
events = new $EventManager<RouterEventMap>().register('notfound', 'load');
|
events = new $EventManager<RouterEventMap>().register('notfound', 'load');
|
||||||
basePath: string;
|
basePath: string;
|
||||||
static currentPath: URL = new URL(location.href);
|
|
||||||
static readonly SCROLL_HISTORY_KEY = '$router_scroll_history';
|
|
||||||
constructor(basePath: string, view?: $View) {
|
constructor(basePath: string, view?: $View) {
|
||||||
this.basePath = basePath;
|
this.basePath = basePath;
|
||||||
this.$view = view ?? new $View();
|
this.$view = view ?? new $View();
|
||||||
@ -32,10 +35,10 @@ export class Router {
|
|||||||
} else {
|
} else {
|
||||||
Router.index = history.state.index
|
Router.index = history.state.index
|
||||||
}
|
}
|
||||||
addEventListener('popstate', this.popstate)
|
Router.routers.add(this);
|
||||||
$.routers.add(this);
|
Router.navigationDirection = NavigationDirection.Forward;
|
||||||
this.resolvePath();
|
this.resolvePath();
|
||||||
Router.events.fire('pathchange', {prevURL: undefined, nextURL: Router.currentPath, navigation: 'Forward'});
|
Router.events.fire('pathchange', {prevURL: undefined, nextURL: Router.currentPath, navigation: NavigationDirection.Forward});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,10 +52,10 @@ export class Router {
|
|||||||
this.index += 1;
|
this.index += 1;
|
||||||
const routeData: RouteData = { index: this.index, data: {} };
|
const routeData: RouteData = { index: this.index, data: {} };
|
||||||
history.pushState(routeData, '', url);
|
history.pushState(routeData, '', url);
|
||||||
|
Router.navigationDirection = NavigationDirection.Forward;
|
||||||
Router.currentPath = new URL(location.href);
|
Router.currentPath = new URL(location.href);
|
||||||
$.routers.forEach(router => router.resolvePath())
|
Router.routers.forEach(router => router.resolvePath())
|
||||||
Router.recoveryScrollPosition();
|
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: NavigationDirection.Forward});
|
||||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'});
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +64,8 @@ export class Router {
|
|||||||
const prevPath = Router.currentPath;
|
const prevPath = Router.currentPath;
|
||||||
history.back();
|
history.back();
|
||||||
Router.currentPath = new URL(location.href);
|
Router.currentPath = new URL(location.href);
|
||||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Back'});
|
Router.navigationDirection = NavigationDirection.Back;
|
||||||
|
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: NavigationDirection.Back});
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,8 +78,9 @@ export class Router {
|
|||||||
const prevPath = Router.currentPath;
|
const prevPath = Router.currentPath;
|
||||||
history.replaceState({index: Router.index}, '', url)
|
history.replaceState({index: Router.index}, '', url)
|
||||||
Router.currentPath = new URL(location.href);
|
Router.currentPath = new URL(location.href);
|
||||||
$.routers.forEach(router => router.resolvePath(url.pathname));
|
Router.navigationDirection = NavigationDirection.Forward;
|
||||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'});
|
Router.routers.forEach(router => router.resolvePath(url.pathname));
|
||||||
|
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: NavigationDirection.Forward});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,34 +90,42 @@ export class Router {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private popstate = (() => {
|
static popstate = (() => {
|
||||||
|
let dir: NavigationDirection = NavigationDirection.Forward;
|
||||||
// Forward
|
// Forward
|
||||||
if (history.state.index > Router.index) { }
|
if (history.state.index > Router.index) dir = NavigationDirection.Forward;
|
||||||
// Back
|
// Back
|
||||||
else if (history.state.index < Router.index) { }
|
else if (history.state.index < Router.index) dir = NavigationDirection.Back;
|
||||||
const prevPath = Router.currentPath;
|
const prevPath = Router.currentPath;
|
||||||
Router.index = history.state.index;
|
Router.index = history.state.index;
|
||||||
this.resolvePath();
|
Router.navigationDirection = dir;
|
||||||
Router.recoveryScrollPosition();
|
Router.routers.forEach(router => router.resolvePath());
|
||||||
Router.currentPath = new URL(location.href);
|
Router.currentPath = new URL(location.href);
|
||||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'});
|
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: dir});
|
||||||
}).bind(this)
|
})
|
||||||
|
|
||||||
private resolvePath(path = location.pathname) {
|
private resolvePath(path = location.pathname) {
|
||||||
if (!path.startsWith(this.basePath)) return;
|
if (!path.startsWith(this.basePath)) return;
|
||||||
path = path.replace(this.basePath, '/').replace('//', '/');
|
path = path.replace(this.basePath, '/').replace('//', '/');
|
||||||
let found = false;
|
let found = false;
|
||||||
const openCached = (pathId: string) => {
|
const openCachedView = (pathId: string) => {
|
||||||
const record = this.recordMap.get(pathId);
|
const record = this.recordMap.get(pathId);
|
||||||
if (record) {
|
if (record) {
|
||||||
found = true;
|
found = true;
|
||||||
if (record.content && !this.$view.contains(record.content)) this.$view.switchView(pathId);
|
if (record.content && !this.$view.contains(record.content)) {
|
||||||
|
Router.preventSetScrollHistory = true;
|
||||||
|
this.$view.events.once('afterSwitch', () => {
|
||||||
|
Router.preventSetScrollHistory = false;
|
||||||
|
Router.recoveryScrollPosition()
|
||||||
|
});
|
||||||
|
this.$view.switchView(pathId);
|
||||||
|
}
|
||||||
record.events.fire('open', {path, record});
|
record.events.fire('open', {path, record});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const create = (pathId: string, route: Route<any>, data: any) => {
|
const createView = (pathId: string, route: Route<any>, data: any) => {
|
||||||
const record = new RouteRecord(pathId);
|
const record = new RouteRecord(pathId);
|
||||||
let content = route.builder({
|
let content = route.builder({
|
||||||
params: data,
|
params: data,
|
||||||
@ -122,11 +135,18 @@ export class Router {
|
|||||||
this.events.fire('load', {path: pathId});
|
this.events.fire('load', {path: pathId});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (typeof content === 'string') content = new $Text(content);
|
if (typeof content === 'string') return new $Text(content);
|
||||||
if (content === undefined) return;
|
if (content === undefined) return;
|
||||||
(record as Mutable<RouteRecord>).content = content;
|
(record as Mutable<RouteRecord>).content = content;
|
||||||
this.recordMap.set(pathId, record);
|
this.recordMap.set(pathId, record);
|
||||||
|
// switch view
|
||||||
|
Router.preventSetScrollHistory = true;
|
||||||
|
this.$view.events.once('afterSwitch', () => {
|
||||||
|
Router.preventSetScrollHistory = false;
|
||||||
|
Router.recoveryScrollPosition()
|
||||||
|
});
|
||||||
this.$view.setView(pathId, content).switchView(pathId);
|
this.$view.setView(pathId, content).switchView(pathId);
|
||||||
|
//
|
||||||
record.events.fire('open', {path, record});
|
record.events.fire('open', {path, record});
|
||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
@ -134,7 +154,7 @@ export class Router {
|
|||||||
// PathResolverFn
|
// PathResolverFn
|
||||||
if (pathResolver instanceof Function) {
|
if (pathResolver instanceof Function) {
|
||||||
const routeId = pathResolver(path)
|
const routeId = pathResolver(path)
|
||||||
if (routeId) { if (!openCached(routeId)) create(routeId, route, undefined) }
|
if (routeId) { if (!openCachedView(routeId)) createView(routeId, route, undefined) }
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// string
|
// string
|
||||||
@ -148,7 +168,7 @@ export class Router {
|
|||||||
if (routePart === pathPart) {
|
if (routePart === pathPart) {
|
||||||
pathString += pathPart;
|
pathString += pathPart;
|
||||||
if (routePart === _routeParts.at(-1)) {
|
if (routePart === _routeParts.at(-1)) {
|
||||||
if (!openCached(pathString)) create(pathString, route, data);
|
if (!openCachedView(pathString)) createView(pathString, route, data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,7 +178,7 @@ export class Router {
|
|||||||
Object.assign(data, {[param]: pathPart.replace(prefix, '')})
|
Object.assign(data, {[param]: pathPart.replace(prefix, '')})
|
||||||
pathString += pathPart;
|
pathString += pathPart;
|
||||||
if (routePart === _routeParts.at(-1)) {
|
if (routePart === _routeParts.at(-1)) {
|
||||||
if (!openCached(pathString)) create(pathString, route, data);
|
if (!openCachedView(pathString)) createView(pathString, route, data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,6 +209,7 @@ export class Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static setScrollHistory(pageIndex: number, href: string, scroll: number) {
|
static setScrollHistory(pageIndex: number, href: string, scroll: number) {
|
||||||
|
if (Router.preventSetScrollHistory) return;
|
||||||
let history = this.scrollHistoryData;
|
let history = this.scrollHistoryData;
|
||||||
if (!history) {
|
if (!history) {
|
||||||
history = {[pageIndex]: {href, scroll}};
|
history = {[pageIndex]: {href, scroll}};
|
||||||
@ -219,14 +240,16 @@ export class Router {
|
|||||||
static off<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.off(type, callback); return this }
|
static off<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.off(type, callback); return this }
|
||||||
static once<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.once(type, callback); return this }
|
static once<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.once(type, callback); return this }
|
||||||
}
|
}
|
||||||
|
|
||||||
$Util.mixin(Router, $EventMethod);
|
$Util.mixin(Router, $EventMethod);
|
||||||
|
|
||||||
interface RouterEventMap {
|
interface RouterEventMap {
|
||||||
notfound: [{path: string, preventDefault: () => any}];
|
notfound: [{path: string, preventDefault: () => any}];
|
||||||
load: [{path: string}];
|
load: [{path: string}];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RouterGlobalEventMap {
|
interface RouterGlobalEventMap {
|
||||||
pathchange: [{prevURL?: URL, nextURL: URL, navigation: 'Back' | 'Forward'}];
|
pathchange: [{prevURL?: URL, nextURL: URL, navigation: NavigationDirection}];
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteData = {
|
type RouteData = {
|
||||||
@ -239,6 +262,13 @@ type RouteScrollHistoryData = {
|
|||||||
scroll: number;
|
scroll: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
export enum NavigationDirection {
|
||||||
|
Forward,
|
||||||
|
Back
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', (e) => {
|
||||||
|
console.debug(document.documentElement.scrollTop)
|
||||||
Router.setScrollHistory(Router.index, location.href, document.documentElement.scrollTop);
|
Router.setScrollHistory(Router.index, location.href, document.documentElement.scrollTop);
|
||||||
})
|
})
|
||||||
|
history.scrollRestoration = 'manual';
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@elexis/router",
|
"name": "@elexis/router",
|
||||||
"description": "A simple router for ElexisJS",
|
"description": "A simple router for ElexisJS",
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "defaultkavy",
|
"name": "defaultkavy",
|
||||||
"email": "defaultkavy@gmail.com",
|
"email": "defaultkavy@gmail.com",
|
||||||
|
Loading…
Reference in New Issue
Block a user