diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
| commit | 9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch) | |
| tree | ce5959571a981b9c4047da3c7b3fd080aa44222c /packages/frontend/src/nirax.ts | |
| parent | wip: retention for dashboard (diff) | |
| download | sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2 sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip | |
rename: client -> frontend
Diffstat (limited to 'packages/frontend/src/nirax.ts')
| -rw-r--r-- | packages/frontend/src/nirax.ts | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts new file mode 100644 index 0000000000..53e73a8d48 --- /dev/null +++ b/packages/frontend/src/nirax.ts @@ -0,0 +1,275 @@ +// NIRAX --- A lightweight router + +import { EventEmitter } from 'eventemitter3'; +import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue'; +import { pleaseLogin } from '@/scripts/please-login'; +import { safeURIDecode } from '@/scripts/safe-uri-decode'; + +type RouteDef = { + path: string; + component: Component; + query?: Record<string, string>; + loginRequired?: boolean; + name?: string; + hash?: string; + globalCacheKey?: string; + children?: RouteDef[]; +}; + +type ParsedPath = (string | { + name: string; + startsWith?: string; + wildcard?: boolean; + optional?: boolean; +})[]; + +export type Resolved = { route: RouteDef; props: Map<string, string>; child?: Resolved; }; + +function parsePath(path: string): ParsedPath { + const res = [] as ParsedPath; + + path = path.substring(1); + + for (const part of path.split('/')) { + if (part.includes(':')) { + const prefix = part.substring(0, part.indexOf(':')); + const placeholder = part.substring(part.indexOf(':') + 1); + const wildcard = placeholder.includes('(*)'); + const optional = placeholder.endsWith('?'); + res.push({ + name: placeholder.replace('(*)', '').replace('?', ''), + startsWith: prefix !== '' ? prefix : undefined, + wildcard, + optional, + }); + } else if (part.length !== 0) { + res.push(part); + } + } + + return res; +} + +export class Router extends EventEmitter<{ + change: (ctx: { + beforePath: string; + path: string; + resolved: Resolved; + key: string; + }) => void; + replace: (ctx: { + path: string; + key: string; + }) => void; + push: (ctx: { + beforePath: string; + path: string; + route: RouteDef | null; + props: Map<string, string> | null; + key: string; + }) => void; + same: () => void; +}> { + private routes: RouteDef[]; + public current: Resolved; + public currentRef: ShallowRef<Resolved> = shallowRef(); + public currentRoute: ShallowRef<RouteDef> = shallowRef(); + private currentPath: string; + private currentKey = Date.now().toString(); + + public navHook: ((path: string, flag?: any) => boolean) | null = null; + + constructor(routes: Router['routes'], currentPath: Router['currentPath']) { + super(); + + this.routes = routes; + this.currentPath = currentPath; + this.navigate(currentPath, null, false); + } + + public resolve(path: string): Resolved | null { + let queryString: string | null = null; + let hash: string | null = null; + if (path[0] === '/') path = path.substring(1); + if (path.includes('#')) { + hash = path.substring(path.indexOf('#') + 1); + path = path.substring(0, path.indexOf('#')); + } + if (path.includes('?')) { + queryString = path.substring(path.indexOf('?') + 1); + path = path.substring(0, path.indexOf('?')); + } + + if (_DEV_) console.log('Routing: ', path, queryString); + + function check(routes: RouteDef[], _parts: string[]): Resolved | null { + forEachRouteLoop: + for (const route of routes) { + let parts = [..._parts]; + const props = new Map<string, string>(); + + pathMatchLoop: + for (const p of parsePath(route.path)) { + if (typeof p === 'string') { + if (p === parts[0]) { + parts.shift(); + } else { + continue forEachRouteLoop; + } + } else { + if (parts[0] == null && !p.optional) { + continue forEachRouteLoop; + } + if (p.wildcard) { + if (parts.length !== 0) { + props.set(p.name, safeURIDecode(parts.join('/'))); + parts = []; + } + break pathMatchLoop; + } else { + if (p.startsWith) { + if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop; + + props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length))); + parts.shift(); + } else { + if (parts[0]) { + props.set(p.name, safeURIDecode(parts[0])); + } + parts.shift(); + } + } + } + } + + if (parts.length === 0) { + if (route.children) { + const child = check(route.children, []); + if (child) { + return { + route, + props, + child, + }; + } else { + continue forEachRouteLoop; + } + } + + if (route.hash != null && hash != null) { + props.set(route.hash, safeURIDecode(hash)); + } + + if (route.query != null && queryString != null) { + const queryObject = [...new URLSearchParams(queryString).entries()] + .reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); + + for (const q in route.query) { + const as = route.query[q]; + if (queryObject[q]) { + props.set(as, safeURIDecode(queryObject[q])); + } + } + } + + return { + route, + props, + }; + } else { + if (route.children) { + const child = check(route.children, parts); + if (child) { + return { + route, + props, + child, + }; + } else { + continue forEachRouteLoop; + } + } else { + continue forEachRouteLoop; + } + } + } + + return null; + } + + const _parts = path.split('/').filter(part => part.length !== 0); + + return check(this.routes, _parts); + } + + private navigate(path: string, key: string | null | undefined, emitChange = true) { + const beforePath = this.currentPath; + this.currentPath = path; + + const res = this.resolve(this.currentPath); + + if (res == null) { + throw new Error('no route found for: ' + path); + } + + if (res.route.loginRequired) { + pleaseLogin('/'); + } + + const isSamePath = beforePath === path; + if (isSamePath && key == null) key = this.currentKey; + this.current = res; + this.currentRef.value = res; + this.currentRoute.value = res.route; + this.currentKey = res.route.globalCacheKey ?? key ?? path; + + if (emitChange) { + this.emit('change', { + beforePath, + path, + resolved: res, + key: this.currentKey, + }); + } + + return res; + } + + public getCurrentPath() { + return this.currentPath; + } + + public getCurrentKey() { + return this.currentKey; + } + + public push(path: string, flag?: any) { + const beforePath = this.currentPath; + if (path === beforePath) { + this.emit('same'); + return; + } + if (this.navHook) { + const cancel = this.navHook(path, flag); + if (cancel) return; + } + const res = this.navigate(path, null); + this.emit('push', { + beforePath, + path, + route: res.route, + props: res.props, + key: this.currentKey, + }); + } + + public replace(path: string, key?: string | null, emitEvent = true) { + this.navigate(path, key); + if (emitEvent) { + this.emit('replace', { + path, + key: this.currentKey, + }); + } + } +} |