@equinor/fusion-framework-module-navigation 6.0.1 → 7.0.0-next.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/CHANGELOG.md +83 -5
- package/README.md +694 -0
- package/dist/esm/NavigationConfigurator.interface.js +2 -0
- package/dist/esm/NavigationConfigurator.interface.js.map +1 -0
- package/dist/esm/NavigationConfigurator.js +144 -0
- package/dist/esm/NavigationConfigurator.js.map +1 -0
- package/dist/esm/NavigationProvider.interface.js +2 -0
- package/dist/esm/NavigationProvider.interface.js.map +1 -0
- package/dist/esm/NavigationProvider.js +256 -0
- package/dist/esm/NavigationProvider.js.map +1 -0
- package/dist/esm/enable-navigation.js +52 -0
- package/dist/esm/enable-navigation.js.map +1 -0
- package/dist/esm/events.js +23 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.js +6 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/BaseHistory.js +189 -0
- package/dist/esm/lib/BaseHistory.js.map +1 -0
- package/dist/esm/lib/BrowserHistory.js +91 -0
- package/dist/esm/lib/BrowserHistory.js.map +1 -0
- package/dist/esm/lib/BrowserHistoryHashStack.js +53 -0
- package/dist/esm/lib/BrowserHistoryHashStack.js.map +1 -0
- package/dist/esm/lib/BrowserHistoryStack.js +77 -0
- package/dist/esm/lib/BrowserHistoryStack.js.map +1 -0
- package/dist/esm/lib/MemoryHistory.js +47 -0
- package/dist/esm/lib/MemoryHistory.js.map +1 -0
- package/dist/esm/lib/MemoryStack.js +98 -0
- package/dist/esm/lib/MemoryStack.js.map +1 -0
- package/dist/esm/lib/create-history.js +48 -0
- package/dist/esm/lib/create-history.js.map +1 -0
- package/dist/esm/lib/index.js +8 -1
- package/dist/esm/lib/index.js.map +1 -1
- package/dist/esm/lib/state/history.actions.js +71 -0
- package/dist/esm/lib/state/history.actions.js.map +1 -0
- package/dist/esm/lib/state/history.flows.js +212 -0
- package/dist/esm/lib/state/history.flows.js.map +1 -0
- package/dist/esm/lib/state/history.reducer.js +59 -0
- package/dist/esm/lib/state/history.reducer.js.map +1 -0
- package/dist/esm/lib/state/history.state.js +37 -0
- package/dist/esm/lib/state/history.state.js.map +1 -0
- package/dist/esm/lib/state/index.js +4 -0
- package/dist/esm/lib/state/index.js.map +1 -0
- package/dist/esm/lib/types.js +25 -0
- package/dist/esm/lib/types.js.map +1 -0
- package/dist/esm/lib/utils/encode-trailing-whitespace.js +19 -0
- package/dist/esm/lib/utils/encode-trailing-whitespace.js.map +1 -0
- package/dist/esm/lib/utils/has-protocol.js +22 -0
- package/dist/esm/lib/utils/has-protocol.js.map +1 -0
- package/dist/esm/lib/utils/index.js +6 -0
- package/dist/esm/lib/utils/index.js.map +1 -0
- package/dist/esm/lib/utils/path-to-string.js +23 -0
- package/dist/esm/lib/utils/path-to-string.js.map +1 -0
- package/dist/esm/lib/utils/path-to-url.js +47 -0
- package/dist/esm/lib/utils/path-to-url.js.map +1 -0
- package/dist/esm/lib/utils/resolve-browser-location.js +53 -0
- package/dist/esm/lib/utils/resolve-browser-location.js.map +1 -0
- package/dist/esm/lib/utils/resolve-path.js +21 -0
- package/dist/esm/lib/utils/resolve-path.js.map +1 -0
- package/dist/esm/module.js +34 -25
- package/dist/esm/module.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/NavigationConfigurator.d.ts +60 -0
- package/dist/types/NavigationConfigurator.interface.d.ts +17 -0
- package/dist/types/NavigationProvider.d.ts +112 -0
- package/dist/types/NavigationProvider.interface.d.ts +75 -0
- package/dist/types/enable-navigation.d.ts +40 -0
- package/dist/types/events.d.ts +48 -0
- package/dist/types/index.d.ts +13 -6
- package/dist/types/lib/BaseHistory.d.ts +122 -0
- package/dist/types/lib/BrowserHistory.d.ts +53 -0
- package/dist/types/lib/BrowserHistoryHashStack.d.ts +41 -0
- package/dist/types/lib/BrowserHistoryStack.d.ts +51 -0
- package/dist/types/lib/MemoryHistory.d.ts +27 -0
- package/dist/types/lib/MemoryStack.d.ts +57 -0
- package/dist/types/lib/create-history.d.ts +35 -0
- package/dist/types/lib/index.d.ts +7 -1
- package/dist/types/lib/state/history.actions.d.ts +61 -0
- package/dist/types/lib/state/history.flows.d.ts +102 -0
- package/dist/types/lib/state/history.reducer.d.ts +85 -0
- package/dist/types/lib/state/history.state.d.ts +27 -0
- package/dist/types/lib/state/index.d.ts +3 -0
- package/dist/types/lib/types.d.ts +145 -0
- package/dist/types/lib/utils/encode-trailing-whitespace.d.ts +16 -0
- package/dist/types/lib/utils/has-protocol.d.ts +19 -0
- package/dist/types/lib/utils/index.d.ts +5 -0
- package/dist/types/lib/utils/path-to-string.d.ts +17 -0
- package/dist/types/lib/utils/path-to-url.d.ts +32 -0
- package/dist/types/lib/utils/resolve-browser-location.d.ts +42 -0
- package/dist/types/lib/utils/resolve-path.d.ts +17 -0
- package/dist/types/module.d.ts +33 -5
- package/dist/types/version.d.ts +1 -1
- package/package.json +25 -7
- package/src/NavigationConfigurator.interface.ts +18 -0
- package/src/NavigationConfigurator.ts +184 -0
- package/src/NavigationProvider.interface.ts +86 -0
- package/src/NavigationProvider.ts +313 -0
- package/src/__tests__/BrowserHistory.test.ts +151 -0
- package/src/__tests__/HashHistory.test.ts +121 -0
- package/src/__tests__/MemoryHistory.test.ts +185 -0
- package/src/__tests__/NavigationProvider.test.ts +60 -0
- package/src/__tests__/setup.ts +8 -0
- package/src/enable-navigation.ts +59 -0
- package/src/events.ts +65 -0
- package/src/index.ts +27 -6
- package/src/lib/BaseHistory.ts +229 -0
- package/src/lib/BrowserHistory.ts +123 -0
- package/src/lib/BrowserHistoryHashStack.ts +56 -0
- package/src/lib/BrowserHistoryStack.ts +82 -0
- package/src/lib/MemoryHistory.ts +66 -0
- package/src/lib/MemoryStack.ts +106 -0
- package/src/lib/create-history.ts +63 -0
- package/src/lib/index.ts +22 -1
- package/src/lib/state/history.actions.ts +108 -0
- package/src/lib/state/history.flows.ts +279 -0
- package/src/lib/state/history.reducer.ts +70 -0
- package/src/lib/state/history.state.ts +62 -0
- package/src/lib/state/index.ts +3 -0
- package/src/lib/types.ts +159 -0
- package/src/lib/utils/encode-trailing-whitespace.ts +18 -0
- package/src/lib/utils/has-protocol.ts +21 -0
- package/src/lib/utils/index.ts +5 -0
- package/src/lib/utils/path-to-string.ts +24 -0
- package/src/lib/utils/path-to-url.ts +52 -0
- package/src/lib/utils/resolve-browser-location.ts +60 -0
- package/src/lib/utils/resolve-path.ts +22 -0
- package/src/module.ts +53 -37
- package/src/version.ts +1 -1
- package/tsconfig.json +10 -1
- package/vitest.config.ts +14 -0
- package/dist/esm/configurator.js +0 -5
- package/dist/esm/configurator.js.map +0 -1
- package/dist/esm/createHistory.js +0 -18
- package/dist/esm/createHistory.js.map +0 -1
- package/dist/esm/lib/provider/INavigationProvider.js +0 -2
- package/dist/esm/lib/provider/INavigationProvider.js.map +0 -1
- package/dist/esm/lib/provider/NavigationProvider.js +0 -82
- package/dist/esm/lib/provider/NavigationProvider.js.map +0 -1
- package/dist/esm/lib/provider/index.js +0 -2
- package/dist/esm/lib/provider/index.js.map +0 -1
- package/dist/esm/navigator.js +0 -158
- package/dist/esm/navigator.js.map +0 -1
- package/dist/esm/types.js +0 -2
- package/dist/esm/types.js.map +0 -1
- package/dist/types/configurator.d.ts +0 -9
- package/dist/types/createHistory.d.ts +0 -4
- package/dist/types/lib/provider/INavigationProvider.d.ts +0 -38
- package/dist/types/lib/provider/NavigationProvider.d.ts +0 -22
- package/dist/types/lib/provider/index.d.ts +0 -2
- package/dist/types/navigator.d.ts +0 -105
- package/dist/types/types.d.ts +0 -3
- package/src/configurator.ts +0 -11
- package/src/createHistory.ts +0 -21
- package/src/lib/provider/INavigationProvider.ts +0 -46
- package/src/lib/provider/NavigationProvider.ts +0 -117
- package/src/lib/provider/index.ts +0 -2
- package/src/navigator.ts +0 -204
- package/src/types.ts +0 -4
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { firstValueFrom, skip } from 'rxjs';
|
|
3
|
+
import { MemoryHistory } from '../lib/MemoryHistory';
|
|
4
|
+
|
|
5
|
+
describe('MemoryHistory', () => {
|
|
6
|
+
let history: MemoryHistory;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
history = new MemoryHistory();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
history[Symbol.dispose]();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('initialization', () => {
|
|
17
|
+
it('should initialize with default location', () => {
|
|
18
|
+
expect(history.location.pathname).toBe('/');
|
|
19
|
+
expect(history.action).toBe('POP');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should initialize with custom initial location', () => {
|
|
23
|
+
const customHistory = new MemoryHistory({
|
|
24
|
+
initialLocation: {
|
|
25
|
+
action: 'POP',
|
|
26
|
+
location: {
|
|
27
|
+
pathname: '/custom',
|
|
28
|
+
search: '',
|
|
29
|
+
hash: '',
|
|
30
|
+
key: 'custom-key',
|
|
31
|
+
state: { test: 'value' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(customHistory.location.pathname).toBe('/custom');
|
|
37
|
+
expect(customHistory.location.state).toEqual({ test: 'value' });
|
|
38
|
+
expect(customHistory.action).toBe('POP');
|
|
39
|
+
|
|
40
|
+
customHistory[Symbol.dispose]();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('in-memory navigation', () => {
|
|
45
|
+
it('should push without affecting browser', async () => {
|
|
46
|
+
const initialPathname = window.location.pathname;
|
|
47
|
+
const initialHash = window.location.hash;
|
|
48
|
+
|
|
49
|
+
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
50
|
+
history.push('/about', { test: 'test' });
|
|
51
|
+
await updatePromise;
|
|
52
|
+
|
|
53
|
+
expect(history.location.pathname).toBe('/about');
|
|
54
|
+
expect(history.action).toBe('PUSH');
|
|
55
|
+
expect(history.location.state).toEqual({ test: 'test' });
|
|
56
|
+
// Browser should not be affected
|
|
57
|
+
expect(window.location.pathname).toBe(initialPathname);
|
|
58
|
+
expect(window.location.hash).toBe(initialHash);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should replace without affecting browser', async () => {
|
|
62
|
+
const initialPathname = window.location.pathname;
|
|
63
|
+
const initialHash = window.location.hash;
|
|
64
|
+
|
|
65
|
+
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
66
|
+
history.replace('/profile', { userId: 123 });
|
|
67
|
+
await updatePromise;
|
|
68
|
+
|
|
69
|
+
expect(history.location.pathname).toBe('/profile');
|
|
70
|
+
expect(history.action).toBe('REPLACE');
|
|
71
|
+
expect(history.location.state).toEqual({ userId: 123 });
|
|
72
|
+
// Browser should not be affected
|
|
73
|
+
expect(window.location.pathname).toBe(initialPathname);
|
|
74
|
+
expect(window.location.hash).toBe(initialHash);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should maintain in-memory history stack', async () => {
|
|
78
|
+
// Push multiple entries
|
|
79
|
+
history.push('/page1', { page: 1 });
|
|
80
|
+
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
81
|
+
expect(history.location.pathname).toBe('/page1');
|
|
82
|
+
|
|
83
|
+
history.push('/page2', { page: 2 });
|
|
84
|
+
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
85
|
+
expect(history.location.pathname).toBe('/page2');
|
|
86
|
+
|
|
87
|
+
history.push('/page3', { page: 3 });
|
|
88
|
+
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
89
|
+
expect(history.location.pathname).toBe('/page3');
|
|
90
|
+
|
|
91
|
+
// Go back
|
|
92
|
+
const goBackPromise1 = firstValueFrom(history.state$.pipe(skip(1)));
|
|
93
|
+
history.go(-1);
|
|
94
|
+
await goBackPromise1;
|
|
95
|
+
expect(history.location.pathname).toBe('/page2');
|
|
96
|
+
expect(history.action).toBe('POP');
|
|
97
|
+
expect(history.location.state).toEqual({ page: 2 });
|
|
98
|
+
|
|
99
|
+
// Go back again
|
|
100
|
+
const goBackPromise2 = firstValueFrom(history.state$.pipe(skip(1)));
|
|
101
|
+
history.go(-1);
|
|
102
|
+
await goBackPromise2;
|
|
103
|
+
expect(history.location.pathname).toBe('/page1');
|
|
104
|
+
expect(history.action).toBe('POP');
|
|
105
|
+
expect(history.location.state).toEqual({ page: 1 });
|
|
106
|
+
|
|
107
|
+
// Go forward
|
|
108
|
+
const goForwardPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
109
|
+
history.go(1);
|
|
110
|
+
await goForwardPromise;
|
|
111
|
+
expect(history.location.pathname).toBe('/page2');
|
|
112
|
+
expect(history.action).toBe('POP');
|
|
113
|
+
expect(history.location.state).toEqual({ page: 2 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should clamp go() to history bounds', async () => {
|
|
117
|
+
history.push('/page1');
|
|
118
|
+
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
119
|
+
expect(history.location.pathname).toBe('/page1');
|
|
120
|
+
|
|
121
|
+
// Try to go back beyond start
|
|
122
|
+
const goBackPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
123
|
+
history.go(-10);
|
|
124
|
+
await goBackPromise;
|
|
125
|
+
// Should clamp to first entry
|
|
126
|
+
expect(history.location.pathname).toBe('/');
|
|
127
|
+
expect(history.action).toBe('POP');
|
|
128
|
+
|
|
129
|
+
// Try to go forward beyond end
|
|
130
|
+
const goForwardPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
131
|
+
history.go(10);
|
|
132
|
+
await goForwardPromise;
|
|
133
|
+
// Should clamp to last entry
|
|
134
|
+
expect(history.location.pathname).toBe('/page1');
|
|
135
|
+
expect(history.action).toBe('POP');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('createURL', () => {
|
|
140
|
+
it('should create URLs with memory:// protocol', () => {
|
|
141
|
+
const url = history.createURL('/test');
|
|
142
|
+
expect(url.protocol).toBe('memory:');
|
|
143
|
+
expect(url.pathname).toBe('/test');
|
|
144
|
+
expect(url.href).toContain('memory://');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should create URLs with search and hash', () => {
|
|
148
|
+
const url = history.createURL('/test?query=1#section');
|
|
149
|
+
expect(url.protocol).toBe('memory:');
|
|
150
|
+
expect(url.pathname).toBe('/test');
|
|
151
|
+
expect(url.search).toBe('?query=1');
|
|
152
|
+
expect(url.hash).toBe('#section');
|
|
153
|
+
expect(url.href).toContain('memory://');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('state$ observable', () => {
|
|
158
|
+
it('should emit state updates on navigation', async () => {
|
|
159
|
+
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
160
|
+
history.push('/test', { data: 'test' });
|
|
161
|
+
const update = await updatePromise;
|
|
162
|
+
|
|
163
|
+
expect(update.action).toBe('PUSH');
|
|
164
|
+
expect(update.location.pathname).toBe('/test');
|
|
165
|
+
expect(update.location.state).toEqual({ data: 'test' });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should emit state updates on go()', async () => {
|
|
169
|
+
history.push('/page1');
|
|
170
|
+
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
171
|
+
expect(history.location.pathname).toBe('/page1');
|
|
172
|
+
|
|
173
|
+
history.push('/page2');
|
|
174
|
+
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
175
|
+
expect(history.location.pathname).toBe('/page2');
|
|
176
|
+
|
|
177
|
+
const goPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
178
|
+
history.go(-1);
|
|
179
|
+
const update = await goPromise;
|
|
180
|
+
|
|
181
|
+
expect(update.action).toBe('POP');
|
|
182
|
+
expect(update.location.pathname).toBe('/page1');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { NavigationProvider } from '../NavigationProvider';
|
|
3
|
+
import { MemoryHistory } from '../lib/MemoryHistory';
|
|
4
|
+
import type { INavigationConfigurator } from '../NavigationConfigurator.interface';
|
|
5
|
+
import type { History } from '../lib/types';
|
|
6
|
+
|
|
7
|
+
describe('NavigationProvider', () => {
|
|
8
|
+
let history: History;
|
|
9
|
+
let provider: NavigationProvider;
|
|
10
|
+
let config: INavigationConfigurator;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
history = new MemoryHistory();
|
|
14
|
+
config = { history };
|
|
15
|
+
provider = new NavigationProvider({
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
config,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
provider?.dispose();
|
|
23
|
+
history[Symbol.dispose]();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should have version', () => {
|
|
27
|
+
expect(provider.version).toBeDefined();
|
|
28
|
+
expect(provider.version.toString()).toBe('1.0.0');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should have history property', () => {
|
|
32
|
+
expect(provider.history).toBe(history);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should have path property', () => {
|
|
36
|
+
expect(provider.path).toBeDefined();
|
|
37
|
+
expect(provider.path.pathname).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should have state$ observable', () => {
|
|
41
|
+
expect(provider.state$).toBeDefined();
|
|
42
|
+
expect(typeof provider.state$.subscribe).toBe('function');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should throw error if no history provided', () => {
|
|
46
|
+
config = {};
|
|
47
|
+
expect(() => {
|
|
48
|
+
new NavigationProvider({
|
|
49
|
+
version: '1.0.0',
|
|
50
|
+
config,
|
|
51
|
+
});
|
|
52
|
+
}).toThrow('no history provided!');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should dispose correctly', () => {
|
|
56
|
+
const disposeSpy = vi.spyOn(history as { [Symbol.dispose]: () => void }, Symbol.dispose);
|
|
57
|
+
provider.dispose();
|
|
58
|
+
expect(disposeSpy).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { IModulesConfigurator, ModuleConfigType } from '@equinor/fusion-framework-module';
|
|
2
|
+
import { module, type NavigationModule } from './module';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Helper function to enable the navigation module.
|
|
6
|
+
*
|
|
7
|
+
* This is the main entry point for consumers to add navigation capabilities to their
|
|
8
|
+
* framework configuration. It registers the navigation module and allows configuration
|
|
9
|
+
* of basename and other navigation settings.
|
|
10
|
+
*
|
|
11
|
+
* The navigation module provides routing and navigation capabilities compatible with
|
|
12
|
+
* industry-standard routers (Remix/React Router), with support for browser history,
|
|
13
|
+
* hash routing, and memory history.
|
|
14
|
+
*
|
|
15
|
+
* @param configurator - The modules configurator to add navigation to
|
|
16
|
+
* @param basenameOrOptions - Optional basename string or configuration object
|
|
17
|
+
* @param basenameOrOptions.configure - Configuration callback for advanced setup
|
|
18
|
+
* @typeParam TRef - Reference type for module composition
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // Simple usage with basename
|
|
23
|
+
* enableNavigation(configurator, '/app');
|
|
24
|
+
*
|
|
25
|
+
* // Advanced configuration
|
|
26
|
+
* enableNavigation(configurator, {
|
|
27
|
+
* configure(config, ref) {
|
|
28
|
+
* config.setBasename('/app');
|
|
29
|
+
* config.setHistory(createHistory('browser'));
|
|
30
|
+
* }
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @see {@link NavigationModule} - The navigation module type
|
|
35
|
+
* @see {@link INavigationConfigurator} - Configuration interface
|
|
36
|
+
* @see {@link INavigationProvider} - Provider interface for accessing navigation
|
|
37
|
+
*/
|
|
38
|
+
export const enableNavigation = <TRef = unknown>(
|
|
39
|
+
// biome-ignore lint/suspicious/noExplicitAny: must be any to support all module types
|
|
40
|
+
configurator: IModulesConfigurator<any, any>,
|
|
41
|
+
basenameOrOptions?:
|
|
42
|
+
| string
|
|
43
|
+
| {
|
|
44
|
+
configure: (config: ModuleConfigType<NavigationModule>, ref: TRef) => void;
|
|
45
|
+
},
|
|
46
|
+
): void => {
|
|
47
|
+
configurator.addConfig({
|
|
48
|
+
module,
|
|
49
|
+
configure(config, ref) {
|
|
50
|
+
if (typeof basenameOrOptions === 'string') {
|
|
51
|
+
config.setBasename(basenameOrOptions);
|
|
52
|
+
} else if (typeof basenameOrOptions === 'object' && 'configure' in basenameOrOptions) {
|
|
53
|
+
basenameOrOptions.configure(config, ref);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default enableNavigation;
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { FrameworkEvent, type FrameworkEventInit } from '@equinor/fusion-framework-module-event';
|
|
2
|
+
|
|
3
|
+
import type { INavigationProvider } from './NavigationProvider.interface';
|
|
4
|
+
import type { Action, NavigationUpdate, Path } from './lib/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Event detail for navigation events (before navigation).
|
|
8
|
+
*/
|
|
9
|
+
export interface NavigateEventDetail {
|
|
10
|
+
/** The target path or location to navigate to */
|
|
11
|
+
location: NavigationUpdate;
|
|
12
|
+
/** Optional current location before navigation */
|
|
13
|
+
currentLocation?: Path;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Event emitted before navigation occurs.
|
|
18
|
+
* Can be canceled by calling `preventDefault()`.
|
|
19
|
+
*/
|
|
20
|
+
export class NavigateEvent extends FrameworkEvent<
|
|
21
|
+
FrameworkEventInit<NavigateEventDetail, INavigationProvider>
|
|
22
|
+
> {
|
|
23
|
+
constructor(detail: NavigateEventDetail, source: INavigationProvider) {
|
|
24
|
+
super('onNavigate', {
|
|
25
|
+
detail,
|
|
26
|
+
source,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Event detail for navigated events (after navigation).
|
|
33
|
+
*/
|
|
34
|
+
export interface NavigatedEventDetail {
|
|
35
|
+
/** The navigation action that occurred (PUSH, POP, REPLACE) */
|
|
36
|
+
action: Action;
|
|
37
|
+
/** The new location after navigation */
|
|
38
|
+
current: NavigationUpdate;
|
|
39
|
+
/** The previous location before navigation */
|
|
40
|
+
previous: NavigationUpdate;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Event emitted after navigation occurs.
|
|
45
|
+
* Contains the navigation action and location details.
|
|
46
|
+
*/
|
|
47
|
+
export class NavigatedEvent extends FrameworkEvent<
|
|
48
|
+
FrameworkEventInit<NavigatedEventDetail, INavigationProvider>
|
|
49
|
+
> {
|
|
50
|
+
constructor(detail: NavigatedEventDetail, source: INavigationProvider) {
|
|
51
|
+
super('onNavigated', { detail, source });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Declares navigation events in the FrameworkEventMap for type safety.
|
|
57
|
+
*/
|
|
58
|
+
declare module '@equinor/fusion-framework-module-event' {
|
|
59
|
+
interface FrameworkEventMap {
|
|
60
|
+
/** Event emitted before navigation occurs, can be canceled to prevent navigation */
|
|
61
|
+
onNavigate: NavigateEvent;
|
|
62
|
+
/** Event emitted after navigation occurs */
|
|
63
|
+
onNavigated: NavigatedEvent;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { INavigationConfigurator } from './NavigationConfigurator.interface';
|
|
2
|
+
export { NavigationConfigurator } from './NavigationConfigurator';
|
|
2
3
|
|
|
3
|
-
export {
|
|
4
|
+
export { NavigationModule, module, moduleKey } from './module';
|
|
5
|
+
export { enableNavigation } from './enable-navigation';
|
|
4
6
|
|
|
5
|
-
export {
|
|
7
|
+
export type { INavigationProvider } from './NavigationProvider.interface';
|
|
8
|
+
export { NavigationProvider } from './NavigationProvider';
|
|
6
9
|
|
|
7
|
-
export { createHistory } from './
|
|
10
|
+
export { createHistory } from './lib/create-history';
|
|
8
11
|
|
|
9
|
-
export
|
|
10
|
-
|
|
12
|
+
export {
|
|
13
|
+
NavigateEvent,
|
|
14
|
+
NavigatedEvent,
|
|
15
|
+
type NavigateEventDetail,
|
|
16
|
+
type NavigatedEventDetail,
|
|
17
|
+
} from './events';
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
Path,
|
|
21
|
+
To,
|
|
22
|
+
Location,
|
|
23
|
+
History,
|
|
24
|
+
NavigationBlocker,
|
|
25
|
+
NavigationListener,
|
|
26
|
+
} from './lib/types';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @deprecated use History instead
|
|
30
|
+
*/
|
|
31
|
+
export type { History as INavigator } from './lib';
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Subscription } from 'rxjs';
|
|
2
|
+
import { filter } from 'rxjs/operators';
|
|
3
|
+
|
|
4
|
+
import type { Actions, HistoryState } from './state';
|
|
5
|
+
|
|
6
|
+
import { Action } from './types';
|
|
7
|
+
import type {
|
|
8
|
+
NavigateOptions,
|
|
9
|
+
NavigationListener,
|
|
10
|
+
NavigationUpdate,
|
|
11
|
+
Path,
|
|
12
|
+
To,
|
|
13
|
+
History,
|
|
14
|
+
NavigationBlocker,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Abstract base class for history implementations.
|
|
19
|
+
* Provides common state management and navigation logic that can be shared
|
|
20
|
+
* across different history implementations (browser, memory, etc.).
|
|
21
|
+
*/
|
|
22
|
+
export abstract class BaseHistory implements History {
|
|
23
|
+
// Subscriptions for cleanup
|
|
24
|
+
#teardowns = new Subscription();
|
|
25
|
+
|
|
26
|
+
// internal state
|
|
27
|
+
#state: HistoryState;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Gets the current location.
|
|
31
|
+
*/
|
|
32
|
+
public get location(): History['location'] {
|
|
33
|
+
return this.#state.subject.value.current.location;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Gets the current action.
|
|
38
|
+
*/
|
|
39
|
+
public get action(): History['action'] {
|
|
40
|
+
return this.#state.subject.value.current.action;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Observable stream of navigation state changes.
|
|
45
|
+
* Emits on all navigation events (push, replace, pop).
|
|
46
|
+
*/
|
|
47
|
+
public get state$(): History['state$'] {
|
|
48
|
+
return this.#state.subject.select((state) => state.current);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Observable stream of navigation actions.
|
|
53
|
+
*/
|
|
54
|
+
public get action$(): History['action$'] {
|
|
55
|
+
return this.#state.subject.action$;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks if there are any active navigation blockers.
|
|
60
|
+
*/
|
|
61
|
+
public get hasBlockers(): boolean {
|
|
62
|
+
return this.#state.subject.value.blockers.length > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected constructor(state: HistoryState) {
|
|
66
|
+
this.#state = state;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a valid href string for a given path.
|
|
71
|
+
*/
|
|
72
|
+
public createHref(to: To): string {
|
|
73
|
+
return this.createURL(to).href;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates a URL object for a given path.
|
|
78
|
+
*/
|
|
79
|
+
public createURL(to: To): URL {
|
|
80
|
+
return this.#state.stack.createURL(to);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Encodes a location by properly URL-encoding the pathname.
|
|
85
|
+
*/
|
|
86
|
+
public encodeLocation(to: To): Path {
|
|
87
|
+
return this.#state.stack.createURL(to);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Navigate to a location with explicit options.
|
|
92
|
+
*
|
|
93
|
+
* @param to - The target path (string, Path object, or Location object)
|
|
94
|
+
* @param options - Navigation options specifying action (PUSH/REPLACE) and optional state
|
|
95
|
+
*/
|
|
96
|
+
public navigate(to: To, options: NavigateOptions): void {
|
|
97
|
+
const action = this.#state.actions.navigate(to, options);
|
|
98
|
+
this.#state.subject.next(action);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Pushes a new location onto the history stack.
|
|
103
|
+
*/
|
|
104
|
+
public push(to: To, state?: unknown): void {
|
|
105
|
+
this.navigate(to, { state });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Replaces the current location in the history stack.
|
|
110
|
+
*/
|
|
111
|
+
public replace(to: To, state?: unknown): void {
|
|
112
|
+
this.navigate(to, { replace: true, state });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Navigates backward or forward in the history stack.
|
|
117
|
+
*
|
|
118
|
+
* @param delta - The number of steps to move (negative for backward, positive for forward)
|
|
119
|
+
*/
|
|
120
|
+
public go(delta: number): void {
|
|
121
|
+
const action = this.#state.actions.go(delta);
|
|
122
|
+
this.#state.subject.next(action);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sets up a listener for navigation changes.
|
|
127
|
+
*
|
|
128
|
+
* Only listens for POP actions (browser back/forward navigation). PUSH/REPLACE
|
|
129
|
+
* actions are programmatic and synchronous - the caller already knows about them.
|
|
130
|
+
* POP actions come from browser events and are asynchronous, so we need to listen.
|
|
131
|
+
* This matches industry-standard router behavior (Remix/React Router).
|
|
132
|
+
*
|
|
133
|
+
* @param listener - Function to call on navigation changes
|
|
134
|
+
* @returns Function to unsubscribe the listener
|
|
135
|
+
*/
|
|
136
|
+
public listen(listener: NavigationListener): () => void {
|
|
137
|
+
// Filter for POP actions only - PUSH/REPLACE are handled synchronously
|
|
138
|
+
const subscription = this.#state.subject
|
|
139
|
+
.select((state) => state.current)
|
|
140
|
+
.pipe(
|
|
141
|
+
filter((update): update is NavigationUpdate<Action.Pop> => update.action === Action.Pop),
|
|
142
|
+
)
|
|
143
|
+
.subscribe((update) => {
|
|
144
|
+
listener(update);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Register subscription for cleanup on dispose
|
|
148
|
+
this._addTeardown(subscription);
|
|
149
|
+
|
|
150
|
+
// Return unsubscribe function
|
|
151
|
+
return () => {
|
|
152
|
+
subscription.unsubscribe();
|
|
153
|
+
this._removeTeardown(subscription);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Registers a blocker to intercept navigation attempts.
|
|
159
|
+
*
|
|
160
|
+
* @param blocker - Navigation blocker function to register
|
|
161
|
+
* @returns Function to unsubscribe the blocker
|
|
162
|
+
*/
|
|
163
|
+
public block(blocker: NavigationBlocker): VoidFunction {
|
|
164
|
+
this.#state.subject.next(this.#state.actions.addBlocker(blocker));
|
|
165
|
+
const removeBlocker = () => {
|
|
166
|
+
this.#state.subject.next(this.#state.actions.removeBlocker(blocker));
|
|
167
|
+
};
|
|
168
|
+
return this._addTeardown(removeBlocker, { executeOnRemove: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public pop(): void {
|
|
172
|
+
this.#state.subject.next(this.#state.actions.pop());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Disposes of the history instance and cleans up all subscriptions.
|
|
177
|
+
*/
|
|
178
|
+
public [Symbol.dispose](): void {
|
|
179
|
+
this.#teardowns.unsubscribe();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Dispatches an action to trigger state updates.
|
|
184
|
+
*
|
|
185
|
+
* Use this to dispatch actions when external events occur (e.g., browser popstate).
|
|
186
|
+
* The action will be processed by flows and update the history state.
|
|
187
|
+
*
|
|
188
|
+
* @param action - The action to dispatch
|
|
189
|
+
*/
|
|
190
|
+
protected _dispatch(action: Actions): void {
|
|
191
|
+
this.#state.subject.next(action);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Registers a cleanup function or subscription for automatic disposal.
|
|
196
|
+
*
|
|
197
|
+
* All teardowns are automatically cleaned up when the history instance is disposed.
|
|
198
|
+
* Use `executeOnRemove: true` if the teardown should run when manually removed.
|
|
199
|
+
*
|
|
200
|
+
* @param teardown - Function or subscription to clean up
|
|
201
|
+
* @param options - Optional configuration
|
|
202
|
+
* @param options.executeOnRemove - Execute teardown when removed (default: false)
|
|
203
|
+
* @returns Function to manually remove the teardown
|
|
204
|
+
*/
|
|
205
|
+
protected _addTeardown(
|
|
206
|
+
teardown: VoidFunction | Subscription,
|
|
207
|
+
options?: { executeOnRemove: boolean },
|
|
208
|
+
): VoidFunction {
|
|
209
|
+
this.#teardowns.add(teardown);
|
|
210
|
+
return () => {
|
|
211
|
+
if (options?.executeOnRemove) {
|
|
212
|
+
typeof teardown === 'function' ? teardown() : teardown.unsubscribe();
|
|
213
|
+
}
|
|
214
|
+
return this._removeTeardown(teardown);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Removes a teardown from the cleanup collection.
|
|
220
|
+
*
|
|
221
|
+
* Typically called automatically by the function returned from `_addTeardown`.
|
|
222
|
+
* Only call directly if you need to remove a teardown without executing it.
|
|
223
|
+
*
|
|
224
|
+
* @param teardown - The teardown to remove
|
|
225
|
+
*/
|
|
226
|
+
protected _removeTeardown(teardown: VoidFunction | Subscription) {
|
|
227
|
+
this.#teardowns.remove(teardown);
|
|
228
|
+
}
|
|
229
|
+
}
|