@dharmax/state-router 2.0.1 → 2.1.0
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 +10 -5
- package/dist/router.js +55 -22
- package/dist/state-manager.js +4 -6
- package/package.json +1 -1
- package/src/router.ts +66 -28
- package/src/state-manager.ts +4 -7
- package/test/index.html +21 -0
package/dist/router.d.ts
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
type RouteHandler = (...args: string[]) => void;
|
|
2
2
|
export type RoutingMode = 'history' | 'hash';
|
|
3
3
|
declare class Router {
|
|
4
|
-
mode
|
|
4
|
+
private mode;
|
|
5
5
|
private routes;
|
|
6
6
|
private root;
|
|
7
7
|
private baseLocation;
|
|
8
8
|
staticFilters: ((url: string) => boolean)[];
|
|
9
9
|
constructor();
|
|
10
|
-
private
|
|
10
|
+
private cleanPathString;
|
|
11
11
|
private clearQuery;
|
|
12
12
|
private isStaticFile;
|
|
13
13
|
resetRoot(root: string): void;
|
|
14
14
|
getLocation(): string;
|
|
15
15
|
add(pattern: RegExp | RouteHandler, handler?: RouteHandler): Router;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @param location
|
|
19
|
+
* @return true if it was intercepted or false if not handled
|
|
20
|
+
*/
|
|
21
|
+
handleChange(location?: string): boolean;
|
|
22
|
+
listen(mode?: RoutingMode): void;
|
|
23
|
+
navigate(path?: string): boolean;
|
|
19
24
|
}
|
|
20
25
|
export declare const router: Router;
|
|
21
26
|
export {};
|
package/dist/router.js
CHANGED
|
@@ -6,13 +6,13 @@ class Router {
|
|
|
6
6
|
staticFilters = [];
|
|
7
7
|
constructor() {
|
|
8
8
|
this.staticFilters.push(url => {
|
|
9
|
-
const staticFileExtensions = ['.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp', 'md'];
|
|
9
|
+
const staticFileExtensions = ['.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp', '.md', '.ejs', '.jsm', '.txt'];
|
|
10
10
|
return staticFileExtensions.some(ext => url.endsWith(ext));
|
|
11
11
|
});
|
|
12
|
-
window.addEventListener(this.mode === 'hash' ? 'hashchange' : 'popstate', this.listen.bind(this));
|
|
13
12
|
}
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
cleanPathString(path) {
|
|
14
|
+
path = path.replace(/\/$/, '').replace(/^\//, '');
|
|
15
|
+
return path = path.replace(/#{2,}/g, '#');
|
|
16
16
|
}
|
|
17
17
|
clearQuery(url) {
|
|
18
18
|
const [path, query] = url.split('?');
|
|
@@ -25,11 +25,11 @@ class Router {
|
|
|
25
25
|
return (this.staticFilters || []).some(filter => filter(url));
|
|
26
26
|
}
|
|
27
27
|
resetRoot(root) {
|
|
28
|
-
this.root = '/' + this.
|
|
28
|
+
this.root = '/' + this.cleanPathString(root) + '/';
|
|
29
29
|
}
|
|
30
30
|
getLocation() {
|
|
31
31
|
if (this.mode === 'history') {
|
|
32
|
-
let fragment = this.
|
|
32
|
+
let fragment = this.cleanPathString(decodeURI(window.location.pathname + window.location.search));
|
|
33
33
|
fragment = this.clearQuery(fragment);
|
|
34
34
|
return this.root !== '/' ? fragment.replace(this.root, '') : fragment;
|
|
35
35
|
}
|
|
@@ -46,36 +46,69 @@ class Router {
|
|
|
46
46
|
this.routes.push({ pattern, handler: handler });
|
|
47
47
|
return this;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
*
|
|
51
|
+
* @param location
|
|
52
|
+
* @return true if it was intercepted or false if not handled
|
|
53
|
+
*/
|
|
54
|
+
handleChange(location) {
|
|
50
55
|
const path = location || this.getLocation();
|
|
51
56
|
if (this.isStaticFile(path))
|
|
52
|
-
return
|
|
57
|
+
return false; // Bypass routing for static files
|
|
53
58
|
for (const route of this.routes) {
|
|
54
59
|
const match = path.match(route.pattern);
|
|
55
60
|
if (match) {
|
|
56
61
|
match.shift(); // Remove the full match element
|
|
57
62
|
route.handler.apply({}, match);
|
|
58
|
-
return
|
|
63
|
+
return true;
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
console.warn(`No routing found for ${path}`);
|
|
62
|
-
return
|
|
67
|
+
return false;
|
|
63
68
|
}
|
|
64
|
-
listen() {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
listen(mode = 'hash') {
|
|
70
|
+
const self = this;
|
|
71
|
+
this.mode = mode;
|
|
72
|
+
switch (mode) {
|
|
73
|
+
case "hash":
|
|
74
|
+
window.addEventListener('hashchange', () => handler());
|
|
75
|
+
break;
|
|
76
|
+
case "history":
|
|
77
|
+
window.addEventListener('popstate', event => handler(event.state?.path));
|
|
78
|
+
document.addEventListener('click', handleInternalNavigation);
|
|
79
|
+
document.addEventListener('keydown', event => {
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
if (event.key === 'Enter' && event.target.tagName === 'A')
|
|
82
|
+
handleInternalNavigation(event);
|
|
83
|
+
});
|
|
69
84
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
function handleInternalNavigation(event) {
|
|
86
|
+
const node = event.target;
|
|
87
|
+
const href = node.getAttribute('href');
|
|
88
|
+
if (href) {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
history.pushState({ path: href }, '', href);
|
|
91
|
+
handler(href);
|
|
92
|
+
}
|
|
74
93
|
}
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
function handler(path) {
|
|
95
|
+
path = path || location.href.split('#')[0];
|
|
96
|
+
if (self.isStaticFile(path))
|
|
97
|
+
return;
|
|
98
|
+
const currentLocation = self.getLocation();
|
|
99
|
+
if (self.baseLocation !== currentLocation) {
|
|
100
|
+
self.baseLocation = currentLocation;
|
|
101
|
+
self.handleChange(currentLocation);
|
|
102
|
+
}
|
|
77
103
|
}
|
|
78
|
-
|
|
104
|
+
handler();
|
|
105
|
+
}
|
|
106
|
+
navigate(path = '') {
|
|
107
|
+
if (this.mode === 'history')
|
|
108
|
+
history.pushState(null, null, this.root + this.cleanPathString(path));
|
|
109
|
+
else
|
|
110
|
+
window.location.hash = this.cleanPathString(path);
|
|
111
|
+
return this.handleChange();
|
|
79
112
|
}
|
|
80
113
|
}
|
|
81
114
|
export const router = new Router();
|
package/dist/state-manager.js
CHANGED
|
@@ -8,8 +8,7 @@ export class StateManager {
|
|
|
8
8
|
static dispatcher = dispatcher;
|
|
9
9
|
changeAuthorities = [];
|
|
10
10
|
constructor(mode = 'hash') {
|
|
11
|
-
router.mode
|
|
12
|
-
router.listen();
|
|
11
|
+
router.listen(mode);
|
|
13
12
|
}
|
|
14
13
|
onChange(handler) {
|
|
15
14
|
return StateManager.dispatcher.on('state:changed', handler);
|
|
@@ -43,10 +42,9 @@ export class StateManager {
|
|
|
43
42
|
}
|
|
44
43
|
/** attempts to restore state from current url. Currently, works only in hash mode */
|
|
45
44
|
restoreState(defaultState) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
router.navigate(dest);
|
|
45
|
+
if (router.navigate(window.location.pathname))
|
|
46
|
+
return;
|
|
47
|
+
router.navigate(defaultState);
|
|
50
48
|
}
|
|
51
49
|
/**
|
|
52
50
|
*
|
package/package.json
CHANGED
package/src/router.ts
CHANGED
|
@@ -8,23 +8,23 @@ interface Route {
|
|
|
8
8
|
export type RoutingMode = 'history' | 'hash';
|
|
9
9
|
|
|
10
10
|
class Router {
|
|
11
|
-
mode: RoutingMode = 'hash';
|
|
11
|
+
private mode: RoutingMode = 'hash';
|
|
12
12
|
private routes: Route[] = [];
|
|
13
13
|
private root: string = '/';
|
|
14
14
|
private baseLocation: string | null = null;
|
|
15
|
-
public staticFilters:((url:string) => boolean)[] = []
|
|
15
|
+
public staticFilters: ((url: string) => boolean)[] = []
|
|
16
16
|
|
|
17
17
|
constructor() {
|
|
18
|
-
this.staticFilters.push(
|
|
19
|
-
const staticFileExtensions = ['.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp','md'];
|
|
18
|
+
this.staticFilters.push(url => {
|
|
19
|
+
const staticFileExtensions = ['.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp', '.md', '.ejs', '.jsm', '.txt'];
|
|
20
20
|
return staticFileExtensions.some(ext => url.endsWith(ext));
|
|
21
21
|
|
|
22
22
|
})
|
|
23
|
-
window.addEventListener(this.mode === 'hash' ? 'hashchange' : 'popstate', this.listen.bind(this));
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
private
|
|
27
|
-
|
|
25
|
+
private cleanPathString(path: string): string {
|
|
26
|
+
path = path.replace(/\/$/, '').replace(/^\//, '');
|
|
27
|
+
return path = path.replace(/#{2,}/g, '#');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
private clearQuery(url: string): string {
|
|
@@ -35,16 +35,16 @@ class Router {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
private isStaticFile(url: string): boolean {
|
|
38
|
-
return (this.staticFilters || []).some(
|
|
38
|
+
return (this.staticFilters || []).some(filter => filter(url))
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
public resetRoot(root: string): void {
|
|
42
|
-
this.root = '/' + this.
|
|
42
|
+
this.root = '/' + this.cleanPathString(root) + '/';
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
public getLocation(): string {
|
|
46
46
|
if (this.mode === 'history') {
|
|
47
|
-
let fragment = this.
|
|
47
|
+
let fragment = this.cleanPathString(decodeURI(window.location.pathname + window.location.search));
|
|
48
48
|
fragment = this.clearQuery(fragment);
|
|
49
49
|
return this.root !== '/' ? fragment.replace(this.root, '') : fragment;
|
|
50
50
|
} else {
|
|
@@ -58,44 +58,82 @@ class Router {
|
|
|
58
58
|
handler = pattern;
|
|
59
59
|
pattern = /^.*$/; // Match any path
|
|
60
60
|
}
|
|
61
|
-
this.routes.push({
|
|
61
|
+
this.routes.push({pattern, handler: handler as RouteHandler});
|
|
62
62
|
return this;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
*
|
|
67
|
+
* @param location
|
|
68
|
+
* @return true if it was intercepted or false if not handled
|
|
69
|
+
*/
|
|
70
|
+
public handleChange(location?: string): boolean {
|
|
66
71
|
const path = location || this.getLocation();
|
|
67
72
|
if (this.isStaticFile(path))
|
|
68
|
-
return
|
|
69
|
-
|
|
73
|
+
return false; // Bypass routing for static files
|
|
70
74
|
|
|
71
75
|
for (const route of this.routes) {
|
|
72
76
|
const match = path.match(route.pattern);
|
|
73
77
|
if (match) {
|
|
74
78
|
match.shift(); // Remove the full match element
|
|
75
79
|
route.handler.apply({}, match);
|
|
76
|
-
return
|
|
80
|
+
return true
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
console.warn(`No routing found for ${path}`);
|
|
81
|
-
return
|
|
85
|
+
return false
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
listen(): void {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
listen(mode: RoutingMode = 'hash'): void {
|
|
89
|
+
const self = this
|
|
90
|
+
this.mode = mode
|
|
91
|
+
switch (mode) {
|
|
92
|
+
case "hash":
|
|
93
|
+
window.addEventListener('hashchange', () => handler())
|
|
94
|
+
break
|
|
95
|
+
case "history":
|
|
96
|
+
window.addEventListener('popstate', event => handler(event.state?.path));
|
|
97
|
+
document.addEventListener('click', handleInternalNavigation);
|
|
98
|
+
document.addEventListener('keydown', event => {
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
if (event.key === 'Enter' && event.target.tagName === 'A')
|
|
101
|
+
handleInternalNavigation(event);
|
|
102
|
+
});
|
|
89
103
|
}
|
|
90
|
-
}
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
function handleInternalNavigation(event: any) {
|
|
106
|
+
const node = event.target
|
|
107
|
+
const href = node.getAttribute('href')
|
|
108
|
+
if (href) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
history.pushState({path: href}, '', href);
|
|
111
|
+
handler(href);
|
|
112
|
+
}
|
|
97
113
|
}
|
|
98
|
-
|
|
114
|
+
|
|
115
|
+
function handler(path?: string) {
|
|
116
|
+
path = path || location.href.split('#')[0]
|
|
117
|
+
|
|
118
|
+
if (self.isStaticFile(path))
|
|
119
|
+
return
|
|
120
|
+
const currentLocation = self.getLocation();
|
|
121
|
+
if (self.baseLocation !== currentLocation) {
|
|
122
|
+
self.baseLocation = currentLocation;
|
|
123
|
+
self.handleChange(currentLocation);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
handler()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
navigate(path: string = ''): boolean {
|
|
132
|
+
if (this.mode === 'history')
|
|
133
|
+
history.pushState(null, null, this.root + this.cleanPathString(path));
|
|
134
|
+
else
|
|
135
|
+
window.location.hash = this.cleanPathString(path);
|
|
136
|
+
return this.handleChange()
|
|
99
137
|
}
|
|
100
138
|
}
|
|
101
139
|
|
package/src/state-manager.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {router, RoutingMode} from "./router";
|
|
2
2
|
import dispatcher, {IPubSubHandle, PubSubEvent} from "@dharmax/pubsub";
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
export type ApplicationStateName = string
|
|
6
5
|
|
|
7
6
|
export type ApplicationState = {
|
|
@@ -24,8 +23,7 @@ export class StateManager {
|
|
|
24
23
|
|
|
25
24
|
constructor(mode: RoutingMode = 'hash') {
|
|
26
25
|
|
|
27
|
-
router.mode
|
|
28
|
-
router.listen()
|
|
26
|
+
router.listen(mode)
|
|
29
27
|
|
|
30
28
|
}
|
|
31
29
|
|
|
@@ -68,10 +66,9 @@ export class StateManager {
|
|
|
68
66
|
|
|
69
67
|
/** attempts to restore state from current url. Currently, works only in hash mode */
|
|
70
68
|
restoreState(defaultState: ApplicationStateName) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
router.navigate(dest)
|
|
69
|
+
if ( router.navigate(window.location.pathname))
|
|
70
|
+
return
|
|
71
|
+
router.navigate(defaultState)
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
/**
|
package/test/index.html
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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>
|