@dharmax/state-router 3.1.2 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ReadMe.md +169 -38
- package/dist/router.d.ts +18 -3
- package/dist/router.js +183 -38
- package/dist/state-manager.d.ts +12 -4
- package/dist/state-manager.js +44 -10
- package/dist-cjs/index.cjs +18 -0
- package/dist-cjs/router.cjs +265 -0
- package/dist-cjs/router.d.ts +41 -0
- package/dist-cjs/state-manager.cjs +150 -0
- package/dist-cjs/state-manager.d.ts +56 -0
- package/package.json +29 -7
- package/src/router.ts +0 -141
- package/src/state-manager.ts +0 -141
- package/test/index.html +0 -21
- package/tsconfig.json +0 -23
- /package/{src/index.ts → dist-cjs/index.d.ts} +0 -0
package/src/router.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
type RouteHandler = (...args: string[]) => void;
|
|
2
|
-
|
|
3
|
-
interface Route {
|
|
4
|
-
pattern: RegExp | null;
|
|
5
|
-
handler: RouteHandler;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type RoutingMode = 'history' | 'hash';
|
|
9
|
-
|
|
10
|
-
class Router {
|
|
11
|
-
private mode: RoutingMode = 'hash';
|
|
12
|
-
private routes: Route[] = [];
|
|
13
|
-
private root: string = '/';
|
|
14
|
-
private baseLocation: string | null = null;
|
|
15
|
-
public staticFilters: ((url: string) => boolean)[] = []
|
|
16
|
-
|
|
17
|
-
constructor() {
|
|
18
|
-
this.staticFilters.push(url => {
|
|
19
|
-
const staticFileExtensions = ['.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp', '.md', '.ejs', '.jsm', '.txt'];
|
|
20
|
-
return staticFileExtensions.some(ext => url.endsWith(ext));
|
|
21
|
-
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private cleanPathString(path: string): string {
|
|
26
|
-
path = path.replace(/\/$/, '').replace(/^\//, '');
|
|
27
|
-
return path = path.replace(/#{2,}/g, '#');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
private clearQuery(url: string): string {
|
|
31
|
-
const [path, query] = url.split('?');
|
|
32
|
-
if (!query) return path;
|
|
33
|
-
const [_, hash] = query.split('#');
|
|
34
|
-
return hash ? `${path}#${hash}` : path;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private isStaticFile(url: string): boolean {
|
|
38
|
-
return (this.staticFilters || []).some(filter => filter(url))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public resetRoot(root: string): void {
|
|
42
|
-
this.root = '/' + this.cleanPathString(root) + '/';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
public getLocation(): string {
|
|
46
|
-
if (this.mode === 'history') {
|
|
47
|
-
let fragment = this.cleanPathString(decodeURI(window.location.pathname + window.location.search));
|
|
48
|
-
fragment = this.clearQuery(fragment);
|
|
49
|
-
return this.root !== '/' ? fragment.replace(this.root, '') : fragment;
|
|
50
|
-
} else {
|
|
51
|
-
const match = window.location.href.match(/#(.*)$/);
|
|
52
|
-
return match ? this.clearQuery(match[1]) : '';
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
public add(pattern: RegExp | RouteHandler, handler?: RouteHandler): Router {
|
|
57
|
-
if (typeof pattern === 'function') {
|
|
58
|
-
handler = pattern;
|
|
59
|
-
pattern = /^.*$/; // Match any path
|
|
60
|
-
}
|
|
61
|
-
this.routes.push({pattern, handler: handler as RouteHandler});
|
|
62
|
-
return this;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
* @param location
|
|
68
|
-
* @return true if it was intercepted or false if not handled
|
|
69
|
-
*/
|
|
70
|
-
public handleChange(location?: string): boolean {
|
|
71
|
-
const path = location || this.getLocation();
|
|
72
|
-
if (this.isStaticFile(path))
|
|
73
|
-
return false; // Bypass routing for static files
|
|
74
|
-
|
|
75
|
-
for (const route of this.routes) {
|
|
76
|
-
const match = path.match(route.pattern);
|
|
77
|
-
if (match) {
|
|
78
|
-
match.shift(); // Remove the full match element
|
|
79
|
-
const queryParams = Object.fromEntries(new URLSearchParams(window.location.search));
|
|
80
|
-
route.handler.apply({queryParams}, match);
|
|
81
|
-
return true
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
console.warn(`No routing found for ${path}`);
|
|
86
|
-
return false
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
listen(mode: RoutingMode = 'hash'): void {
|
|
90
|
-
const self = this
|
|
91
|
-
this.mode = mode
|
|
92
|
-
switch (mode) {
|
|
93
|
-
case "hash":
|
|
94
|
-
window.addEventListener('hashchange', () => handler())
|
|
95
|
-
break
|
|
96
|
-
case "history":
|
|
97
|
-
window.addEventListener('popstate', event => handler(event.state?.path));
|
|
98
|
-
document.addEventListener('click', handleInternalNavigation);
|
|
99
|
-
document.addEventListener('keydown', event => {
|
|
100
|
-
// @ts-ignore
|
|
101
|
-
if (event.key === 'Enter' && event.target.tagName === 'A')
|
|
102
|
-
handleInternalNavigation(event);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function handleInternalNavigation(event: any) {
|
|
107
|
-
const node = event.target
|
|
108
|
-
const href = node.getAttribute('href')
|
|
109
|
-
if (href) {
|
|
110
|
-
event.preventDefault();
|
|
111
|
-
history.pushState({path: href}, '', href);
|
|
112
|
-
handler(href);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function handler(path?: string) {
|
|
117
|
-
path = path || location.href.split('#')[0]
|
|
118
|
-
|
|
119
|
-
if (self.isStaticFile(path))
|
|
120
|
-
return
|
|
121
|
-
const currentLocation = self.getLocation();
|
|
122
|
-
if (self.baseLocation !== currentLocation) {
|
|
123
|
-
self.baseLocation = currentLocation;
|
|
124
|
-
self.handleChange(currentLocation);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
handler()
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
navigate(path: string = ''): boolean {
|
|
133
|
-
if (this.mode === 'history')
|
|
134
|
-
history.pushState(null, null, this.root + this.cleanPathString(path));
|
|
135
|
-
else
|
|
136
|
-
window.location.hash = this.cleanPathString(path);
|
|
137
|
-
return this.handleChange()
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export const router = new Router();
|
package/src/state-manager.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import {router, RoutingMode} from "./router";
|
|
2
|
-
import dispatcher, {IPubSubHandle, PubSubEvent} from "@dharmax/pubsub";
|
|
3
|
-
|
|
4
|
-
export type ApplicationStateName = string
|
|
5
|
-
|
|
6
|
-
export type ApplicationState = {
|
|
7
|
-
name: ApplicationStateName
|
|
8
|
-
pageName: string
|
|
9
|
-
route: RegExp
|
|
10
|
-
mode?: string | string[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type ChangeAuthority = (state: ApplicationState) => Promise<boolean>
|
|
14
|
-
|
|
15
|
-
export class StateManager {
|
|
16
|
-
private allStates: { [name: string]: ApplicationState } = {}
|
|
17
|
-
private appState: ApplicationState
|
|
18
|
-
private previousState: ApplicationState
|
|
19
|
-
private stateContext: ApplicationState
|
|
20
|
-
|
|
21
|
-
public static dispatcher = dispatcher
|
|
22
|
-
private changeAuthorities: ChangeAuthority[] = [];
|
|
23
|
-
|
|
24
|
-
constructor(private mode: RoutingMode = 'hash', autostart = true) {
|
|
25
|
-
|
|
26
|
-
if (autostart)
|
|
27
|
-
router.listen(mode)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
start() {
|
|
31
|
-
router.listen(this.mode)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
onChange(handler: (event: PubSubEvent, data: any) => void): IPubSubHandle {
|
|
35
|
-
|
|
36
|
-
return StateManager.dispatcher.on('state:changed', handler)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/*
|
|
40
|
-
Add a hook which enable conditional approval of state change. It can be more than one; when a state
|
|
41
|
-
change is requested, all the registered authorities must return true (asynchronously) otherwise the change
|
|
42
|
-
requested doesn't happen.
|
|
43
|
-
**/
|
|
44
|
-
registerChangeAuthority(authorityCallback: (targetState: ApplicationState) => Promise<boolean>) {
|
|
45
|
-
this.changeAuthorities.push(authorityCallback)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
getState(): ApplicationState {
|
|
49
|
-
return this.appState || <ApplicationState>{}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
get previous() {
|
|
53
|
-
return this.previousState
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
get context() {
|
|
57
|
-
return this.stateContext
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* set current page state
|
|
62
|
-
* @param state can be either just a state or a state and context (which can be sub-state, or anything else)
|
|
63
|
-
*/
|
|
64
|
-
set state(state: ApplicationStateName | [ApplicationStateName, ...any]) {
|
|
65
|
-
if (Array.isArray(state)) {
|
|
66
|
-
const sName = state.shift()
|
|
67
|
-
this.setState(sName,state)
|
|
68
|
-
} else
|
|
69
|
-
this.setState(state)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** attempts to restore state from current url. Currently, works only in hash mode */
|
|
73
|
-
restoreState(defaultState: ApplicationStateName) {
|
|
74
|
-
if (router.navigate(window.location.pathname))
|
|
75
|
-
return
|
|
76
|
-
router.navigate(defaultState)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
*
|
|
81
|
-
* @param stateName state
|
|
82
|
-
* @param context extra context (e.g. sub-state)
|
|
83
|
-
*/
|
|
84
|
-
async setState(stateName: ApplicationStateName, context?: any): Promise<boolean> {
|
|
85
|
-
|
|
86
|
-
const newState = this.allStates[stateName];
|
|
87
|
-
if (!newState) {
|
|
88
|
-
alert(`Undefined app state ${stateName}`)
|
|
89
|
-
return false
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// check if the state change was declined by any change authority and if so - don't do it and return false
|
|
93
|
-
const changeConfirmations = await Promise.all(this.changeAuthorities.map(authority => authority(newState)))
|
|
94
|
-
if (changeConfirmations.includes(false))
|
|
95
|
-
return false
|
|
96
|
-
|
|
97
|
-
// perform the change
|
|
98
|
-
this.previousState = this.appState
|
|
99
|
-
this.stateContext = context
|
|
100
|
-
this.appState = newState
|
|
101
|
-
dispatcher.trigger('state-manager', 'state', 'changed', this.appState)
|
|
102
|
-
return true
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Define an application state
|
|
107
|
-
* @param name
|
|
108
|
-
* @param pageName by default it equals the name (you can null it)
|
|
109
|
-
* @param route by default it equals the pageName (ditto)
|
|
110
|
-
* @param mode optional
|
|
111
|
-
*/
|
|
112
|
-
addState(name: string, pageName?: string, route?: RegExp | string, mode?: string | string[]) {
|
|
113
|
-
|
|
114
|
-
pageName = pageName || name
|
|
115
|
-
route = route || pageName
|
|
116
|
-
if (typeof route === "string") {
|
|
117
|
-
let newRoute = route.split('%').join('?(.*)')
|
|
118
|
-
route = new RegExp(`^${newRoute}$`)
|
|
119
|
-
}
|
|
120
|
-
this.registerStateByState({
|
|
121
|
-
name,
|
|
122
|
-
pageName,
|
|
123
|
-
route,
|
|
124
|
-
mode
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
registerStateByState(state: ApplicationState) {
|
|
129
|
-
this.allStates[state.name] = state
|
|
130
|
-
router.add(state.route, async (context: any) => {
|
|
131
|
-
if (await this.setState(state.name, context)) {
|
|
132
|
-
|
|
133
|
-
// @ts-ignore
|
|
134
|
-
window.pageChangeHandler && window.pageChangeHandler('send', 'pageview', `/${state.name}/${context || ''}`);
|
|
135
|
-
// @ts-ignore
|
|
136
|
-
window.ga && window.ga('send', 'pageview', `/${state.name}/${context || ''}`);
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
package/test/index.html
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
<html>
|
|
3
|
-
|
|
4
|
-
<a href="one">one</a>
|
|
5
|
-
<a href="two">two</a>
|
|
6
|
-
<a href="three.json">three</a>
|
|
7
|
-
<a href="four">four</a>
|
|
8
|
-
|
|
9
|
-
<script type="module">
|
|
10
|
-
import {router} from '../dist/router.js'
|
|
11
|
-
// router.mode ='history'
|
|
12
|
-
router.add('one', ()=>alert(1))
|
|
13
|
-
router.add('two', ()=>alert(2))
|
|
14
|
-
router.add('four', ()=>alert(4))
|
|
15
|
-
// router.add('one', ()=>alert(1))
|
|
16
|
-
router.listen('history')
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</script>
|
|
20
|
-
|
|
21
|
-
</html>
|
package/tsconfig.json
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"lib": [
|
|
5
|
-
"DOM",
|
|
6
|
-
"ES2022"
|
|
7
|
-
],
|
|
8
|
-
"module": "ES6",
|
|
9
|
-
"moduleResolution": "Node",
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"outDir": "./dist",
|
|
12
|
-
"strictNullChecks": false,
|
|
13
|
-
"strict": true
|
|
14
|
-
},
|
|
15
|
-
"include": [
|
|
16
|
-
"src"
|
|
17
|
-
],
|
|
18
|
-
"exclude": [
|
|
19
|
-
"dist/",
|
|
20
|
-
"node_modules",
|
|
21
|
-
"**/__tests__/*"
|
|
22
|
-
]
|
|
23
|
-
}
|
|
File without changes
|