summaryrefslogtreecommitdiff
path: root/packages/frontend/src/nirax.ts
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-12-27 14:36:33 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-12-27 14:36:33 +0900
commit9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch)
treece5959571a981b9c4047da3c7b3fd080aa44222c /packages/frontend/src/nirax.ts
parentwip: retention for dashboard (diff)
downloadsharkey-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.ts275
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,
+ });
+ }
+ }
+}