@dharmax/state-router 3.2.1 → 4.0.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/dist/router.d.ts +2 -14
- package/dist/router.js +72 -69
- package/dist/state-manager.d.ts +6 -11
- package/dist/state-manager.js +58 -36
- package/dist-cjs/router.cjs +72 -69
- package/dist-cjs/router.d.ts +2 -14
- package/dist-cjs/state-manager.cjs +58 -36
- package/dist-cjs/state-manager.d.ts +6 -11
- package/package.json +2 -2
package/dist/router.d.ts
CHANGED
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
type RouteHandler = (...args: string[]) => void;
|
|
2
2
|
export type RoutingMode = 'history' | 'hash';
|
|
3
3
|
export declare class Router {
|
|
4
|
-
private
|
|
5
|
-
private routes;
|
|
6
|
-
private root;
|
|
7
|
-
private rootCompare;
|
|
8
|
-
private baseLocation;
|
|
4
|
+
#private;
|
|
9
5
|
staticFilters: ((url: string) => boolean)[];
|
|
10
|
-
private isListening;
|
|
11
|
-
private bound;
|
|
12
|
-
private notFoundHandler?;
|
|
13
|
-
private decodeParams;
|
|
14
6
|
constructor();
|
|
15
|
-
private cleanPathString;
|
|
16
|
-
private clearQuery;
|
|
17
|
-
private isStaticFile;
|
|
18
7
|
resetRoot(root: string): void;
|
|
8
|
+
setMode(mode: RoutingMode): void;
|
|
19
9
|
getLocation(): string;
|
|
20
10
|
add(pattern: RegExp | string | RouteHandler, handler?: RouteHandler): Router;
|
|
21
11
|
onNotFound(handler: (path: string) => void): Router;
|
|
22
12
|
setDecodeParams(decode: boolean): Router;
|
|
23
13
|
getQueryParams(search?: string): Record<string, string>;
|
|
24
|
-
private isBrowser;
|
|
25
|
-
private compilePattern;
|
|
26
14
|
/**
|
|
27
15
|
*
|
|
28
16
|
* @param location
|
package/dist/router.js
CHANGED
|
@@ -1,56 +1,59 @@
|
|
|
1
1
|
export class Router {
|
|
2
|
-
mode = 'hash';
|
|
3
|
-
routes = [];
|
|
4
|
-
root = '/';
|
|
5
|
-
rootCompare = '';
|
|
6
|
-
baseLocation = null;
|
|
2
|
+
#mode = 'hash';
|
|
3
|
+
#routes = [];
|
|
4
|
+
#root = '/';
|
|
5
|
+
#rootCompare = '';
|
|
6
|
+
#baseLocation = null;
|
|
7
7
|
staticFilters = [];
|
|
8
|
-
isListening = false;
|
|
9
|
-
bound = {};
|
|
10
|
-
notFoundHandler;
|
|
11
|
-
decodeParams = false;
|
|
8
|
+
#isListening = false;
|
|
9
|
+
#bound = {};
|
|
10
|
+
#notFoundHandler;
|
|
11
|
+
#decodeParams = false;
|
|
12
12
|
constructor() {
|
|
13
13
|
this.staticFilters.push(url => {
|
|
14
14
|
const staticFileExtensions = ['.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp', '.md', '.ejs', '.jsm', '.txt'];
|
|
15
15
|
return staticFileExtensions.some(ext => url.endsWith(ext));
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
cleanPathString(path) {
|
|
18
|
+
#cleanPathString(path) {
|
|
19
19
|
path = path.replace(/\/$/, '').replace(/^\//, '');
|
|
20
20
|
return path = path.replace(/#{2,}/g, '#');
|
|
21
21
|
}
|
|
22
|
-
clearQuery(url) {
|
|
22
|
+
#clearQuery(url) {
|
|
23
23
|
const [path, query] = url.split('?');
|
|
24
24
|
if (!query)
|
|
25
25
|
return path;
|
|
26
26
|
const [_, hash] = query.split('#');
|
|
27
27
|
return hash ? `${path}#${hash}` : path;
|
|
28
28
|
}
|
|
29
|
-
isStaticFile(url) {
|
|
30
|
-
return
|
|
29
|
+
#isStaticFile(url) {
|
|
30
|
+
return this.staticFilters.some(filter => filter(url));
|
|
31
31
|
}
|
|
32
32
|
resetRoot(root) {
|
|
33
|
-
const cleaned = this
|
|
34
|
-
this
|
|
35
|
-
this
|
|
33
|
+
const cleaned = this.#cleanPathString(root);
|
|
34
|
+
this.#root = '/' + cleaned + '/';
|
|
35
|
+
this.#rootCompare = cleaned ? cleaned + '/' : '';
|
|
36
|
+
}
|
|
37
|
+
setMode(mode) {
|
|
38
|
+
this.#mode = mode;
|
|
36
39
|
}
|
|
37
40
|
getLocation() {
|
|
38
|
-
if (!this
|
|
41
|
+
if (!this.#isBrowser())
|
|
39
42
|
return '';
|
|
40
|
-
if (this
|
|
43
|
+
if (this.#mode === 'history') {
|
|
41
44
|
let fragment = decodeURI(window.location.pathname + window.location.search);
|
|
42
|
-
fragment = this
|
|
45
|
+
fragment = this.#clearQuery(fragment);
|
|
43
46
|
// strip leading slash for comparison convenience
|
|
44
47
|
fragment = fragment.replace(/^\//, '');
|
|
45
|
-
if (this
|
|
46
|
-
fragment = fragment.slice(this
|
|
48
|
+
if (this.#root !== '/' && this.#rootCompare && fragment.startsWith(this.#rootCompare)) {
|
|
49
|
+
fragment = fragment.slice(this.#rootCompare.length);
|
|
47
50
|
}
|
|
48
|
-
fragment = this
|
|
51
|
+
fragment = this.#cleanPathString(fragment);
|
|
49
52
|
return fragment;
|
|
50
53
|
}
|
|
51
54
|
else {
|
|
52
55
|
const match = window.location.href.match(/#(.*)$/);
|
|
53
|
-
return match ? this
|
|
56
|
+
return match ? this.#clearQuery(match[1]) : '';
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
add(pattern, handler) {
|
|
@@ -60,23 +63,23 @@ export class Router {
|
|
|
60
63
|
pattern = /^.*$/; // Match any path
|
|
61
64
|
}
|
|
62
65
|
else if (typeof pattern === 'string') {
|
|
63
|
-
const compiled = this
|
|
66
|
+
const compiled = this.#compilePattern(pattern);
|
|
64
67
|
pattern = compiled.regex;
|
|
65
68
|
paramNames = compiled.paramNames;
|
|
66
69
|
}
|
|
67
|
-
this
|
|
70
|
+
this.#routes.push({ pattern: pattern, handler: handler, paramNames });
|
|
68
71
|
return this;
|
|
69
72
|
}
|
|
70
73
|
onNotFound(handler) {
|
|
71
|
-
this
|
|
74
|
+
this.#notFoundHandler = handler;
|
|
72
75
|
return this;
|
|
73
76
|
}
|
|
74
77
|
setDecodeParams(decode) {
|
|
75
|
-
this
|
|
78
|
+
this.#decodeParams = decode;
|
|
76
79
|
return this;
|
|
77
80
|
}
|
|
78
81
|
getQueryParams(search) {
|
|
79
|
-
if (!this
|
|
82
|
+
if (!this.#isBrowser() && !search)
|
|
80
83
|
return {};
|
|
81
84
|
const qs = typeof search === 'string' ? search : window.location.search || '';
|
|
82
85
|
const usp = new URLSearchParams(qs);
|
|
@@ -84,10 +87,10 @@ export class Router {
|
|
|
84
87
|
usp.forEach((v, k) => { obj[k] = v; });
|
|
85
88
|
return obj;
|
|
86
89
|
}
|
|
87
|
-
isBrowser() {
|
|
90
|
+
#isBrowser() {
|
|
88
91
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
89
92
|
}
|
|
90
|
-
compilePattern(pattern) {
|
|
93
|
+
#compilePattern(pattern) {
|
|
91
94
|
// normalize leading slash to align with cleanPathString behavior
|
|
92
95
|
const normalized = pattern.replace(/^\//, '');
|
|
93
96
|
const paramNames = [];
|
|
@@ -107,14 +110,14 @@ export class Router {
|
|
|
107
110
|
*/
|
|
108
111
|
handleChange(location) {
|
|
109
112
|
const path = (location ?? this.getLocation()) || '';
|
|
110
|
-
if (this
|
|
113
|
+
if (this.#isStaticFile(path))
|
|
111
114
|
return false; // Bypass routing for static files
|
|
112
|
-
for (const route of this
|
|
115
|
+
for (const route of this.#routes) {
|
|
113
116
|
const match = route.pattern ? path.match(route.pattern) : null;
|
|
114
117
|
if (match) {
|
|
115
118
|
match.shift(); // Remove the full match element
|
|
116
119
|
const queryParams = this.getQueryParams();
|
|
117
|
-
const captures = this
|
|
120
|
+
const captures = this.#decodeParams ? match.map(v => safeDecode(v)) : match;
|
|
118
121
|
let params;
|
|
119
122
|
if (route.paramNames && route.paramNames.length) {
|
|
120
123
|
params = {};
|
|
@@ -124,8 +127,8 @@ export class Router {
|
|
|
124
127
|
return true;
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
|
-
if (this
|
|
128
|
-
this
|
|
130
|
+
if (this.#notFoundHandler) {
|
|
131
|
+
this.#notFoundHandler(path);
|
|
129
132
|
return true;
|
|
130
133
|
}
|
|
131
134
|
if (path)
|
|
@@ -133,20 +136,20 @@ export class Router {
|
|
|
133
136
|
return false;
|
|
134
137
|
}
|
|
135
138
|
listen(mode = 'hash') {
|
|
136
|
-
if (!this
|
|
139
|
+
if (!this.#isBrowser())
|
|
137
140
|
return;
|
|
138
141
|
// avoid duplicate listeners
|
|
139
|
-
if (this
|
|
142
|
+
if (this.#isListening)
|
|
140
143
|
this.unlisten();
|
|
141
144
|
const self = this;
|
|
142
|
-
this
|
|
145
|
+
this.#mode = mode;
|
|
143
146
|
const handler = (path) => {
|
|
144
147
|
const p = path || location.href.split('#')[0];
|
|
145
|
-
if (self
|
|
148
|
+
if (self.#isStaticFile(p))
|
|
146
149
|
return;
|
|
147
150
|
const currentLocation = self.getLocation();
|
|
148
|
-
if (self
|
|
149
|
-
self
|
|
151
|
+
if (self.#baseLocation !== currentLocation) {
|
|
152
|
+
self.#baseLocation = currentLocation;
|
|
150
153
|
self.handleChange(currentLocation);
|
|
151
154
|
}
|
|
152
155
|
};
|
|
@@ -160,9 +163,9 @@ export class Router {
|
|
|
160
163
|
return;
|
|
161
164
|
}
|
|
162
165
|
const target = event.target;
|
|
163
|
-
if (!target
|
|
166
|
+
if (!target)
|
|
164
167
|
return;
|
|
165
|
-
const anchor = target.closest
|
|
168
|
+
const anchor = target.closest('a');
|
|
166
169
|
if (!anchor)
|
|
167
170
|
return;
|
|
168
171
|
if (event.type === 'keydown') {
|
|
@@ -187,7 +190,7 @@ export class Router {
|
|
|
187
190
|
if (rel && /\bnoreferrer\b/i.test(rel))
|
|
188
191
|
return;
|
|
189
192
|
const pathWithQuery = url.pathname + (url.search || '');
|
|
190
|
-
if (self
|
|
193
|
+
if (self.#isStaticFile(pathWithQuery) || self.#isStaticFile(url.pathname))
|
|
191
194
|
return;
|
|
192
195
|
event.preventDefault();
|
|
193
196
|
history.pushState({ path: pathWithQuery }, '', pathWithQuery);
|
|
@@ -195,53 +198,53 @@ export class Router {
|
|
|
195
198
|
};
|
|
196
199
|
switch (mode) {
|
|
197
200
|
case 'hash':
|
|
198
|
-
this
|
|
199
|
-
window.addEventListener('hashchange', this
|
|
201
|
+
this.#bound.hashchange = () => handler();
|
|
202
|
+
window.addEventListener('hashchange', this.#bound.hashchange);
|
|
200
203
|
break;
|
|
201
204
|
case 'history':
|
|
202
|
-
this
|
|
203
|
-
this
|
|
204
|
-
this
|
|
205
|
-
window.addEventListener('popstate', this
|
|
206
|
-
document.addEventListener('click', this
|
|
207
|
-
document.addEventListener('keydown', this
|
|
205
|
+
this.#bound.popstate = (event) => handler(this.getLocation());
|
|
206
|
+
this.#bound.click = handleInternalNavigation;
|
|
207
|
+
this.#bound.keydown = (event) => handleInternalNavigation(event);
|
|
208
|
+
window.addEventListener('popstate', this.#bound.popstate);
|
|
209
|
+
document.addEventListener('click', this.#bound.click);
|
|
210
|
+
document.addEventListener('keydown', this.#bound.keydown);
|
|
208
211
|
break;
|
|
209
212
|
}
|
|
210
|
-
this
|
|
213
|
+
this.#isListening = true;
|
|
211
214
|
handler();
|
|
212
215
|
}
|
|
213
216
|
unlisten() {
|
|
214
|
-
if (!this
|
|
217
|
+
if (!this.#isBrowser() || !this.#isListening)
|
|
215
218
|
return;
|
|
216
|
-
switch (this
|
|
219
|
+
switch (this.#mode) {
|
|
217
220
|
case 'hash':
|
|
218
|
-
if (this
|
|
219
|
-
window.removeEventListener('hashchange', this
|
|
221
|
+
if (this.#bound.hashchange)
|
|
222
|
+
window.removeEventListener('hashchange', this.#bound.hashchange);
|
|
220
223
|
break;
|
|
221
224
|
case 'history':
|
|
222
|
-
if (this
|
|
223
|
-
window.removeEventListener('popstate', this
|
|
224
|
-
if (this
|
|
225
|
-
document.removeEventListener('click', this
|
|
226
|
-
if (this
|
|
227
|
-
document.removeEventListener('keydown', this
|
|
225
|
+
if (this.#bound.popstate)
|
|
226
|
+
window.removeEventListener('popstate', this.#bound.popstate);
|
|
227
|
+
if (this.#bound.click)
|
|
228
|
+
document.removeEventListener('click', this.#bound.click);
|
|
229
|
+
if (this.#bound.keydown)
|
|
230
|
+
document.removeEventListener('keydown', this.#bound.keydown);
|
|
228
231
|
break;
|
|
229
232
|
}
|
|
230
|
-
this
|
|
231
|
-
this
|
|
233
|
+
this.#bound = {};
|
|
234
|
+
this.#isListening = false;
|
|
232
235
|
}
|
|
233
236
|
navigate(path = '', opts) {
|
|
234
|
-
if (!this
|
|
237
|
+
if (!this.#isBrowser())
|
|
235
238
|
return false;
|
|
236
|
-
if (this
|
|
237
|
-
const url = this
|
|
239
|
+
if (this.#mode === 'history') {
|
|
240
|
+
const url = this.#root + this.#cleanPathString(path);
|
|
238
241
|
if (opts?.replace)
|
|
239
242
|
history.replaceState(null, '', url);
|
|
240
243
|
else
|
|
241
244
|
history.pushState(null, '', url);
|
|
242
245
|
}
|
|
243
246
|
else
|
|
244
|
-
window.location.hash = this
|
|
247
|
+
window.location.hash = this.#cleanPathString(path);
|
|
245
248
|
return this.handleChange();
|
|
246
249
|
}
|
|
247
250
|
replace(path = '') {
|
package/dist/state-manager.d.ts
CHANGED
|
@@ -6,33 +6,27 @@ export type ApplicationState = {
|
|
|
6
6
|
pageName: string;
|
|
7
7
|
route: RegExp | string;
|
|
8
8
|
mode?: string | string[];
|
|
9
|
+
onExit?: (context?: any) => void | Promise<void>;
|
|
9
10
|
};
|
|
10
11
|
export type ChangeAuthority = (state: ApplicationState) => Promise<boolean>;
|
|
11
12
|
export declare class StateManager {
|
|
13
|
+
#private;
|
|
12
14
|
private mode;
|
|
13
|
-
private allStates;
|
|
14
|
-
private appState;
|
|
15
|
-
private previousState;
|
|
16
|
-
private stateContext;
|
|
17
15
|
static dispatcher: import("@dharmax/pubsub").PubSub;
|
|
18
|
-
private changeAuthorities;
|
|
19
|
-
private router;
|
|
20
|
-
private beforeChangeHandlers;
|
|
21
|
-
private afterChangeHandlers;
|
|
22
16
|
constructor(mode?: RoutingMode, autostart?: boolean, routerInstance?: RouterType);
|
|
23
17
|
start(): void;
|
|
24
18
|
stop(): void;
|
|
25
19
|
onChange(handler: (event: PubSubEvent, data: any) => void): IPubSubHandle;
|
|
26
20
|
registerChangeAuthority(authorityCallback: (targetState: ApplicationState) => Promise<boolean>): void;
|
|
27
21
|
getState(): ApplicationState;
|
|
28
|
-
get previous(): ApplicationState;
|
|
22
|
+
get previous(): ApplicationState | null;
|
|
29
23
|
get context(): any;
|
|
30
24
|
/**
|
|
31
25
|
* set current page state
|
|
32
26
|
* @param state can be either just a state or a state and context (which can be sub-state, or anything else)
|
|
33
27
|
*/
|
|
34
28
|
set state(state: ApplicationStateName | [ApplicationStateName, ...any]);
|
|
35
|
-
/** attempts to restore state from current url.
|
|
29
|
+
/** attempts to restore state from current url. */
|
|
36
30
|
restoreState(defaultState: ApplicationStateName): void;
|
|
37
31
|
/**
|
|
38
32
|
*
|
|
@@ -50,7 +44,8 @@ export declare class StateManager {
|
|
|
50
44
|
addState(name: string, pageName?: string, route?: RegExp | string, mode?: string | string[]): void;
|
|
51
45
|
registerStateByState(state: ApplicationState): void;
|
|
52
46
|
onBeforeChange(handler: (target: ApplicationState, context?: any) => boolean | Promise<boolean>): void;
|
|
53
|
-
onAfterChange(handler: (state: ApplicationState, context?: any, previous?: ApplicationState) => void | Promise<void>): void;
|
|
47
|
+
onAfterChange(handler: (state: ApplicationState, context?: any, previous?: ApplicationState | null) => void | Promise<void>): void;
|
|
48
|
+
onExit(stateName: ApplicationStateName, handler: (context?: any) => void | Promise<void>): void;
|
|
54
49
|
onNotFound(handler: (path: string) => void): void;
|
|
55
50
|
}
|
|
56
51
|
export declare function createStateManager(mode?: RoutingMode, autostart?: boolean, routerInstance?: RouterType): StateManager;
|
package/dist/state-manager.js
CHANGED
|
@@ -2,28 +2,30 @@ import { router } from "./router";
|
|
|
2
2
|
import dispatcher from "@dharmax/pubsub";
|
|
3
3
|
export class StateManager {
|
|
4
4
|
mode;
|
|
5
|
-
allStates = {};
|
|
6
|
-
appState;
|
|
7
|
-
previousState;
|
|
8
|
-
stateContext;
|
|
5
|
+
#allStates = {};
|
|
6
|
+
#appState = null;
|
|
7
|
+
#previousState = null;
|
|
8
|
+
#stateContext;
|
|
9
|
+
#currentTransitionId = 0;
|
|
9
10
|
static dispatcher = dispatcher;
|
|
10
|
-
changeAuthorities = [];
|
|
11
|
-
router;
|
|
12
|
-
beforeChangeHandlers = [];
|
|
13
|
-
afterChangeHandlers = [];
|
|
11
|
+
#changeAuthorities = [];
|
|
12
|
+
#router;
|
|
13
|
+
#beforeChangeHandlers = [];
|
|
14
|
+
#afterChangeHandlers = [];
|
|
14
15
|
constructor(mode = 'hash', autostart = true, routerInstance = router) {
|
|
15
16
|
this.mode = mode;
|
|
16
|
-
this
|
|
17
|
+
this.#router = routerInstance;
|
|
18
|
+
this.#router.setMode(mode);
|
|
17
19
|
if (autostart)
|
|
18
|
-
this
|
|
20
|
+
this.#router.listen(mode);
|
|
19
21
|
}
|
|
20
22
|
start() {
|
|
21
|
-
this
|
|
23
|
+
this.#router.listen(this.mode);
|
|
22
24
|
}
|
|
23
25
|
stop() {
|
|
24
26
|
// unlisten router to cleanup event listeners
|
|
25
27
|
// @ts-ignore - older Router versions may not have unlisten
|
|
26
|
-
this
|
|
28
|
+
this.#router.unlisten && this.#router.unlisten();
|
|
27
29
|
}
|
|
28
30
|
onChange(handler) {
|
|
29
31
|
return StateManager.dispatcher.on('state:changed', handler);
|
|
@@ -34,16 +36,16 @@ export class StateManager {
|
|
|
34
36
|
requested doesn't happen.
|
|
35
37
|
**/
|
|
36
38
|
registerChangeAuthority(authorityCallback) {
|
|
37
|
-
this
|
|
39
|
+
this.#changeAuthorities.push(authorityCallback);
|
|
38
40
|
}
|
|
39
41
|
getState() {
|
|
40
|
-
return this
|
|
42
|
+
return this.#appState || {};
|
|
41
43
|
}
|
|
42
44
|
get previous() {
|
|
43
|
-
return this
|
|
45
|
+
return this.#previousState;
|
|
44
46
|
}
|
|
45
47
|
get context() {
|
|
46
|
-
return this
|
|
48
|
+
return this.#stateContext;
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
49
51
|
* set current page state
|
|
@@ -52,16 +54,21 @@ export class StateManager {
|
|
|
52
54
|
set state(state) {
|
|
53
55
|
if (Array.isArray(state)) {
|
|
54
56
|
const sName = state.shift();
|
|
55
|
-
this.setState(sName, state);
|
|
57
|
+
this.setState(sName, state.length === 1 ? state[0] : state);
|
|
56
58
|
}
|
|
57
59
|
else
|
|
58
60
|
this.setState(state);
|
|
59
61
|
}
|
|
60
|
-
/** attempts to restore state from current url.
|
|
62
|
+
/** attempts to restore state from current url. */
|
|
61
63
|
restoreState(defaultState) {
|
|
62
|
-
if (this
|
|
64
|
+
if (this.#router.handleChange())
|
|
63
65
|
return;
|
|
64
|
-
this
|
|
66
|
+
const state = this.#allStates[defaultState];
|
|
67
|
+
let path = defaultState;
|
|
68
|
+
if (state && typeof state.route === 'string') {
|
|
69
|
+
path = state.route;
|
|
70
|
+
}
|
|
71
|
+
this.#router.navigate(path);
|
|
65
72
|
}
|
|
66
73
|
/**
|
|
67
74
|
*
|
|
@@ -69,24 +76,33 @@ export class StateManager {
|
|
|
69
76
|
* @param context extra context (e.g. sub-state)
|
|
70
77
|
*/
|
|
71
78
|
async setState(stateName, context) {
|
|
72
|
-
const
|
|
79
|
+
const transitionId = ++this.#currentTransitionId;
|
|
80
|
+
const newState = this.#allStates[stateName];
|
|
73
81
|
if (!newState) {
|
|
74
|
-
|
|
75
|
-
return false;
|
|
82
|
+
throw new Error(`Undefined app state ${stateName}`);
|
|
76
83
|
}
|
|
77
84
|
// check if the state change was declined by any change authority and if so - don't do it and return false
|
|
78
|
-
const changeConfirmations = await Promise.all(this
|
|
79
|
-
|
|
85
|
+
const changeConfirmations = await Promise.all(this.#changeAuthorities.map(authority => authority(newState)));
|
|
86
|
+
if (transitionId !== this.#currentTransitionId)
|
|
87
|
+
return false;
|
|
88
|
+
const vetoes = await Promise.all(this.#beforeChangeHandlers.map(h => Promise.resolve(h(newState, context))));
|
|
89
|
+
if (transitionId !== this.#currentTransitionId)
|
|
90
|
+
return false;
|
|
80
91
|
if (changeConfirmations.includes(false) || vetoes.includes(false))
|
|
81
92
|
return false;
|
|
93
|
+
if (this.#appState && this.#appState.onExit) {
|
|
94
|
+
await this.#appState.onExit(this.#stateContext);
|
|
95
|
+
if (transitionId !== this.#currentTransitionId)
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
82
98
|
// perform the change
|
|
83
|
-
this
|
|
84
|
-
this
|
|
85
|
-
this
|
|
86
|
-
dispatcher.trigger('state-manager', 'state', 'changed', this
|
|
99
|
+
this.#previousState = this.#appState;
|
|
100
|
+
this.#stateContext = context;
|
|
101
|
+
this.#appState = newState;
|
|
102
|
+
dispatcher.trigger('state-manager', 'state', 'changed', this.#appState);
|
|
87
103
|
// afterChange hooks
|
|
88
|
-
for (const cb of this
|
|
89
|
-
await cb(this
|
|
104
|
+
for (const cb of this.#afterChangeHandlers) {
|
|
105
|
+
await cb(this.#appState, context, this.#previousState);
|
|
90
106
|
}
|
|
91
107
|
return true;
|
|
92
108
|
}
|
|
@@ -113,9 +129,9 @@ export class StateManager {
|
|
|
113
129
|
});
|
|
114
130
|
}
|
|
115
131
|
registerStateByState(state) {
|
|
116
|
-
this
|
|
132
|
+
this.#allStates[state.name] = state;
|
|
117
133
|
const self = this;
|
|
118
|
-
this
|
|
134
|
+
this.#router.add(state.route, async function (...captures) {
|
|
119
135
|
// prefer named params (from router string patterns), else multi-captures array, else first capture
|
|
120
136
|
// @ts-ignore
|
|
121
137
|
const named = (this && this.params) || null;
|
|
@@ -129,15 +145,21 @@ export class StateManager {
|
|
|
129
145
|
});
|
|
130
146
|
}
|
|
131
147
|
onBeforeChange(handler) {
|
|
132
|
-
this
|
|
148
|
+
this.#beforeChangeHandlers.push(handler);
|
|
133
149
|
}
|
|
134
150
|
onAfterChange(handler) {
|
|
135
|
-
this
|
|
151
|
+
this.#afterChangeHandlers.push(handler);
|
|
152
|
+
}
|
|
153
|
+
onExit(stateName, handler) {
|
|
154
|
+
const state = this.#allStates[stateName];
|
|
155
|
+
if (state) {
|
|
156
|
+
state.onExit = handler;
|
|
157
|
+
}
|
|
136
158
|
}
|
|
137
159
|
onNotFound(handler) {
|
|
138
160
|
// proxy to router-level notFound
|
|
139
161
|
// @ts-ignore - older Router versions may not have onNotFound
|
|
140
|
-
this
|
|
162
|
+
this.#router.onNotFound && this.#router.onNotFound(handler);
|
|
141
163
|
}
|
|
142
164
|
}
|
|
143
165
|
export function createStateManager(mode = 'hash', autostart = true, routerInstance = router) {
|
package/dist-cjs/router.cjs
CHANGED
|
@@ -3,58 +3,61 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.router = exports.Router = void 0;
|
|
4
4
|
exports.createRouter = createRouter;
|
|
5
5
|
class Router {
|
|
6
|
-
mode = 'hash';
|
|
7
|
-
routes = [];
|
|
8
|
-
root = '/';
|
|
9
|
-
rootCompare = '';
|
|
10
|
-
baseLocation = null;
|
|
6
|
+
#mode = 'hash';
|
|
7
|
+
#routes = [];
|
|
8
|
+
#root = '/';
|
|
9
|
+
#rootCompare = '';
|
|
10
|
+
#baseLocation = null;
|
|
11
11
|
staticFilters = [];
|
|
12
|
-
isListening = false;
|
|
13
|
-
bound = {};
|
|
14
|
-
notFoundHandler;
|
|
15
|
-
decodeParams = false;
|
|
12
|
+
#isListening = false;
|
|
13
|
+
#bound = {};
|
|
14
|
+
#notFoundHandler;
|
|
15
|
+
#decodeParams = false;
|
|
16
16
|
constructor() {
|
|
17
17
|
this.staticFilters.push(url => {
|
|
18
18
|
const staticFileExtensions = ['.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp', '.md', '.ejs', '.jsm', '.txt'];
|
|
19
19
|
return staticFileExtensions.some(ext => url.endsWith(ext));
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
cleanPathString(path) {
|
|
22
|
+
#cleanPathString(path) {
|
|
23
23
|
path = path.replace(/\/$/, '').replace(/^\//, '');
|
|
24
24
|
return path = path.replace(/#{2,}/g, '#');
|
|
25
25
|
}
|
|
26
|
-
clearQuery(url) {
|
|
26
|
+
#clearQuery(url) {
|
|
27
27
|
const [path, query] = url.split('?');
|
|
28
28
|
if (!query)
|
|
29
29
|
return path;
|
|
30
30
|
const [_, hash] = query.split('#');
|
|
31
31
|
return hash ? `${path}#${hash}` : path;
|
|
32
32
|
}
|
|
33
|
-
isStaticFile(url) {
|
|
34
|
-
return
|
|
33
|
+
#isStaticFile(url) {
|
|
34
|
+
return this.staticFilters.some(filter => filter(url));
|
|
35
35
|
}
|
|
36
36
|
resetRoot(root) {
|
|
37
|
-
const cleaned = this
|
|
38
|
-
this
|
|
39
|
-
this
|
|
37
|
+
const cleaned = this.#cleanPathString(root);
|
|
38
|
+
this.#root = '/' + cleaned + '/';
|
|
39
|
+
this.#rootCompare = cleaned ? cleaned + '/' : '';
|
|
40
|
+
}
|
|
41
|
+
setMode(mode) {
|
|
42
|
+
this.#mode = mode;
|
|
40
43
|
}
|
|
41
44
|
getLocation() {
|
|
42
|
-
if (!this
|
|
45
|
+
if (!this.#isBrowser())
|
|
43
46
|
return '';
|
|
44
|
-
if (this
|
|
47
|
+
if (this.#mode === 'history') {
|
|
45
48
|
let fragment = decodeURI(window.location.pathname + window.location.search);
|
|
46
|
-
fragment = this
|
|
49
|
+
fragment = this.#clearQuery(fragment);
|
|
47
50
|
// strip leading slash for comparison convenience
|
|
48
51
|
fragment = fragment.replace(/^\//, '');
|
|
49
|
-
if (this
|
|
50
|
-
fragment = fragment.slice(this
|
|
52
|
+
if (this.#root !== '/' && this.#rootCompare && fragment.startsWith(this.#rootCompare)) {
|
|
53
|
+
fragment = fragment.slice(this.#rootCompare.length);
|
|
51
54
|
}
|
|
52
|
-
fragment = this
|
|
55
|
+
fragment = this.#cleanPathString(fragment);
|
|
53
56
|
return fragment;
|
|
54
57
|
}
|
|
55
58
|
else {
|
|
56
59
|
const match = window.location.href.match(/#(.*)$/);
|
|
57
|
-
return match ? this
|
|
60
|
+
return match ? this.#clearQuery(match[1]) : '';
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
add(pattern, handler) {
|
|
@@ -64,23 +67,23 @@ class Router {
|
|
|
64
67
|
pattern = /^.*$/; // Match any path
|
|
65
68
|
}
|
|
66
69
|
else if (typeof pattern === 'string') {
|
|
67
|
-
const compiled = this
|
|
70
|
+
const compiled = this.#compilePattern(pattern);
|
|
68
71
|
pattern = compiled.regex;
|
|
69
72
|
paramNames = compiled.paramNames;
|
|
70
73
|
}
|
|
71
|
-
this
|
|
74
|
+
this.#routes.push({ pattern: pattern, handler: handler, paramNames });
|
|
72
75
|
return this;
|
|
73
76
|
}
|
|
74
77
|
onNotFound(handler) {
|
|
75
|
-
this
|
|
78
|
+
this.#notFoundHandler = handler;
|
|
76
79
|
return this;
|
|
77
80
|
}
|
|
78
81
|
setDecodeParams(decode) {
|
|
79
|
-
this
|
|
82
|
+
this.#decodeParams = decode;
|
|
80
83
|
return this;
|
|
81
84
|
}
|
|
82
85
|
getQueryParams(search) {
|
|
83
|
-
if (!this
|
|
86
|
+
if (!this.#isBrowser() && !search)
|
|
84
87
|
return {};
|
|
85
88
|
const qs = typeof search === 'string' ? search : window.location.search || '';
|
|
86
89
|
const usp = new URLSearchParams(qs);
|
|
@@ -88,10 +91,10 @@ class Router {
|
|
|
88
91
|
usp.forEach((v, k) => { obj[k] = v; });
|
|
89
92
|
return obj;
|
|
90
93
|
}
|
|
91
|
-
isBrowser() {
|
|
94
|
+
#isBrowser() {
|
|
92
95
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
93
96
|
}
|
|
94
|
-
compilePattern(pattern) {
|
|
97
|
+
#compilePattern(pattern) {
|
|
95
98
|
// normalize leading slash to align with cleanPathString behavior
|
|
96
99
|
const normalized = pattern.replace(/^\//, '');
|
|
97
100
|
const paramNames = [];
|
|
@@ -111,14 +114,14 @@ class Router {
|
|
|
111
114
|
*/
|
|
112
115
|
handleChange(location) {
|
|
113
116
|
const path = (location ?? this.getLocation()) || '';
|
|
114
|
-
if (this
|
|
117
|
+
if (this.#isStaticFile(path))
|
|
115
118
|
return false; // Bypass routing for static files
|
|
116
|
-
for (const route of this
|
|
119
|
+
for (const route of this.#routes) {
|
|
117
120
|
const match = route.pattern ? path.match(route.pattern) : null;
|
|
118
121
|
if (match) {
|
|
119
122
|
match.shift(); // Remove the full match element
|
|
120
123
|
const queryParams = this.getQueryParams();
|
|
121
|
-
const captures = this
|
|
124
|
+
const captures = this.#decodeParams ? match.map(v => safeDecode(v)) : match;
|
|
122
125
|
let params;
|
|
123
126
|
if (route.paramNames && route.paramNames.length) {
|
|
124
127
|
params = {};
|
|
@@ -128,8 +131,8 @@ class Router {
|
|
|
128
131
|
return true;
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
|
-
if (this
|
|
132
|
-
this
|
|
134
|
+
if (this.#notFoundHandler) {
|
|
135
|
+
this.#notFoundHandler(path);
|
|
133
136
|
return true;
|
|
134
137
|
}
|
|
135
138
|
if (path)
|
|
@@ -137,20 +140,20 @@ class Router {
|
|
|
137
140
|
return false;
|
|
138
141
|
}
|
|
139
142
|
listen(mode = 'hash') {
|
|
140
|
-
if (!this
|
|
143
|
+
if (!this.#isBrowser())
|
|
141
144
|
return;
|
|
142
145
|
// avoid duplicate listeners
|
|
143
|
-
if (this
|
|
146
|
+
if (this.#isListening)
|
|
144
147
|
this.unlisten();
|
|
145
148
|
const self = this;
|
|
146
|
-
this
|
|
149
|
+
this.#mode = mode;
|
|
147
150
|
const handler = (path) => {
|
|
148
151
|
const p = path || location.href.split('#')[0];
|
|
149
|
-
if (self
|
|
152
|
+
if (self.#isStaticFile(p))
|
|
150
153
|
return;
|
|
151
154
|
const currentLocation = self.getLocation();
|
|
152
|
-
if (self
|
|
153
|
-
self
|
|
155
|
+
if (self.#baseLocation !== currentLocation) {
|
|
156
|
+
self.#baseLocation = currentLocation;
|
|
154
157
|
self.handleChange(currentLocation);
|
|
155
158
|
}
|
|
156
159
|
};
|
|
@@ -164,9 +167,9 @@ class Router {
|
|
|
164
167
|
return;
|
|
165
168
|
}
|
|
166
169
|
const target = event.target;
|
|
167
|
-
if (!target
|
|
170
|
+
if (!target)
|
|
168
171
|
return;
|
|
169
|
-
const anchor = target.closest
|
|
172
|
+
const anchor = target.closest('a');
|
|
170
173
|
if (!anchor)
|
|
171
174
|
return;
|
|
172
175
|
if (event.type === 'keydown') {
|
|
@@ -191,7 +194,7 @@ class Router {
|
|
|
191
194
|
if (rel && /\bnoreferrer\b/i.test(rel))
|
|
192
195
|
return;
|
|
193
196
|
const pathWithQuery = url.pathname + (url.search || '');
|
|
194
|
-
if (self
|
|
197
|
+
if (self.#isStaticFile(pathWithQuery) || self.#isStaticFile(url.pathname))
|
|
195
198
|
return;
|
|
196
199
|
event.preventDefault();
|
|
197
200
|
history.pushState({ path: pathWithQuery }, '', pathWithQuery);
|
|
@@ -199,53 +202,53 @@ class Router {
|
|
|
199
202
|
};
|
|
200
203
|
switch (mode) {
|
|
201
204
|
case 'hash':
|
|
202
|
-
this
|
|
203
|
-
window.addEventListener('hashchange', this
|
|
205
|
+
this.#bound.hashchange = () => handler();
|
|
206
|
+
window.addEventListener('hashchange', this.#bound.hashchange);
|
|
204
207
|
break;
|
|
205
208
|
case 'history':
|
|
206
|
-
this
|
|
207
|
-
this
|
|
208
|
-
this
|
|
209
|
-
window.addEventListener('popstate', this
|
|
210
|
-
document.addEventListener('click', this
|
|
211
|
-
document.addEventListener('keydown', this
|
|
209
|
+
this.#bound.popstate = (event) => handler(this.getLocation());
|
|
210
|
+
this.#bound.click = handleInternalNavigation;
|
|
211
|
+
this.#bound.keydown = (event) => handleInternalNavigation(event);
|
|
212
|
+
window.addEventListener('popstate', this.#bound.popstate);
|
|
213
|
+
document.addEventListener('click', this.#bound.click);
|
|
214
|
+
document.addEventListener('keydown', this.#bound.keydown);
|
|
212
215
|
break;
|
|
213
216
|
}
|
|
214
|
-
this
|
|
217
|
+
this.#isListening = true;
|
|
215
218
|
handler();
|
|
216
219
|
}
|
|
217
220
|
unlisten() {
|
|
218
|
-
if (!this
|
|
221
|
+
if (!this.#isBrowser() || !this.#isListening)
|
|
219
222
|
return;
|
|
220
|
-
switch (this
|
|
223
|
+
switch (this.#mode) {
|
|
221
224
|
case 'hash':
|
|
222
|
-
if (this
|
|
223
|
-
window.removeEventListener('hashchange', this
|
|
225
|
+
if (this.#bound.hashchange)
|
|
226
|
+
window.removeEventListener('hashchange', this.#bound.hashchange);
|
|
224
227
|
break;
|
|
225
228
|
case 'history':
|
|
226
|
-
if (this
|
|
227
|
-
window.removeEventListener('popstate', this
|
|
228
|
-
if (this
|
|
229
|
-
document.removeEventListener('click', this
|
|
230
|
-
if (this
|
|
231
|
-
document.removeEventListener('keydown', this
|
|
229
|
+
if (this.#bound.popstate)
|
|
230
|
+
window.removeEventListener('popstate', this.#bound.popstate);
|
|
231
|
+
if (this.#bound.click)
|
|
232
|
+
document.removeEventListener('click', this.#bound.click);
|
|
233
|
+
if (this.#bound.keydown)
|
|
234
|
+
document.removeEventListener('keydown', this.#bound.keydown);
|
|
232
235
|
break;
|
|
233
236
|
}
|
|
234
|
-
this
|
|
235
|
-
this
|
|
237
|
+
this.#bound = {};
|
|
238
|
+
this.#isListening = false;
|
|
236
239
|
}
|
|
237
240
|
navigate(path = '', opts) {
|
|
238
|
-
if (!this
|
|
241
|
+
if (!this.#isBrowser())
|
|
239
242
|
return false;
|
|
240
|
-
if (this
|
|
241
|
-
const url = this
|
|
243
|
+
if (this.#mode === 'history') {
|
|
244
|
+
const url = this.#root + this.#cleanPathString(path);
|
|
242
245
|
if (opts?.replace)
|
|
243
246
|
history.replaceState(null, '', url);
|
|
244
247
|
else
|
|
245
248
|
history.pushState(null, '', url);
|
|
246
249
|
}
|
|
247
250
|
else
|
|
248
|
-
window.location.hash = this
|
|
251
|
+
window.location.hash = this.#cleanPathString(path);
|
|
249
252
|
return this.handleChange();
|
|
250
253
|
}
|
|
251
254
|
replace(path = '') {
|
package/dist-cjs/router.d.ts
CHANGED
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
type RouteHandler = (...args: string[]) => void;
|
|
2
2
|
export type RoutingMode = 'history' | 'hash';
|
|
3
3
|
export declare class Router {
|
|
4
|
-
private
|
|
5
|
-
private routes;
|
|
6
|
-
private root;
|
|
7
|
-
private rootCompare;
|
|
8
|
-
private baseLocation;
|
|
4
|
+
#private;
|
|
9
5
|
staticFilters: ((url: string) => boolean)[];
|
|
10
|
-
private isListening;
|
|
11
|
-
private bound;
|
|
12
|
-
private notFoundHandler?;
|
|
13
|
-
private decodeParams;
|
|
14
6
|
constructor();
|
|
15
|
-
private cleanPathString;
|
|
16
|
-
private clearQuery;
|
|
17
|
-
private isStaticFile;
|
|
18
7
|
resetRoot(root: string): void;
|
|
8
|
+
setMode(mode: RoutingMode): void;
|
|
19
9
|
getLocation(): string;
|
|
20
10
|
add(pattern: RegExp | string | RouteHandler, handler?: RouteHandler): Router;
|
|
21
11
|
onNotFound(handler: (path: string) => void): Router;
|
|
22
12
|
setDecodeParams(decode: boolean): Router;
|
|
23
13
|
getQueryParams(search?: string): Record<string, string>;
|
|
24
|
-
private isBrowser;
|
|
25
|
-
private compilePattern;
|
|
26
14
|
/**
|
|
27
15
|
*
|
|
28
16
|
* @param location
|
|
@@ -6,28 +6,30 @@ const router_1 = require("./router");
|
|
|
6
6
|
const pubsub_1 = require("@dharmax/pubsub");
|
|
7
7
|
class StateManager {
|
|
8
8
|
mode;
|
|
9
|
-
allStates = {};
|
|
10
|
-
appState;
|
|
11
|
-
previousState;
|
|
12
|
-
stateContext;
|
|
9
|
+
#allStates = {};
|
|
10
|
+
#appState = null;
|
|
11
|
+
#previousState = null;
|
|
12
|
+
#stateContext;
|
|
13
|
+
#currentTransitionId = 0;
|
|
13
14
|
static dispatcher = pubsub_1.default;
|
|
14
|
-
changeAuthorities = [];
|
|
15
|
-
router;
|
|
16
|
-
beforeChangeHandlers = [];
|
|
17
|
-
afterChangeHandlers = [];
|
|
15
|
+
#changeAuthorities = [];
|
|
16
|
+
#router;
|
|
17
|
+
#beforeChangeHandlers = [];
|
|
18
|
+
#afterChangeHandlers = [];
|
|
18
19
|
constructor(mode = 'hash', autostart = true, routerInstance = router_1.router) {
|
|
19
20
|
this.mode = mode;
|
|
20
|
-
this
|
|
21
|
+
this.#router = routerInstance;
|
|
22
|
+
this.#router.setMode(mode);
|
|
21
23
|
if (autostart)
|
|
22
|
-
this
|
|
24
|
+
this.#router.listen(mode);
|
|
23
25
|
}
|
|
24
26
|
start() {
|
|
25
|
-
this
|
|
27
|
+
this.#router.listen(this.mode);
|
|
26
28
|
}
|
|
27
29
|
stop() {
|
|
28
30
|
// unlisten router to cleanup event listeners
|
|
29
31
|
// @ts-ignore - older Router versions may not have unlisten
|
|
30
|
-
this
|
|
32
|
+
this.#router.unlisten && this.#router.unlisten();
|
|
31
33
|
}
|
|
32
34
|
onChange(handler) {
|
|
33
35
|
return StateManager.dispatcher.on('state:changed', handler);
|
|
@@ -38,16 +40,16 @@ class StateManager {
|
|
|
38
40
|
requested doesn't happen.
|
|
39
41
|
**/
|
|
40
42
|
registerChangeAuthority(authorityCallback) {
|
|
41
|
-
this
|
|
43
|
+
this.#changeAuthorities.push(authorityCallback);
|
|
42
44
|
}
|
|
43
45
|
getState() {
|
|
44
|
-
return this
|
|
46
|
+
return this.#appState || {};
|
|
45
47
|
}
|
|
46
48
|
get previous() {
|
|
47
|
-
return this
|
|
49
|
+
return this.#previousState;
|
|
48
50
|
}
|
|
49
51
|
get context() {
|
|
50
|
-
return this
|
|
52
|
+
return this.#stateContext;
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* set current page state
|
|
@@ -56,16 +58,21 @@ class StateManager {
|
|
|
56
58
|
set state(state) {
|
|
57
59
|
if (Array.isArray(state)) {
|
|
58
60
|
const sName = state.shift();
|
|
59
|
-
this.setState(sName, state);
|
|
61
|
+
this.setState(sName, state.length === 1 ? state[0] : state);
|
|
60
62
|
}
|
|
61
63
|
else
|
|
62
64
|
this.setState(state);
|
|
63
65
|
}
|
|
64
|
-
/** attempts to restore state from current url.
|
|
66
|
+
/** attempts to restore state from current url. */
|
|
65
67
|
restoreState(defaultState) {
|
|
66
|
-
if (this
|
|
68
|
+
if (this.#router.handleChange())
|
|
67
69
|
return;
|
|
68
|
-
this
|
|
70
|
+
const state = this.#allStates[defaultState];
|
|
71
|
+
let path = defaultState;
|
|
72
|
+
if (state && typeof state.route === 'string') {
|
|
73
|
+
path = state.route;
|
|
74
|
+
}
|
|
75
|
+
this.#router.navigate(path);
|
|
69
76
|
}
|
|
70
77
|
/**
|
|
71
78
|
*
|
|
@@ -73,24 +80,33 @@ class StateManager {
|
|
|
73
80
|
* @param context extra context (e.g. sub-state)
|
|
74
81
|
*/
|
|
75
82
|
async setState(stateName, context) {
|
|
76
|
-
const
|
|
83
|
+
const transitionId = ++this.#currentTransitionId;
|
|
84
|
+
const newState = this.#allStates[stateName];
|
|
77
85
|
if (!newState) {
|
|
78
|
-
|
|
79
|
-
return false;
|
|
86
|
+
throw new Error(`Undefined app state ${stateName}`);
|
|
80
87
|
}
|
|
81
88
|
// check if the state change was declined by any change authority and if so - don't do it and return false
|
|
82
|
-
const changeConfirmations = await Promise.all(this
|
|
83
|
-
|
|
89
|
+
const changeConfirmations = await Promise.all(this.#changeAuthorities.map(authority => authority(newState)));
|
|
90
|
+
if (transitionId !== this.#currentTransitionId)
|
|
91
|
+
return false;
|
|
92
|
+
const vetoes = await Promise.all(this.#beforeChangeHandlers.map(h => Promise.resolve(h(newState, context))));
|
|
93
|
+
if (transitionId !== this.#currentTransitionId)
|
|
94
|
+
return false;
|
|
84
95
|
if (changeConfirmations.includes(false) || vetoes.includes(false))
|
|
85
96
|
return false;
|
|
97
|
+
if (this.#appState && this.#appState.onExit) {
|
|
98
|
+
await this.#appState.onExit(this.#stateContext);
|
|
99
|
+
if (transitionId !== this.#currentTransitionId)
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
86
102
|
// perform the change
|
|
87
|
-
this
|
|
88
|
-
this
|
|
89
|
-
this
|
|
90
|
-
pubsub_1.default.trigger('state-manager', 'state', 'changed', this
|
|
103
|
+
this.#previousState = this.#appState;
|
|
104
|
+
this.#stateContext = context;
|
|
105
|
+
this.#appState = newState;
|
|
106
|
+
pubsub_1.default.trigger('state-manager', 'state', 'changed', this.#appState);
|
|
91
107
|
// afterChange hooks
|
|
92
|
-
for (const cb of this
|
|
93
|
-
await cb(this
|
|
108
|
+
for (const cb of this.#afterChangeHandlers) {
|
|
109
|
+
await cb(this.#appState, context, this.#previousState);
|
|
94
110
|
}
|
|
95
111
|
return true;
|
|
96
112
|
}
|
|
@@ -117,9 +133,9 @@ class StateManager {
|
|
|
117
133
|
});
|
|
118
134
|
}
|
|
119
135
|
registerStateByState(state) {
|
|
120
|
-
this
|
|
136
|
+
this.#allStates[state.name] = state;
|
|
121
137
|
const self = this;
|
|
122
|
-
this
|
|
138
|
+
this.#router.add(state.route, async function (...captures) {
|
|
123
139
|
// prefer named params (from router string patterns), else multi-captures array, else first capture
|
|
124
140
|
// @ts-ignore
|
|
125
141
|
const named = (this && this.params) || null;
|
|
@@ -133,15 +149,21 @@ class StateManager {
|
|
|
133
149
|
});
|
|
134
150
|
}
|
|
135
151
|
onBeforeChange(handler) {
|
|
136
|
-
this
|
|
152
|
+
this.#beforeChangeHandlers.push(handler);
|
|
137
153
|
}
|
|
138
154
|
onAfterChange(handler) {
|
|
139
|
-
this
|
|
155
|
+
this.#afterChangeHandlers.push(handler);
|
|
156
|
+
}
|
|
157
|
+
onExit(stateName, handler) {
|
|
158
|
+
const state = this.#allStates[stateName];
|
|
159
|
+
if (state) {
|
|
160
|
+
state.onExit = handler;
|
|
161
|
+
}
|
|
140
162
|
}
|
|
141
163
|
onNotFound(handler) {
|
|
142
164
|
// proxy to router-level notFound
|
|
143
165
|
// @ts-ignore - older Router versions may not have onNotFound
|
|
144
|
-
this
|
|
166
|
+
this.#router.onNotFound && this.#router.onNotFound(handler);
|
|
145
167
|
}
|
|
146
168
|
}
|
|
147
169
|
exports.StateManager = StateManager;
|
|
@@ -6,33 +6,27 @@ export type ApplicationState = {
|
|
|
6
6
|
pageName: string;
|
|
7
7
|
route: RegExp | string;
|
|
8
8
|
mode?: string | string[];
|
|
9
|
+
onExit?: (context?: any) => void | Promise<void>;
|
|
9
10
|
};
|
|
10
11
|
export type ChangeAuthority = (state: ApplicationState) => Promise<boolean>;
|
|
11
12
|
export declare class StateManager {
|
|
13
|
+
#private;
|
|
12
14
|
private mode;
|
|
13
|
-
private allStates;
|
|
14
|
-
private appState;
|
|
15
|
-
private previousState;
|
|
16
|
-
private stateContext;
|
|
17
15
|
static dispatcher: import("@dharmax/pubsub").PubSub;
|
|
18
|
-
private changeAuthorities;
|
|
19
|
-
private router;
|
|
20
|
-
private beforeChangeHandlers;
|
|
21
|
-
private afterChangeHandlers;
|
|
22
16
|
constructor(mode?: RoutingMode, autostart?: boolean, routerInstance?: RouterType);
|
|
23
17
|
start(): void;
|
|
24
18
|
stop(): void;
|
|
25
19
|
onChange(handler: (event: PubSubEvent, data: any) => void): IPubSubHandle;
|
|
26
20
|
registerChangeAuthority(authorityCallback: (targetState: ApplicationState) => Promise<boolean>): void;
|
|
27
21
|
getState(): ApplicationState;
|
|
28
|
-
get previous(): ApplicationState;
|
|
22
|
+
get previous(): ApplicationState | null;
|
|
29
23
|
get context(): any;
|
|
30
24
|
/**
|
|
31
25
|
* set current page state
|
|
32
26
|
* @param state can be either just a state or a state and context (which can be sub-state, or anything else)
|
|
33
27
|
*/
|
|
34
28
|
set state(state: ApplicationStateName | [ApplicationStateName, ...any]);
|
|
35
|
-
/** attempts to restore state from current url.
|
|
29
|
+
/** attempts to restore state from current url. */
|
|
36
30
|
restoreState(defaultState: ApplicationStateName): void;
|
|
37
31
|
/**
|
|
38
32
|
*
|
|
@@ -50,7 +44,8 @@ export declare class StateManager {
|
|
|
50
44
|
addState(name: string, pageName?: string, route?: RegExp | string, mode?: string | string[]): void;
|
|
51
45
|
registerStateByState(state: ApplicationState): void;
|
|
52
46
|
onBeforeChange(handler: (target: ApplicationState, context?: any) => boolean | Promise<boolean>): void;
|
|
53
|
-
onAfterChange(handler: (state: ApplicationState, context?: any, previous?: ApplicationState) => void | Promise<void>): void;
|
|
47
|
+
onAfterChange(handler: (state: ApplicationState, context?: any, previous?: ApplicationState | null) => void | Promise<void>): void;
|
|
48
|
+
onExit(stateName: ApplicationStateName, handler: (context?: any) => void | Promise<void>): void;
|
|
54
49
|
onNotFound(handler: (path: string) => void): void;
|
|
55
50
|
}
|
|
56
51
|
export declare function createStateManager(mode?: RoutingMode, autostart?: boolean, routerInstance?: RouterType): StateManager;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dharmax/state-router",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "A cute and tight router and application state controller",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist-cjs/index.cjs",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "vitest run --coverage",
|
|
19
|
+
"test": "vitest run --coverage && sed -i \"s/<a name='/<a id='/g\" coverage/*.html",
|
|
20
20
|
"test:watch": "vitest",
|
|
21
21
|
"build": "npm run build:esm && npm run build:cjs",
|
|
22
22
|
"build:esm": "npx tsc",
|