@equinor/fusion-framework-module-navigation 7.0.3 → 7.0.5
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 +24 -0
- package/dist/esm/NavigateEvent.js +24 -0
- package/dist/esm/NavigateEvent.js.map +1 -0
- package/dist/esm/NavigatedEvent.js +20 -0
- package/dist/esm/NavigatedEvent.js.map +1 -0
- package/dist/esm/NavigationConfigurator.js +13 -1
- package/dist/esm/NavigationConfigurator.js.map +1 -1
- package/dist/esm/NavigationProvider.js +15 -3
- package/dist/esm/NavigationProvider.js.map +1 -1
- package/dist/esm/enable-navigation.js +1 -0
- package/dist/esm/enable-navigation.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/BaseHistory.js +9 -0
- package/dist/esm/lib/BaseHistory.js.map +1 -1
- package/dist/esm/lib/BrowserHistory.js +1 -0
- package/dist/esm/lib/BrowserHistory.js.map +1 -1
- package/dist/esm/lib/BrowserHistoryHashStack.js +1 -0
- package/dist/esm/lib/BrowserHistoryHashStack.js.map +1 -1
- package/dist/esm/lib/BrowserHistoryStack.js +15 -0
- package/dist/esm/lib/BrowserHistoryStack.js.map +1 -1
- package/dist/esm/lib/MemoryHistory.js +4 -1
- package/dist/esm/lib/MemoryHistory.js.map +1 -1
- package/dist/esm/lib/{MemoryStack.js → MemoryHistoryStack.js} +12 -1
- package/dist/esm/lib/MemoryHistoryStack.js.map +1 -0
- package/dist/esm/lib/ProxyHistory.js +1 -0
- package/dist/esm/lib/ProxyHistory.js.map +1 -1
- package/dist/esm/lib/create-history.js +1 -0
- package/dist/esm/lib/create-history.js.map +1 -1
- package/dist/esm/lib/index.js +1 -1
- package/dist/esm/lib/index.js.map +1 -1
- package/dist/esm/lib/state/{history.actions.js → actions.js} +1 -1
- package/dist/esm/lib/state/actions.js.map +1 -0
- package/dist/esm/lib/state/check-blockers.js +36 -0
- package/dist/esm/lib/state/check-blockers.js.map +1 -0
- package/dist/esm/lib/state/create-flow.js +27 -0
- package/dist/esm/lib/state/create-flow.js.map +1 -0
- package/dist/esm/lib/state/{history.reducer.js → create-history-reducer.js} +5 -2
- package/dist/esm/lib/state/create-history-reducer.js.map +1 -0
- package/dist/esm/lib/state/create-store.js +26 -0
- package/dist/esm/lib/state/create-store.js.map +1 -0
- package/dist/esm/lib/state/flow-creators.js +7 -0
- package/dist/esm/lib/state/flow-creators.js.map +1 -0
- package/dist/esm/lib/state/go.js +12 -0
- package/dist/esm/lib/state/go.js.map +1 -0
- package/dist/esm/lib/state/history.state.js +1 -39
- package/dist/esm/lib/state/history.state.js.map +1 -1
- package/dist/esm/lib/state/index.js +3 -3
- package/dist/esm/lib/state/index.js.map +1 -1
- package/dist/esm/lib/state/navigate.js +43 -0
- package/dist/esm/lib/state/navigate.js.map +1 -0
- package/dist/esm/lib/state/pop.js +14 -0
- package/dist/esm/lib/state/pop.js.map +1 -0
- package/dist/esm/lib/state/validate-current-location.js +26 -0
- package/dist/esm/lib/state/validate-current-location.js.map +1 -0
- package/dist/esm/lib/utils/index.js +2 -2
- package/dist/esm/lib/utils/index.js.map +1 -1
- package/dist/esm/lib/utils/resolve-browser-location.js +1 -52
- package/dist/esm/lib/utils/resolve-browser-location.js.map +1 -1
- package/dist/esm/lib/utils/resolve-hash-location.js +20 -0
- package/dist/esm/lib/utils/resolve-hash-location.js.map +1 -0
- package/dist/esm/lib/utils/resolve-window-location.js +18 -0
- package/dist/esm/lib/utils/resolve-window-location.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/NavigateEvent.d.ts +36 -0
- package/dist/types/NavigatedEvent.d.ts +28 -0
- package/dist/types/NavigationConfigurator.d.ts +3 -0
- package/dist/types/NavigationProvider.d.ts +3 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/lib/BaseHistory.d.ts +8 -0
- package/dist/types/lib/BrowserHistoryHashStack.d.ts +1 -0
- package/dist/types/lib/BrowserHistoryStack.d.ts +14 -0
- package/dist/types/lib/MemoryHistory.d.ts +3 -0
- package/dist/types/lib/{MemoryStack.d.ts → MemoryHistoryStack.d.ts} +6 -0
- package/dist/types/lib/ProxyHistory.d.ts +1 -1
- package/dist/types/lib/index.d.ts +1 -1
- package/dist/types/lib/state/check-blockers.d.ts +3 -0
- package/dist/types/lib/state/create-flow.d.ts +10 -0
- package/dist/types/lib/state/{history.reducer.d.ts → create-history-reducer.d.ts} +32 -32
- package/dist/types/lib/state/create-store.d.ts +17 -0
- package/dist/types/lib/state/flow-creators.d.ts +7 -0
- package/dist/types/lib/state/go.d.ts +3 -0
- package/dist/types/lib/state/history.state.d.ts +2 -23
- package/dist/types/lib/state/index.d.ts +4 -3
- package/dist/types/lib/state/navigate.d.ts +9 -0
- package/dist/types/lib/state/pop.d.ts +3 -0
- package/dist/types/lib/state/validate-current-location.d.ts +3 -0
- package/dist/types/lib/types.d.ts +1 -1
- package/dist/types/lib/utils/index.d.ts +2 -2
- package/dist/types/lib/utils/resolve-browser-location.d.ts +1 -42
- package/dist/types/lib/utils/resolve-hash-location.d.ts +10 -0
- package/dist/types/lib/utils/resolve-window-location.d.ts +10 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +8 -8
- package/src/NavigateEvent.ts +47 -0
- package/src/NavigatedEvent.ts +35 -0
- package/src/NavigationConfigurator.ts +13 -3
- package/src/NavigationProvider.interface.ts +1 -1
- package/src/NavigationProvider.ts +15 -3
- package/src/__tests__/NavigationProvider.test.ts +11 -0
- package/src/enable-navigation.ts +1 -0
- package/src/index.ts +2 -6
- package/src/lib/BaseHistory.ts +9 -0
- package/src/lib/BrowserHistory.ts +1 -0
- package/src/lib/BrowserHistoryHashStack.ts +1 -0
- package/src/lib/BrowserHistoryStack.ts +15 -0
- package/src/lib/MemoryHistory.ts +4 -1
- package/src/lib/{MemoryStack.ts → MemoryHistoryStack.ts} +11 -0
- package/src/lib/ProxyHistory.ts +2 -1
- package/src/lib/create-history.ts +1 -0
- package/src/lib/index.ts +1 -1
- package/src/lib/state/check-blockers.ts +52 -0
- package/src/lib/state/create-flow.ts +36 -0
- package/src/lib/state/{history.reducer.ts → create-history-reducer.ts} +4 -1
- package/src/lib/state/create-store.ts +39 -0
- package/src/lib/state/flow-creators.ts +7 -0
- package/src/lib/state/go.ts +20 -0
- package/src/lib/state/history.state.ts +2 -56
- package/src/lib/state/index.ts +4 -3
- package/src/lib/state/navigate.ts +58 -0
- package/src/lib/state/pop.ts +24 -0
- package/src/lib/state/validate-current-location.ts +33 -0
- package/src/lib/types.ts +1 -1
- package/src/lib/utils/index.ts +2 -2
- package/src/lib/utils/resolve-browser-location.ts +1 -60
- package/src/lib/utils/resolve-hash-location.ts +26 -0
- package/src/lib/utils/resolve-window-location.ts +24 -0
- package/src/version.ts +1 -1
- package/dist/esm/events.js +0 -40
- package/dist/esm/events.js.map +0 -1
- package/dist/esm/lib/MemoryStack.js.map +0 -1
- package/dist/esm/lib/state/history.actions.js.map +0 -1
- package/dist/esm/lib/state/history.flows.js +0 -207
- package/dist/esm/lib/state/history.flows.js.map +0 -1
- package/dist/esm/lib/state/history.reducer.js.map +0 -1
- package/dist/types/events.d.ts +0 -65
- package/dist/types/lib/state/history.flows.d.ts +0 -97
- package/src/events.ts +0 -82
- package/src/lib/state/history.flows.ts +0 -274
- /package/dist/types/lib/state/{history.actions.d.ts → actions.d.ts} +0 -0
- /package/src/lib/state/{history.actions.ts → actions.ts} +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { FrameworkEvent, type FrameworkEventInit } from '@equinor/fusion-framework-module-event';
|
|
2
|
+
|
|
3
|
+
import type { INavigationProvider } from './NavigationProvider.interface';
|
|
4
|
+
import type { NavigationUpdate, Path } from './lib/types';
|
|
5
|
+
|
|
6
|
+
/** Event detail for navigation events (before navigation). */
|
|
7
|
+
export interface NavigateEventDetail {
|
|
8
|
+
/** The target path or location to navigate to */
|
|
9
|
+
location: NavigationUpdate;
|
|
10
|
+
/** Optional current location before navigation */
|
|
11
|
+
currentLocation?: Path;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Event emitted before navigation occurs.
|
|
16
|
+
* Can be canceled by calling `preventDefault()` to block the navigation.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* eventProvider.addEventListener('onNavigate', (event) => {
|
|
21
|
+
* if (hasUnsavedChanges) {
|
|
22
|
+
* event.preventDefault();
|
|
23
|
+
* }
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class NavigateEvent extends FrameworkEvent<
|
|
28
|
+
FrameworkEventInit<NavigateEventDetail, INavigationProvider>
|
|
29
|
+
> {
|
|
30
|
+
/** @param detail - Navigation target details. @param source - Provider dispatching the event. */
|
|
31
|
+
constructor(detail: NavigateEventDetail, source: INavigationProvider) {
|
|
32
|
+
super('onNavigate', {
|
|
33
|
+
detail,
|
|
34
|
+
source,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Declares navigation events in the FrameworkEventMap for type safety. */
|
|
40
|
+
declare module '@equinor/fusion-framework-module-event' {
|
|
41
|
+
interface FrameworkEventMap {
|
|
42
|
+
/** Event emitted before navigation occurs, can be canceled to prevent navigation */
|
|
43
|
+
onNavigate: NavigateEvent;
|
|
44
|
+
/** Event emitted after navigation occurs */
|
|
45
|
+
onNavigated: import('./NavigatedEvent').NavigatedEvent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FrameworkEvent, type FrameworkEventInit } from '@equinor/fusion-framework-module-event';
|
|
2
|
+
|
|
3
|
+
import type { INavigationProvider } from './NavigationProvider.interface';
|
|
4
|
+
import type { Action, NavigationUpdate } from './lib/types';
|
|
5
|
+
|
|
6
|
+
/** Event detail for navigated events (after navigation). */
|
|
7
|
+
export interface NavigatedEventDetail {
|
|
8
|
+
/** The navigation action that occurred (PUSH, POP, REPLACE) */
|
|
9
|
+
action: Action;
|
|
10
|
+
/** The new location after navigation */
|
|
11
|
+
current: NavigationUpdate;
|
|
12
|
+
/** The previous location before navigation */
|
|
13
|
+
previous: NavigationUpdate;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Event emitted after navigation completes.
|
|
18
|
+
* Contains the navigation action type and both current and previous locations.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* eventProvider.addEventListener('onNavigated', (event) => {
|
|
23
|
+
* const { action, current, previous } = event.detail;
|
|
24
|
+
* console.log(`${action}: ${previous.location.pathname} -> ${current.location.pathname}`);
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class NavigatedEvent extends FrameworkEvent<
|
|
29
|
+
FrameworkEventInit<NavigatedEventDetail, INavigationProvider>
|
|
30
|
+
> {
|
|
31
|
+
/** @param detail - Completed navigation details. @param source - Provider dispatching the event. */
|
|
32
|
+
constructor(detail: NavigatedEventDetail, source: INavigationProvider) {
|
|
33
|
+
super('onNavigated', { detail, source });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -67,9 +67,11 @@ const parseNavigationConfig = (config: unknown): INavigationConfigurator => {
|
|
|
67
67
|
* Extends BaseConfigBuilder to provide fluent configuration API with Zod validation.
|
|
68
68
|
*/
|
|
69
69
|
export class NavigationConfigurator extends BaseConfigBuilder<INavigationConfigurator> {
|
|
70
|
+
/** Creates a navigation configurator with default history and event providers. */
|
|
70
71
|
constructor() {
|
|
71
72
|
super();
|
|
72
73
|
this.setEventProvider(async (args) => {
|
|
74
|
+
// Only resolve the optional provider when the event module participates in this framework.
|
|
73
75
|
if (args.hasModule('event')) {
|
|
74
76
|
return await args.requireInstance('event');
|
|
75
77
|
}
|
|
@@ -77,10 +79,12 @@ export class NavigationConfigurator extends BaseConfigBuilder<INavigationConfigu
|
|
|
77
79
|
this.setHistory(
|
|
78
80
|
async (args) => {
|
|
79
81
|
const history = (args.ref as ModulesInstance<[NavigationModule]>)?.navigation?.history;
|
|
82
|
+
// Wrap supplied history so the module owns only the proxy lifecycle.
|
|
80
83
|
if (history) {
|
|
81
84
|
// Wrap the provided history in a ProxyHistory to ensure the module can manage its own teardowns without affecting the original instance.
|
|
82
85
|
return new ProxyHistory(history);
|
|
83
86
|
}
|
|
87
|
+
// Prefer browser history when a DOM is available, otherwise use memory history for SSR.
|
|
84
88
|
if (typeof window !== 'undefined') {
|
|
85
89
|
return createHistory('browser');
|
|
86
90
|
}
|
|
@@ -92,8 +96,10 @@ export class NavigationConfigurator extends BaseConfigBuilder<INavigationConfigu
|
|
|
92
96
|
}
|
|
93
97
|
/**
|
|
94
98
|
* @deprecated Use `setBasename()` method instead
|
|
99
|
+
* @param value - Deprecated basename value to configure.
|
|
95
100
|
*/
|
|
96
101
|
public set basename(value: string | undefined) {
|
|
102
|
+
// Warn only during development so deprecated setters remain silent in production.
|
|
97
103
|
if (process.env.NODE_ENV === 'development') {
|
|
98
104
|
console.warn('NavigationConfigurator.basename', 'use setBasename() method instead');
|
|
99
105
|
}
|
|
@@ -102,8 +108,10 @@ export class NavigationConfigurator extends BaseConfigBuilder<INavigationConfigu
|
|
|
102
108
|
|
|
103
109
|
/**
|
|
104
110
|
* @deprecated Use `setHistory()` method instead
|
|
111
|
+
* @param value - Deprecated history value to configure.
|
|
105
112
|
*/
|
|
106
113
|
public set history(value: History | undefined) {
|
|
114
|
+
// Warn only during development so deprecated setters remain silent in production.
|
|
107
115
|
if (process.env.NODE_ENV === 'development') {
|
|
108
116
|
console.warn('NavigationConfigurator.history', 'use setHistory() method instead');
|
|
109
117
|
}
|
|
@@ -149,14 +157,16 @@ export class NavigationConfigurator extends BaseConfigBuilder<INavigationConfigu
|
|
|
149
157
|
: // Normalize a direct instance to a callback for consistent handling.
|
|
150
158
|
async () => historyOrCallback;
|
|
151
159
|
|
|
160
|
+
// Proxying isolates teardown ownership when callers provide an existing history instance.
|
|
152
161
|
if (proxy) {
|
|
153
162
|
// Wrap each emitted history in a ProxyHistory so dispose only tears down
|
|
154
163
|
// proxy-owned listeners/blockers, never the underlying history itself.
|
|
155
164
|
this._set('history', (args) =>
|
|
156
|
-
from(resolve(args) as ObservableInput<History | undefined>)
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
from(resolve(args) as ObservableInput<History | undefined>)
|
|
166
|
+
// Transform each resolved history into an independently disposable proxy.
|
|
167
|
+
.pipe(map((history) => (history ? new ProxyHistory(history) : undefined))),
|
|
159
168
|
);
|
|
169
|
+
// Direct histories are already owned by the navigation module and can be used unchanged.
|
|
160
170
|
} else {
|
|
161
171
|
this._set('history', resolve);
|
|
162
172
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// TODO: Remove @remix-run/router dependency once all apps have migrated to @equinor/fusion-framework-react-router
|
|
1
|
+
// TODO(#5158): Remove @remix-run/router dependency once all apps have migrated to @equinor/fusion-framework-react-router
|
|
2
2
|
import type { AgnosticRouteObject, Router } from '@remix-run/router';
|
|
3
3
|
|
|
4
4
|
import type { History, NavigateOptions } from './lib/types';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// TODO: Remove @remix-run/router dependency once all apps have migrated to @equinor/fusion-framework-react-router
|
|
1
|
+
// TODO(#5158): Remove @remix-run/router dependency once all apps have migrated to @equinor/fusion-framework-react-router
|
|
2
2
|
import { type AgnosticRouteObject, createRouter } from '@remix-run/router';
|
|
3
3
|
import type { Observable } from 'rxjs';
|
|
4
4
|
import { filter, pairwise, shareReplay } from 'rxjs/operators';
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type ITelemetryProvider,
|
|
18
18
|
} from '@equinor/fusion-framework-module-telemetry';
|
|
19
19
|
import type { IEventModuleProvider } from '@equinor/fusion-framework-module-event';
|
|
20
|
-
import { NavigatedEvent } from './
|
|
20
|
+
import { NavigatedEvent } from './NavigatedEvent';
|
|
21
21
|
import { pathToString } from './lib/utils';
|
|
22
22
|
import type { BaseHistory } from './lib';
|
|
23
23
|
|
|
@@ -68,6 +68,7 @@ export class NavigationProvider
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Observable stream of navigation state updates.
|
|
71
|
+
* @returns Observable of navigation updates.
|
|
71
72
|
*
|
|
72
73
|
* Emits localized paths (with basename removed) and filters to only
|
|
73
74
|
* paths within the basename scope. Late subscribers receive the last
|
|
@@ -88,6 +89,7 @@ export class NavigationProvider
|
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
91
|
* @deprecated Use `history` instead
|
|
92
|
+
* @returns The underlying `History` instance.
|
|
91
93
|
*/
|
|
92
94
|
public get navigator(): History {
|
|
93
95
|
this.#telemetry?.trackException({
|
|
@@ -101,6 +103,7 @@ export class NavigationProvider
|
|
|
101
103
|
|
|
102
104
|
/**
|
|
103
105
|
* Gets the underlying history instance.
|
|
106
|
+
* @returns The {@link History} instance used for navigation
|
|
104
107
|
*
|
|
105
108
|
* @returns The {@link History} instance used for navigation
|
|
106
109
|
*/
|
|
@@ -129,7 +132,13 @@ export class NavigationProvider
|
|
|
129
132
|
// Extract configuration values
|
|
130
133
|
const { basename, history, telemetry, eventProvider } = args.config;
|
|
131
134
|
|
|
132
|
-
|
|
135
|
+
// Normalize the basename to strip trailing slashes. React Router requires
|
|
136
|
+
// the current URL to start with the exact basename string, so a basename
|
|
137
|
+
// of "/apps/my-app/" would fail to match the URL "/apps/my-app" and
|
|
138
|
+
// render nothing (blank page).
|
|
139
|
+
// Preserve slash-only basenames (e.g. "/") by falling back to the
|
|
140
|
+
// original input when normalization collapses to an empty string.
|
|
141
|
+
this.#basename = basename ? normalizePathname(basename) || basename : basename;
|
|
133
142
|
this.#event = eventProvider;
|
|
134
143
|
this.#telemetry = telemetry;
|
|
135
144
|
|
|
@@ -229,6 +238,9 @@ export class NavigationProvider
|
|
|
229
238
|
level: TelemetryLevel.Warning,
|
|
230
239
|
scope: ['navigation', 'deprecated', TelemetryScope.Application],
|
|
231
240
|
});
|
|
241
|
+
// `this.#history` is typed as the framework's minimal history interface, but Remix Router
|
|
242
|
+
// requires its own richer `History` type — the two are runtime-compatible, so cast through
|
|
243
|
+
// `unknown`.
|
|
232
244
|
const router = createRouter({
|
|
233
245
|
basename: this.#basename,
|
|
234
246
|
history: this.#history as unknown as import('@remix-run/router').History,
|
|
@@ -57,4 +57,15 @@ describe('NavigationProvider', () => {
|
|
|
57
57
|
provider.dispose();
|
|
58
58
|
expect(disposeSpy).toHaveBeenCalled();
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
it('should preserve root basename when set to slash', () => {
|
|
62
|
+
const providerWithRootBasename = new NavigationProvider({
|
|
63
|
+
version: '1.0.0',
|
|
64
|
+
config: { history, basename: '/' },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(providerWithRootBasename.basename).toBe('/');
|
|
68
|
+
|
|
69
|
+
providerWithRootBasename.dispose();
|
|
70
|
+
});
|
|
60
71
|
});
|
package/src/enable-navigation.ts
CHANGED
|
@@ -47,6 +47,7 @@ export const enableNavigation = <TRef = unknown>(
|
|
|
47
47
|
configurator.addConfig({
|
|
48
48
|
module,
|
|
49
49
|
configure(config, ref) {
|
|
50
|
+
// A string shortcut sets the basename directly; otherwise defer to the caller's configure callback.
|
|
50
51
|
if (typeof basenameOrOptions === 'string') {
|
|
51
52
|
config.setBasename(basenameOrOptions);
|
|
52
53
|
} else if (typeof basenameOrOptions === 'object' && 'configure' in basenameOrOptions) {
|
package/src/index.ts
CHANGED
|
@@ -33,12 +33,8 @@ export { NavigationProvider } from './NavigationProvider';
|
|
|
33
33
|
|
|
34
34
|
export { createHistory } from './lib/create-history';
|
|
35
35
|
|
|
36
|
-
export {
|
|
37
|
-
|
|
38
|
-
NavigatedEvent,
|
|
39
|
-
type NavigateEventDetail,
|
|
40
|
-
type NavigatedEventDetail,
|
|
41
|
-
} from './events';
|
|
36
|
+
export { NavigateEvent, type NavigateEventDetail } from './NavigateEvent';
|
|
37
|
+
export { NavigatedEvent, type NavigatedEventDetail } from './NavigatedEvent';
|
|
42
38
|
|
|
43
39
|
export type {
|
|
44
40
|
Path,
|
package/src/lib/BaseHistory.ts
CHANGED
|
@@ -49,6 +49,7 @@ export abstract class BaseHistory implements History {
|
|
|
49
49
|
/**
|
|
50
50
|
* Observable stream of navigation state changes.
|
|
51
51
|
* Emits on all navigation events (push, replace, pop).
|
|
52
|
+
* @returns Observable of the current navigation update.
|
|
52
53
|
*/
|
|
53
54
|
public get state$(): History['state$'] {
|
|
54
55
|
return this.#state.subject.select((state) => state.current);
|
|
@@ -56,6 +57,7 @@ export abstract class BaseHistory implements History {
|
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
59
|
* Observable stream of navigation actions.
|
|
60
|
+
* @returns Observable of navigation actions.
|
|
59
61
|
*/
|
|
60
62
|
public get action$(): History['action$'] {
|
|
61
63
|
return this.#state.subject.action$;
|
|
@@ -70,6 +72,7 @@ export abstract class BaseHistory implements History {
|
|
|
70
72
|
return this.#state.subject.value.blockers.length > 0;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
/** @param state - Reactive history state backing this implementation. */
|
|
73
76
|
protected constructor(state: HistoryState) {
|
|
74
77
|
this.#state = state;
|
|
75
78
|
}
|
|
@@ -117,6 +120,8 @@ export abstract class BaseHistory implements History {
|
|
|
117
120
|
|
|
118
121
|
/**
|
|
119
122
|
* Pushes a new location onto the history stack.
|
|
123
|
+
* @param to - Target path or partial path object.
|
|
124
|
+
* @param state - Optional state associated with the new location.
|
|
120
125
|
*/
|
|
121
126
|
public push(to: To, state?: unknown): void {
|
|
122
127
|
this.navigate(to, { state });
|
|
@@ -124,6 +129,8 @@ export abstract class BaseHistory implements History {
|
|
|
124
129
|
|
|
125
130
|
/**
|
|
126
131
|
* Replaces the current location in the history stack.
|
|
132
|
+
* @param to - Target path or partial path object.
|
|
133
|
+
* @param state - Optional state associated with the replacement location.
|
|
127
134
|
*/
|
|
128
135
|
public replace(to: To, state?: unknown): void {
|
|
129
136
|
this.navigate(to, { replace: true, state });
|
|
@@ -185,6 +192,7 @@ export abstract class BaseHistory implements History {
|
|
|
185
192
|
return this._addTeardown(removeBlocker, { executeOnRemove: true });
|
|
186
193
|
}
|
|
187
194
|
|
|
195
|
+
/** Triggers a POP action for the current history location. */
|
|
188
196
|
public pop(): void {
|
|
189
197
|
this.#state.subject.next(this.#state.actions.pop());
|
|
190
198
|
}
|
|
@@ -225,6 +233,7 @@ export abstract class BaseHistory implements History {
|
|
|
225
233
|
): VoidFunction {
|
|
226
234
|
this.#teardowns.add(teardown);
|
|
227
235
|
return () => {
|
|
236
|
+
// Execute teardown callbacks immediately when callers explicitly request removal side effects.
|
|
228
237
|
if (options?.executeOnRemove) {
|
|
229
238
|
typeof teardown === 'function' ? teardown() : teardown.unsubscribe();
|
|
230
239
|
}
|
|
@@ -59,6 +59,7 @@ export class BrowserHistory extends BaseHistory {
|
|
|
59
59
|
// Use provided stack or default to BrowserHistoryStack
|
|
60
60
|
const Stack: StackConstructor = options.stack ?? BrowserHistoryStack;
|
|
61
61
|
const browserWindow = options.window ?? document.defaultView;
|
|
62
|
+
// Fail fast without a window since the browser stack depends on the DOM history API.
|
|
62
63
|
if (!browserWindow) {
|
|
63
64
|
throw new Error('Window is required');
|
|
64
65
|
}
|
|
@@ -18,6 +18,7 @@ import { resolveHashLocation, resolvePath, pathToString } from './utils';
|
|
|
18
18
|
export class BrowserHistoryHashStack extends BrowserHistoryStack {
|
|
19
19
|
/**
|
|
20
20
|
* Gets the current location from window.location.hash.
|
|
21
|
+
* @returns The current hash-derived navigation location.
|
|
21
22
|
*/
|
|
22
23
|
public get current(): Readonly<Location> {
|
|
23
24
|
return resolveHashLocation(this._window);
|
|
@@ -22,6 +22,7 @@ export class BrowserHistoryStack implements HistoryStack {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Gets the origin of the history stack.
|
|
25
|
+
* @returns The browser origin used as the URL base.
|
|
25
26
|
*/
|
|
26
27
|
public get origin(): string {
|
|
27
28
|
return this._window.location.origin;
|
|
@@ -29,6 +30,7 @@ export class BrowserHistoryStack implements HistoryStack {
|
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Gets the current location.
|
|
33
|
+
* @returns The current browser location converted to a navigation location.
|
|
32
34
|
*/
|
|
33
35
|
public get current(): Location {
|
|
34
36
|
return resolveWindowLocation(this._window);
|
|
@@ -36,6 +38,7 @@ export class BrowserHistoryStack implements HistoryStack {
|
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Pushes a new entry onto the history stack.
|
|
41
|
+
* @param location - Location to append to browser history.
|
|
39
42
|
*/
|
|
40
43
|
public push(location: Location): void {
|
|
41
44
|
this.navigate(location, 'PUSH');
|
|
@@ -43,6 +46,7 @@ export class BrowserHistoryStack implements HistoryStack {
|
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Replaces the current entry in the history stack.
|
|
49
|
+
* @param location - Location to replace in browser history.
|
|
46
50
|
*/
|
|
47
51
|
public replace(location: Location): void {
|
|
48
52
|
this.navigate(location, 'REPLACE');
|
|
@@ -50,10 +54,13 @@ export class BrowserHistoryStack implements HistoryStack {
|
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
56
|
* Navigates to a location with the specified action.
|
|
57
|
+
* @param location - Location to navigate to.
|
|
58
|
+
* @param action - Native history action to perform.
|
|
53
59
|
*/
|
|
54
60
|
public navigate(location: Location, action: 'PUSH' | 'REPLACE'): void {
|
|
55
61
|
const relativePath = this._createRelativePath(location);
|
|
56
62
|
const state = { value: location.state, key: location.key };
|
|
63
|
+
// Select the native operation matching the requested navigation action.
|
|
57
64
|
if (action === 'PUSH') {
|
|
58
65
|
this._window.history.pushState(state, '', relativePath);
|
|
59
66
|
} else {
|
|
@@ -63,6 +70,7 @@ export class BrowserHistoryStack implements HistoryStack {
|
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
72
|
* Navigates backward or forward in the history stack.
|
|
73
|
+
* @param delta - Number of entries to move backward or forward.
|
|
66
74
|
*/
|
|
67
75
|
public go(delta: number): void {
|
|
68
76
|
this._window.history.go(delta);
|
|
@@ -70,11 +78,18 @@ export class BrowserHistoryStack implements HistoryStack {
|
|
|
70
78
|
|
|
71
79
|
/**
|
|
72
80
|
* Creates a URL object for a given path.
|
|
81
|
+
* @param to - Target path or partial path object.
|
|
82
|
+
* @returns URL resolved against the browser origin.
|
|
73
83
|
*/
|
|
74
84
|
public createURL(to: To): URL {
|
|
75
85
|
return new URL(pathToString(resolvePath(to)), this.origin);
|
|
76
86
|
}
|
|
77
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Creates the relative URL path used by native browser history.
|
|
90
|
+
* @param to - Target path or partial path object.
|
|
91
|
+
* @returns Relative URL path for browser history.
|
|
92
|
+
*/
|
|
78
93
|
protected _createRelativePath(to: To): string {
|
|
79
94
|
return pathToString(this.createURL(to));
|
|
80
95
|
}
|
package/src/lib/MemoryHistory.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type To, type LocationState, type NavigationUpdate, Action } from './types';
|
|
2
2
|
import { pathToString, resolvePath } from './utils';
|
|
3
|
-
import { MemoryHistoryStack } from './
|
|
3
|
+
import { MemoryHistoryStack } from './MemoryHistoryStack';
|
|
4
4
|
import { BaseHistory } from './BaseHistory';
|
|
5
5
|
import { createHistoryReducer, createStore } from './state';
|
|
6
6
|
|
|
@@ -40,6 +40,7 @@ export type MemoryHistoryOptions = {
|
|
|
40
40
|
* - **Node.js** — any environment without browser history APIs
|
|
41
41
|
*/
|
|
42
42
|
export class MemoryHistory extends BaseHistory {
|
|
43
|
+
/** @param options - Optional initial location and history entries. */
|
|
43
44
|
public constructor(options?: MemoryHistoryOptions) {
|
|
44
45
|
const { initialLocation, initialHistory } = options ?? {};
|
|
45
46
|
const initial = initialLocation ?? defaultInitialLocation;
|
|
@@ -65,6 +66,8 @@ export class MemoryHistory extends BaseHistory {
|
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
68
|
* Creates a URL object for a given path using memory:// origin.
|
|
69
|
+
* @param to - Target path or partial path object.
|
|
70
|
+
* @returns URL resolved against memory://.
|
|
68
71
|
*/
|
|
69
72
|
public createURL(to: To): URL {
|
|
70
73
|
const path = pathToString(resolvePath(to));
|
|
@@ -14,6 +14,7 @@ export class MemoryHistoryStack implements HistoryStack {
|
|
|
14
14
|
/**
|
|
15
15
|
* Gets the origin of the history stack.
|
|
16
16
|
* Always returns 'memory://' for in-memory storage.
|
|
17
|
+
* @returns The fixed origin used for in-memory URLs.
|
|
17
18
|
*/
|
|
18
19
|
get origin(): string {
|
|
19
20
|
return 'memory://';
|
|
@@ -21,6 +22,7 @@ export class MemoryHistoryStack implements HistoryStack {
|
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Gets the current location.
|
|
25
|
+
* @returns The current in-memory navigation location.
|
|
24
26
|
*/
|
|
25
27
|
get current(): Location {
|
|
26
28
|
return this.#current;
|
|
@@ -47,6 +49,7 @@ export class MemoryHistoryStack implements HistoryStack {
|
|
|
47
49
|
* Pushes a new entry onto the history stack.
|
|
48
50
|
*
|
|
49
51
|
* Only updates the current location. History entries are managed by the reducer.
|
|
52
|
+
* @param location - Location to make current.
|
|
50
53
|
*/
|
|
51
54
|
push(location: Location): void {
|
|
52
55
|
this.#current = location;
|
|
@@ -56,6 +59,7 @@ export class MemoryHistoryStack implements HistoryStack {
|
|
|
56
59
|
* Replaces the current entry in the history stack.
|
|
57
60
|
*
|
|
58
61
|
* Only updates the current location. History entries are managed by the reducer.
|
|
62
|
+
* @param location - Location to make current.
|
|
59
63
|
*/
|
|
60
64
|
replace(location: Location): void {
|
|
61
65
|
this.#current = location;
|
|
@@ -74,10 +78,13 @@ export class MemoryHistoryStack implements HistoryStack {
|
|
|
74
78
|
const { history, current } = state;
|
|
75
79
|
const currentLocation = current.location ?? this.#current;
|
|
76
80
|
// Find current location in history by key
|
|
81
|
+
// Locate the current entry so relative navigation can preserve stack boundaries.
|
|
77
82
|
const currentIndex = history.findIndex((entry) => entry.location.key === currentLocation.key);
|
|
78
83
|
|
|
79
84
|
// If current location not found, use last entry
|
|
85
|
+
// Fall back to the newest entry when the stack cannot identify the current location.
|
|
80
86
|
if (currentIndex === -1) {
|
|
87
|
+
// Restore the latest known entry when the history cannot locate the current key.
|
|
81
88
|
if (history.length > 0) {
|
|
82
89
|
this.#current = history[history.length - 1].location;
|
|
83
90
|
}
|
|
@@ -86,8 +93,10 @@ export class MemoryHistoryStack implements HistoryStack {
|
|
|
86
93
|
|
|
87
94
|
// Calculate target index and clamp to valid range
|
|
88
95
|
const newIndex = currentIndex + delta;
|
|
96
|
+
// Clamp the destination to the available history range.
|
|
89
97
|
if (newIndex < 0) {
|
|
90
98
|
this.#current = state.history[0].location;
|
|
99
|
+
// Use the oldest entry when navigation moves beyond the end of the stack.
|
|
91
100
|
} else if (newIndex >= state.history.length) {
|
|
92
101
|
this.#current = state.history[state.history.length - 1].location;
|
|
93
102
|
} else {
|
|
@@ -99,6 +108,8 @@ export class MemoryHistoryStack implements HistoryStack {
|
|
|
99
108
|
* Creates a URL object for a given path.
|
|
100
109
|
*
|
|
101
110
|
* All URLs use the 'memory://' origin since this is in-memory storage.
|
|
111
|
+
* @param to - Target path or partial path object.
|
|
112
|
+
* @returns URL resolved against memory://.
|
|
102
113
|
*/
|
|
103
114
|
createURL(to: To): URL {
|
|
104
115
|
const path = resolvePath(to);
|
package/src/lib/ProxyHistory.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
Path,
|
|
12
12
|
To,
|
|
13
13
|
} from './types';
|
|
14
|
-
import type { Actions } from './state/
|
|
14
|
+
import type { Actions } from './state/actions';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* A lightweight proxy that delegates every {@link History} operation to an
|
|
@@ -109,6 +109,7 @@ export class ProxyHistory implements History {
|
|
|
109
109
|
* instance; otherwise this is a no-op.
|
|
110
110
|
*/
|
|
111
111
|
pop(): void {
|
|
112
|
+
// Only BaseHistory implementations expose pop(); anything else is a no-op.
|
|
112
113
|
if ('pop' in this.#target && typeof this.#target.pop === 'function') {
|
|
113
114
|
(this.#target as BaseHistory).pop();
|
|
114
115
|
}
|
|
@@ -33,6 +33,7 @@ export const createHistory = <T extends keyof HistoryCtorMap>(
|
|
|
33
33
|
type: T,
|
|
34
34
|
...args: Parameters<HistoryCtorMap[T]>
|
|
35
35
|
): ReturnType<HistoryCtorMap[T]> => {
|
|
36
|
+
// Dispatch to the history implementation matching the requested type.
|
|
36
37
|
switch (type) {
|
|
37
38
|
case 'memory':
|
|
38
39
|
return new MemoryHistory(...(args as [MemoryHistoryOptions])) as ReturnType<
|
package/src/lib/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { ProxyHistory } from './ProxyHistory';
|
|
|
17
17
|
// History stacks
|
|
18
18
|
export { BrowserHistoryStack } from './BrowserHistoryStack';
|
|
19
19
|
export { BrowserHistoryHashStack as HashHistoryStack } from './BrowserHistoryHashStack';
|
|
20
|
-
export { MemoryHistoryStack } from './
|
|
20
|
+
export { MemoryHistoryStack } from './MemoryHistoryStack';
|
|
21
21
|
|
|
22
22
|
// Types
|
|
23
23
|
export type {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Observable, of } from 'rxjs';
|
|
2
|
+
import { concatMap, map, withLatestFrom } from 'rxjs/operators';
|
|
3
|
+
import type { HistoryFlowCreator } from './navigate';
|
|
4
|
+
import { actions, type Actions } from './actions';
|
|
5
|
+
import { filterAction } from '@equinor/fusion-observable/operators';
|
|
6
|
+
import { Action, type HistoryStack, type LocationState } from '../types';
|
|
7
|
+
|
|
8
|
+
/** Flow creator that gates navigation actions through registered blockers. */
|
|
9
|
+
export const checkBlockers: HistoryFlowCreator =
|
|
10
|
+
(stack: HistoryStack) => (action$: Observable<Actions>, state$: Observable<LocationState>) =>
|
|
11
|
+
// Normalize each navigation-related action into a common shape before gating on blockers.
|
|
12
|
+
action$.pipe(
|
|
13
|
+
filterAction(
|
|
14
|
+
actions.navigate.type,
|
|
15
|
+
actions.pop.type,
|
|
16
|
+
actions.go.type,
|
|
17
|
+
actions.validateLocation.type,
|
|
18
|
+
),
|
|
19
|
+
map((action) => {
|
|
20
|
+
// Only `navigate` carries a replace flag; the rest are treated as POP transitions.
|
|
21
|
+
switch (action.type) {
|
|
22
|
+
case actions.navigate.type:
|
|
23
|
+
return { replace: action.payload.options.replace, action };
|
|
24
|
+
case actions.go.type:
|
|
25
|
+
case actions.pop.type:
|
|
26
|
+
case actions.validateLocation.type:
|
|
27
|
+
return { type: 'POP' as Action, action };
|
|
28
|
+
}
|
|
29
|
+
}),
|
|
30
|
+
withLatestFrom(state$),
|
|
31
|
+
concatMap(([{ action, replace }, state]) => {
|
|
32
|
+
// Allow actions through immediately when no blocker can veto the transition.
|
|
33
|
+
if (!state.blockers.length) {
|
|
34
|
+
return of(action);
|
|
35
|
+
}
|
|
36
|
+
return new Observable<Actions>((subscriber) => {
|
|
37
|
+
const location = stack.current;
|
|
38
|
+
// Invoke all blockers and complete once every asynchronous decision settles.
|
|
39
|
+
const blockers = state.blockers.map((blocker) =>
|
|
40
|
+
Promise.resolve(
|
|
41
|
+
blocker({
|
|
42
|
+
delta: 0,
|
|
43
|
+
action: replace ? Action.Replace : Action.Push,
|
|
44
|
+
location,
|
|
45
|
+
retry: () => subscriber.next(action),
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
Promise.allSettled(blockers).then(() => subscriber.complete());
|
|
50
|
+
});
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { merge, type Observable } from 'rxjs';
|
|
2
|
+
import type { HistoryFlow, HistoryFlowCreator } from './navigate';
|
|
3
|
+
import { checkBlockers } from './check-blockers';
|
|
4
|
+
import type { Actions } from './actions';
|
|
5
|
+
import type { HistoryStack, LocationState } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a combined history flow from multiple flow creators.
|
|
9
|
+
* @param flowCreators - Flow creators to combine.
|
|
10
|
+
* @param options - Optional blocker configuration.
|
|
11
|
+
* @returns A combined history flow creator.
|
|
12
|
+
*/
|
|
13
|
+
export const createFlow = (
|
|
14
|
+
flowCreators: HistoryFlowCreator[],
|
|
15
|
+
options?: { skipBlockCheck?: boolean },
|
|
16
|
+
): HistoryFlowCreator => {
|
|
17
|
+
return (stack: HistoryStack): HistoryFlow => {
|
|
18
|
+
const preProcessActions = options?.skipBlockCheck
|
|
19
|
+
? (action$: Observable<Actions>) => action$
|
|
20
|
+
: checkBlockers(stack);
|
|
21
|
+
const flows: HistoryFlow = (
|
|
22
|
+
action$: Observable<Actions>,
|
|
23
|
+
state$: Observable<LocationState>,
|
|
24
|
+
) => {
|
|
25
|
+
// Initialize each creator against the same stack before merging their action streams.
|
|
26
|
+
const initializedFlows = flowCreators
|
|
27
|
+
.map((initializer) => initializer(stack))
|
|
28
|
+
.map((flow) => flow(action$, state$));
|
|
29
|
+
return merge(...initializedFlows);
|
|
30
|
+
};
|
|
31
|
+
return (action$: Observable<Actions>, state$: Observable<LocationState>) => {
|
|
32
|
+
// Gate actions before dispatching them to the parallel history flows.
|
|
33
|
+
return preProcessActions(action$, state$).pipe((source$) => flows(source$, state$));
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createReducer } from '@equinor/fusion-observable';
|
|
2
2
|
import type { LocationState, NavigationUpdate } from '../types';
|
|
3
|
-
import { actions, type Actions } from './
|
|
3
|
+
import { actions, type Actions } from './actions';
|
|
4
4
|
import { isSuccessAction } from '@equinor/fusion-observable/actions';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -8,6 +8,7 @@ import { isSuccessAction } from '@equinor/fusion-observable/actions';
|
|
|
8
8
|
* Used to determine where to insert/replace entries for REPLACE actions.
|
|
9
9
|
*/
|
|
10
10
|
const findIndex = (state: LocationState) => {
|
|
11
|
+
// Locate the current entry by key rather than reference, since updates are cloned.
|
|
11
12
|
return state.history.findIndex((update) => update.location.key === state.current.location.key);
|
|
12
13
|
};
|
|
13
14
|
|
|
@@ -47,6 +48,7 @@ export const createHistoryReducer = (
|
|
|
47
48
|
// and removes any forward history when not at the end
|
|
48
49
|
if (update.action === 'REPLACE') {
|
|
49
50
|
const currentIndex = findIndex(state);
|
|
51
|
+
// Only trim when the current entry is actually present in history.
|
|
50
52
|
if (currentIndex !== -1) {
|
|
51
53
|
// Remove all entries from current position onwards
|
|
52
54
|
state.history.splice(currentIndex, state.history.length - currentIndex);
|
|
@@ -64,6 +66,7 @@ export const createHistoryReducer = (
|
|
|
64
66
|
builder.addMatcher(
|
|
65
67
|
() => true,
|
|
66
68
|
(state) => {
|
|
69
|
+
// Trim only once history has actually grown past the configured cap.
|
|
67
70
|
if (state.history.length > maxHistory) {
|
|
68
71
|
// Keep only the most recent entries, removing oldest ones
|
|
69
72
|
state.history = state.history.slice(-maxHistory);
|