@dharmax/state-router 2.0.1 → 2.1.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 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: RoutingMode;
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 clearSlashes;
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
- process(location?: string): Router;
17
- listen(): void;
18
- navigate(path?: string): Router;
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
- clearSlashes(path) {
15
- return path.replace(/\/$/, '').replace(/^\//, '');
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.clearSlashes(root) + '/';
28
+ this.root = '/' + this.cleanPathString(root) + '/';
29
29
  }
30
30
  getLocation() {
31
31
  if (this.mode === 'history') {
32
- let fragment = this.clearSlashes(decodeURI(window.location.pathname + window.location.search));
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
- process(location) {
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 this; // Bypass routing for static files
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 this;
63
+ return true;
59
64
  }
60
65
  }
61
66
  console.warn(`No routing found for ${path}`);
62
- return this;
67
+ return false;
63
68
  }
64
- listen() {
65
- const currentLocation = this.getLocation();
66
- if (this.baseLocation !== currentLocation) {
67
- this.baseLocation = currentLocation;
68
- this.process(currentLocation);
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
- navigate(path = '') {
72
- if (this.mode === 'history') {
73
- history.pushState(null, null, this.root + this.clearSlashes(path));
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
- else {
76
- window.location.hash = '#' + this.clearSlashes(path);
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
- return this;
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();
@@ -8,8 +8,7 @@ export class StateManager {
8
8
  static dispatcher = dispatcher;
9
9
  changeAuthorities = [];
10
10
  constructor(mode = 'hash') {
11
- router.mode = 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
- let dest = window.location.hash;
47
- if (dest == '#login' || dest == '')
48
- dest = '#' + defaultState;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dharmax/state-router",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "A cute and tight router and application state controller",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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( url => {
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 clearSlashes(path: string): string {
27
- return path.replace(/\/$/, '').replace(/^\//, '');
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( filter => filter(url))
38
+ return (this.staticFilters || []).some(filter => filter(url))
39
39
  }
40
40
 
41
41
  public resetRoot(root: string): void {
42
- this.root = '/' + this.clearSlashes(root) + '/';
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.clearSlashes(decodeURI(window.location.pathname + window.location.search));
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({ pattern, handler: handler as RouteHandler });
61
+ this.routes.push({pattern, handler: handler as RouteHandler});
62
62
  return this;
63
63
  }
64
64
 
65
- public process(location?: string): Router {
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 this; // Bypass routing for static files
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 this;
80
+ return true
77
81
  }
78
82
  }
79
83
 
80
84
  console.warn(`No routing found for ${path}`);
81
- return this;
85
+ return false
82
86
  }
83
87
 
84
- listen(): void {
85
- const currentLocation = this.getLocation();
86
- if (this.baseLocation !== currentLocation) {
87
- this.baseLocation = currentLocation;
88
- this.process(currentLocation);
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
- public navigate(path: string = ''): Router {
93
- if (this.mode === 'history') {
94
- history.pushState(null, null, this.root + this.clearSlashes(path));
95
- } else {
96
- window.location.hash = '#' + this.clearSlashes(path);
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
- return this;
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
 
@@ -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 = {
@@ -22,11 +21,14 @@ export class StateManager {
22
21
  public static dispatcher = dispatcher
23
22
  private changeAuthorities: ChangeAuthority[] = [];
24
23
 
25
- constructor(mode: RoutingMode = 'hash') {
24
+ constructor(private mode: RoutingMode = 'hash', autostart= true) {
26
25
 
27
- router.mode = mode
28
- router.listen()
26
+ if (autostart)
27
+ router.listen(mode)
28
+ }
29
29
 
30
+ start() {
31
+ router.listen( this.mode )
30
32
  }
31
33
 
32
34
  onChange(handler: (event: PubSubEvent, data: any) => void): IPubSubHandle {
@@ -68,10 +70,9 @@ export class StateManager {
68
70
 
69
71
  /** attempts to restore state from current url. Currently, works only in hash mode */
70
72
  restoreState(defaultState: ApplicationStateName) {
71
- let dest = window.location.hash
72
- if (dest == '#login' || dest == '')
73
- dest = '#' + defaultState
74
- router.navigate(dest)
73
+ if ( router.navigate(window.location.pathname))
74
+ return
75
+ router.navigate(defaultState)
75
76
  }
76
77
 
77
78
  /**
@@ -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>