@equinor/fusion-framework-module-navigation 7.0.9-next.0 → 7.0.10
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/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +10 -7
- package/CHANGELOG.md +0 -600
- package/src/NavigateEvent.ts +0 -47
- package/src/NavigatedEvent.ts +0 -35
- package/src/NavigationConfigurator.interface.ts +0 -41
- package/src/NavigationConfigurator.ts +0 -226
- package/src/NavigationProvider.interface.ts +0 -99
- package/src/NavigationProvider.ts +0 -436
- package/src/__tests__/BrowserHistory.test.ts +0 -151
- package/src/__tests__/HashHistory.test.ts +0 -121
- package/src/__tests__/MemoryHistory.test.ts +0 -185
- package/src/__tests__/NavigationProvider.test.ts +0 -302
- package/src/__tests__/ProxyHistory.test.ts +0 -149
- package/src/__tests__/setup.ts +0 -8
- package/src/enable-navigation.ts +0 -60
- package/src/index.ts +0 -51
- package/src/lib/BaseHistory.ts +0 -255
- package/src/lib/BrowserHistory.ts +0 -124
- package/src/lib/BrowserHistoryHashStack.ts +0 -57
- package/src/lib/BrowserHistoryStack.ts +0 -96
- package/src/lib/MemoryHistory.ts +0 -76
- package/src/lib/MemoryHistoryStack.ts +0 -118
- package/src/lib/ProxyHistory.ts +0 -145
- package/src/lib/create-history.ts +0 -58
- package/src/lib/index.ts +0 -33
- package/src/lib/state/actions.ts +0 -108
- package/src/lib/state/check-blockers.ts +0 -52
- package/src/lib/state/create-flow.ts +0 -36
- package/src/lib/state/create-history-reducer.ts +0 -77
- package/src/lib/state/create-store.ts +0 -39
- package/src/lib/state/flow-creators.ts +0 -7
- package/src/lib/state/go.ts +0 -20
- package/src/lib/state/history.state.ts +0 -14
- package/src/lib/state/index.ts +0 -4
- package/src/lib/state/navigate.ts +0 -58
- package/src/lib/state/pop.ts +0 -24
- package/src/lib/state/validate-current-location.ts +0 -33
- package/src/lib/types.ts +0 -190
- package/src/lib/utils/encode-trailing-whitespace.ts +0 -18
- package/src/lib/utils/has-protocol.ts +0 -21
- package/src/lib/utils/index.ts +0 -5
- package/src/lib/utils/path-to-string.ts +0 -24
- package/src/lib/utils/path-to-url.ts +0 -52
- package/src/lib/utils/resolve-browser-location.ts +0 -1
- package/src/lib/utils/resolve-hash-location.ts +0 -26
- package/src/lib/utils/resolve-path.ts +0 -22
- package/src/lib/utils/resolve-window-location.ts +0 -24
- package/src/module.ts +0 -80
- package/src/version.ts +0 -2
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -14
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { firstValueFrom, skip } from 'rxjs';
|
|
3
|
-
import { MemoryHistory } from '../lib/MemoryHistory';
|
|
4
|
-
import { ProxyHistory } from '../lib/ProxyHistory';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Awaits the next state emission after performing a navigation action.
|
|
8
|
-
* MemoryHistory processes state updates asynchronously through the subject.
|
|
9
|
-
*/
|
|
10
|
-
const awaitNavigation = async (
|
|
11
|
-
history: MemoryHistory | ProxyHistory,
|
|
12
|
-
action: () => void,
|
|
13
|
-
): Promise<void> => {
|
|
14
|
-
const next = firstValueFrom(history.state$.pipe(skip(1)));
|
|
15
|
-
action();
|
|
16
|
-
await next;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
describe('ProxyHistory', () => {
|
|
20
|
-
let target: MemoryHistory;
|
|
21
|
-
let proxy: ProxyHistory;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
target = new MemoryHistory();
|
|
25
|
-
proxy = new ProxyHistory(target);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
proxy[Symbol.dispose]();
|
|
30
|
-
target[Symbol.dispose]();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('delegation', () => {
|
|
34
|
-
it('should delegate location to the target', () => {
|
|
35
|
-
expect(proxy.location).toBe(target.location);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should delegate action to the target', () => {
|
|
39
|
-
expect(proxy.action).toBe(target.action);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should delegate push to the target', async () => {
|
|
43
|
-
await awaitNavigation(proxy, () => proxy.push('/test'));
|
|
44
|
-
|
|
45
|
-
expect(target.location.pathname).toBe('/test');
|
|
46
|
-
expect(proxy.location.pathname).toBe('/test');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should delegate replace to the target', async () => {
|
|
50
|
-
await awaitNavigation(proxy, () => proxy.replace('/replaced'));
|
|
51
|
-
|
|
52
|
-
expect(target.location.pathname).toBe('/replaced');
|
|
53
|
-
expect(proxy.location.pathname).toBe('/replaced');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should delegate navigate to the target', async () => {
|
|
57
|
-
await awaitNavigation(proxy, () =>
|
|
58
|
-
proxy.navigate('/nav', { replace: true, state: { foo: 1 } }),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
expect(target.location.pathname).toBe('/nav');
|
|
62
|
-
expect(target.location.state).toEqual({ foo: 1 });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should delegate createHref to the target', () => {
|
|
66
|
-
expect(proxy.createHref('/path')).toBe(target.createHref('/path'));
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should delegate createURL to the target', () => {
|
|
70
|
-
expect(proxy.createURL('/path').href).toBe(target.createURL('/path').href);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should delegate encodeLocation to the target', () => {
|
|
74
|
-
const encoded = proxy.encodeLocation('/path');
|
|
75
|
-
expect(encoded).toEqual(target.encodeLocation('/path'));
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('pop()', () => {
|
|
80
|
-
it('should delegate pop to the target when target supports it', () => {
|
|
81
|
-
const popSpy = vi.spyOn(target, 'pop');
|
|
82
|
-
proxy.pop();
|
|
83
|
-
expect(popSpy).toHaveBeenCalledOnce();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('teardown isolation', () => {
|
|
88
|
-
it('should not dispose the target when proxy is disposed', async () => {
|
|
89
|
-
// Dispose the proxy
|
|
90
|
-
proxy[Symbol.dispose]();
|
|
91
|
-
|
|
92
|
-
// Target should still be functional — push should work
|
|
93
|
-
await awaitNavigation(target, () => target.push('/after-proxy-dispose'));
|
|
94
|
-
expect(target.location.pathname).toBe('/after-proxy-dispose');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should clean up proxy-owned listeners on dispose', async () => {
|
|
98
|
-
const proxyListener = vi.fn();
|
|
99
|
-
proxy.listen(proxyListener);
|
|
100
|
-
|
|
101
|
-
// Dispose the proxy — listener teardown should be called on the target
|
|
102
|
-
proxy[Symbol.dispose]();
|
|
103
|
-
|
|
104
|
-
// Target is still alive, push something — the disposed proxy listener
|
|
105
|
-
// should not fire (listen only triggers on POP, but the underlying
|
|
106
|
-
// subscription should be removed entirely)
|
|
107
|
-
await awaitNavigation(target, () => target.push('/after-dispose'));
|
|
108
|
-
expect(target.location.pathname).toBe('/after-dispose');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should not affect target listeners when proxy is disposed', async () => {
|
|
112
|
-
// Register a listener directly on the target
|
|
113
|
-
const targetListener = vi.fn();
|
|
114
|
-
target.listen(targetListener);
|
|
115
|
-
|
|
116
|
-
// Register a listener through the proxy
|
|
117
|
-
proxy.listen(vi.fn());
|
|
118
|
-
|
|
119
|
-
// Dispose proxy — only proxy listener should be removed
|
|
120
|
-
proxy[Symbol.dispose]();
|
|
121
|
-
|
|
122
|
-
// Target should still be functional
|
|
123
|
-
await awaitNavigation(target, () => target.push('/still-works'));
|
|
124
|
-
expect(target.location.pathname).toBe('/still-works');
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('listen/block unsubscribe', () => {
|
|
129
|
-
it('should allow manual unsubscribe of a listener', () => {
|
|
130
|
-
const listener = vi.fn();
|
|
131
|
-
const unlisten = proxy.listen(listener);
|
|
132
|
-
|
|
133
|
-
unlisten();
|
|
134
|
-
|
|
135
|
-
// No error on dispose — the listener was already removed
|
|
136
|
-
proxy[Symbol.dispose]();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should allow manual unsubscribe of a blocker', () => {
|
|
140
|
-
const blocker = vi.fn();
|
|
141
|
-
const unblock = proxy.block(blocker);
|
|
142
|
-
|
|
143
|
-
unblock();
|
|
144
|
-
|
|
145
|
-
// No error on dispose — the blocker was already removed
|
|
146
|
-
proxy[Symbol.dispose]();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
});
|
package/src/__tests__/setup.ts
DELETED
package/src/enable-navigation.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
// A string shortcut sets the basename directly; otherwise defer to the caller's configure callback.
|
|
51
|
-
if (typeof basenameOrOptions === 'string') {
|
|
52
|
-
config.setBasename(basenameOrOptions);
|
|
53
|
-
} else if (typeof basenameOrOptions === 'object' && 'configure' in basenameOrOptions) {
|
|
54
|
-
basenameOrOptions.configure(config, ref);
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export default enableNavigation;
|
package/src/index.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module @equinor/fusion-framework-module-navigation
|
|
3
|
-
*
|
|
4
|
-
* Navigation module for Fusion Framework providing routing and navigation capabilities.
|
|
5
|
-
*
|
|
6
|
-
* Manages observable navigation state with automatic basename localization,
|
|
7
|
-
* so consumers work with clean paths while the underlying history receives
|
|
8
|
-
* full paths including the basename prefix.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* Supports browser, hash, and memory history types. Integrates with
|
|
12
|
-
* `@remix-run/router` for router creation and is compatible with
|
|
13
|
-
* industry-standard routers (Remix / React Router).
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* import { enableNavigation, createHistory } from '@equinor/fusion-framework-module-navigation';
|
|
18
|
-
*
|
|
19
|
-
* enableNavigation(configurator, '/apps/my-app');
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @packageDocumentation
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
export type { INavigationConfigurator } from './NavigationConfigurator.interface';
|
|
26
|
-
export { NavigationConfigurator } from './NavigationConfigurator';
|
|
27
|
-
|
|
28
|
-
export { NavigationModule, module, moduleKey } from './module';
|
|
29
|
-
export { enableNavigation } from './enable-navigation';
|
|
30
|
-
|
|
31
|
-
export type { INavigationProvider } from './NavigationProvider.interface';
|
|
32
|
-
export { NavigationProvider } from './NavigationProvider';
|
|
33
|
-
|
|
34
|
-
export { createHistory } from './lib/create-history';
|
|
35
|
-
|
|
36
|
-
export { NavigateEvent, type NavigateEventDetail } from './NavigateEvent';
|
|
37
|
-
export { NavigatedEvent, type NavigatedEventDetail } from './NavigatedEvent';
|
|
38
|
-
|
|
39
|
-
export type {
|
|
40
|
-
Path,
|
|
41
|
-
To,
|
|
42
|
-
Location,
|
|
43
|
-
History,
|
|
44
|
-
NavigationBlocker,
|
|
45
|
-
NavigationListener,
|
|
46
|
-
} from './lib/types';
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @deprecated Use {@link History} instead.
|
|
50
|
-
*/
|
|
51
|
-
export type { History as INavigator } from './lib';
|
package/src/lib/BaseHistory.ts
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
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
|
-
*
|
|
20
|
-
* Provides common state management, navigation logic, and subscription
|
|
21
|
-
* lifecycle that is shared across different history backends (browser,
|
|
22
|
-
* hash, memory).
|
|
23
|
-
*/
|
|
24
|
-
export abstract class BaseHistory implements History {
|
|
25
|
-
// Subscriptions for cleanup
|
|
26
|
-
#teardowns = new Subscription();
|
|
27
|
-
|
|
28
|
-
// internal state
|
|
29
|
-
#state: HistoryState;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Gets the current location in the history stack.
|
|
33
|
-
*
|
|
34
|
-
* @returns The current {@link Location} including pathname, search, hash, state, and key
|
|
35
|
-
*/
|
|
36
|
-
public get location(): History['location'] {
|
|
37
|
-
return this.#state.subject.value.current.location;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Gets the current navigation action type.
|
|
42
|
-
*
|
|
43
|
-
* @returns The most recent {@link Action} (`Pop`, `Push`, or `Replace`)
|
|
44
|
-
*/
|
|
45
|
-
public get action(): History['action'] {
|
|
46
|
-
return this.#state.subject.value.current.action;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Observable stream of navigation state changes.
|
|
51
|
-
* Emits on all navigation events (push, replace, pop).
|
|
52
|
-
* @returns Observable of the current navigation update.
|
|
53
|
-
*/
|
|
54
|
-
public get state$(): History['state$'] {
|
|
55
|
-
return this.#state.subject.select((state) => state.current);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Observable stream of navigation actions.
|
|
60
|
-
* @returns Observable of navigation actions.
|
|
61
|
-
*/
|
|
62
|
-
public get action$(): History['action$'] {
|
|
63
|
-
return this.#state.subject.action$;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Checks whether there are any active navigation blockers.
|
|
68
|
-
*
|
|
69
|
-
* @returns `true` if one or more blockers are registered
|
|
70
|
-
*/
|
|
71
|
-
public get hasBlockers(): boolean {
|
|
72
|
-
return this.#state.subject.value.blockers.length > 0;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** @param state - Reactive history state backing this implementation. */
|
|
76
|
-
protected constructor(state: HistoryState) {
|
|
77
|
-
this.#state = state;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Creates a valid href string for a given path.
|
|
82
|
-
*
|
|
83
|
-
* @param to - Target path or partial path object
|
|
84
|
-
* @returns Fully-qualified href string
|
|
85
|
-
*/
|
|
86
|
-
public createHref(to: To): string {
|
|
87
|
-
return this.createURL(to).href;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Creates a {@link URL} object for a given path.
|
|
92
|
-
*
|
|
93
|
-
* @param to - Target path or partial path object
|
|
94
|
-
* @returns Resolved {@link URL} instance
|
|
95
|
-
*/
|
|
96
|
-
public createURL(to: To): URL {
|
|
97
|
-
return this.#state.stack.createURL(to);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Encodes a location by properly URL-encoding the pathname.
|
|
102
|
-
*
|
|
103
|
-
* @param to - Target path or partial path object
|
|
104
|
-
* @returns A {@link Path} with URL-encoded components
|
|
105
|
-
*/
|
|
106
|
-
public encodeLocation(to: To): Path {
|
|
107
|
-
return this.#state.stack.createURL(to);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Navigate to a location with explicit options.
|
|
112
|
-
*
|
|
113
|
-
* @param to - The target path (string, Path object, or Location object)
|
|
114
|
-
* @param options - Navigation options specifying action (PUSH/REPLACE) and optional state
|
|
115
|
-
*/
|
|
116
|
-
public navigate(to: To, options: NavigateOptions): void {
|
|
117
|
-
const action = this.#state.actions.navigate(to, options);
|
|
118
|
-
this.#state.subject.next(action);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
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.
|
|
125
|
-
*/
|
|
126
|
-
public push(to: To, state?: unknown): void {
|
|
127
|
-
this.navigate(to, { state });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
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.
|
|
134
|
-
*/
|
|
135
|
-
public replace(to: To, state?: unknown): void {
|
|
136
|
-
this.navigate(to, { replace: true, state });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Navigates backward or forward in the history stack.
|
|
141
|
-
*
|
|
142
|
-
* @param delta - The number of steps to move (negative for backward, positive for forward)
|
|
143
|
-
*/
|
|
144
|
-
public go(delta: number): void {
|
|
145
|
-
const action = this.#state.actions.go(delta);
|
|
146
|
-
this.#state.subject.next(action);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Sets up a listener for navigation changes.
|
|
151
|
-
*
|
|
152
|
-
* Only listens for POP actions (browser back/forward navigation). PUSH/REPLACE
|
|
153
|
-
* actions are programmatic and synchronous - the caller already knows about them.
|
|
154
|
-
* POP actions come from browser events and are asynchronous, so we need to listen.
|
|
155
|
-
* This matches industry-standard router behavior (Remix/React Router).
|
|
156
|
-
*
|
|
157
|
-
* @param listener - Function to call on navigation changes
|
|
158
|
-
* @returns Function to unsubscribe the listener
|
|
159
|
-
*/
|
|
160
|
-
public listen(listener: NavigationListener): () => void {
|
|
161
|
-
// Filter for POP actions only - PUSH/REPLACE are handled synchronously
|
|
162
|
-
const subscription = this.#state.subject
|
|
163
|
-
.select((state) => state.current)
|
|
164
|
-
.pipe(
|
|
165
|
-
filter((update): update is NavigationUpdate<Action.Pop> => update.action === Action.Pop),
|
|
166
|
-
)
|
|
167
|
-
.subscribe((update) => {
|
|
168
|
-
listener(update);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// Register subscription for cleanup on dispose
|
|
172
|
-
this._addTeardown(subscription);
|
|
173
|
-
|
|
174
|
-
// Return unsubscribe function
|
|
175
|
-
return () => {
|
|
176
|
-
subscription.unsubscribe();
|
|
177
|
-
this._removeTeardown(subscription);
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Registers a blocker to intercept navigation attempts.
|
|
183
|
-
*
|
|
184
|
-
* @param blocker - Navigation blocker function to register
|
|
185
|
-
* @returns Function to unsubscribe the blocker
|
|
186
|
-
*/
|
|
187
|
-
public block(blocker: NavigationBlocker): VoidFunction {
|
|
188
|
-
this.#state.subject.next(this.#state.actions.addBlocker(blocker));
|
|
189
|
-
const removeBlocker = () => {
|
|
190
|
-
this.#state.subject.next(this.#state.actions.removeBlocker(blocker));
|
|
191
|
-
};
|
|
192
|
-
return this._addTeardown(removeBlocker, { executeOnRemove: true });
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Triggers a POP action for the current history location. */
|
|
196
|
-
public pop(): void {
|
|
197
|
-
this.#state.subject.next(this.#state.actions.pop());
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Disposes of the history instance and cleans up all subscriptions.
|
|
202
|
-
*/
|
|
203
|
-
public [Symbol.dispose](): void {
|
|
204
|
-
this.#teardowns.unsubscribe();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Dispatches an action to trigger state updates.
|
|
209
|
-
*
|
|
210
|
-
* Use this to dispatch actions when external events occur (e.g., browser popstate).
|
|
211
|
-
* The action will be processed by flows and update the history state.
|
|
212
|
-
*
|
|
213
|
-
* @param action - The action to dispatch
|
|
214
|
-
*/
|
|
215
|
-
protected _dispatch(action: Actions): void {
|
|
216
|
-
this.#state.subject.next(action);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Registers a cleanup function or subscription for automatic disposal.
|
|
221
|
-
*
|
|
222
|
-
* All teardowns are automatically cleaned up when the history instance is disposed.
|
|
223
|
-
* Use `executeOnRemove: true` if the teardown should run when manually removed.
|
|
224
|
-
*
|
|
225
|
-
* @param teardown - Function or subscription to clean up
|
|
226
|
-
* @param options - Optional configuration
|
|
227
|
-
* @param options.executeOnRemove - Execute teardown when removed (default: false)
|
|
228
|
-
* @returns Function to manually remove the teardown
|
|
229
|
-
*/
|
|
230
|
-
protected _addTeardown(
|
|
231
|
-
teardown: VoidFunction | Subscription,
|
|
232
|
-
options?: { executeOnRemove: boolean },
|
|
233
|
-
): VoidFunction {
|
|
234
|
-
this.#teardowns.add(teardown);
|
|
235
|
-
return () => {
|
|
236
|
-
// Execute teardown callbacks immediately when callers explicitly request removal side effects.
|
|
237
|
-
if (options?.executeOnRemove) {
|
|
238
|
-
typeof teardown === 'function' ? teardown() : teardown.unsubscribe();
|
|
239
|
-
}
|
|
240
|
-
return this._removeTeardown(teardown);
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Removes a teardown from the cleanup collection.
|
|
246
|
-
*
|
|
247
|
-
* Typically called automatically by the function returned from `_addTeardown`.
|
|
248
|
-
* Only call directly if you need to remove a teardown without executing it.
|
|
249
|
-
*
|
|
250
|
-
* @param teardown - The teardown to remove
|
|
251
|
-
*/
|
|
252
|
-
protected _removeTeardown(teardown: VoidFunction | Subscription) {
|
|
253
|
-
this.#teardowns.remove(teardown);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { fromEvent } from 'rxjs';
|
|
2
|
-
import { map } from 'rxjs/operators';
|
|
3
|
-
import { BaseHistory } from './BaseHistory';
|
|
4
|
-
import { BrowserHistoryStack } from './BrowserHistoryStack';
|
|
5
|
-
import { BrowserHistoryHashStack } from './BrowserHistoryHashStack';
|
|
6
|
-
import { resolveWindowLocation } from './utils';
|
|
7
|
-
import { Action, type HistoryStack, type NavigationBlocker } from './types';
|
|
8
|
-
import { createStore, createHistoryReducer, actions } from './state';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Handler for beforeunload events.
|
|
12
|
-
* Prevents page unload when navigation blockers are active.
|
|
13
|
-
*/
|
|
14
|
-
const onBeforeUnload = (event: BeforeUnloadEvent) => {
|
|
15
|
-
event.preventDefault();
|
|
16
|
-
event.returnValue = '';
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Constructor for a HistoryStack implementation.
|
|
20
|
-
*
|
|
21
|
-
* @param window - The window object to use for history operations
|
|
22
|
-
* @returns A new instance of the HistoryStack implementation
|
|
23
|
-
*/
|
|
24
|
-
export interface StackConstructor {
|
|
25
|
-
new (window: Window): HistoryStack;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Options for configuring a BrowserHistory instance.
|
|
30
|
-
*/
|
|
31
|
-
export type BrowserHistoryOptions = {
|
|
32
|
-
/** Optional window object (defaults to global window) */
|
|
33
|
-
window?: Window;
|
|
34
|
-
/** Optional stack constructor (defaults to BrowserHistoryStack, use BrowserHistoryHashStack for hash routing) */
|
|
35
|
-
stack?: StackConstructor;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Browser history implementation using native browser APIs.
|
|
40
|
-
*
|
|
41
|
-
* Uses the browser's History API (pushState/replaceState) for navigation.
|
|
42
|
-
* Automatically listens for popstate/hashchange events to detect browser back/forward navigation.
|
|
43
|
-
* Compatible with industry-standard routers (Remix/React Router).
|
|
44
|
-
*/
|
|
45
|
-
export class BrowserHistory extends BaseHistory {
|
|
46
|
-
#window: Window;
|
|
47
|
-
/**
|
|
48
|
-
* Creates a browser history instance.
|
|
49
|
-
*
|
|
50
|
-
* Initializes with the current window location and sets up listeners for
|
|
51
|
-
* browser navigation events (popstate for regular routing, hashchange for hash routing).
|
|
52
|
-
*
|
|
53
|
-
* @param options - Configuration options
|
|
54
|
-
* @param options.window - Window object to use (defaults to global window)
|
|
55
|
-
* @param options.stack - Stack implementation (defaults to BrowserHistoryStack, use BrowserHistoryHashStack for hash routing)
|
|
56
|
-
* @throws {Error} If window is not available
|
|
57
|
-
*/
|
|
58
|
-
constructor(options: BrowserHistoryOptions = {}) {
|
|
59
|
-
// Use provided stack or default to BrowserHistoryStack
|
|
60
|
-
const Stack: StackConstructor = options.stack ?? BrowserHistoryStack;
|
|
61
|
-
const browserWindow = options.window ?? document.defaultView;
|
|
62
|
-
// Fail fast without a window since the browser stack depends on the DOM history API.
|
|
63
|
-
if (!browserWindow) {
|
|
64
|
-
throw new Error('Window is required');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Initialize state with current window location
|
|
68
|
-
const state = createStore(
|
|
69
|
-
new Stack(browserWindow),
|
|
70
|
-
createHistoryReducer({
|
|
71
|
-
delta: 0,
|
|
72
|
-
action: Action.Pop,
|
|
73
|
-
location: resolveWindowLocation(browserWindow, browserWindow.history),
|
|
74
|
-
}),
|
|
75
|
-
);
|
|
76
|
-
super(state);
|
|
77
|
-
|
|
78
|
-
this.#window = browserWindow;
|
|
79
|
-
|
|
80
|
-
// Determine event type based on stack implementation
|
|
81
|
-
// Hash routing uses 'hashchange', regular routing uses 'popstate'
|
|
82
|
-
const isHashHistory = state.stack instanceof BrowserHistoryHashStack;
|
|
83
|
-
const eventName = isHashHistory ? 'hashchange' : 'popstate';
|
|
84
|
-
|
|
85
|
-
// Listen for browser navigation events and dispatch POP actions
|
|
86
|
-
// This handles browser back/forward button clicks
|
|
87
|
-
this._addTeardown(
|
|
88
|
-
fromEvent(browserWindow, eventName)
|
|
89
|
-
.pipe(
|
|
90
|
-
map(() => {
|
|
91
|
-
const location = state.stack.current;
|
|
92
|
-
return actions.pop({ delta: 0, action: Action.Pop, location });
|
|
93
|
-
}),
|
|
94
|
-
)
|
|
95
|
-
.subscribe(this._dispatch.bind(this)),
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Registers a blocker to intercept navigation attempts.
|
|
101
|
-
*
|
|
102
|
-
* Adds a beforeunload event listener when blockers are active to prevent
|
|
103
|
-
* page unload (e.g., when user tries to close the tab). This provides
|
|
104
|
-
* browser-level protection in addition to in-app navigation blocking.
|
|
105
|
-
*
|
|
106
|
-
* @param blocker - Navigation blocker function to register
|
|
107
|
-
* @returns Function to unsubscribe the blocker
|
|
108
|
-
*/
|
|
109
|
-
public override block(blocker: NavigationBlocker): VoidFunction {
|
|
110
|
-
const unblock = super.block(blocker);
|
|
111
|
-
// Add beforeunload listener when blockers are active
|
|
112
|
-
// This shows browser's "Leave site?" dialog on page unload
|
|
113
|
-
if (this.hasBlockers) {
|
|
114
|
-
window.addEventListener('beforeunload', onBeforeUnload);
|
|
115
|
-
}
|
|
116
|
-
return () => {
|
|
117
|
-
unblock();
|
|
118
|
-
// Remove beforeunload listener if no blockers remain
|
|
119
|
-
if (this.hasBlockers) {
|
|
120
|
-
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|