@dharmax/state-router 3.2.1 → 4.0.2
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 +3 -14
- package/dist/router.js +91 -69
- package/dist/state-manager.d.ts +6 -11
- package/dist/state-manager.js +58 -36
- package/dist-cjs/router.cjs +91 -69
- package/dist-cjs/router.d.ts +3 -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,17 @@
|
|
|
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;
|
|
13
|
+
willHandle(path: string): boolean;
|
|
23
14
|
getQueryParams(search?: string): Record<string, string>;
|
|
24
|
-
private isBrowser;
|
|
25
|
-
private compilePattern;
|
|
26
15
|
/**
|
|
27
16
|
*
|
|
28
17
|
* @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,39 @@ 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
|
}
|
|
81
|
+
willHandle(path) {
|
|
82
|
+
path = decodeURI(path);
|
|
83
|
+
path = this.#clearQuery(path);
|
|
84
|
+
path = path.replace(/^\//, '');
|
|
85
|
+
if (this.#root !== '/' && this.#rootCompare && path.startsWith(this.#rootCompare)) {
|
|
86
|
+
path = path.slice(this.#rootCompare.length);
|
|
87
|
+
}
|
|
88
|
+
path = this.#cleanPathString(path);
|
|
89
|
+
if (this.#isStaticFile(path))
|
|
90
|
+
return false;
|
|
91
|
+
for (const route of this.#routes) {
|
|
92
|
+
if (route.pattern && route.pattern.test(path))
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return !!this.#notFoundHandler;
|
|
96
|
+
}
|
|
78
97
|
getQueryParams(search) {
|
|
79
|
-
if (!this
|
|
98
|
+
if (!this.#isBrowser() && !search)
|
|
80
99
|
return {};
|
|
81
100
|
const qs = typeof search === 'string' ? search : window.location.search || '';
|
|
82
101
|
const usp = new URLSearchParams(qs);
|
|
@@ -84,10 +103,10 @@ export class Router {
|
|
|
84
103
|
usp.forEach((v, k) => { obj[k] = v; });
|
|
85
104
|
return obj;
|
|
86
105
|
}
|
|
87
|
-
isBrowser() {
|
|
106
|
+
#isBrowser() {
|
|
88
107
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
89
108
|
}
|
|
90
|
-
compilePattern(pattern) {
|
|
109
|
+
#compilePattern(pattern) {
|
|
91
110
|
// normalize leading slash to align with cleanPathString behavior
|
|
92
111
|
const normalized = pattern.replace(/^\//, '');
|
|
93
112
|
const paramNames = [];
|
|
@@ -107,14 +126,14 @@ export class Router {
|
|
|
107
126
|
*/
|
|
108
127
|
handleChange(location) {
|
|
109
128
|
const path = (location ?? this.getLocation()) || '';
|
|
110
|
-
if (this
|
|
129
|
+
if (this.#isStaticFile(path))
|
|
111
130
|
return false; // Bypass routing for static files
|
|
112
|
-
for (const route of this
|
|
131
|
+
for (const route of this.#routes) {
|
|
113
132
|
const match = route.pattern ? path.match(route.pattern) : null;
|
|
114
133
|
if (match) {
|
|
115
134
|
match.shift(); // Remove the full match element
|
|
116
135
|
const queryParams = this.getQueryParams();
|
|
117
|
-
const captures = this
|
|
136
|
+
const captures = this.#decodeParams ? match.map(v => safeDecode(v)) : match;
|
|
118
137
|
let params;
|
|
119
138
|
if (route.paramNames && route.paramNames.length) {
|
|
120
139
|
params = {};
|
|
@@ -124,8 +143,8 @@ export class Router {
|
|
|
124
143
|
return true;
|
|
125
144
|
}
|
|
126
145
|
}
|
|
127
|
-
if (this
|
|
128
|
-
this
|
|
146
|
+
if (this.#notFoundHandler) {
|
|
147
|
+
this.#notFoundHandler(path);
|
|
129
148
|
return true;
|
|
130
149
|
}
|
|
131
150
|
if (path)
|
|
@@ -133,20 +152,20 @@ export class Router {
|
|
|
133
152
|
return false;
|
|
134
153
|
}
|
|
135
154
|
listen(mode = 'hash') {
|
|
136
|
-
if (!this
|
|
155
|
+
if (!this.#isBrowser())
|
|
137
156
|
return;
|
|
138
157
|
// avoid duplicate listeners
|
|
139
|
-
if (this
|
|
158
|
+
if (this.#isListening)
|
|
140
159
|
this.unlisten();
|
|
141
160
|
const self = this;
|
|
142
|
-
this
|
|
161
|
+
this.#mode = mode;
|
|
143
162
|
const handler = (path) => {
|
|
144
163
|
const p = path || location.href.split('#')[0];
|
|
145
|
-
if (self
|
|
164
|
+
if (self.#isStaticFile(p))
|
|
146
165
|
return;
|
|
147
166
|
const currentLocation = self.getLocation();
|
|
148
|
-
if (self
|
|
149
|
-
self
|
|
167
|
+
if (self.#baseLocation !== currentLocation) {
|
|
168
|
+
self.#baseLocation = currentLocation;
|
|
150
169
|
self.handleChange(currentLocation);
|
|
151
170
|
}
|
|
152
171
|
};
|
|
@@ -160,9 +179,9 @@ export class Router {
|
|
|
160
179
|
return;
|
|
161
180
|
}
|
|
162
181
|
const target = event.target;
|
|
163
|
-
if (!target
|
|
182
|
+
if (!target)
|
|
164
183
|
return;
|
|
165
|
-
const anchor = target.closest
|
|
184
|
+
const anchor = target.closest('a');
|
|
166
185
|
if (!anchor)
|
|
167
186
|
return;
|
|
168
187
|
if (event.type === 'keydown') {
|
|
@@ -187,7 +206,10 @@ export class Router {
|
|
|
187
206
|
if (rel && /\bnoreferrer\b/i.test(rel))
|
|
188
207
|
return;
|
|
189
208
|
const pathWithQuery = url.pathname + (url.search || '');
|
|
190
|
-
if (self
|
|
209
|
+
if (self.#isStaticFile(pathWithQuery) || self.#isStaticFile(url.pathname))
|
|
210
|
+
return;
|
|
211
|
+
// Only intercept if we have a handler for this route
|
|
212
|
+
if (!self.willHandle(pathWithQuery))
|
|
191
213
|
return;
|
|
192
214
|
event.preventDefault();
|
|
193
215
|
history.pushState({ path: pathWithQuery }, '', pathWithQuery);
|
|
@@ -195,53 +217,53 @@ export class Router {
|
|
|
195
217
|
};
|
|
196
218
|
switch (mode) {
|
|
197
219
|
case 'hash':
|
|
198
|
-
this
|
|
199
|
-
window.addEventListener('hashchange', this
|
|
220
|
+
this.#bound.hashchange = () => handler();
|
|
221
|
+
window.addEventListener('hashchange', this.#bound.hashchange);
|
|
200
222
|
break;
|
|
201
223
|
case 'history':
|
|
202
|
-
this
|
|
203
|
-
this
|
|
204
|
-
this
|
|
205
|
-
window.addEventListener('popstate', this
|
|
206
|
-
document.addEventListener('click', this
|
|
207
|
-
document.addEventListener('keydown', this
|
|
224
|
+
this.#bound.popstate = (event) => handler(this.getLocation());
|
|
225
|
+
this.#bound.click = handleInternalNavigation;
|
|
226
|
+
this.#bound.keydown = (event) => handleInternalNavigation(event);
|
|
227
|
+
window.addEventListener('popstate', this.#bound.popstate);
|
|
228
|
+
document.addEventListener('click', this.#bound.click);
|
|
229
|
+
document.addEventListener('keydown', this.#bound.keydown);
|
|
208
230
|
break;
|
|
209
231
|
}
|
|
210
|
-
this
|
|
232
|
+
this.#isListening = true;
|
|
211
233
|
handler();
|
|
212
234
|
}
|
|
213
235
|
unlisten() {
|
|
214
|
-
if (!this
|
|
236
|
+
if (!this.#isBrowser() || !this.#isListening)
|
|
215
237
|
return;
|
|
216
|
-
switch (this
|
|
238
|
+
switch (this.#mode) {
|
|
217
239
|
case 'hash':
|
|
218
|
-
if (this
|
|
219
|
-
window.removeEventListener('hashchange', this
|
|
240
|
+
if (this.#bound.hashchange)
|
|
241
|
+
window.removeEventListener('hashchange', this.#bound.hashchange);
|
|
220
242
|
break;
|
|
221
243
|
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
|
|
244
|
+
if (this.#bound.popstate)
|
|
245
|
+
window.removeEventListener('popstate', this.#bound.popstate);
|
|
246
|
+
if (this.#bound.click)
|
|
247
|
+
document.removeEventListener('click', this.#bound.click);
|
|
248
|
+
if (this.#bound.keydown)
|
|
249
|
+
document.removeEventListener('keydown', this.#bound.keydown);
|
|
228
250
|
break;
|
|
229
251
|
}
|
|
230
|
-
this
|
|
231
|
-
this
|
|
252
|
+
this.#bound = {};
|
|
253
|
+
this.#isListening = false;
|
|
232
254
|
}
|
|
233
255
|
navigate(path = '', opts) {
|
|
234
|
-
if (!this
|
|
256
|
+
if (!this.#isBrowser())
|
|
235
257
|
return false;
|
|
236
|
-
if (this
|
|
237
|
-
const url = this
|
|
258
|
+
if (this.#mode === 'history') {
|
|
259
|
+
const url = this.#root + this.#cleanPathString(path);
|
|
238
260
|
if (opts?.replace)
|
|
239
261
|
history.replaceState(null, '', url);
|
|
240
262
|
else
|
|
241
263
|
history.pushState(null, '', url);
|
|
242
264
|
}
|
|
243
265
|
else
|
|
244
|
-
window.location.hash = this
|
|
266
|
+
window.location.hash = this.#cleanPathString(path);
|
|
245
267
|
return this.handleChange();
|
|
246
268
|
}
|
|
247
269
|
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,39 @@ 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
|
}
|
|
85
|
+
willHandle(path) {
|
|
86
|
+
path = decodeURI(path);
|
|
87
|
+
path = this.#clearQuery(path);
|
|
88
|
+
path = path.replace(/^\//, '');
|
|
89
|
+
if (this.#root !== '/' && this.#rootCompare && path.startsWith(this.#rootCompare)) {
|
|
90
|
+
path = path.slice(this.#rootCompare.length);
|
|
91
|
+
}
|
|
92
|
+
path = this.#cleanPathString(path);
|
|
93
|
+
if (this.#isStaticFile(path))
|
|
94
|
+
return false;
|
|
95
|
+
for (const route of this.#routes) {
|
|
96
|
+
if (route.pattern && route.pattern.test(path))
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return !!this.#notFoundHandler;
|
|
100
|
+
}
|
|
82
101
|
getQueryParams(search) {
|
|
83
|
-
if (!this
|
|
102
|
+
if (!this.#isBrowser() && !search)
|
|
84
103
|
return {};
|
|
85
104
|
const qs = typeof search === 'string' ? search : window.location.search || '';
|
|
86
105
|
const usp = new URLSearchParams(qs);
|
|
@@ -88,10 +107,10 @@ class Router {
|
|
|
88
107
|
usp.forEach((v, k) => { obj[k] = v; });
|
|
89
108
|
return obj;
|
|
90
109
|
}
|
|
91
|
-
isBrowser() {
|
|
110
|
+
#isBrowser() {
|
|
92
111
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
93
112
|
}
|
|
94
|
-
compilePattern(pattern) {
|
|
113
|
+
#compilePattern(pattern) {
|
|
95
114
|
// normalize leading slash to align with cleanPathString behavior
|
|
96
115
|
const normalized = pattern.replace(/^\//, '');
|
|
97
116
|
const paramNames = [];
|
|
@@ -111,14 +130,14 @@ class Router {
|
|
|
111
130
|
*/
|
|
112
131
|
handleChange(location) {
|
|
113
132
|
const path = (location ?? this.getLocation()) || '';
|
|
114
|
-
if (this
|
|
133
|
+
if (this.#isStaticFile(path))
|
|
115
134
|
return false; // Bypass routing for static files
|
|
116
|
-
for (const route of this
|
|
135
|
+
for (const route of this.#routes) {
|
|
117
136
|
const match = route.pattern ? path.match(route.pattern) : null;
|
|
118
137
|
if (match) {
|
|
119
138
|
match.shift(); // Remove the full match element
|
|
120
139
|
const queryParams = this.getQueryParams();
|
|
121
|
-
const captures = this
|
|
140
|
+
const captures = this.#decodeParams ? match.map(v => safeDecode(v)) : match;
|
|
122
141
|
let params;
|
|
123
142
|
if (route.paramNames && route.paramNames.length) {
|
|
124
143
|
params = {};
|
|
@@ -128,8 +147,8 @@ class Router {
|
|
|
128
147
|
return true;
|
|
129
148
|
}
|
|
130
149
|
}
|
|
131
|
-
if (this
|
|
132
|
-
this
|
|
150
|
+
if (this.#notFoundHandler) {
|
|
151
|
+
this.#notFoundHandler(path);
|
|
133
152
|
return true;
|
|
134
153
|
}
|
|
135
154
|
if (path)
|
|
@@ -137,20 +156,20 @@ class Router {
|
|
|
137
156
|
return false;
|
|
138
157
|
}
|
|
139
158
|
listen(mode = 'hash') {
|
|
140
|
-
if (!this
|
|
159
|
+
if (!this.#isBrowser())
|
|
141
160
|
return;
|
|
142
161
|
// avoid duplicate listeners
|
|
143
|
-
if (this
|
|
162
|
+
if (this.#isListening)
|
|
144
163
|
this.unlisten();
|
|
145
164
|
const self = this;
|
|
146
|
-
this
|
|
165
|
+
this.#mode = mode;
|
|
147
166
|
const handler = (path) => {
|
|
148
167
|
const p = path || location.href.split('#')[0];
|
|
149
|
-
if (self
|
|
168
|
+
if (self.#isStaticFile(p))
|
|
150
169
|
return;
|
|
151
170
|
const currentLocation = self.getLocation();
|
|
152
|
-
if (self
|
|
153
|
-
self
|
|
171
|
+
if (self.#baseLocation !== currentLocation) {
|
|
172
|
+
self.#baseLocation = currentLocation;
|
|
154
173
|
self.handleChange(currentLocation);
|
|
155
174
|
}
|
|
156
175
|
};
|
|
@@ -164,9 +183,9 @@ class Router {
|
|
|
164
183
|
return;
|
|
165
184
|
}
|
|
166
185
|
const target = event.target;
|
|
167
|
-
if (!target
|
|
186
|
+
if (!target)
|
|
168
187
|
return;
|
|
169
|
-
const anchor = target.closest
|
|
188
|
+
const anchor = target.closest('a');
|
|
170
189
|
if (!anchor)
|
|
171
190
|
return;
|
|
172
191
|
if (event.type === 'keydown') {
|
|
@@ -191,7 +210,10 @@ class Router {
|
|
|
191
210
|
if (rel && /\bnoreferrer\b/i.test(rel))
|
|
192
211
|
return;
|
|
193
212
|
const pathWithQuery = url.pathname + (url.search || '');
|
|
194
|
-
if (self
|
|
213
|
+
if (self.#isStaticFile(pathWithQuery) || self.#isStaticFile(url.pathname))
|
|
214
|
+
return;
|
|
215
|
+
// Only intercept if we have a handler for this route
|
|
216
|
+
if (!self.willHandle(pathWithQuery))
|
|
195
217
|
return;
|
|
196
218
|
event.preventDefault();
|
|
197
219
|
history.pushState({ path: pathWithQuery }, '', pathWithQuery);
|
|
@@ -199,53 +221,53 @@ class Router {
|
|
|
199
221
|
};
|
|
200
222
|
switch (mode) {
|
|
201
223
|
case 'hash':
|
|
202
|
-
this
|
|
203
|
-
window.addEventListener('hashchange', this
|
|
224
|
+
this.#bound.hashchange = () => handler();
|
|
225
|
+
window.addEventListener('hashchange', this.#bound.hashchange);
|
|
204
226
|
break;
|
|
205
227
|
case 'history':
|
|
206
|
-
this
|
|
207
|
-
this
|
|
208
|
-
this
|
|
209
|
-
window.addEventListener('popstate', this
|
|
210
|
-
document.addEventListener('click', this
|
|
211
|
-
document.addEventListener('keydown', this
|
|
228
|
+
this.#bound.popstate = (event) => handler(this.getLocation());
|
|
229
|
+
this.#bound.click = handleInternalNavigation;
|
|
230
|
+
this.#bound.keydown = (event) => handleInternalNavigation(event);
|
|
231
|
+
window.addEventListener('popstate', this.#bound.popstate);
|
|
232
|
+
document.addEventListener('click', this.#bound.click);
|
|
233
|
+
document.addEventListener('keydown', this.#bound.keydown);
|
|
212
234
|
break;
|
|
213
235
|
}
|
|
214
|
-
this
|
|
236
|
+
this.#isListening = true;
|
|
215
237
|
handler();
|
|
216
238
|
}
|
|
217
239
|
unlisten() {
|
|
218
|
-
if (!this
|
|
240
|
+
if (!this.#isBrowser() || !this.#isListening)
|
|
219
241
|
return;
|
|
220
|
-
switch (this
|
|
242
|
+
switch (this.#mode) {
|
|
221
243
|
case 'hash':
|
|
222
|
-
if (this
|
|
223
|
-
window.removeEventListener('hashchange', this
|
|
244
|
+
if (this.#bound.hashchange)
|
|
245
|
+
window.removeEventListener('hashchange', this.#bound.hashchange);
|
|
224
246
|
break;
|
|
225
247
|
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
|
|
248
|
+
if (this.#bound.popstate)
|
|
249
|
+
window.removeEventListener('popstate', this.#bound.popstate);
|
|
250
|
+
if (this.#bound.click)
|
|
251
|
+
document.removeEventListener('click', this.#bound.click);
|
|
252
|
+
if (this.#bound.keydown)
|
|
253
|
+
document.removeEventListener('keydown', this.#bound.keydown);
|
|
232
254
|
break;
|
|
233
255
|
}
|
|
234
|
-
this
|
|
235
|
-
this
|
|
256
|
+
this.#bound = {};
|
|
257
|
+
this.#isListening = false;
|
|
236
258
|
}
|
|
237
259
|
navigate(path = '', opts) {
|
|
238
|
-
if (!this
|
|
260
|
+
if (!this.#isBrowser())
|
|
239
261
|
return false;
|
|
240
|
-
if (this
|
|
241
|
-
const url = this
|
|
262
|
+
if (this.#mode === 'history') {
|
|
263
|
+
const url = this.#root + this.#cleanPathString(path);
|
|
242
264
|
if (opts?.replace)
|
|
243
265
|
history.replaceState(null, '', url);
|
|
244
266
|
else
|
|
245
267
|
history.pushState(null, '', url);
|
|
246
268
|
}
|
|
247
269
|
else
|
|
248
|
-
window.location.hash = this
|
|
270
|
+
window.location.hash = this.#cleanPathString(path);
|
|
249
271
|
return this.handleChange();
|
|
250
272
|
}
|
|
251
273
|
replace(path = '') {
|
package/dist-cjs/router.d.ts
CHANGED
|
@@ -1,28 +1,17 @@
|
|
|
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;
|
|
13
|
+
willHandle(path: string): boolean;
|
|
23
14
|
getQueryParams(search?: string): Record<string, string>;
|
|
24
|
-
private isBrowser;
|
|
25
|
-
private compilePattern;
|
|
26
15
|
/**
|
|
27
16
|
*
|
|
28
17
|
* @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.2",
|
|
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",
|