diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbacd7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +bun.lockb \ No newline at end of file diff --git a/README.md b/README.md index affde95..8a49e84 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,108 @@ # @elexis/router -Single page app router extension for [ElexisJS](https://github.com/defaultkavy/elexis) +一个基于 ElexisJS 的布局网页路由工具。 -## Installation -``` -npm i @elexis/router -``` - -## Usage -```ts -import 'elexis'; -import '@elexis/router'; -import { Router, Route } from '@elexis/router' - -// create new Router with base path '/', -const router = new Router('/'); - -// append router view element -const $app = $('app').content([ - router.$view -]) - -const home_page_route = new Route('/', () => { - // which this callback function return will be appended - // into router view element when path match - return $('h1').content('This is a Homepage!') -}) - -// Add home_page_route into router -router.addRoute(home_page_route); -// Router starting listen location path change -router.listen(); -``` -> Without `Router.listen()`, Router will do nothing when page loaded. - -## Global $ Methods +## 初步认识 $Router 以及 $Route +这个工具基于两个基本的概念模块来构建:解析模块以及蓝图模块。我们先来看看如何利用此工具实现一个简单的网页路径布局: ```ts import 'elexis'; import '@elexis/router'; -$.open('/about') // open /about page without load page -$.back() // back -$.replace('/hello') // replace current page history state with url -``` +$(document.body).content([ + // Router base on '/' path + $('router').base('/').map([ + // Root page + $('route').path('/').builder(() => [ + $('h1').content('Hello, World!'), + $('a').content('Home').href('/home') + ]), -## Events - -### Router event: load -```ts -const gallery_route = new Route('/gallery', ({loaded}) => { - - async function $ImageList() { - // fetch images and return elements - - // after all image loaded, using loaded function. - // this will trigger load event on anccestor router - loaded(); - } - return $('div').content([ - $ImageList() + // Home page + $('route').path('/home').builder(() => [ + $('h1').content('Hello, Home!'), + ]) ]) -}) +]) +``` +### 关于 $Router +这是一个用以解析路径并将正确内容显示在页面中的工具,由多个 `$Router` 组件构建的网页可以实现复杂的路径导航。 -router.on('load', () => {...}) +在上面的例子中,我们直接在 `document.body` 组件中置入一个 `$Router` 组件,并将它的基本路径设置为 `/`。这意味着我们将所有开头为 `/` 的路径都交给这个 `$Router` 进行解析。 + +最后,我们使用 `map()` 函数来规划不同子路径所指向的页面蓝图,也就是 `$Route` 组件。 + +### 关于 $Route +在传入 `$Router.map()` 方法的参数当中,`$Route` 组件并不会被使用在真实的 DOM 当中。它更像是一个蓝图的概念,你可以为这个组件添加多个属性,并使用 `builder()` 函数来规划这个页面会出现的内容。 + +在 Router 解析地址后,它会创建一个指向该地址的 `$Route` 组件,并将蓝图上的属性复制到新组件上,以及构建传入 `builder()` 函数的页面内容。 + +## 如何实现单页应用路由(Single Page App Routing) +只要使用了 `$Router` 进行路径规划,你的网页就已经具备了单页应用路由的功能。使用 `$.open('PATH')` 就能在不跳转页面的情况下打开目标页面了。 +```ts +$('button').content('Open Home Page').on('click', () => $.open('/home')) +``` +在浏览器的预设中,`` 组件的链接是会以跳转页面的形式打开链接的。你可以单独对每一个 `` 设置触发事件来避免跳转,或者你可以直接使用一行代码将所有 `` 组件的链接都预设为路由器控制: +```ts +// 将这一行代码写在程序的入口文件中 +$.anchorHandler = ($a) => $.open($a.href()); ``` -### RouteRecord event: open +## 路径参数 +在规划路径时,你可以在路径中的某一段设置变量,并且可以直接在构建内容时获取该变量对应在路径中的值: ```ts -const viewer_route = new Route('/viewer', ({record}) => { - const page_open_count$ = $.state(0); +$(document.body).content([ + $('router').base('/').map([ + // Root page + $('route').path('/').builder(() => [ + $('h1').content('Hello, World!'), + $('a').content('Elexis').href('/Elexis/greating') + ]), - // this event will be fire everytime this page opened - record.on('open', () => { - page_open_count$.set( page_open_count$.value + 1 ); - }) - - return $('div').content(page_open_count$) -}) + // Greating page + $('route').path('/:name/greating').builder(({params}) => [ + $('h1').content(`Hello, ${params.name}!`), + ]) + ]) +]) ``` -### Router event: notfound +## 路由嵌套 +多个路由器组成的路由嵌套可实现更复杂且精确的网页内容展示,这些路由组件可根据网址变化并在遵循路径规划的规则下改变显示内容。以下代码能够展示路由嵌套所带来的可拓展性: ```ts -// Route will remove all child of view when path is not exist. -// Using preventDefault function to prevent this action. -router.on('notfound', ({preventDefault}) => { - preventDefault(); // prevent remove all child of view - ... // do something -}) +$(document.body).content([ + $('router').base('/').map([ + $('route').path('/').builder(() => [ + $('h1').content('Welcome!'), + // navigation + $('ul').content([ + $('li').content($('a').content('Intro').href('/')), + $('li').content($('a').content('About Me').href('/about')), + $('li').content($('a').content('Contact').href('/contact')) + ]), + // nested router + $('router').base('/').map([ + $('route').path('/').builder(() => $('h2').content('Intro')), + $('route').path('/about').builder(() => $('h2').content('About')), + $('route').path('/contact').builder(() => [ + $('h2').content('Contact'), + // navigation + $('ul').content([ + $('li').content($('a').content('Email').href('#email')), + $('li').content($('a').content('Phone').href('#phone')), + ]), + // nested router + $('router').base('/contact').map([ + $('route').path(['/', '#email']).builder(() => $('p').content('elexis@example.com')), + $('route').path('#phone').builder(() => $('p').content('012-456789')), + ]) + ]) + ]) + ]), + ]) +]) ``` -### `static` Router event: pathchange +## 多路径指向单一页面 +将类型 `string[]` 导入 `path()` 函数中,能够实现多个路径指向同一个页面的结果。 ```ts -// This event fired on location change happened -Router.on('pathchange', () => { - ... // do something -}) +$('route').path(['/', '/intro']).builder(() => $('h1').content('Intro')); ``` \ No newline at end of file diff --git a/index.ts b/index.ts index 6dceff1..a113bb4 100644 --- a/index.ts +++ b/index.ts @@ -1,22 +1,25 @@ import 'elexis'; -import { Router } from './lib/Router'; +import { $Router } from './lib/$Router'; +import { $Route } from './lib/$Route'; declare module 'elexis' { export namespace $ { - export const routers: Set; - export function open(path: string | URL | undefined): typeof Router; - export function replace(path: string | URL | undefined): typeof Router; - export function back(): typeof Router; + export interface TagNameElementMap { + 'router': typeof $Router; + 'route': typeof $Route; + } + export function open(path: string | URL | undefined): typeof $Router; + export function replace(path: string | URL | undefined): typeof $Router; + export function back(): typeof $Router; } } +$.registerTagName('router', $Router); +$.registerTagName('route', $Route); Object.assign($, { - routers: Router.routers, - open(path: string | URL | undefined) { return Router.open(path) }, - replace(path: string | URL | undefined) { return Router.replace(path) }, - back() { return Router.back() } + open(path: string | URL | undefined) { return $Router.open(path) }, + replace(path: string | URL | undefined) { return $Router.replace(path) }, + back() { return $Router.back() } }) -addEventListener('popstate', Router.popstate); // Start listening - -export * from './lib/Route'; -export * from './lib/Router'; \ No newline at end of file +export * from './lib/$Route'; +export * from './lib/$Router'; diff --git a/lib/$Route.ts b/lib/$Route.ts new file mode 100644 index 0000000..99c91a3 --- /dev/null +++ b/lib/$Route.ts @@ -0,0 +1,49 @@ +import { $Container, $ContainerContentType, $ContainerOptions, $EventManager } from "elexis"; + +export interface $RouteOptions extends $ContainerOptions {} +export class $Route> extends $Container { + #path: $RoutePathType = ''; + #builder?: (record: $RouteRecord) => OrMatrix<$ContainerContentType>; + events = new $EventManager<$RouteEventMap>().register('opened', 'closed') + readonly rendered: boolean = false; + constructor(options?: $RouteOptions) { + super('route', options); + } + + path(): $RoutePathType; + path

(pathname: P): $Route

: ''>; + path(pathname?: $RoutePathType): $RoutePathType | $Route { return $.fluent(this, arguments, () => this.#path, () => this.#path = pathname ?? this.#path ) } + + builder(builder: (record: $RouteRecord) => OrMatrix<$ContainerContentType>) { + this.#builder = builder; + return this; + } + + render(options: {params: Path}) { + if (this.#builder) this.content(this.#builder({ + $route: this, + params: options.params + })); + (this as Mutable<$Route>).rendered = true; + return this; + } + + build(options: {params: Path}) { + return new $Route({dom: this.dom.cloneNode() as HTMLElement}).self(($route) => { + if (this.#builder) $route.builder(this.#builder as any).render({params: options.params as any}) + }) + } +} + +interface $RouteRecord { + $route: $Route; + params: Path +} +interface $RouteEventMap { + opened: []; + closed: [] +} +export type $RoutePathType = string | string[]; +type PathParams = Path extends `${infer Segment}/${infer Rest}` + ? Segment extends `${string}:${infer Param}` ? Record & PathParams : PathParams + : Path extends `${string}:${infer Param}` ? Record : {} \ No newline at end of file diff --git a/lib/$Router.ts b/lib/$Router.ts new file mode 100644 index 0000000..1bb6656 --- /dev/null +++ b/lib/$Router.ts @@ -0,0 +1,181 @@ +import { $EventManager } from "elexis"; +import { $Route, $RoutePathType } from "./$Route"; +import { $View, $ViewOptions } from "@elexis/view"; + +export interface $RouterOptions extends $ViewOptions {} +export class $Router extends $View { + #base: string = ''; + routes = new Map<$RoutePathType, $Route>(); + static routers = new Set<$Router>(); + static events = new $EventManager<$RouterEventMap>().register('stateChange') + static navigationDirection: $RouterNavigationDirection + static historyIndex = 0; + static url = new URL(location.href); + private static scrollHistoryKey = `$ROUTER_SCROLL_HISTORY`; + constructor(options?: $RouterOptions) { + super({tagname: 'router', ...options}); + $Router.routers.add(this); + } + + base(): string; + base(pathname: string): this; + base(pathname?: string) { return $.fluent(this, arguments, () => this.#base, () => { this.#base = pathname ?? this.#base }) } + + map(routes: OrMatrix<$Route>) { + routes = $.orArrayResolve(routes); + for (const route of routes) { + if (route instanceof Array) this.map(route); + else { + this.routes.set(route.path(), route); + } + } + this.resolve(); + return this; + } + + protected resolve(): Promise<$RouterResolveResult> { + return new Promise(resolve => { + if (!location.pathname.startsWith(this.#base)) return resolve($RouterResolveResult.NotMatchBase); + const locationPath = location.pathname.replace(this.#base, '/').replace('//', '/') // /a/b + const locationParts = locationPath.split('/').map(path => `/${path}`); + const find = () => { + const matchedRoutes: {deep: number, $route: $Route, params: {[key: string]: string}, pathId: string}[] = [] + for (const [routePathResolve, $route] of this.routes) { + const routePathList = $.orArrayResolve(routePathResolve); + for (const routePath of routePathList) { + let deep = 0, params = {}; + if (routePath.startsWith('#')) { + // multiple route path will set the first path as pathId + if (routePath === location.hash) return {deep, $route, params, pathId: routePathList[0]}; + else continue; + } + const routeParts = routePath.split('/').map(path => `/${path}`) + if (locationParts.length < routeParts.length) continue; + for (let i = 0; i < routeParts.length; i ++) { + if (routeParts[i].startsWith('/:')) { deep++; Object.assign(params, {[routeParts[i].replace('/:', '')]: locationParts[i].replace('/', '')}); continue; } + else if (routeParts[i] === locationParts[i]) { deep++; continue; } + else { break; } + } + // route path with params will set the locationPath as pathId + matchedRoutes.push({deep, $route, params, pathId: Object.keys(params).length === 0 ? routePathList[0] : locationPath}) + } + } + return matchedRoutes.sort((a, b) => b.deep - a.deep).at(0); + } + + const $routeData = find(); + if (!$routeData) return resolve($RouterResolveResult.NotFound); + const {$route, params, pathId} = $routeData; + if (pathId === this.contentId) return resolve($RouterResolveResult.OK); // current route + this.events.once('rendered', ({nextContent, previousContent}) => { + if (previousContent instanceof $Route) previousContent.events.fire('closed'); + if (nextContent instanceof $Route) nextContent.events.fire('opened'); + resolve($RouterResolveResult.OK); + }); + if (!this.viewCache.get(pathId)) { + const $buildedRoute = $route.build({params}); + this.setView(pathId, $buildedRoute); + } + this.switchView(pathId); + }) + } + + static init() { + if (!history.state || 'index' in history.state === false) { + const state: $RouterState = { index: $Router.historyIndex } + history.replaceState(state, '') + } else { + $Router.historyIndex = history.state.index + } + $Router.navigationDirection = $RouterNavigationDirection.Forward; + $Router.resolve(); + window.addEventListener('popstate', () => $Router.popstate()); + window.addEventListener('scroll', () => { this.setScrollHistory(this.historyIndex, location.href, document.documentElement.scrollTop) }) + history.scrollRestoration = 'manual'; + return this; + } + + static open(url: string | URL | undefined) { + if (url === undefined) return this; + url = new URL(url); + if (url.href === this.url.href) return this; + this.historyIndex++; + history.pushState($Router.historyState, '', url); + this.stateChange($RouterNavigationDirection.Forward); + $Router.resolve(); + return this; + } + + static back() { + this.historyIndex--; + history.back() + this.stateChange($RouterNavigationDirection.Back); + return this; + } + + static replace(url: string | URL | undefined) { + if (url === undefined) return this; + if (typeof url === 'string' && !url.startsWith(location.origin)) url = location.origin + url; + history.replaceState($Router.historyState, '', url); + this.stateChange($RouterNavigationDirection.Replace); + $Router.resolve(); + return this; + } + + protected static popstate() { + const direction: $RouterNavigationDirection + = history.state.index > $Router.historyIndex + ? $RouterNavigationDirection.Forward + : history.state.index < $Router.historyIndex + ? $RouterNavigationDirection.Back + : $RouterNavigationDirection.Replace + + $Router.historyIndex = history.state.index; + $Router.stateChange(direction); + $Router.resolve(); + } + + protected static async resolve() { + await Promise.all([...$Router.routers.values()].map($router => $router.resolve())); + this.scrollRestoration(); + } + + protected static get historyState() { return { index: $Router.historyIndex, } } + + protected static stateChange(direction: $RouterNavigationDirection) { + const beforeURL = this.url; + const afterURL = new URL(location.href); + this.url = afterURL; + $Router.events.fire('stateChange', {beforeURL, afterURL, direction}) + $Router.navigationDirection = direction; + } + + protected static setScrollHistory(index: number, url: string, value: number) { + const record = this.getScrollHistory() + if (!record) return sessionStorage.setItem(this.scrollHistoryKey, JSON.stringify({[index]: {url, value}})); + record[index] = {url, value}; + sessionStorage.setItem(this.scrollHistoryKey, JSON.stringify(record)); + } + + protected static getScrollHistory() { + const data = sessionStorage.getItem(this.scrollHistoryKey); + if (!data) return undefined; + else return JSON.parse(data) as $RouterScrollHistoryData; + } + + protected static scrollRestoration() { + const record = this.getScrollHistory(); + if (!record) return; + document.documentElement.scrollTop = record[this.historyIndex]?.value ?? 0; + } +} + +enum $RouterResolveResult { OK, NotFound, NotMatchBase } +export enum $RouterNavigationDirection { Forward, Back, Replace } +interface $RouterState { index: number } +export interface $RouterEventMap { + 'stateChange': [{beforeURL: URL, afterURL: URL, direction: $RouterNavigationDirection}] +} +interface $RouterScrollHistoryData {[index: number]: {url: string, value: number}} + +$Router.init(); \ No newline at end of file diff --git a/lib/Route.ts b/lib/Route.ts deleted file mode 100644 index 7bf2099..0000000 --- a/lib/Route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { $EventMethod, $Node, $EventManager } from "elexis"; -import { $Util } from "elexis/lib/$Util"; - -export class Route { - path: string | PathResolverFn; - builder: (req: RouteRequest) => RouteContent; - constructor(path: Path, builder: ((req: RouteRequest) => RouteContent) | RouteContent) { - this.path = path; - this.builder = builder instanceof Function ? builder : (req: RouteRequest) => builder; - } -} - -type PathParams = Path extends `${infer Segment}/${infer Rest}` - ? Segment extends `${string}:${infer Param}` ? Record & PathParams : PathParams - : Path extends `${string}:${infer Param}` ? Record : {} - -export type PathResolverFn = (path: string) => undefined | string; - -type PathParamResolver

= P extends PathResolverFn - ? undefined : PathParams

- -// type PathResolverRecord

= { -// [key in keyof ReturnType

]: ReturnType

[key] -// } - - -export interface RouteRecord extends $EventMethod {}; -export class RouteRecord { - id: string; - readonly content?: $Node; - events = new $EventManager().register('open', 'load') - constructor(id: string) { - this.id = id; - } -} -$Util.mixin(RouteRecord, $EventMethod) -export interface RouteRecordEventMap { - 'open': [{path: string, record: RouteRecord}]; - 'load': [{path: string, record: RouteRecord}]; -} - -export interface RouteRequest { - params: PathParamResolver, - record: RouteRecord, - loaded: () => void; -} - -export type RouteContent = $Node | string | void; \ No newline at end of file diff --git a/lib/Router.ts b/lib/Router.ts deleted file mode 100644 index 3e9d9cd..0000000 --- a/lib/Router.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { $EventMethod, $View, $EventManager, $Text } from "elexis"; -import { $Util } from "elexis/lib/$Util"; -import { PathResolverFn, Route, RouteRecord } from "./Route"; -export interface Router extends $EventMethod {}; -export class Router { - static routers = new Set(); - static index: number = 0; - static events = new $EventManager().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>(); - recordMap = new Map(); - $view: $View; - events = new $EventManager().register('notfound', 'load'); - basePath: string; - constructor(basePath: string, view?: $View) { - this.basePath = basePath; - this.$view = view ?? new $View(); - } - - /**Add route to Router. @example Router.addRoute(new Route('/', 'Hello World')) */ - addRoute(routes: OrArray>) { - routes = $.orArrayResolve(routes); - for (const route of routes) this.routeMap.set(route.path, route); - return this; - } - - /**Start listen to the path change */ - listen() { - if (!history.state || 'index' in history.state === false) { - const routeData: RouteData = {index: Router.index, data: {}} - history.replaceState(routeData, '') - } else { - Router.index = history.state.index - } - Router.routers.add(this); - Router.navigationDirection = NavigationDirection.Forward; - this.resolvePath(); - Router.events.fire('pathchange', {prevURL: undefined, nextURL: Router.currentPath, navigation: NavigationDirection.Forward}); - return this; - } - - /**Open URL */ - static open(url: string | URL | undefined) { - if (url === undefined) return this; - url = new URL(url); - if (url.origin !== location.origin) return this; - if (url.href === location.href) return this; - const prevPath = Router.currentPath; - this.index += 1; - const routeData: RouteData = { index: this.index, data: {} }; - history.pushState(routeData, '', url); - Router.navigationDirection = NavigationDirection.Forward; - Router.currentPath = new URL(location.href); - Router.routers.forEach(router => router.resolvePath()) - Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: NavigationDirection.Forward}); - return this; - } - - /**Back to previous page */ - static back() { - const prevPath = Router.currentPath; - history.back(); - Router.currentPath = new URL(location.href); - Router.navigationDirection = NavigationDirection.Back; - Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: NavigationDirection.Back}); - return this - } - - static replace(url: string | URL | undefined) { - if (url === undefined) return this; - if (typeof url === 'string' && !url.startsWith(location.origin)) url = location.origin + url; - url = new URL(url); - if (url.origin !== location.origin) return this; - if (url.href === location.href) return this; - const prevPath = Router.currentPath; - history.replaceState({index: Router.index}, '', url) - Router.currentPath = new URL(location.href); - Router.navigationDirection = NavigationDirection.Forward; - Router.routers.forEach(router => router.resolvePath(url.pathname)); - Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: NavigationDirection.Forward}); - return this; - } - - setStateData(key: string, value: any) { - if (history.state.data === undefined) history.state.data = {}; - history.state.data[key] = value; - return this; - } - - static popstate = (() => { - let dir: NavigationDirection = NavigationDirection.Forward; - // Forward - if (history.state.index > Router.index) dir = NavigationDirection.Forward; - // Back - else if (history.state.index < Router.index) dir = NavigationDirection.Back; - const prevPath = Router.currentPath; - Router.index = history.state.index; - Router.navigationDirection = dir; - Router.routers.forEach(router => router.resolvePath()); - Router.currentPath = new URL(location.href); - Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: dir}); - }) - - private resolvePath(path = location.pathname) { - if (!path.startsWith(this.basePath)) return; - path = path.replace(this.basePath, '/').replace('//', '/'); - let found = false; - const openCachedView = (pathId: string) => { - const record = this.recordMap.get(pathId); - if (record) { - found = true; - 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}); - return true; - } - return false; - } - const createView = (pathId: string, route: Route, data: any) => { - const record = new RouteRecord(pathId); - let content = route.builder({ - params: data, - record: record, - loaded: () => { - record.events.fire('load', {path: pathId, record}); - this.events.fire('load', {path: pathId}); - } - }); - if (typeof content === 'string') return new $Text(content); - if (content === undefined) return; - (record as Mutable).content = content; - 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); - // - record.events.fire('open', {path, record}); - found = true; - } - for (const [pathResolver, route] of this.routeMap.entries()) { - // PathResolverFn - if (pathResolver instanceof Function) { - const routeId = pathResolver(path) - if (routeId) { if (!openCachedView(routeId)) createView(routeId, route, undefined) } - continue; - } - // string - const [_routeParts, _pathParts] = [pathResolver.split('/').map(p => `/${p}`), path.split('/').map(p => `/${p}`)]; - _routeParts.shift(); _pathParts.shift(); - const data = {}; - let pathString = ''; - for (let i = 0; i < _pathParts.length; i++) { - const [routePart, pathPart] = [_routeParts.at(i), _pathParts.at(i)]; - if (!routePart || !pathPart) continue; - if (routePart === pathPart) { - pathString += pathPart; - if (routePart === _routeParts.at(-1)) { - if (!openCachedView(pathString)) createView(pathString, route, data); - return; - } - } - else if (routePart.includes(':')) { - const [prefix, param] = routePart.split(':'); - if (!pathPart.startsWith(prefix)) continue; - Object.assign(data, {[param]: pathPart.replace(prefix, '')}) - pathString += pathPart; - if (routePart === _routeParts.at(-1)) { - if (!openCachedView(pathString)) createView(pathString, route, data); - return; - } - } - } - } - - if (!found) { - let preventDefaultState = false; - const preventDefault = () => preventDefaultState = true; - this.events.fire('notfound', {path, preventDefault}); - if (!preventDefaultState) this.$view.clear(); - } - } - - static recoveryScrollPosition() { - const history = this.getScrollHistory(this.index, location.href); - if (!history) { - document.documentElement.scrollTop = 0; - this.setScrollHistory(this.index, location.href, 0); - } - else document.documentElement.scrollTop = history.scroll; - } - - static getScrollHistory(pageIndex: number, href: string) { - const data = this.scrollHistoryData; - if (!data || !data[pageIndex]) return null; - return data[pageIndex].href === href ? data[pageIndex] : null; - } - - static setScrollHistory(pageIndex: number, href: string, scroll: number) { - if (Router.preventSetScrollHistory) return; - let history = this.scrollHistoryData; - if (!history) { - history = {[pageIndex]: {href, scroll}}; - sessionStorage.setItem(this.SCROLL_HISTORY_KEY, JSON.stringify(history)); - } - else { - const data = history[pageIndex]; - if (data && data.href !== href) { - let i = 0; - while (history[pageIndex + i]) { - delete history[pageIndex + i]; - i++; - } - } - history[pageIndex] = {href, scroll} - sessionStorage.setItem(this.SCROLL_HISTORY_KEY, JSON.stringify(history)); - } - return history[pageIndex]; - } - - static get scrollHistoryData() { - const json = sessionStorage.getItem(this.SCROLL_HISTORY_KEY) - if (!json) return null; - return JSON.parse(json) as {[key: number]: RouteScrollHistoryData}; - } - - static on(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.on(type, callback); return this } - static off(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.off(type, callback); return this } - static once(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.once(type, callback); return this } -} - -$Util.mixin(Router, $EventMethod); - -interface RouterEventMap { - notfound: [{path: string, preventDefault: () => any}]; - load: [{path: string}]; -} - -interface RouterGlobalEventMap { - pathchange: [{prevURL?: URL, nextURL: URL, navigation: NavigationDirection}]; -} - -type RouteData = { - index: number; - data: {[key: string]: any}; -} - -type RouteScrollHistoryData = { - href: string; - scroll: number; -} - -export enum NavigationDirection { - Forward, - Back -} - -window.addEventListener('scroll', (e) => { - console.debug(document.documentElement.scrollTop) - Router.setScrollHistory(Router.index, location.href, document.documentElement.scrollTop); -}) -history.scrollRestoration = 'manual'; \ No newline at end of file diff --git a/package.json b/package.json index 030a083..ab8c7ab 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "license": "ISC", "type": "module", "dependencies": { - "elexis": "^0.1.0" + "@elexis/view": "../view", + "elexis": "../../elexis" } } \ No newline at end of file