@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,52 +0,0 @@
|
|
|
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
|
-
);
|
|
@@ -1,36 +0,0 @@
|
|
|
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,77 +0,0 @@
|
|
|
1
|
-
import { createReducer } from '@equinor/fusion-observable';
|
|
2
|
-
import type { LocationState, NavigationUpdate } from '../types';
|
|
3
|
-
import { actions, type Actions } from './actions';
|
|
4
|
-
import { isSuccessAction } from '@equinor/fusion-observable/actions';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Finds the index of the current location in the history array.
|
|
8
|
-
* Used to determine where to insert/replace entries for REPLACE actions.
|
|
9
|
-
*/
|
|
10
|
-
const findIndex = (state: LocationState) => {
|
|
11
|
-
// Locate the current entry by key rather than reference, since updates are cloned.
|
|
12
|
-
return state.history.findIndex((update) => update.location.key === state.current.location.key);
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Creates a reducer for history state management.
|
|
17
|
-
*
|
|
18
|
-
* Handles navigate, go, and pop success actions by updating the
|
|
19
|
-
* history array and current location. Enforces a maximum history
|
|
20
|
-
* length to prevent unbounded memory growth.
|
|
21
|
-
*
|
|
22
|
-
* @param initial - Initial navigation update or factory returning initial state
|
|
23
|
-
* @param options - Optional configuration
|
|
24
|
-
* @param options.maxHistory - Maximum number of history entries to keep (default: `100`)
|
|
25
|
-
* @returns A reducer with initial state for use with {@link createStore}
|
|
26
|
-
*/
|
|
27
|
-
export const createHistoryReducer = (
|
|
28
|
-
initial: NavigationUpdate | (() => LocationState),
|
|
29
|
-
options?: { maxHistory?: number },
|
|
30
|
-
) => {
|
|
31
|
-
const maxHistory = options?.maxHistory ?? 100;
|
|
32
|
-
|
|
33
|
-
const initialState: LocationState =
|
|
34
|
-
initial instanceof Function
|
|
35
|
-
? initial()
|
|
36
|
-
: ({
|
|
37
|
-
current: initial,
|
|
38
|
-
history: [initial],
|
|
39
|
-
blockers: [],
|
|
40
|
-
} satisfies LocationState);
|
|
41
|
-
|
|
42
|
-
return createReducer<LocationState, Actions>(initialState, (builder) => {
|
|
43
|
-
// Handle navigate.success actions to update history array
|
|
44
|
-
builder.addCase(actions.navigate.success, (state, action) => {
|
|
45
|
-
const { update } = action.payload;
|
|
46
|
-
// For REPLACE actions, remove all entries after the current location (the tail)
|
|
47
|
-
// This ensures REPLACE doesn't add to history length when at the end,
|
|
48
|
-
// and removes any forward history when not at the end
|
|
49
|
-
if (update.action === 'REPLACE') {
|
|
50
|
-
const currentIndex = findIndex(state);
|
|
51
|
-
// Only trim when the current entry is actually present in history.
|
|
52
|
-
if (currentIndex !== -1) {
|
|
53
|
-
// Remove all entries from current position onwards
|
|
54
|
-
state.history.splice(currentIndex, state.history.length - currentIndex);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
state.history.push(update);
|
|
58
|
-
});
|
|
59
|
-
// Update current location for all successful actions (navigate, go, pop, etc.)
|
|
60
|
-
builder.addMatcher(isSuccessAction, (state, action) => {
|
|
61
|
-
const { update } = action.payload;
|
|
62
|
-
state.current = update;
|
|
63
|
-
});
|
|
64
|
-
// Limit history size to prevent unbounded growth
|
|
65
|
-
// This matcher always runs (returns true) to check history length after every action
|
|
66
|
-
builder.addMatcher(
|
|
67
|
-
() => true,
|
|
68
|
-
(state) => {
|
|
69
|
-
// Trim only once history has actually grown past the configured cap.
|
|
70
|
-
if (state.history.length > maxHistory) {
|
|
71
|
-
// Keep only the most recent entries, removing oldest ones
|
|
72
|
-
state.history = state.history.slice(-maxHistory);
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { createState, type ReducerWithInitialState } from '@equinor/fusion-observable';
|
|
2
|
-
import { actions, type Actions } from './actions';
|
|
3
|
-
import { flowCreators } from './flow-creators';
|
|
4
|
-
import { createFlow } from './create-flow';
|
|
5
|
-
import type { HistoryFlowCreator } from './navigate';
|
|
6
|
-
import type { HistoryStack, LocationState } from '../types';
|
|
7
|
-
import type { HistoryState } from './history.state';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Creates a history store with the specified stack and reducer.
|
|
11
|
-
* @param stack - The history stack implementation.
|
|
12
|
-
* @param reducer - The reducer that handles state transitions.
|
|
13
|
-
* @param options - Optional flow configuration.
|
|
14
|
-
* @returns A fully-initialized history state with flows attached.
|
|
15
|
-
*/
|
|
16
|
-
export const createStore = (
|
|
17
|
-
stack: HistoryStack,
|
|
18
|
-
reducer: ReducerWithInitialState<LocationState, Actions>,
|
|
19
|
-
options?: {
|
|
20
|
-
flows?: HistoryFlowCreator[];
|
|
21
|
-
skipBlockCheck?: boolean;
|
|
22
|
-
validateCurrentLocation?: boolean;
|
|
23
|
-
},
|
|
24
|
-
): HistoryState => {
|
|
25
|
-
// Attach the stack onto the created state so flows can reach it without prop-drilling.
|
|
26
|
-
const state = Object.assign(createState(actions, reducer), { stack }) as HistoryState;
|
|
27
|
-
const flows = new Set<HistoryFlowCreator>(
|
|
28
|
-
options?.flows ?? [flowCreators.navigate, flowCreators.go, flowCreators.pop],
|
|
29
|
-
);
|
|
30
|
-
// Validation is opt-in since it re-checks the current location on every relevant action.
|
|
31
|
-
if (options?.validateCurrentLocation) {
|
|
32
|
-
flows.add(flowCreators.validateCurrentLocation);
|
|
33
|
-
}
|
|
34
|
-
const flow = createFlow([...flows], {
|
|
35
|
-
skipBlockCheck: options?.skipBlockCheck,
|
|
36
|
-
});
|
|
37
|
-
state.subject.addFlow(flow(stack));
|
|
38
|
-
return state;
|
|
39
|
-
};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { go } from './go';
|
|
2
|
-
import { navigate } from './navigate';
|
|
3
|
-
import { pop } from './pop';
|
|
4
|
-
import { validateCurrentLocation } from './validate-current-location';
|
|
5
|
-
|
|
6
|
-
/** Collection of flow creators for history state management. */
|
|
7
|
-
export const flowCreators = { navigate, go, pop, validateCurrentLocation };
|
package/src/lib/state/go.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { Observable } from 'rxjs';
|
|
2
|
-
import { 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 type { LocationState } from '../types';
|
|
7
|
-
import { Action } from '../types';
|
|
8
|
-
|
|
9
|
-
/** Flow creator for handling go back and forward actions. */
|
|
10
|
-
export const go: HistoryFlowCreator =
|
|
11
|
-
(stack) => (action$: Observable<Actions>, state$: Observable<LocationState>) =>
|
|
12
|
-
// Move the stack by the requested delta and emit the resulting location.
|
|
13
|
-
action$.pipe(
|
|
14
|
-
filterAction(actions.go.type),
|
|
15
|
-
withLatestFrom(state$),
|
|
16
|
-
map(([action, state]) => {
|
|
17
|
-
stack.go(action.payload.delta, state);
|
|
18
|
-
return actions.go.success({ delta: 0, action: Action.Pop, location: stack.current });
|
|
19
|
-
}),
|
|
20
|
-
);
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { FlowState } from '@equinor/fusion-observable';
|
|
2
|
-
import type { actions } from './actions';
|
|
3
|
-
import type { HistoryStack, LocationState } from '../types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* History state containing flow state and stack.
|
|
7
|
-
*/
|
|
8
|
-
export type HistoryState = FlowState<LocationState, typeof actions> & {
|
|
9
|
-
stack: HistoryStack;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Default flows for history state management.
|
|
14
|
-
*/
|
package/src/lib/state/index.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { Observable } from 'rxjs';
|
|
2
|
-
import { map } from 'rxjs/operators';
|
|
3
|
-
import type { Flow } from '@equinor/fusion-observable';
|
|
4
|
-
import { filterAction } from '@equinor/fusion-observable/operators';
|
|
5
|
-
import { actions, type Actions } from './actions';
|
|
6
|
-
import { resolvePath } from '../utils';
|
|
7
|
-
import { type Location, type LocationState, type HistoryStack, Action } from '../types';
|
|
8
|
-
|
|
9
|
-
/** History flow for processing navigation actions. */
|
|
10
|
-
export type HistoryFlow = Flow<Actions, LocationState>;
|
|
11
|
-
|
|
12
|
-
/** Factory function that creates a history flow from a stack. */
|
|
13
|
-
export type HistoryFlowCreator = (stack: HistoryStack) => HistoryFlow;
|
|
14
|
-
|
|
15
|
-
const compareLocation = (a: Location, b: Location): boolean => {
|
|
16
|
-
return (
|
|
17
|
-
a.hash === b.hash &&
|
|
18
|
-
a.search === b.search &&
|
|
19
|
-
a.pathname === b.pathname &&
|
|
20
|
-
JSON.stringify(a.state) === JSON.stringify(b.state)
|
|
21
|
-
);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/** Flow creator for handling navigate actions. */
|
|
25
|
-
export const navigate: HistoryFlowCreator =
|
|
26
|
-
(stack: HistoryStack): HistoryFlow =>
|
|
27
|
-
(action$: Observable<Actions>) => {
|
|
28
|
-
const { navigate } = actions;
|
|
29
|
-
// Transform matching navigation actions into stack updates and success actions.
|
|
30
|
-
return action$.pipe(
|
|
31
|
-
filterAction(navigate.type),
|
|
32
|
-
map((action) => {
|
|
33
|
-
const { payload, meta } = action;
|
|
34
|
-
const path = resolvePath(payload.to);
|
|
35
|
-
const nextLocation = {
|
|
36
|
-
...path,
|
|
37
|
-
key: meta.key,
|
|
38
|
-
state: payload.options.state,
|
|
39
|
-
unstable_mask: undefined,
|
|
40
|
-
} satisfies Location;
|
|
41
|
-
// Abort duplicate navigations to prevent a feedback loop in history state.
|
|
42
|
-
if (compareLocation(nextLocation, stack.current)) {
|
|
43
|
-
return actions.abortNavigate('Location is the same as the current location');
|
|
44
|
-
}
|
|
45
|
-
// Apply replacement or append semantics selected by the navigation request.
|
|
46
|
-
if (payload.options.replace) {
|
|
47
|
-
stack.replace(nextLocation);
|
|
48
|
-
} else {
|
|
49
|
-
stack.push(nextLocation);
|
|
50
|
-
}
|
|
51
|
-
return navigate.success({
|
|
52
|
-
delta: 0,
|
|
53
|
-
action: payload.options.replace ? Action.Replace : Action.Push,
|
|
54
|
-
location: nextLocation,
|
|
55
|
-
});
|
|
56
|
-
}),
|
|
57
|
-
);
|
|
58
|
-
};
|
package/src/lib/state/pop.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Observable } from 'rxjs';
|
|
2
|
-
import { concatMap } 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 type { HistoryStack } from '../types';
|
|
7
|
-
import { Action } from '../types';
|
|
8
|
-
|
|
9
|
-
/** Flow creator for handling POP actions from browser navigation. */
|
|
10
|
-
export const pop: HistoryFlowCreator = (stack: HistoryStack) => (action$: Observable<Actions>) =>
|
|
11
|
-
// Translate a browser-driven POP into a success action carrying its location.
|
|
12
|
-
action$.pipe(
|
|
13
|
-
filterAction(actions.pop.type),
|
|
14
|
-
concatMap(
|
|
15
|
-
(action) =>
|
|
16
|
-
new Observable<Actions>((subscriber) => {
|
|
17
|
-
const currentLocation = action.payload.update?.location ?? stack.current;
|
|
18
|
-
subscriber.next(
|
|
19
|
-
actions.pop.success({ delta: 0, action: Action.Pop, location: currentLocation }),
|
|
20
|
-
);
|
|
21
|
-
subscriber.complete();
|
|
22
|
-
}),
|
|
23
|
-
),
|
|
24
|
-
);
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { Observable } from 'rxjs';
|
|
2
|
-
import { 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 LocationState, type HistoryStack } from '../types';
|
|
7
|
-
|
|
8
|
-
/** Flow creator that validates the current stack location against reducer state. */
|
|
9
|
-
export const validateCurrentLocation: HistoryFlowCreator =
|
|
10
|
-
(stack: HistoryStack) => (action$: Observable<Actions>, state$: Observable<LocationState>) =>
|
|
11
|
-
// Confirm the browser stack's current key still matches reducer state.
|
|
12
|
-
action$.pipe(
|
|
13
|
-
filterAction(actions.validateLocation.type),
|
|
14
|
-
withLatestFrom(state$),
|
|
15
|
-
map(([, state]) => {
|
|
16
|
-
const currentLocation = stack.current;
|
|
17
|
-
// Find the reducer entry matching the stack key before comparing location state.
|
|
18
|
-
const record = state.history.find(({ location }) => location?.key === currentLocation?.key);
|
|
19
|
-
// Reject validation when the browser stack key is absent from reducer state.
|
|
20
|
-
if (!record) {
|
|
21
|
-
return actions.validateLocation.failure(new Error('Stack state not found'));
|
|
22
|
-
}
|
|
23
|
-
// Reject validation when the stack and reducer carry different location state.
|
|
24
|
-
if (record.location?.state !== currentLocation?.state) {
|
|
25
|
-
return actions.validateLocation.failure(new Error('Stack state mismatch'));
|
|
26
|
-
}
|
|
27
|
-
return actions.validateLocation.success({
|
|
28
|
-
delta: 0,
|
|
29
|
-
action: Action.Pop,
|
|
30
|
-
location: currentLocation,
|
|
31
|
-
});
|
|
32
|
-
}),
|
|
33
|
-
);
|
package/src/lib/types.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import type { Observable } from 'rxjs';
|
|
2
|
-
import type { Actions } from './state/actions';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Actions represent the type of change to a location value.
|
|
6
|
-
*/
|
|
7
|
-
export enum Action {
|
|
8
|
-
/**
|
|
9
|
-
* A POP indicates a change to an arbitrary index in the history stack, such
|
|
10
|
-
* as a back or forward navigation. It does not describe the direction of the
|
|
11
|
-
* navigation, only that the current index changed.
|
|
12
|
-
*
|
|
13
|
-
* Note: This is the default action for newly created history objects.
|
|
14
|
-
*/
|
|
15
|
-
Pop = 'POP',
|
|
16
|
-
/**
|
|
17
|
-
* A PUSH indicates a new entry being added to the history stack, such as when
|
|
18
|
-
* a link is clicked and a new page loads.
|
|
19
|
-
*/
|
|
20
|
-
Push = 'PUSH',
|
|
21
|
-
/**
|
|
22
|
-
* A REPLACE indicates the entry at the current index in the history stack
|
|
23
|
-
* being replaced by a new one.
|
|
24
|
-
*/
|
|
25
|
-
Replace = 'REPLACE',
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Target path for navigation operations.
|
|
30
|
-
* Can be a string path or a Path object.
|
|
31
|
-
*/
|
|
32
|
-
export type To = string | Partial<Path>;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Path object containing pathname, optional search, and optional hash.
|
|
36
|
-
*/
|
|
37
|
-
export type Path = {
|
|
38
|
-
pathname: string;
|
|
39
|
-
search: string;
|
|
40
|
-
hash: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Location object representing a navigation entry.
|
|
45
|
-
* Extends {@link Path} with arbitrary state data and a unique key.
|
|
46
|
-
*
|
|
47
|
-
* @template T - The type of the state payload stored in this location entry
|
|
48
|
-
*/
|
|
49
|
-
// biome-ignore lint/suspicious/noExplicitAny: necessary
|
|
50
|
-
export type Location<T = any> = Path & {
|
|
51
|
-
state: T;
|
|
52
|
-
key: string;
|
|
53
|
-
/** Masked path used by react-router 7.13+ for unstable view-transition masking. */
|
|
54
|
-
unstable_mask: Path | undefined;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Internal state for history management.
|
|
59
|
-
*/
|
|
60
|
-
export type LocationState = {
|
|
61
|
-
current: NavigationUpdate;
|
|
62
|
-
history: NavigationUpdate[];
|
|
63
|
-
blockers: NavigationBlocker[];
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Callback function for listening to navigation changes.
|
|
68
|
-
*/
|
|
69
|
-
export type NavigationListener = (update: Readonly<NavigationUpdate>) => void;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Navigation update event containing the action, location, and stack delta.
|
|
73
|
-
*
|
|
74
|
-
* @template A - The type of navigation action (defaults to {@link Action})
|
|
75
|
-
* @template T - The type of the state payload in the location
|
|
76
|
-
*/
|
|
77
|
-
export type NavigationUpdate<A extends string = Action, T = unknown> = {
|
|
78
|
-
delta: number;
|
|
79
|
-
action: Readonly<A>;
|
|
80
|
-
location: Readonly<Location<T>>;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Navigation transition that was blocked.
|
|
85
|
-
* Provides a retry method to allow the navigation to proceed.
|
|
86
|
-
*/
|
|
87
|
-
export interface NavigationTransition extends NavigationUpdate {
|
|
88
|
-
/**
|
|
89
|
-
* Retries the navigation that was blocked.
|
|
90
|
-
*/
|
|
91
|
-
retry(): void;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Callback function for blocking navigation attempts.
|
|
96
|
-
*/
|
|
97
|
-
export type NavigationBlocker = (transition: NavigationTransition) => void;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Options for controlling navigation behavior.
|
|
101
|
-
*
|
|
102
|
-
* @interface NavigateOptions
|
|
103
|
-
*
|
|
104
|
-
* @property {boolean} [replace] - Determines the navigation action:
|
|
105
|
-
* - `false` (default): Pushes a new entry onto the history stack
|
|
106
|
-
* - `true`: Replaces the current entry in the history stack
|
|
107
|
-
*
|
|
108
|
-
* @property {unknown} [state] - Optional state object to associate with the navigation entry.
|
|
109
|
-
* This state can be accessed later when navigating back to this entry.
|
|
110
|
-
*/
|
|
111
|
-
export interface NavigateOptions {
|
|
112
|
-
replace?: boolean;
|
|
113
|
-
state?: unknown;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* History interface for managing navigation state.
|
|
118
|
-
*
|
|
119
|
-
* Compatible with Remix / React Router and provides observable
|
|
120
|
-
* state management via RxJS.
|
|
121
|
-
*
|
|
122
|
-
* @example
|
|
123
|
-
* ```ts
|
|
124
|
-
* const history = createHistory('browser');
|
|
125
|
-
* history.push('/dashboard');
|
|
126
|
-
* history.state$.subscribe(update => console.log(update.location.pathname));
|
|
127
|
-
* ```
|
|
128
|
-
*/
|
|
129
|
-
export interface History extends Disposable {
|
|
130
|
-
/** Observable stream of navigation state updates. */
|
|
131
|
-
readonly state$: Observable<NavigationUpdate>;
|
|
132
|
-
/** Observable stream of navigation actions. */
|
|
133
|
-
readonly action$: Observable<Actions>;
|
|
134
|
-
/** Current navigation action. */
|
|
135
|
-
readonly action: Action;
|
|
136
|
-
/** Current location in the history stack. */
|
|
137
|
-
readonly location: Location;
|
|
138
|
-
|
|
139
|
-
/** Creates a valid href string for a given path.
|
|
140
|
-
* @param to - Target path or partial path object
|
|
141
|
-
* @returns Fully-qualified href string
|
|
142
|
-
*/
|
|
143
|
-
createHref(to: To): string;
|
|
144
|
-
/** Creates a {@link URL} object for a given path.
|
|
145
|
-
* @param to - Target path or partial path object
|
|
146
|
-
* @returns Resolved {@link URL} instance
|
|
147
|
-
*/
|
|
148
|
-
createURL(to: To): URL;
|
|
149
|
-
/** Encodes a location by properly URL-encoding the pathname.
|
|
150
|
-
* @param to - Target path or partial path object
|
|
151
|
-
* @returns A {@link Path} with URL-encoded components
|
|
152
|
-
*/
|
|
153
|
-
encodeLocation(to: To): Path;
|
|
154
|
-
/** Pushes a new navigation entry onto the history stack. */
|
|
155
|
-
push(to: To, state?: unknown): void;
|
|
156
|
-
/** Replaces the current history entry with a new one. */
|
|
157
|
-
replace(to: To, state?: unknown): void;
|
|
158
|
-
/** Navigate to a location with explicit options. */
|
|
159
|
-
navigate(to: To, options?: NavigateOptions): void;
|
|
160
|
-
/** Navigates backward or forward in the history stack. */
|
|
161
|
-
go(delta: number): void;
|
|
162
|
-
/** Sets up a listener for navigation changes.
|
|
163
|
-
* @param listener - Callback invoked on POP actions (browser back/forward)
|
|
164
|
-
* @returns A function that unsubscribes the listener when called
|
|
165
|
-
*/
|
|
166
|
-
listen(listener: NavigationListener): () => void;
|
|
167
|
-
/** Registers a blocker to intercept navigation attempts.
|
|
168
|
-
* @param blocker - Callback invoked before each navigation to allow or prevent it
|
|
169
|
-
* @returns A function that removes the blocker when called
|
|
170
|
-
*/
|
|
171
|
-
block(blocker: NavigationBlocker): VoidFunction;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* History stack interface for managing navigation entries.
|
|
176
|
-
*/
|
|
177
|
-
export interface HistoryStack {
|
|
178
|
-
/** Origin URL for the history stack. */
|
|
179
|
-
readonly origin: string;
|
|
180
|
-
/** Current location in the stack. */
|
|
181
|
-
readonly current: Location;
|
|
182
|
-
/** Pushes a new entry onto the history stack. */
|
|
183
|
-
push(location: Location): void;
|
|
184
|
-
/** Replaces the current entry in the history stack. */
|
|
185
|
-
replace(location: Location): void;
|
|
186
|
-
/** Navigates backward or forward in the history stack. */
|
|
187
|
-
go(delta: number, state: Readonly<LocationState>): void;
|
|
188
|
-
/** Creates a URL object for a given path. */
|
|
189
|
-
createURL(to: To): URL;
|
|
190
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Encodes trailing whitespace in a string by replacing it with '%20'.
|
|
3
|
-
*
|
|
4
|
-
* @param input - The string to encode
|
|
5
|
-
* @returns The string with trailing whitespace encoded
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* encodeTrailingWhitespace('hello ')
|
|
10
|
-
* // 'hello%20'
|
|
11
|
-
*
|
|
12
|
-
* encodeTrailingWhitespace('hello')
|
|
13
|
-
* // 'hello'
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
export const encodeTrailingWhitespace = (input: string): string => {
|
|
17
|
-
return input.replace(/ $/, '%20');
|
|
18
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checks if a string has a protocol prefix.
|
|
3
|
-
*
|
|
4
|
-
* @param input - The string to check
|
|
5
|
-
* @returns True if the string starts with a protocol (e.g., 'http:', 'https:', 'file:')
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* hasProtocol('https://example.com')
|
|
10
|
-
* // true
|
|
11
|
-
*
|
|
12
|
-
* hasProtocol('/users')
|
|
13
|
-
* // false
|
|
14
|
-
*
|
|
15
|
-
* hasProtocol('file:///path/to/file')
|
|
16
|
-
* // true
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
export const hasProtocol = (input: string): boolean => {
|
|
20
|
-
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(input);
|
|
21
|
-
};
|
package/src/lib/utils/index.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { pathToString } from './path-to-string';
|
|
2
|
-
export { pathToUrl } from './path-to-url';
|
|
3
|
-
export { resolvePath } from './resolve-path';
|
|
4
|
-
export { resolveWindowLocation } from './resolve-window-location';
|
|
5
|
-
export { resolveHashLocation } from './resolve-hash-location';
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { Path } from '../types';
|
|
2
|
-
import { encodeTrailingWhitespace } from './encode-trailing-whitespace';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Converts a Partial<Path> object to a path string.
|
|
6
|
-
*
|
|
7
|
-
* @param to - The path object with optional pathname, search, and hash
|
|
8
|
-
* @returns A path string with properly formatted search and hash
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* pathToString({ pathname: '/users', search: '?id=1', hash: '#section' })
|
|
13
|
-
* // '/users?id=1#section'
|
|
14
|
-
*
|
|
15
|
-
* pathToString({ pathname: '/users', search: 'id=1', hash: 'section' })
|
|
16
|
-
* // '/users?id=1#section'
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
export const pathToString = (to: Partial<Path>): string => {
|
|
20
|
-
const pathname = encodeTrailingWhitespace(to.pathname ?? '/');
|
|
21
|
-
const search = encodeTrailingWhitespace(to.search?.replace(/^\?/, '') ?? '');
|
|
22
|
-
const hash = encodeTrailingWhitespace(to.hash?.replace(/^#/, '') ?? '');
|
|
23
|
-
return `${pathname}${search ? `?${search}` : ''}${hash ? `#${hash}` : ''}`;
|
|
24
|
-
};
|