@dharmax/state-router 3.2.0 → 3.2.1

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