@sigmela/router 0.3.7 → 0.5.0
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 +38 -0
- package/LICENSE +2 -1
- package/README.md +127 -471
- package/dist/Router.d.ts +190 -0
- package/dist/Router.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +56 -0
- package/dist/components/Modal.d.ts.map +1 -0
- package/dist/components/OverlayContentScope.d.ts +8 -0
- package/dist/components/OverlayContentScope.d.ts.map +1 -0
- package/dist/components/RouterCloseButton.d.ts +9 -0
- package/dist/components/RouterCloseButton.d.ts.map +1 -0
- package/dist/components/Sheet.d.ts +56 -0
- package/dist/components/Sheet.d.ts.map +1 -0
- package/dist/components/SheetSurface.d.ts +27 -0
- package/dist/components/SheetSurface.d.ts.map +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/pageScrollLock.d.ts +3 -0
- package/dist/components/pageScrollLock.d.ts.map +1 -0
- package/dist/components/sheetDragEngine.d.ts +27 -0
- package/dist/components/sheetDragEngine.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3696 -0
- package/dist/index.js.map +1 -0
- package/dist/navigationHistory.d.ts +17 -0
- package/dist/navigationHistory.d.ts.map +1 -0
- package/dist/router.css +975 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/package.json +73 -174
- package/lib/module/Drawer/Drawer.js +0 -250
- package/lib/module/Drawer/DrawerContext.js +0 -4
- package/lib/module/Drawer/DrawerIcon.web.js +0 -47
- package/lib/module/Drawer/RenderDrawer.native.js +0 -248
- package/lib/module/Drawer/RenderDrawer.web.js +0 -197
- package/lib/module/Drawer/useDrawer.js +0 -11
- package/lib/module/Navigation.js +0 -80
- package/lib/module/NavigationStack.js +0 -254
- package/lib/module/Router.js +0 -2171
- package/lib/module/RouterContext.js +0 -33
- package/lib/module/ScreenStack/ScreenStack.native.js +0 -3
- package/lib/module/ScreenStack/ScreenStack.web.js +0 -561
- package/lib/module/ScreenStack/ScreenStackContext.js +0 -21
- package/lib/module/ScreenStack/animationHelpers.js +0 -87
- package/lib/module/ScreenStack/index.js +0 -3
- package/lib/module/ScreenStackItem/ScreenStackItem.js +0 -70
- package/lib/module/ScreenStackItem/ScreenStackItem.types.js +0 -3
- package/lib/module/ScreenStackItem/ScreenStackItem.web.js +0 -171
- package/lib/module/ScreenStackItem/index.js +0 -3
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +0 -70
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +0 -5
- package/lib/module/ScreenStackSheetItem/index.js +0 -3
- package/lib/module/SplitView/RenderSplitView.native.js +0 -101
- package/lib/module/SplitView/RenderSplitView.web.js +0 -107
- package/lib/module/SplitView/SplitView.js +0 -97
- package/lib/module/SplitView/SplitViewContext.js +0 -4
- package/lib/module/SplitView/index.js +0 -5
- package/lib/module/SplitView/useSplitView.js +0 -11
- package/lib/module/StackRenderer.js +0 -42
- package/lib/module/TabBar/RenderTabBar.native.js +0 -418
- package/lib/module/TabBar/RenderTabBar.web.js +0 -218
- package/lib/module/TabBar/TabBar.js +0 -208
- package/lib/module/TabBar/TabBarContext.js +0 -4
- package/lib/module/TabBar/TabIcon.web.js +0 -47
- package/lib/module/TabBar/index.js +0 -6
- package/lib/module/TabBar/useTabBar.js +0 -11
- package/lib/module/TabBar/useTabBarHeight.js +0 -22
- package/lib/module/createController.js +0 -5
- package/lib/module/index.js +0 -15
- package/lib/module/navigationNode.js +0 -3
- package/lib/module/package.json +0 -1
- package/lib/module/styles.css +0 -1111
- package/lib/module/types.js +0 -13
- package/lib/typescript/package.json +0 -1
- package/lib/typescript/src/Drawer/Drawer.d.ts +0 -100
- package/lib/typescript/src/Drawer/DrawerContext.d.ts +0 -3
- package/lib/typescript/src/Drawer/DrawerIcon.web.d.ts +0 -7
- package/lib/typescript/src/Drawer/RenderDrawer.native.d.ts +0 -8
- package/lib/typescript/src/Drawer/RenderDrawer.web.d.ts +0 -8
- package/lib/typescript/src/Drawer/useDrawer.d.ts +0 -2
- package/lib/typescript/src/Navigation.d.ts +0 -17
- package/lib/typescript/src/NavigationStack.d.ts +0 -46
- package/lib/typescript/src/Router.d.ts +0 -232
- package/lib/typescript/src/RouterContext.d.ts +0 -18
- package/lib/typescript/src/ScreenStack/ScreenStack.native.d.ts +0 -2
- package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -9
- package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +0 -31
- package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +0 -6
- package/lib/typescript/src/ScreenStack/index.d.ts +0 -2
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.d.ts +0 -3
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +0 -14
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +0 -3
- package/lib/typescript/src/ScreenStackItem/index.d.ts +0 -3
- package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.native.d.ts +0 -11
- package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.web.d.ts +0 -2
- package/lib/typescript/src/ScreenStackSheetItem/index.d.ts +0 -2
- package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +0 -19
- package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +0 -8
- package/lib/typescript/src/SplitView/SplitView.d.ts +0 -33
- package/lib/typescript/src/SplitView/SplitViewContext.d.ts +0 -3
- package/lib/typescript/src/SplitView/index.d.ts +0 -5
- package/lib/typescript/src/SplitView/useSplitView.d.ts +0 -2
- package/lib/typescript/src/StackRenderer.d.ts +0 -8
- package/lib/typescript/src/TabBar/RenderTabBar.native.d.ts +0 -8
- package/lib/typescript/src/TabBar/RenderTabBar.web.d.ts +0 -8
- package/lib/typescript/src/TabBar/TabBar.d.ts +0 -112
- package/lib/typescript/src/TabBar/TabBarContext.d.ts +0 -3
- package/lib/typescript/src/TabBar/TabIcon.web.d.ts +0 -7
- package/lib/typescript/src/TabBar/index.d.ts +0 -5
- package/lib/typescript/src/TabBar/useTabBar.d.ts +0 -2
- package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +0 -18
- package/lib/typescript/src/createController.d.ts +0 -15
- package/lib/typescript/src/index.d.ts +0 -21
- package/lib/typescript/src/navigationNode.d.ts +0 -41
- package/lib/typescript/src/types.d.ts +0 -140
package/lib/module/Router.js
DELETED
|
@@ -1,2171 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { nanoid } from 'nanoid/non-secure';
|
|
4
|
-
import { Platform } from 'react-native';
|
|
5
|
-
import qs from 'query-string';
|
|
6
|
-
import { isModalLikePresentation } from "./types.js";
|
|
7
|
-
function canSwitchToRoute(node) {
|
|
8
|
-
return node !== undefined && typeof node.switchToRoute === 'function';
|
|
9
|
-
}
|
|
10
|
-
function canLookupRoutes(node) {
|
|
11
|
-
return node !== undefined && typeof node.hasRoute === 'function';
|
|
12
|
-
}
|
|
13
|
-
const EMPTY_ARRAY = [];
|
|
14
|
-
export class Router {
|
|
15
|
-
root = null;
|
|
16
|
-
listeners = new Set();
|
|
17
|
-
registry = [];
|
|
18
|
-
state = {
|
|
19
|
-
history: []
|
|
20
|
-
};
|
|
21
|
-
debugEnabled = false;
|
|
22
|
-
sheetDismissers = new Map();
|
|
23
|
-
dismissedStackIds = new Set();
|
|
24
|
-
stackListeners = new Map();
|
|
25
|
-
stackById = new Map();
|
|
26
|
-
routeById = new Map();
|
|
27
|
-
stackActivators = new Map();
|
|
28
|
-
compiledRouteById = new Map();
|
|
29
|
-
stackHistories = new Map();
|
|
30
|
-
activeRoute = null;
|
|
31
|
-
activeRouteListeners = new Set();
|
|
32
|
-
rootListeners = new Set();
|
|
33
|
-
rootTransition = undefined;
|
|
34
|
-
// Root swaps should behave like a fresh initial mount (no enter animation).
|
|
35
|
-
// We keep the API option for compatibility, but suppress transition application.
|
|
36
|
-
suppressRootTransitionOnNextRead = false;
|
|
37
|
-
lastBrowserIndex = 0;
|
|
38
|
-
suppressHistorySyncCount = 0;
|
|
39
|
-
// Used to prevent stale controller-driven navigations (controller calls present later).
|
|
40
|
-
navigationToken = 0;
|
|
41
|
-
// parsePath one-slot cache
|
|
42
|
-
lastParsedPath = null;
|
|
43
|
-
lastParsedResult = null;
|
|
44
|
-
|
|
45
|
-
// matchBaseRoute one-slot cache
|
|
46
|
-
lastMatchedPathname = null;
|
|
47
|
-
lastMatchedQuery = null;
|
|
48
|
-
lastMatchedResult = null;
|
|
49
|
-
constructor(config) {
|
|
50
|
-
this.debugEnabled = config.debug ?? false;
|
|
51
|
-
this.routerScreenOptions = config.screenOptions;
|
|
52
|
-
this.log('ctor');
|
|
53
|
-
this.roots = config.roots;
|
|
54
|
-
this.activeRootKey = config.root;
|
|
55
|
-
const initialRoot = this.roots[this.activeRootKey];
|
|
56
|
-
if (!initialRoot) {
|
|
57
|
-
throw new Error(`Router: root "${String(this.activeRootKey)}" not found in config.roots`);
|
|
58
|
-
}
|
|
59
|
-
this.root = initialRoot;
|
|
60
|
-
this.buildRegistry();
|
|
61
|
-
if (this.isWebEnv()) {
|
|
62
|
-
this.setupBrowserHistory();
|
|
63
|
-
const url = this.getCurrentUrl();
|
|
64
|
-
this.buildHistoryFromUrl(url);
|
|
65
|
-
} else {
|
|
66
|
-
this.seedInitialHistory();
|
|
67
|
-
}
|
|
68
|
-
this.recomputeActiveRoute();
|
|
69
|
-
}
|
|
70
|
-
isDebugEnabled() {
|
|
71
|
-
return this.debugEnabled;
|
|
72
|
-
}
|
|
73
|
-
isStackBeingDismissed(stackId) {
|
|
74
|
-
if (!stackId) return false;
|
|
75
|
-
return this.dismissedStackIds.has(stackId);
|
|
76
|
-
}
|
|
77
|
-
clearStackDismissed(stackId) {
|
|
78
|
-
if (!stackId) return;
|
|
79
|
-
this.dismissedStackIds.delete(stackId);
|
|
80
|
-
}
|
|
81
|
-
markStackDismissed(stackId) {
|
|
82
|
-
this.dismissedStackIds.add(stackId);
|
|
83
|
-
}
|
|
84
|
-
log(message, data) {
|
|
85
|
-
if (this.debugEnabled) {
|
|
86
|
-
if (data !== undefined) {
|
|
87
|
-
console.log(`[Router] ${message}`, data);
|
|
88
|
-
} else {
|
|
89
|
-
console.log(`[Router] ${message}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
navigate = path => {
|
|
94
|
-
if (!path || typeof path !== 'string') {
|
|
95
|
-
if (__DEV__) throw new Error('Router.navigate: path must be a non-empty string');
|
|
96
|
-
console.warn('[Router] navigate called with invalid path');
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (this.isWebEnv()) {
|
|
100
|
-
const syncWithUrl = this.shouldSyncPathWithUrl(path);
|
|
101
|
-
this.pushUrl(path, {
|
|
102
|
-
syncWithUrl
|
|
103
|
-
});
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
this.performNavigation(path, 'push');
|
|
107
|
-
};
|
|
108
|
-
replace = (path, dedupe) => {
|
|
109
|
-
if (!path || typeof path !== 'string') {
|
|
110
|
-
if (__DEV__) throw new Error('Router.replace: path must be a non-empty string');
|
|
111
|
-
console.warn('[Router] replace called with invalid path');
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (this.isWebEnv()) {
|
|
115
|
-
const syncWithUrl = this.shouldSyncPathWithUrl(path);
|
|
116
|
-
this.replaceUrl(path, {
|
|
117
|
-
syncWithUrl,
|
|
118
|
-
dedupe: !!dedupe
|
|
119
|
-
});
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
this.performNavigation(path, 'replace', {
|
|
123
|
-
dedupe: !!dedupe
|
|
124
|
-
});
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Web-only convenience: resets Router navigation state to what a fresh
|
|
129
|
-
* deep-link render would produce for the given URL.
|
|
130
|
-
*
|
|
131
|
-
* This intentionally clears preserved per-tab stacks (useful when tab stacks
|
|
132
|
-
* and browser URL can get out of sync).
|
|
133
|
-
*/
|
|
134
|
-
reset = path => {
|
|
135
|
-
if (!path || typeof path !== 'string') {
|
|
136
|
-
if (__DEV__) throw new Error('Router.reset: path must be a non-empty string');
|
|
137
|
-
console.warn('[Router] reset called with invalid path');
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (this.isWebEnv()) {
|
|
141
|
-
const prevHistory = this.state.history;
|
|
142
|
-
// Important: clear current history before matching routes for the new URL.
|
|
143
|
-
// Otherwise, matchBaseRoute() tie-breakers may pick a wrong stack for ambiguous
|
|
144
|
-
// paths like '/' based on previously visited stacks, breaking tab switches.
|
|
145
|
-
this.state = {
|
|
146
|
-
history: []
|
|
147
|
-
};
|
|
148
|
-
this.stackHistories.clear();
|
|
149
|
-
// Update browser URL without triggering onHistory navigation.
|
|
150
|
-
this.replaceUrlSilently(path);
|
|
151
|
-
// Rebuild history exactly like initial URL parsing (wipes other stacks).
|
|
152
|
-
this.buildHistoryFromUrl(path, prevHistory);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
// Native fallback: closest semantics is a replace navigation.
|
|
156
|
-
this.performNavigation(path, 'replace', {
|
|
157
|
-
dedupe: true
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
|
-
registerSheetDismisser = (key, dismisser) => {
|
|
161
|
-
this.sheetDismissers.set(key, dismisser);
|
|
162
|
-
};
|
|
163
|
-
unregisterSheetDismisser = key => {
|
|
164
|
-
this.sheetDismissers.delete(key);
|
|
165
|
-
};
|
|
166
|
-
goBack = () => {
|
|
167
|
-
if (this.isWebEnv()) {
|
|
168
|
-
const prevHistoryRef = this.state.history;
|
|
169
|
-
const popped = this.popFromActiveStack();
|
|
170
|
-
// Sync URL only if Router state actually changed (avoid syncing URL when
|
|
171
|
-
// a UI-level dismisser is used and the history pop will happen later).
|
|
172
|
-
if (popped && this.state.history !== prevHistoryRef) {
|
|
173
|
-
this.syncUrlAfterInternalPop(popped);
|
|
174
|
-
}
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
this.popFromActiveStack();
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Closes the nearest modal or sheet, regardless of navigation depth inside it.
|
|
182
|
-
* Useful when a NavigationStack is rendered inside a modal and you want to
|
|
183
|
-
* close the entire modal from any screen within it.
|
|
184
|
-
*/
|
|
185
|
-
dismiss = () => {
|
|
186
|
-
// Find the nearest modal/sheet item in history (searching from end)
|
|
187
|
-
let modalItem = null;
|
|
188
|
-
for (let i = this.state.history.length - 1; i >= 0; i--) {
|
|
189
|
-
const item = this.state.history[i];
|
|
190
|
-
if (item && isModalLikePresentation(item.options?.stackPresentation)) {
|
|
191
|
-
modalItem = item;
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (!modalItem) {
|
|
196
|
-
this.log('dismiss: no modal found in history');
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
this.log('dismiss: closing modal', {
|
|
200
|
-
key: modalItem.key,
|
|
201
|
-
routeId: modalItem.routeId,
|
|
202
|
-
stackId: modalItem.stackId,
|
|
203
|
-
presentation: modalItem.options?.stackPresentation
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Handle sheet dismisser if registered
|
|
207
|
-
if (modalItem.options?.stackPresentation === 'sheet') {
|
|
208
|
-
const dismisser = this.sheetDismissers.get(modalItem.key);
|
|
209
|
-
if (dismisser) {
|
|
210
|
-
this.unregisterSheetDismisser(modalItem.key);
|
|
211
|
-
dismisser();
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Check if modal has a childNode (NavigationStack added via addModal)
|
|
217
|
-
const compiled = this.getCompiledRoute(modalItem.routeId);
|
|
218
|
-
const childNode = compiled?.childNode;
|
|
219
|
-
if (childNode) {
|
|
220
|
-
// Modal stack: remove all items from child stack AND the modal wrapper item
|
|
221
|
-
const childStackId = childNode.getId();
|
|
222
|
-
this.log('dismiss: closing modal stack', {
|
|
223
|
-
childStackId,
|
|
224
|
-
modalKey: modalItem.key
|
|
225
|
-
});
|
|
226
|
-
this.markStackDismissed(childStackId);
|
|
227
|
-
|
|
228
|
-
// Clear child stack's history cache BEFORE setState so listeners see clean state
|
|
229
|
-
this.stackHistories.delete(childStackId);
|
|
230
|
-
const newHistory = this.state.history.filter(item => item.stackId !== childStackId && item.key !== modalItem.key);
|
|
231
|
-
this.setState({
|
|
232
|
-
history: newHistory
|
|
233
|
-
});
|
|
234
|
-
} else {
|
|
235
|
-
// Simple modal: just pop the modal item
|
|
236
|
-
// applyHistoryChange already calls recomputeActiveRoute + emit(this.listeners)
|
|
237
|
-
this.applyHistoryChange('pop', modalItem);
|
|
238
|
-
}
|
|
239
|
-
if (childNode) {
|
|
240
|
-
// childNode branch uses setState directly, so we need to emit manually
|
|
241
|
-
this.recomputeActiveRoute();
|
|
242
|
-
this.emit(this.listeners);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Sync URL on web
|
|
246
|
-
if (this.isWebEnv()) {
|
|
247
|
-
this.syncUrlAfterInternalPop(modalItem);
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
getState = () => {
|
|
251
|
-
return this.state;
|
|
252
|
-
};
|
|
253
|
-
subscribe(listener) {
|
|
254
|
-
this.listeners.add(listener);
|
|
255
|
-
return () => {
|
|
256
|
-
this.listeners.delete(listener);
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
subscribeActiveRoute = listener => {
|
|
260
|
-
this.activeRouteListeners.add(listener);
|
|
261
|
-
return () => {
|
|
262
|
-
this.activeRouteListeners.delete(listener);
|
|
263
|
-
};
|
|
264
|
-
};
|
|
265
|
-
getStackHistory = stackId => {
|
|
266
|
-
if (!stackId) return EMPTY_ARRAY;
|
|
267
|
-
if (!this.stackHistories.has(stackId)) {
|
|
268
|
-
const current = this.state.history.filter(item => item.stackId === stackId);
|
|
269
|
-
this.stackHistories.set(stackId, current);
|
|
270
|
-
return current;
|
|
271
|
-
}
|
|
272
|
-
return this.stackHistories.get(stackId) ?? EMPTY_ARRAY;
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Returns all history items in navigation order.
|
|
277
|
-
* Useful for rendering all screens including modal stacks.
|
|
278
|
-
*/
|
|
279
|
-
getFullHistory = () => {
|
|
280
|
-
return this.state.history;
|
|
281
|
-
};
|
|
282
|
-
subscribeStack = (stackId, cb) => {
|
|
283
|
-
if (!stackId) return () => {};
|
|
284
|
-
let set = this.stackListeners.get(stackId);
|
|
285
|
-
if (!set) {
|
|
286
|
-
set = new Set();
|
|
287
|
-
this.stackListeners.set(stackId, set);
|
|
288
|
-
}
|
|
289
|
-
set.add(cb);
|
|
290
|
-
return () => {
|
|
291
|
-
set.delete(cb);
|
|
292
|
-
if (set.size === 0) {
|
|
293
|
-
this.stackListeners.delete(stackId);
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
};
|
|
297
|
-
getRootStackId() {
|
|
298
|
-
return this.root?.getId();
|
|
299
|
-
}
|
|
300
|
-
subscribeRoot(listener) {
|
|
301
|
-
this.rootListeners.add(listener);
|
|
302
|
-
return () => this.rootListeners.delete(listener);
|
|
303
|
-
}
|
|
304
|
-
emitRootChange() {
|
|
305
|
-
this.emit(this.rootListeners);
|
|
306
|
-
}
|
|
307
|
-
getRootTransition() {
|
|
308
|
-
if (this.suppressRootTransitionOnNextRead) {
|
|
309
|
-
this.suppressRootTransitionOnNextRead = false;
|
|
310
|
-
// Ensure we don't accidentally apply it on subsequent renders.
|
|
311
|
-
this.rootTransition = undefined;
|
|
312
|
-
return undefined;
|
|
313
|
-
}
|
|
314
|
-
return this.rootTransition;
|
|
315
|
-
}
|
|
316
|
-
setRoot(nextRootKey, options) {
|
|
317
|
-
const nextRoot = this.roots[nextRootKey];
|
|
318
|
-
if (!nextRoot) {
|
|
319
|
-
throw new Error(`Router: root "${String(nextRootKey)}" not found in config.roots`);
|
|
320
|
-
}
|
|
321
|
-
this.activeRootKey = nextRootKey;
|
|
322
|
-
this.root = nextRoot;
|
|
323
|
-
this.rootTransition = options?.transition ?? undefined;
|
|
324
|
-
// Make the incoming root behave like initial: suppress enter animation.
|
|
325
|
-
this.suppressRootTransitionOnNextRead = true;
|
|
326
|
-
this.registry.length = 0;
|
|
327
|
-
this.stackById.clear();
|
|
328
|
-
this.routeById.clear();
|
|
329
|
-
this.compiledRouteById.clear();
|
|
330
|
-
this.stackActivators.clear();
|
|
331
|
-
this.lastMatchedPathname = null;
|
|
332
|
-
this.lastMatchedQuery = null;
|
|
333
|
-
this.lastMatchedResult = null;
|
|
334
|
-
this.state = {
|
|
335
|
-
history: []
|
|
336
|
-
};
|
|
337
|
-
this.stackHistories.clear();
|
|
338
|
-
this.sheetDismissers.clear();
|
|
339
|
-
this.dismissedStackIds.clear();
|
|
340
|
-
this.stackListeners.clear();
|
|
341
|
-
this.buildRegistry();
|
|
342
|
-
this.seedInitialHistory();
|
|
343
|
-
this.recomputeActiveRoute();
|
|
344
|
-
this.emitRootChange();
|
|
345
|
-
this.emit(this.listeners);
|
|
346
|
-
}
|
|
347
|
-
getActiveRoute = () => {
|
|
348
|
-
return this.activeRoute;
|
|
349
|
-
};
|
|
350
|
-
debugGetState() {
|
|
351
|
-
return {
|
|
352
|
-
history: this.state.history.map(h => ({
|
|
353
|
-
key: h.key,
|
|
354
|
-
routeId: h.routeId,
|
|
355
|
-
stackId: h.stackId,
|
|
356
|
-
path: h.path,
|
|
357
|
-
params: h.params,
|
|
358
|
-
query: h.query,
|
|
359
|
-
stackPresentation: h.options?.stackPresentation
|
|
360
|
-
})),
|
|
361
|
-
stackSlices: Array.from(new Set(this.state.history.map(h => h.stackId).filter(Boolean))).map(stackId => ({
|
|
362
|
-
stackId,
|
|
363
|
-
items: this.getStackHistory(stackId).map(i => ({
|
|
364
|
-
key: i.key,
|
|
365
|
-
routeId: i.routeId,
|
|
366
|
-
path: i.path,
|
|
367
|
-
params: i.params,
|
|
368
|
-
query: i.query,
|
|
369
|
-
stackPresentation: i.options?.stackPresentation
|
|
370
|
-
}))
|
|
371
|
-
})),
|
|
372
|
-
activeRoute: this.activeRoute,
|
|
373
|
-
registry: this.registry.map(r => ({
|
|
374
|
-
routeId: r.routeId,
|
|
375
|
-
path: r.path,
|
|
376
|
-
pathnamePattern: r.pathnamePattern,
|
|
377
|
-
stackId: r.stackId,
|
|
378
|
-
isWildcardPath: r.isWildcardPath,
|
|
379
|
-
baseSpecificity: r.baseSpecificity,
|
|
380
|
-
queryPattern: r.queryPattern,
|
|
381
|
-
options: r.options
|
|
382
|
-
}))
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
debugMatchRoute(path) {
|
|
386
|
-
const {
|
|
387
|
-
pathname,
|
|
388
|
-
query
|
|
389
|
-
} = this.parsePath(path);
|
|
390
|
-
const matches = [];
|
|
391
|
-
for (const r of this.registry) {
|
|
392
|
-
if (!r.stackId) continue;
|
|
393
|
-
let pathMatch;
|
|
394
|
-
if (r.isWildcardPath) {
|
|
395
|
-
pathMatch = {
|
|
396
|
-
params: {}
|
|
397
|
-
};
|
|
398
|
-
} else {
|
|
399
|
-
pathMatch = r.matchPath(pathname);
|
|
400
|
-
}
|
|
401
|
-
const queryMatch = this.matchQueryPattern(r.queryPattern, query);
|
|
402
|
-
matches.push({
|
|
403
|
-
routeId: r.routeId,
|
|
404
|
-
path: r.path,
|
|
405
|
-
pathnamePattern: r.pathnamePattern,
|
|
406
|
-
stackId: r.stackId,
|
|
407
|
-
isWildcardPath: r.isWildcardPath,
|
|
408
|
-
baseSpecificity: r.baseSpecificity,
|
|
409
|
-
pathMatch: !!pathMatch,
|
|
410
|
-
queryMatch,
|
|
411
|
-
options: r.options
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
const best = this.matchBaseRoute(pathname, query);
|
|
415
|
-
return {
|
|
416
|
-
input: {
|
|
417
|
-
path,
|
|
418
|
-
pathname,
|
|
419
|
-
query
|
|
420
|
-
},
|
|
421
|
-
matches,
|
|
422
|
-
best: best ? {
|
|
423
|
-
routeId: best.routeId,
|
|
424
|
-
path: best.path,
|
|
425
|
-
stackId: best.stackId,
|
|
426
|
-
options: best.options
|
|
427
|
-
} : null
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
debugGetStackInfo(stackId) {
|
|
431
|
-
const slice = this.getStackHistory(stackId);
|
|
432
|
-
return {
|
|
433
|
-
stackId,
|
|
434
|
-
historyLength: slice.length,
|
|
435
|
-
items: slice.map(i => ({
|
|
436
|
-
key: i.key,
|
|
437
|
-
routeId: i.routeId,
|
|
438
|
-
path: i.path,
|
|
439
|
-
params: i.params,
|
|
440
|
-
query: i.query,
|
|
441
|
-
stackPresentation: i.options?.stackPresentation
|
|
442
|
-
}))
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
debugGetAllStacks() {
|
|
446
|
-
const stackIds = Array.from(new Set(this.state.history.map(h => h.stackId).filter(Boolean)));
|
|
447
|
-
return stackIds.map(stackId => this.debugGetStackInfo(stackId));
|
|
448
|
-
}
|
|
449
|
-
recomputeActiveRoute() {
|
|
450
|
-
const history = this.state.history;
|
|
451
|
-
if (history.length > 0) {
|
|
452
|
-
const top = history[history.length - 1];
|
|
453
|
-
if (top) {
|
|
454
|
-
const meta = this.routeById.get(top.routeId);
|
|
455
|
-
const newRoute = meta ? {
|
|
456
|
-
...meta,
|
|
457
|
-
routeId: top.routeId,
|
|
458
|
-
params: top.params,
|
|
459
|
-
query: top.query,
|
|
460
|
-
path: top.path
|
|
461
|
-
} : {
|
|
462
|
-
routeId: top.routeId,
|
|
463
|
-
stackId: top.stackId,
|
|
464
|
-
params: top.params,
|
|
465
|
-
query: top.query,
|
|
466
|
-
path: top.path
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
// Shallow compare to keep stable reference and avoid unnecessary re-renders
|
|
470
|
-
if (this.activeRoute && this.activeRoute.routeId === newRoute.routeId && this.activeRoute.stackId === newRoute.stackId && this.activeRoute.path === newRoute.path && this.activeRoute.params === newRoute.params && this.activeRoute.query === newRoute.query) {
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
this.activeRoute = newRoute;
|
|
474
|
-
this.emit(this.activeRouteListeners);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
if (this.activeRoute !== null) {
|
|
479
|
-
this.activeRoute = null;
|
|
480
|
-
this.emit(this.activeRouteListeners);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
performNavigation(path, action, opts) {
|
|
484
|
-
if (this.destroyed) return;
|
|
485
|
-
const {
|
|
486
|
-
pathname,
|
|
487
|
-
query
|
|
488
|
-
} = this.parsePath(path);
|
|
489
|
-
const navigationToken = ++this.navigationToken;
|
|
490
|
-
this.log('performNavigation', {
|
|
491
|
-
path,
|
|
492
|
-
pathname,
|
|
493
|
-
query,
|
|
494
|
-
action,
|
|
495
|
-
dedupe: opts?.dedupe
|
|
496
|
-
});
|
|
497
|
-
const base = this.matchBaseRoute(pathname, query);
|
|
498
|
-
this.log('resolveNavigation', {
|
|
499
|
-
base: base ? {
|
|
500
|
-
routeId: base.routeId,
|
|
501
|
-
path: base.path,
|
|
502
|
-
stackId: base.stackId
|
|
503
|
-
} : null
|
|
504
|
-
});
|
|
505
|
-
if (!base) {
|
|
506
|
-
if (__DEV__) {
|
|
507
|
-
throw new Error(`Route not found: "${pathname}"`);
|
|
508
|
-
}
|
|
509
|
-
console.warn(`[Router] Route not found: "${pathname}"`);
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Ensure the root-level container (e.g. TabBar) is activated for the matched route.
|
|
514
|
-
// This is important when the matched route belongs to a nested stack inside a composite node
|
|
515
|
-
// like SplitView: stackActivators won't fire for the tab itself in that case.
|
|
516
|
-
const rootStackId = this.root?.getId();
|
|
517
|
-
if (rootStackId) {
|
|
518
|
-
this.activateContainerForRoute(base.routeId, rootStackId);
|
|
519
|
-
}
|
|
520
|
-
const activator = base.stackId ? this.stackActivators.get(base.stackId) : undefined;
|
|
521
|
-
if (activator) {
|
|
522
|
-
activator();
|
|
523
|
-
}
|
|
524
|
-
const matchResult = base.matchPath(pathname);
|
|
525
|
-
const params = matchResult ? matchResult.params : undefined;
|
|
526
|
-
|
|
527
|
-
// Smart navigate:
|
|
528
|
-
// If navigate(push) targets the currently active routeId within the same stack, treat it as
|
|
529
|
-
// "same screen, new data" and perform a replace (preserving the existing key) by default.
|
|
530
|
-
//
|
|
531
|
-
// Consumers can opt out per-route via ScreenOptions.allowMultipleInstances=true.
|
|
532
|
-
if (action === 'push' && base.stackId) {
|
|
533
|
-
const mergedOptions = this.mergeOptions(base.options, base.stackId);
|
|
534
|
-
const allowMultipleInstances = mergedOptions?.allowMultipleInstances === true;
|
|
535
|
-
const isActiveSameStack = this.activeRoute?.stackId === base.stackId;
|
|
536
|
-
const isActiveSameRoute = this.activeRoute?.routeId === base.routeId;
|
|
537
|
-
|
|
538
|
-
// Optional safety: only apply to push-presentation screens by default.
|
|
539
|
-
const presentation = mergedOptions?.stackPresentation ?? 'push';
|
|
540
|
-
const isPushPresentation = presentation === 'push';
|
|
541
|
-
if (!allowMultipleInstances && isPushPresentation && isActiveSameStack && isActiveSameRoute) {
|
|
542
|
-
const newItem = this.createHistoryItem(base, params, query, pathname);
|
|
543
|
-
this.applyHistoryChange('replace', newItem);
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
if (action === 'push') {
|
|
548
|
-
if (base.stackId) {
|
|
549
|
-
let existing = this.findExistingRoute(base.stackId, base.routeId, pathname, params ?? {});
|
|
550
|
-
if (!existing) {
|
|
551
|
-
existing = this.findExistingRouteByPathname(base.stackId, pathname, params ?? {});
|
|
552
|
-
}
|
|
553
|
-
if (existing) {
|
|
554
|
-
this.log('push: found existing item in stack history, using popTo', {
|
|
555
|
-
key: existing.key,
|
|
556
|
-
routeId: existing.routeId,
|
|
557
|
-
path: existing.path
|
|
558
|
-
});
|
|
559
|
-
const normalizedParams = params && Object.keys(params).length > 0 ? params : undefined;
|
|
560
|
-
const updatedExisting = {
|
|
561
|
-
...existing,
|
|
562
|
-
routeId: base.routeId,
|
|
563
|
-
params: normalizedParams,
|
|
564
|
-
query,
|
|
565
|
-
path: pathname,
|
|
566
|
-
component: base.component,
|
|
567
|
-
options: this.mergeOptions(base.options, base.stackId),
|
|
568
|
-
pattern: base.path
|
|
569
|
-
};
|
|
570
|
-
this.applyHistoryChange('popTo', updatedExisting);
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (action === 'replace' && opts?.dedupe) {
|
|
576
|
-
const top = this.getTopOfStack(base.stackId);
|
|
577
|
-
this.log('dedupe: checking top of stack', {
|
|
578
|
-
top: top ? {
|
|
579
|
-
key: top.key,
|
|
580
|
-
routeId: top.routeId,
|
|
581
|
-
path: top.path
|
|
582
|
-
} : null,
|
|
583
|
-
matched: {
|
|
584
|
-
routeId: base.routeId,
|
|
585
|
-
pathname
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
if (top && top.routeId === base.routeId) {
|
|
589
|
-
const sameParams = this.areShallowEqual(top.params ?? {}, params ?? {});
|
|
590
|
-
const samePath = (top.path ?? '') === pathname;
|
|
591
|
-
const sameIdentity = sameParams && samePath;
|
|
592
|
-
const sameQuery = this.areShallowEqual(top.query ?? {}, query ?? {});
|
|
593
|
-
this.log('dedupe: top matches routeId, checking identity (params+path) and query', {
|
|
594
|
-
sameParams,
|
|
595
|
-
samePath,
|
|
596
|
-
sameIdentity,
|
|
597
|
-
sameQuery
|
|
598
|
-
});
|
|
599
|
-
if (sameIdentity && sameQuery) {
|
|
600
|
-
this.log('dedupe: already at target, syncing state');
|
|
601
|
-
this.syncStateForSameRoute(base, pathname);
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
if (sameIdentity && !sameQuery) {
|
|
605
|
-
this.log('dedupe: same identity, updating query on top via replace');
|
|
606
|
-
const normalizedParams = params && Object.keys(params).length > 0 ? params : undefined;
|
|
607
|
-
const updatedTop = {
|
|
608
|
-
...top,
|
|
609
|
-
params: normalizedParams,
|
|
610
|
-
query,
|
|
611
|
-
path: pathname
|
|
612
|
-
};
|
|
613
|
-
this.applyHistoryChange('replace', updatedTop);
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (base.stackId) {
|
|
618
|
-
const existing = this.findExistingRoute(base.stackId, base.routeId, pathname, params ?? {});
|
|
619
|
-
if (existing) {
|
|
620
|
-
const normalizedParams = params && Object.keys(params).length > 0 ? params : undefined;
|
|
621
|
-
const updatedExisting = {
|
|
622
|
-
...existing,
|
|
623
|
-
params: normalizedParams,
|
|
624
|
-
query,
|
|
625
|
-
path: pathname
|
|
626
|
-
};
|
|
627
|
-
this.log('dedupe: found existing item, calling popTo', {
|
|
628
|
-
key: updatedExisting.key
|
|
629
|
-
});
|
|
630
|
-
this.applyHistoryChange('popTo', updatedExisting);
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (base.controller) {
|
|
636
|
-
const controllerInput = {
|
|
637
|
-
params,
|
|
638
|
-
query
|
|
639
|
-
};
|
|
640
|
-
let didPresent = false;
|
|
641
|
-
const present = passProps => {
|
|
642
|
-
if (didPresent) return;
|
|
643
|
-
didPresent = true;
|
|
644
|
-
if (navigationToken !== this.navigationToken) {
|
|
645
|
-
this.log('controller: present ignored (stale navigation)', {
|
|
646
|
-
navigationToken,
|
|
647
|
-
current: this.navigationToken
|
|
648
|
-
});
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
const newItem = this.createHistoryItem(base, params, query, pathname, passProps);
|
|
652
|
-
this.applyHistoryChange(action, newItem);
|
|
653
|
-
|
|
654
|
-
// Seed child node if present
|
|
655
|
-
if (base.childNode) {
|
|
656
|
-
this.addChildNodeSeedsToHistory(base.routeId);
|
|
657
|
-
}
|
|
658
|
-
};
|
|
659
|
-
base.controller(controllerInput, present);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
const newItem = this.createHistoryItem(base, params, query, pathname);
|
|
663
|
-
this.applyHistoryChange(action, newItem);
|
|
664
|
-
|
|
665
|
-
// If the matched route has a childNode (e.g., NavigationStack added via addModal),
|
|
666
|
-
// seed the child stack's history so StackRenderer has items to render.
|
|
667
|
-
if (base.childNode) {
|
|
668
|
-
this.addChildNodeSeedsToHistory(base.routeId);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
createHistoryItem(matched, params, query, pathname, passProps) {
|
|
672
|
-
const normalizedParams = params && Object.keys(params).length > 0 ? params : undefined;
|
|
673
|
-
return {
|
|
674
|
-
key: this.generateKey(),
|
|
675
|
-
routeId: matched.routeId,
|
|
676
|
-
component: matched.component,
|
|
677
|
-
options: this.mergeOptions(matched.options, matched.stackId),
|
|
678
|
-
params: normalizedParams,
|
|
679
|
-
query,
|
|
680
|
-
passProps,
|
|
681
|
-
stackId: matched.stackId,
|
|
682
|
-
pattern: matched.path,
|
|
683
|
-
path: pathname
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
applyHistoryChange(action, item) {
|
|
687
|
-
const stackId = item.stackId;
|
|
688
|
-
if (!stackId) {
|
|
689
|
-
if (__DEV__) {
|
|
690
|
-
console.warn(`[Router] Navigation dropped: item has no stackId (routeId: ${item.routeId}, path: ${item.path})`);
|
|
691
|
-
}
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
const prevHist = this.state.history;
|
|
695
|
-
let nextHist = prevHist;
|
|
696
|
-
this.log('applyHistoryChange', {
|
|
697
|
-
action,
|
|
698
|
-
stackId,
|
|
699
|
-
item: {
|
|
700
|
-
key: item.key,
|
|
701
|
-
routeId: item.routeId,
|
|
702
|
-
path: item.path
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
if (action === 'push') {
|
|
706
|
-
nextHist = [...prevHist, item];
|
|
707
|
-
} else if (action === 'replace') {
|
|
708
|
-
let replaced = false;
|
|
709
|
-
const copy = [...prevHist];
|
|
710
|
-
for (let i = copy.length - 1; i >= 0; i--) {
|
|
711
|
-
const h = copy[i];
|
|
712
|
-
if (h) {
|
|
713
|
-
if (h.stackId === stackId) {
|
|
714
|
-
copy[i] = {
|
|
715
|
-
...item,
|
|
716
|
-
key: h.key
|
|
717
|
-
};
|
|
718
|
-
replaced = true;
|
|
719
|
-
break;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
if (!replaced) {
|
|
724
|
-
copy.push(item);
|
|
725
|
-
}
|
|
726
|
-
nextHist = copy;
|
|
727
|
-
} else if (action === 'pop') {
|
|
728
|
-
const copy = [...prevHist];
|
|
729
|
-
for (let i = copy.length - 1; i >= 0; i--) {
|
|
730
|
-
const h = copy[i];
|
|
731
|
-
if (!h) continue;
|
|
732
|
-
if (h.stackId === stackId) {
|
|
733
|
-
copy.splice(i, 1);
|
|
734
|
-
break;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
nextHist = copy;
|
|
738
|
-
} else if (action === 'popTo') {
|
|
739
|
-
const targetKey = item.key;
|
|
740
|
-
const keysToRemove = new Set();
|
|
741
|
-
let foundIndex = -1;
|
|
742
|
-
let foundItem = null;
|
|
743
|
-
for (let i = prevHist.length - 1; i >= 0; i--) {
|
|
744
|
-
const h = prevHist[i];
|
|
745
|
-
if (!h) continue;
|
|
746
|
-
if (h.stackId !== stackId) {
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
if (h.key === targetKey) {
|
|
750
|
-
foundIndex = i;
|
|
751
|
-
foundItem = h;
|
|
752
|
-
break;
|
|
753
|
-
}
|
|
754
|
-
keysToRemove.add(h.key);
|
|
755
|
-
}
|
|
756
|
-
if (foundIndex === -1 || !foundItem) {
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
const copy = prevHist.filter(h => !keysToRemove.has(h.key));
|
|
760
|
-
const updatedItem = {
|
|
761
|
-
...foundItem,
|
|
762
|
-
...item,
|
|
763
|
-
key: targetKey
|
|
764
|
-
};
|
|
765
|
-
const itemIndex = copy.findIndex(h => h.key === targetKey);
|
|
766
|
-
if (itemIndex >= 0) {
|
|
767
|
-
copy.splice(itemIndex, 1);
|
|
768
|
-
}
|
|
769
|
-
let insertPos = copy.length;
|
|
770
|
-
for (let i = copy.length - 1; i >= 0; i--) {
|
|
771
|
-
if (copy[i]?.stackId === stackId) {
|
|
772
|
-
insertPos = i + 1;
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
copy.splice(insertPos, 0, updatedItem);
|
|
777
|
-
nextHist = copy;
|
|
778
|
-
}
|
|
779
|
-
this.setState({
|
|
780
|
-
history: nextHist
|
|
781
|
-
}, item.stackId);
|
|
782
|
-
this.recomputeActiveRoute();
|
|
783
|
-
this.emit(this.listeners);
|
|
784
|
-
}
|
|
785
|
-
setState(next, affectedStackId) {
|
|
786
|
-
const prev = this.state;
|
|
787
|
-
const nextState = {
|
|
788
|
-
history: next.history ?? prev.history
|
|
789
|
-
};
|
|
790
|
-
this.state = nextState;
|
|
791
|
-
if (nextState.history !== prev.history) {
|
|
792
|
-
this.updateStackHistories(affectedStackId);
|
|
793
|
-
}
|
|
794
|
-
this.log('setState', nextState);
|
|
795
|
-
}
|
|
796
|
-
updateStackHistories(affectedStackId) {
|
|
797
|
-
if (affectedStackId) {
|
|
798
|
-
// Fast path: only update the one affected stack — O(H) instead of O(S×H)
|
|
799
|
-
const newHist = this.state.history.filter(h => h.stackId === affectedStackId);
|
|
800
|
-
const oldHist = this.stackHistories.get(affectedStackId);
|
|
801
|
-
if (!this.areArraysEqual(oldHist, newHist)) {
|
|
802
|
-
if (newHist.length === 0) {
|
|
803
|
-
this.stackHistories.delete(affectedStackId);
|
|
804
|
-
} else {
|
|
805
|
-
this.stackHistories.set(affectedStackId, newHist);
|
|
806
|
-
}
|
|
807
|
-
const listeners = this.stackListeners.get(affectedStackId);
|
|
808
|
-
if (listeners) this.emit(listeners);
|
|
809
|
-
}
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Slow path: full recompute — used by buildHistoryFromUrl, setRoot, etc.
|
|
814
|
-
const stackIds = new Set();
|
|
815
|
-
this.state.history.forEach(item => {
|
|
816
|
-
if (item.stackId) stackIds.add(item.stackId);
|
|
817
|
-
});
|
|
818
|
-
const changedStackIds = new Set();
|
|
819
|
-
stackIds.forEach(stackId => {
|
|
820
|
-
const current = this.state.history.filter(item => item.stackId === stackId);
|
|
821
|
-
const previous = this.stackHistories.get(stackId);
|
|
822
|
-
if (previous && this.areArraysEqual(previous, current)) {
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
this.stackHistories.set(stackId, current);
|
|
826
|
-
changedStackIds.add(stackId);
|
|
827
|
-
});
|
|
828
|
-
const currentStackIds = new Set(stackIds);
|
|
829
|
-
this.stackHistories.forEach((history, stackId) => {
|
|
830
|
-
if (!currentStackIds.has(stackId) && history.length > 0) {
|
|
831
|
-
this.stackHistories.delete(stackId);
|
|
832
|
-
changedStackIds.add(stackId);
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
changedStackIds.forEach(stackId => {
|
|
836
|
-
this.emit(this.stackListeners.get(stackId));
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
areArraysEqual(a, b) {
|
|
840
|
-
if (!a) return false;
|
|
841
|
-
if (a.length !== b.length) return false;
|
|
842
|
-
for (let i = 0; i < a.length; i++) {
|
|
843
|
-
if (a[i] !== b[i]) return false;
|
|
844
|
-
}
|
|
845
|
-
return true;
|
|
846
|
-
}
|
|
847
|
-
syncStateForSameRoute(base, pathname) {
|
|
848
|
-
if (!base.stackId) {
|
|
849
|
-
this.recomputeActiveRoute();
|
|
850
|
-
this.emit(this.listeners);
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
const {
|
|
854
|
-
targetStackId,
|
|
855
|
-
targetRouteId
|
|
856
|
-
} = this.resolveTargetStackAndRoute(base, pathname);
|
|
857
|
-
const rootStackId = this.root?.getId();
|
|
858
|
-
if (rootStackId) {
|
|
859
|
-
this.activateContainerForRoute(targetRouteId, rootStackId);
|
|
860
|
-
}
|
|
861
|
-
this.activateStack(targetStackId);
|
|
862
|
-
this.ensureStackHasSeed(targetStackId, rootStackId ?? base.stackId);
|
|
863
|
-
this.updateActiveRouteFromStack(targetStackId, pathname);
|
|
864
|
-
this.emit(this.listeners);
|
|
865
|
-
}
|
|
866
|
-
resolveTargetStackAndRoute(base, pathname) {
|
|
867
|
-
const rootStackId = this.root?.getId();
|
|
868
|
-
let targetStackId = base.stackId;
|
|
869
|
-
let targetRouteId = base.routeId;
|
|
870
|
-
const currentActivePath = this.activeRoute?.path;
|
|
871
|
-
const currentActivePathname = currentActivePath ? this.parsePath(currentActivePath).pathname : '';
|
|
872
|
-
if (base.stackId === rootStackId && pathname === '/' && currentActivePathname !== '/') {
|
|
873
|
-
const childStackRoute = this.findChildStackRouteForPathname(rootStackId, pathname);
|
|
874
|
-
if (childStackRoute) {
|
|
875
|
-
targetStackId = childStackRoute.stackId;
|
|
876
|
-
targetRouteId = childStackRoute.routeId;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
return {
|
|
880
|
-
targetStackId,
|
|
881
|
-
targetRouteId
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
findChildStackRouteForPathname(rootStackId, pathname) {
|
|
885
|
-
for (const route of this.registry) {
|
|
886
|
-
if (route.stackId !== rootStackId && route.pathnamePattern === pathname && !route.queryPattern) {
|
|
887
|
-
const rootRoute = this.findRootRouteWithContainer(rootStackId);
|
|
888
|
-
if (rootRoute?.childNode && this.isRouteInContainer(route.routeId, rootRoute.childNode)) {
|
|
889
|
-
return route;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
return undefined;
|
|
894
|
-
}
|
|
895
|
-
findRootRouteWithContainer(rootStackId) {
|
|
896
|
-
return this.registry.find(r => r.stackId === rootStackId && r.childNode && canSwitchToRoute(r.childNode));
|
|
897
|
-
}
|
|
898
|
-
isRouteInContainer(routeId, container) {
|
|
899
|
-
if (canLookupRoutes(container) && container.hasRoute(routeId)) {
|
|
900
|
-
return true;
|
|
901
|
-
}
|
|
902
|
-
const visit = node => {
|
|
903
|
-
const routes = node.getNodeRoutes();
|
|
904
|
-
for (const r of routes) {
|
|
905
|
-
if (r.routeId === routeId) return true;
|
|
906
|
-
if (r.childNode && visit(r.childNode)) return true;
|
|
907
|
-
}
|
|
908
|
-
const children = node.getNodeChildren();
|
|
909
|
-
for (const child of children) {
|
|
910
|
-
if (visit(child.node)) return true;
|
|
911
|
-
}
|
|
912
|
-
return false;
|
|
913
|
-
};
|
|
914
|
-
return visit(container);
|
|
915
|
-
}
|
|
916
|
-
activateContainerForRoute(targetRouteId, rootStackId) {
|
|
917
|
-
const container = this.findContainerInStack(rootStackId);
|
|
918
|
-
if (container && canSwitchToRoute(container)) {
|
|
919
|
-
container.switchToRoute(targetRouteId);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
findContainerInStack(stackId) {
|
|
923
|
-
const stackHistory = this.getStackHistory(stackId);
|
|
924
|
-
for (let i = stackHistory.length - 1; i >= 0; i--) {
|
|
925
|
-
const item = stackHistory[i];
|
|
926
|
-
if (item) {
|
|
927
|
-
const compiled = this.getCompiledRoute(item.routeId);
|
|
928
|
-
if (compiled?.childNode && canSwitchToRoute(compiled.childNode)) {
|
|
929
|
-
return compiled.childNode;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
const rootRoute = this.findRootRouteWithContainer(stackId);
|
|
934
|
-
return rootRoute?.childNode;
|
|
935
|
-
}
|
|
936
|
-
activateStack(stackId) {
|
|
937
|
-
const activator = this.stackActivators.get(stackId);
|
|
938
|
-
if (activator) {
|
|
939
|
-
activator();
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
ensureStackHasSeed(stackId, rootStackId) {
|
|
943
|
-
const stackHistory = this.getStackHistory(stackId);
|
|
944
|
-
if (stackHistory.length > 0) return;
|
|
945
|
-
const stackNode = this.stackById.get(stackId);
|
|
946
|
-
if (!stackNode) return;
|
|
947
|
-
const seed = this.getAutoSeed(stackNode);
|
|
948
|
-
if (!seed) return;
|
|
949
|
-
const compiled = this.getCompiledRoute(seed.routeId);
|
|
950
|
-
const meta = this.routeById.get(seed.routeId);
|
|
951
|
-
const path = compiled?.path ?? meta?.path ?? seed.path;
|
|
952
|
-
const seedStackId = seed.stackId ?? stackNode.getId();
|
|
953
|
-
const item = {
|
|
954
|
-
key: this.generateKey(),
|
|
955
|
-
routeId: seed.routeId,
|
|
956
|
-
component: compiled?.component ?? (() => null),
|
|
957
|
-
options: this.mergeOptions(compiled?.options, seedStackId),
|
|
958
|
-
params: seed.params ?? {},
|
|
959
|
-
stackId: seedStackId,
|
|
960
|
-
path,
|
|
961
|
-
pattern: compiled?.path ?? seed.path
|
|
962
|
-
};
|
|
963
|
-
const prevHist = this.state.history;
|
|
964
|
-
const nextHist = [...prevHist, item];
|
|
965
|
-
this.setState({
|
|
966
|
-
history: nextHist
|
|
967
|
-
}, seedStackId);
|
|
968
|
-
// setState with affectedStackId already emits to the stack's listeners
|
|
969
|
-
|
|
970
|
-
if (seedStackId !== stackId && rootStackId) {
|
|
971
|
-
this.activateContainerForRoute(seed.routeId, rootStackId);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
updateActiveRouteFromStack(stackId, pathname) {
|
|
975
|
-
const stackHistory = this.getStackHistory(stackId);
|
|
976
|
-
if (stackHistory.length > 0) {
|
|
977
|
-
const topOfStack = stackHistory[stackHistory.length - 1];
|
|
978
|
-
if (topOfStack) {
|
|
979
|
-
const meta = this.routeById.get(topOfStack.routeId);
|
|
980
|
-
this.activeRoute = meta ? {
|
|
981
|
-
...meta,
|
|
982
|
-
routeId: topOfStack.routeId,
|
|
983
|
-
params: topOfStack.params,
|
|
984
|
-
query: topOfStack.query,
|
|
985
|
-
path: topOfStack.path
|
|
986
|
-
} : {
|
|
987
|
-
routeId: topOfStack.routeId,
|
|
988
|
-
stackId: topOfStack.stackId,
|
|
989
|
-
params: topOfStack.params,
|
|
990
|
-
query: topOfStack.query,
|
|
991
|
-
path: topOfStack.path
|
|
992
|
-
};
|
|
993
|
-
this.emit(this.activeRouteListeners);
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
const targetRoute = this.registry.find(r => r.stackId === stackId && r.pathnamePattern === pathname);
|
|
998
|
-
if (targetRoute) {
|
|
999
|
-
const meta = this.routeById.get(targetRoute.routeId);
|
|
1000
|
-
this.activeRoute = meta ? {
|
|
1001
|
-
...meta,
|
|
1002
|
-
routeId: targetRoute.routeId,
|
|
1003
|
-
params: {},
|
|
1004
|
-
query: {},
|
|
1005
|
-
path: targetRoute.path
|
|
1006
|
-
} : {
|
|
1007
|
-
routeId: targetRoute.routeId,
|
|
1008
|
-
stackId: stackId,
|
|
1009
|
-
params: {},
|
|
1010
|
-
query: {},
|
|
1011
|
-
path: targetRoute.path
|
|
1012
|
-
};
|
|
1013
|
-
this.emit(this.activeRouteListeners);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
emitDepth = 0;
|
|
1017
|
-
emit(set) {
|
|
1018
|
-
if (!set) return;
|
|
1019
|
-
if (this.emitDepth > 5) {
|
|
1020
|
-
if (this.debugEnabled) console.warn('[Router] emit recursion limit reached');
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
this.emitDepth++;
|
|
1024
|
-
try {
|
|
1025
|
-
for (const l of Array.from(set)) {
|
|
1026
|
-
try {
|
|
1027
|
-
l();
|
|
1028
|
-
} catch (e) {
|
|
1029
|
-
if (this.debugEnabled) {
|
|
1030
|
-
console.error('[Router] listener error', e);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
} finally {
|
|
1035
|
-
this.emitDepth--;
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
getTopOfStack(stackId) {
|
|
1039
|
-
if (!stackId) return undefined;
|
|
1040
|
-
const slice = this.getStackHistory(stackId);
|
|
1041
|
-
return slice.length > 0 ? slice[slice.length - 1] : undefined;
|
|
1042
|
-
}
|
|
1043
|
-
findExistingRoute(stackId, routeId, pathname, params) {
|
|
1044
|
-
const stackHistory = this.getStackHistory(stackId);
|
|
1045
|
-
return stackHistory.find(item => {
|
|
1046
|
-
if (item.routeId !== routeId) return false;
|
|
1047
|
-
const itemPathname = item.path ? this.parsePath(item.path).pathname : '';
|
|
1048
|
-
if (itemPathname !== pathname) return false;
|
|
1049
|
-
return this.areShallowEqual(item.params ?? {}, params);
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
findExistingRouteByPathname(stackId, pathname, params) {
|
|
1053
|
-
const stackHistory = this.getStackHistory(stackId);
|
|
1054
|
-
return stackHistory.find(item => {
|
|
1055
|
-
const itemPathname = item.path ? this.parsePath(item.path).pathname : '';
|
|
1056
|
-
if (itemPathname !== pathname) return false;
|
|
1057
|
-
return this.areShallowEqual(item.params ?? {}, params);
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
buildRegistry() {
|
|
1061
|
-
this.registry.length = 0;
|
|
1062
|
-
const addFromNode = (node, basePath, inheritedOptions) => {
|
|
1063
|
-
const normalizedBasePath = this.normalizeBasePath(basePath);
|
|
1064
|
-
const baseSpecificity = this.computeBasePathSpecificity(normalizedBasePath);
|
|
1065
|
-
const routes = node.getNodeRoutes();
|
|
1066
|
-
const stackId = routes.length > 0 ? node.getId() : undefined;
|
|
1067
|
-
if (stackId) {
|
|
1068
|
-
this.stackById.set(stackId, node);
|
|
1069
|
-
}
|
|
1070
|
-
let isFirstRoute = true;
|
|
1071
|
-
for (const r of routes) {
|
|
1072
|
-
// Merge options: first route inherits parent options (e.g., for nested stacks)
|
|
1073
|
-
const mergedOptions = isFirstRoute && inheritedOptions ? {
|
|
1074
|
-
...inheritedOptions,
|
|
1075
|
-
...r.options
|
|
1076
|
-
} : r.options;
|
|
1077
|
-
|
|
1078
|
-
// Always register the route.
|
|
1079
|
-
// If it has a childNode, r.component is already childNode.getRenderer() (set by extractComponent).
|
|
1080
|
-
const compiled = {
|
|
1081
|
-
routeId: r.routeId,
|
|
1082
|
-
path: this.combinePathWithBase(r.path, normalizedBasePath),
|
|
1083
|
-
pathnamePattern: r.pathnamePattern === '*' ? '*' : this.joinPaths(normalizedBasePath, r.pathnamePattern),
|
|
1084
|
-
isWildcardPath: r.isWildcardPath,
|
|
1085
|
-
queryPattern: r.queryPattern,
|
|
1086
|
-
baseSpecificity: r.baseSpecificity + baseSpecificity,
|
|
1087
|
-
matchPath: pathname => {
|
|
1088
|
-
const stripped = this.stripBasePath(normalizedBasePath, pathname);
|
|
1089
|
-
if (stripped === null) return false;
|
|
1090
|
-
if (r.isWildcardPath) {
|
|
1091
|
-
return {
|
|
1092
|
-
params: {}
|
|
1093
|
-
};
|
|
1094
|
-
}
|
|
1095
|
-
const target = stripped === '' ? '/' : stripped;
|
|
1096
|
-
return r.matchPath(target);
|
|
1097
|
-
},
|
|
1098
|
-
component: r.component,
|
|
1099
|
-
controller: r.controller,
|
|
1100
|
-
options: mergedOptions,
|
|
1101
|
-
stackId,
|
|
1102
|
-
childNode: r.childNode
|
|
1103
|
-
};
|
|
1104
|
-
this.registry.push(compiled);
|
|
1105
|
-
this.compiledRouteById.set(compiled.routeId, compiled);
|
|
1106
|
-
if (stackId) {
|
|
1107
|
-
this.routeById.set(r.routeId, {
|
|
1108
|
-
path: compiled.path,
|
|
1109
|
-
stackId
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
this.log('buildRegistry route', {
|
|
1113
|
-
routeId: compiled.routeId,
|
|
1114
|
-
path: compiled.path,
|
|
1115
|
-
pathnamePattern: compiled.pathnamePattern,
|
|
1116
|
-
isWildcardPath: compiled.isWildcardPath,
|
|
1117
|
-
baseSpecificity: compiled.baseSpecificity,
|
|
1118
|
-
stackId,
|
|
1119
|
-
hasChildNode: !!compiled.childNode
|
|
1120
|
-
});
|
|
1121
|
-
isFirstRoute = false;
|
|
1122
|
-
|
|
1123
|
-
// Also register routes from childNode (for navigation inside the nested stack)
|
|
1124
|
-
if (r.childNode) {
|
|
1125
|
-
const nextBaseForChild = r.isWildcardPath ? normalizedBasePath : this.joinPaths(normalizedBasePath, r.pathnamePattern);
|
|
1126
|
-
// Child routes don't inherit parent options - they use their own
|
|
1127
|
-
addFromNode(r.childNode, nextBaseForChild);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
const children = node.getNodeChildren();
|
|
1131
|
-
for (const child of children) {
|
|
1132
|
-
const nextBase = child.prefix === '' ? normalizedBasePath : this.joinPaths(normalizedBasePath, child.prefix);
|
|
1133
|
-
const childId = child.node.getId();
|
|
1134
|
-
if (child.onMatch) {
|
|
1135
|
-
this.stackActivators.set(childId, child.onMatch);
|
|
1136
|
-
}
|
|
1137
|
-
addFromNode(child.node, nextBase);
|
|
1138
|
-
}
|
|
1139
|
-
};
|
|
1140
|
-
if (this.root) {
|
|
1141
|
-
addFromNode(this.root, '');
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
normalizeBasePath(input) {
|
|
1145
|
-
if (!input || input === '/') {
|
|
1146
|
-
return '';
|
|
1147
|
-
}
|
|
1148
|
-
let normalized = input.startsWith('/') ? input : `/${input}`;
|
|
1149
|
-
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
1150
|
-
normalized = normalized.slice(0, -1);
|
|
1151
|
-
}
|
|
1152
|
-
return normalized;
|
|
1153
|
-
}
|
|
1154
|
-
joinPaths(basePath, childPath) {
|
|
1155
|
-
if (childPath === '*') return '*';
|
|
1156
|
-
const base = this.normalizeBasePath(basePath);
|
|
1157
|
-
const child = this.normalizeBasePath(childPath || '/');
|
|
1158
|
-
if (!base && !child) return '/';
|
|
1159
|
-
if (!base) return child || '/';
|
|
1160
|
-
if (!child) return base || '/';
|
|
1161
|
-
const joined = `${base}${child.startsWith('/') ? child : `/${child}`}`;
|
|
1162
|
-
return joined || '/';
|
|
1163
|
-
}
|
|
1164
|
-
stripBasePath(basePath, pathname) {
|
|
1165
|
-
const normalizedBase = this.normalizeBasePath(basePath);
|
|
1166
|
-
if (!normalizedBase) {
|
|
1167
|
-
return pathname || '/';
|
|
1168
|
-
}
|
|
1169
|
-
if (normalizedBase === '/') {
|
|
1170
|
-
return pathname || '/';
|
|
1171
|
-
}
|
|
1172
|
-
if (pathname === normalizedBase) {
|
|
1173
|
-
return '/';
|
|
1174
|
-
}
|
|
1175
|
-
if (pathname.startsWith(`${normalizedBase}/`)) {
|
|
1176
|
-
const rest = pathname.slice(normalizedBase.length);
|
|
1177
|
-
return rest.length ? rest : '/';
|
|
1178
|
-
}
|
|
1179
|
-
return null;
|
|
1180
|
-
}
|
|
1181
|
-
combinePathWithBase(path, basePath) {
|
|
1182
|
-
const parsed = qs.parseUrl(path);
|
|
1183
|
-
const urlPart = parsed.url || '/';
|
|
1184
|
-
const query = parsed.query;
|
|
1185
|
-
const isWildcard = urlPart === '*';
|
|
1186
|
-
const combinedPathname = isWildcard ? `${this.normalizeBasePath(basePath)}*` : this.joinPaths(basePath, urlPart);
|
|
1187
|
-
const hasQuery = query && Object.keys(query).length > 0;
|
|
1188
|
-
if (!hasQuery) {
|
|
1189
|
-
return combinedPathname || '/';
|
|
1190
|
-
}
|
|
1191
|
-
return `${combinedPathname}?${qs.stringify(query)}`;
|
|
1192
|
-
}
|
|
1193
|
-
computeBasePathSpecificity(basePath) {
|
|
1194
|
-
if (!basePath) return 0;
|
|
1195
|
-
const segments = basePath.split('/').filter(Boolean);
|
|
1196
|
-
return segments.length * 2;
|
|
1197
|
-
}
|
|
1198
|
-
getAutoSeed(node) {
|
|
1199
|
-
const explicitSeed = node.seed?.();
|
|
1200
|
-
if (explicitSeed) {
|
|
1201
|
-
return explicitSeed;
|
|
1202
|
-
}
|
|
1203
|
-
const activeChildId = node.getActiveChildId?.();
|
|
1204
|
-
if (activeChildId) {
|
|
1205
|
-
const activeChild = node.getNodeChildren().find(child => child.node.getId() === activeChildId);
|
|
1206
|
-
if (activeChild) {
|
|
1207
|
-
const childSeed = this.getAutoSeed(activeChild.node);
|
|
1208
|
-
if (childSeed) {
|
|
1209
|
-
return childSeed;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
const routes = node.getNodeRoutes();
|
|
1214
|
-
if (routes.length > 0 && routes[0]) {
|
|
1215
|
-
const firstRoute = routes[0];
|
|
1216
|
-
return {
|
|
1217
|
-
routeId: firstRoute.routeId,
|
|
1218
|
-
params: {},
|
|
1219
|
-
path: firstRoute.path,
|
|
1220
|
-
stackId: node.getId()
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
return null;
|
|
1224
|
-
}
|
|
1225
|
-
seedInitialHistory() {
|
|
1226
|
-
if (this.state.history.length > 0) return;
|
|
1227
|
-
if (this.root) {
|
|
1228
|
-
const seed = this.getAutoSeed(this.root);
|
|
1229
|
-
if (seed) {
|
|
1230
|
-
const compiled = this.getCompiledRoute(seed.routeId);
|
|
1231
|
-
const meta = this.routeById.get(seed.routeId);
|
|
1232
|
-
const path = compiled?.path ?? meta?.path ?? seed.path;
|
|
1233
|
-
const stackId = seed.stackId ?? this.root.getId();
|
|
1234
|
-
const item = {
|
|
1235
|
-
key: this.generateKey(),
|
|
1236
|
-
routeId: seed.routeId,
|
|
1237
|
-
component: compiled?.component ?? (() => null),
|
|
1238
|
-
options: this.mergeOptions(compiled?.options, stackId),
|
|
1239
|
-
params: seed.params ?? {},
|
|
1240
|
-
stackId,
|
|
1241
|
-
path,
|
|
1242
|
-
pattern: compiled?.path ?? seed.path
|
|
1243
|
-
};
|
|
1244
|
-
this.applyHistoryChange('push', item);
|
|
1245
|
-
this.addChildNodeSeedsToHistory(seed.routeId);
|
|
1246
|
-
}
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
addChildNodeSeedsToItems(routeId, items, finalRouteId, visited = new Set()) {
|
|
1251
|
-
if (visited.has(routeId) || visited.size > 50) return;
|
|
1252
|
-
visited.add(routeId);
|
|
1253
|
-
this.log('addChildNodeSeeds: called', {
|
|
1254
|
-
routeId,
|
|
1255
|
-
finalRouteId,
|
|
1256
|
-
itemsLength: items.length,
|
|
1257
|
-
items: items.map(i => ({
|
|
1258
|
-
routeId: i.routeId,
|
|
1259
|
-
stackId: i.stackId,
|
|
1260
|
-
path: i.path
|
|
1261
|
-
}))
|
|
1262
|
-
});
|
|
1263
|
-
const compiled = this.getCompiledRoute(routeId);
|
|
1264
|
-
if (!compiled || !compiled.childNode) {
|
|
1265
|
-
this.log('addChildNodeSeeds: no childNode', {
|
|
1266
|
-
routeId
|
|
1267
|
-
});
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
const childNode = compiled.childNode;
|
|
1271
|
-
if (finalRouteId && canSwitchToRoute(childNode)) {
|
|
1272
|
-
this.log('addChildNodeSeeds: setting active child', {
|
|
1273
|
-
finalRouteId
|
|
1274
|
-
});
|
|
1275
|
-
childNode.switchToRoute(finalRouteId);
|
|
1276
|
-
}
|
|
1277
|
-
const childSeed = this.getAutoSeed(childNode);
|
|
1278
|
-
if (!childSeed) {
|
|
1279
|
-
this.log('addChildNodeSeeds: no seed returned', {
|
|
1280
|
-
routeId
|
|
1281
|
-
});
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
this.log('addChildNodeSeeds: got seed', {
|
|
1285
|
-
routeId: childSeed.routeId,
|
|
1286
|
-
stackId: childSeed.stackId,
|
|
1287
|
-
path: childSeed.path
|
|
1288
|
-
});
|
|
1289
|
-
const childCompiled = this.getCompiledRoute(childSeed.routeId);
|
|
1290
|
-
const childMeta = this.routeById.get(childSeed.routeId);
|
|
1291
|
-
const childPath = childCompiled?.path ?? childMeta?.path ?? childSeed.path;
|
|
1292
|
-
const childStackId = childSeed.stackId ?? childNode.getId();
|
|
1293
|
-
const existingItem = items.find(item => item.routeId === childSeed.routeId && item.stackId === childStackId);
|
|
1294
|
-
if (existingItem) {
|
|
1295
|
-
this.log('addChildNodeSeeds: skipping duplicate', {
|
|
1296
|
-
routeId: childSeed.routeId,
|
|
1297
|
-
stackId: childStackId,
|
|
1298
|
-
path: childPath,
|
|
1299
|
-
existingItem: {
|
|
1300
|
-
key: existingItem.key,
|
|
1301
|
-
routeId: existingItem.routeId,
|
|
1302
|
-
stackId: existingItem.stackId,
|
|
1303
|
-
path: existingItem.path
|
|
1304
|
-
}
|
|
1305
|
-
});
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
this.log('addChildNodeSeeds: adding child item', {
|
|
1309
|
-
routeId: childSeed.routeId,
|
|
1310
|
-
stackId: childStackId,
|
|
1311
|
-
path: childPath,
|
|
1312
|
-
itemsLength: items.length
|
|
1313
|
-
});
|
|
1314
|
-
const childItem = {
|
|
1315
|
-
key: this.generateKey(),
|
|
1316
|
-
routeId: childSeed.routeId,
|
|
1317
|
-
component: childCompiled?.component ?? (() => null),
|
|
1318
|
-
options: this.mergeOptions(childCompiled?.options, childStackId),
|
|
1319
|
-
params: childSeed.params ?? {},
|
|
1320
|
-
stackId: childStackId,
|
|
1321
|
-
path: childPath,
|
|
1322
|
-
pattern: childCompiled?.path ?? childSeed.path
|
|
1323
|
-
};
|
|
1324
|
-
// Insert right after the parent route item, so that child stack seeds do not
|
|
1325
|
-
// accidentally become the active route for deep-links (order matters).
|
|
1326
|
-
let parentIndex = -1;
|
|
1327
|
-
for (let i = items.length - 1; i >= 0; i--) {
|
|
1328
|
-
const it = items[i];
|
|
1329
|
-
if (it && it.routeId === routeId) {
|
|
1330
|
-
parentIndex = i;
|
|
1331
|
-
break;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
const insertIndex = parentIndex >= 0 ? parentIndex + 1 : items.length;
|
|
1335
|
-
items.splice(insertIndex, 0, childItem);
|
|
1336
|
-
if (childSeed.routeId) {
|
|
1337
|
-
this.addChildNodeSeedsToItems(childSeed.routeId, items, finalRouteId, visited);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
addChildNodeSeedsToHistory(routeId, visited = new Set()) {
|
|
1341
|
-
if (visited.has(routeId) || visited.size > 50) return;
|
|
1342
|
-
visited.add(routeId);
|
|
1343
|
-
const compiled = this.getCompiledRoute(routeId);
|
|
1344
|
-
if (!compiled || !compiled.childNode) return;
|
|
1345
|
-
const childNode = compiled.childNode;
|
|
1346
|
-
const childSeed = this.getAutoSeed(childNode);
|
|
1347
|
-
if (!childSeed) return;
|
|
1348
|
-
const childCompiled = this.getCompiledRoute(childSeed.routeId);
|
|
1349
|
-
const childMeta = this.routeById.get(childSeed.routeId);
|
|
1350
|
-
const childPath = childCompiled?.path ?? childMeta?.path ?? childSeed.path;
|
|
1351
|
-
const childStackId = childSeed.stackId ?? childNode.getId();
|
|
1352
|
-
const childItem = {
|
|
1353
|
-
key: this.generateKey(),
|
|
1354
|
-
routeId: childSeed.routeId,
|
|
1355
|
-
component: childCompiled?.component ?? (() => null),
|
|
1356
|
-
options: this.mergeOptions(childCompiled?.options, childStackId),
|
|
1357
|
-
params: childSeed.params ?? {},
|
|
1358
|
-
stackId: childStackId,
|
|
1359
|
-
path: childPath,
|
|
1360
|
-
pattern: childCompiled?.path ?? childSeed.path
|
|
1361
|
-
};
|
|
1362
|
-
this.applyHistoryChange('push', childItem);
|
|
1363
|
-
if (childSeed.routeId) {
|
|
1364
|
-
this.addChildNodeSeedsToHistory(childSeed.routeId, visited);
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
matchBaseRoute(pathname, query) {
|
|
1368
|
-
// One-slot cache: uses reference equality for query object
|
|
1369
|
-
if (this.lastMatchedPathname !== null && pathname === this.lastMatchedPathname && query === this.lastMatchedQuery) {
|
|
1370
|
-
return this.lastMatchedResult ?? undefined;
|
|
1371
|
-
}
|
|
1372
|
-
this.log('matchBaseRoute', {
|
|
1373
|
-
pathname,
|
|
1374
|
-
query
|
|
1375
|
-
});
|
|
1376
|
-
let best;
|
|
1377
|
-
const candidates = [];
|
|
1378
|
-
for (const r of this.registry) {
|
|
1379
|
-
if (!r.stackId) continue;
|
|
1380
|
-
let pathMatch;
|
|
1381
|
-
if (r.isWildcardPath) {
|
|
1382
|
-
pathMatch = {
|
|
1383
|
-
params: {}
|
|
1384
|
-
};
|
|
1385
|
-
} else {
|
|
1386
|
-
pathMatch = r.matchPath(pathname);
|
|
1387
|
-
}
|
|
1388
|
-
if (!pathMatch) continue;
|
|
1389
|
-
if (!this.matchQueryPattern(r.queryPattern, query)) continue;
|
|
1390
|
-
let spec = r.baseSpecificity;
|
|
1391
|
-
const hasQueryPattern = r.queryPattern && Object.keys(r.queryPattern).length > 0;
|
|
1392
|
-
if (hasQueryPattern) {
|
|
1393
|
-
spec += 1000;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
// Routes with childNode AND modal/sheet presentation are "wrapper" routes
|
|
1397
|
-
// that should take priority over the child stack's own routes when both match.
|
|
1398
|
-
// This ensures addModal('/path', NavigationStack) renders the wrapper modal
|
|
1399
|
-
// and not the child stack's first screen directly.
|
|
1400
|
-
if (r.childNode && isModalLikePresentation(r.options?.stackPresentation)) {
|
|
1401
|
-
spec += 1;
|
|
1402
|
-
}
|
|
1403
|
-
this.log('matchBaseRoute candidate', {
|
|
1404
|
-
routeId: r.routeId,
|
|
1405
|
-
path: r.path,
|
|
1406
|
-
baseSpecificity: r.baseSpecificity,
|
|
1407
|
-
adjustedSpecificity: spec,
|
|
1408
|
-
hasQueryPattern,
|
|
1409
|
-
hasChildNode: !!r.childNode,
|
|
1410
|
-
stackId: r.stackId
|
|
1411
|
-
});
|
|
1412
|
-
if (!best || spec > best.specificity) {
|
|
1413
|
-
best = {
|
|
1414
|
-
route: r,
|
|
1415
|
-
specificity: spec
|
|
1416
|
-
};
|
|
1417
|
-
candidates.length = 0;
|
|
1418
|
-
candidates.push(best);
|
|
1419
|
-
} else if (spec === best.specificity) {
|
|
1420
|
-
candidates.push({
|
|
1421
|
-
route: r,
|
|
1422
|
-
specificity: spec
|
|
1423
|
-
});
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
if (candidates.length > 1) {
|
|
1427
|
-
this.log('matchBaseRoute: multiple candidates with same specificity', {
|
|
1428
|
-
candidatesCount: candidates.length,
|
|
1429
|
-
candidates: candidates.map(c => ({
|
|
1430
|
-
routeId: c.route.routeId,
|
|
1431
|
-
stackId: c.route.stackId,
|
|
1432
|
-
path: c.route.path
|
|
1433
|
-
}))
|
|
1434
|
-
});
|
|
1435
|
-
const rootStackId = this.root?.getId();
|
|
1436
|
-
let bestFromHistory;
|
|
1437
|
-
let bestChildStack;
|
|
1438
|
-
let bestChildStackWithHistory;
|
|
1439
|
-
for (const candidate of candidates) {
|
|
1440
|
-
const candidateStackId = candidate.route.stackId;
|
|
1441
|
-
const stackHistory = this.getStackHistory(candidateStackId);
|
|
1442
|
-
const hasMatchingItem = stackHistory.some(item => {
|
|
1443
|
-
const itemPathname = item.path ? this.parsePath(item.path).pathname : '';
|
|
1444
|
-
return itemPathname === pathname;
|
|
1445
|
-
});
|
|
1446
|
-
this.log('matchBaseRoute: checking candidate', {
|
|
1447
|
-
routeId: candidate.route.routeId,
|
|
1448
|
-
stackId: candidateStackId,
|
|
1449
|
-
isRootStack: candidateStackId === rootStackId,
|
|
1450
|
-
stackHistoryLength: stackHistory.length,
|
|
1451
|
-
hasMatchingItem
|
|
1452
|
-
});
|
|
1453
|
-
if (hasMatchingItem) {
|
|
1454
|
-
if (candidateStackId !== rootStackId) {
|
|
1455
|
-
bestChildStack = candidate.route;
|
|
1456
|
-
this.log('matchBaseRoute: found child stack candidate with matching item', {
|
|
1457
|
-
routeId: candidate.route.routeId,
|
|
1458
|
-
stackId: candidateStackId
|
|
1459
|
-
});
|
|
1460
|
-
break;
|
|
1461
|
-
} else if (!bestFromHistory) {
|
|
1462
|
-
bestFromHistory = candidate.route;
|
|
1463
|
-
}
|
|
1464
|
-
} else if (candidateStackId !== rootStackId && stackHistory.length > 0) {
|
|
1465
|
-
if (!bestChildStackWithHistory) {
|
|
1466
|
-
bestChildStackWithHistory = candidate.route;
|
|
1467
|
-
this.log('matchBaseRoute: found child stack candidate with history', {
|
|
1468
|
-
routeId: candidate.route.routeId,
|
|
1469
|
-
stackId: candidateStackId,
|
|
1470
|
-
stackHistoryLength: stackHistory.length
|
|
1471
|
-
});
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
if (bestChildStack && best) {
|
|
1476
|
-
best = {
|
|
1477
|
-
route: bestChildStack,
|
|
1478
|
-
specificity: best.specificity
|
|
1479
|
-
};
|
|
1480
|
-
this.log('matchBaseRoute: selected child stack with matching item', {
|
|
1481
|
-
routeId: bestChildStack.routeId,
|
|
1482
|
-
stackId: bestChildStack.stackId
|
|
1483
|
-
});
|
|
1484
|
-
} else if (bestChildStackWithHistory && best) {
|
|
1485
|
-
best = {
|
|
1486
|
-
route: bestChildStackWithHistory,
|
|
1487
|
-
specificity: best.specificity
|
|
1488
|
-
};
|
|
1489
|
-
this.log('matchBaseRoute: selected child stack', {
|
|
1490
|
-
routeId: bestChildStackWithHistory.routeId,
|
|
1491
|
-
stackId: bestChildStackWithHistory.stackId
|
|
1492
|
-
});
|
|
1493
|
-
} else if (bestFromHistory && best) {
|
|
1494
|
-
best = {
|
|
1495
|
-
route: bestFromHistory,
|
|
1496
|
-
specificity: best.specificity
|
|
1497
|
-
};
|
|
1498
|
-
this.log('matchBaseRoute: selected root stack with history', {
|
|
1499
|
-
routeId: bestFromHistory.routeId,
|
|
1500
|
-
stackId: bestFromHistory.stackId
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
if (best) {
|
|
1505
|
-
this.log('matchBaseRoute winner', {
|
|
1506
|
-
routeId: best.route.routeId,
|
|
1507
|
-
path: best.route.path,
|
|
1508
|
-
stackId: best.route.stackId,
|
|
1509
|
-
specificity: best.specificity,
|
|
1510
|
-
candidatesCount: candidates.length
|
|
1511
|
-
});
|
|
1512
|
-
} else {
|
|
1513
|
-
this.log('matchBaseRoute no match');
|
|
1514
|
-
}
|
|
1515
|
-
const result = best?.route;
|
|
1516
|
-
this.lastMatchedPathname = pathname;
|
|
1517
|
-
this.lastMatchedQuery = query;
|
|
1518
|
-
this.lastMatchedResult = result ?? null;
|
|
1519
|
-
return result;
|
|
1520
|
-
}
|
|
1521
|
-
generateKey() {
|
|
1522
|
-
return `route-${nanoid()}`;
|
|
1523
|
-
}
|
|
1524
|
-
parsePath(path) {
|
|
1525
|
-
if (path === this.lastParsedPath && this.lastParsedResult) {
|
|
1526
|
-
return this.lastParsedResult;
|
|
1527
|
-
}
|
|
1528
|
-
const parsed = qs.parseUrl(path);
|
|
1529
|
-
const result = {
|
|
1530
|
-
pathname: parsed.url,
|
|
1531
|
-
query: parsed.query
|
|
1532
|
-
};
|
|
1533
|
-
this.lastParsedPath = path;
|
|
1534
|
-
this.lastParsedResult = result;
|
|
1535
|
-
this.log('parsePath', {
|
|
1536
|
-
input: path,
|
|
1537
|
-
output: result
|
|
1538
|
-
});
|
|
1539
|
-
return result;
|
|
1540
|
-
}
|
|
1541
|
-
mergeOptions(routeOptions, stackId) {
|
|
1542
|
-
const stackNode = stackId ? this.findStackById(stackId) : undefined;
|
|
1543
|
-
const stackDefaults = stackNode?.getDefaultOptions?.();
|
|
1544
|
-
const routerDefaults = this.routerScreenOptions;
|
|
1545
|
-
if (!routerDefaults && !stackDefaults && !routeOptions) return undefined;
|
|
1546
|
-
const merged = {
|
|
1547
|
-
...(routerDefaults ?? {}),
|
|
1548
|
-
...(stackDefaults ?? {}),
|
|
1549
|
-
...(routeOptions ?? {})
|
|
1550
|
-
};
|
|
1551
|
-
if (merged.stackPresentation === 'modal' && merged.convertModalToSheetForAndroid && Platform.OS === 'android') {
|
|
1552
|
-
merged.stackPresentation = 'sheet';
|
|
1553
|
-
}
|
|
1554
|
-
return merged;
|
|
1555
|
-
}
|
|
1556
|
-
findStackById(stackId) {
|
|
1557
|
-
return this.stackById.get(stackId);
|
|
1558
|
-
}
|
|
1559
|
-
getCompiledRoute(routeId) {
|
|
1560
|
-
return this.compiledRouteById.get(routeId);
|
|
1561
|
-
}
|
|
1562
|
-
areShallowEqual(a, b) {
|
|
1563
|
-
if (a === b) return true;
|
|
1564
|
-
const na = a ?? {};
|
|
1565
|
-
const nb = b ?? {};
|
|
1566
|
-
const aKeys = Object.keys(na);
|
|
1567
|
-
const bKeys = Object.keys(nb);
|
|
1568
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
1569
|
-
for (const k of aKeys) {
|
|
1570
|
-
if (!(k in nb)) return false;
|
|
1571
|
-
const av = na[k];
|
|
1572
|
-
const bv = nb[k];
|
|
1573
|
-
if (av === bv) continue;
|
|
1574
|
-
const aArr = Array.isArray(av);
|
|
1575
|
-
const bArr = Array.isArray(bv);
|
|
1576
|
-
if (aArr || bArr) {
|
|
1577
|
-
if (!aArr || !bArr) return false;
|
|
1578
|
-
if (av.length !== bv.length) return false;
|
|
1579
|
-
for (let i = 0; i < av.length; i++) {
|
|
1580
|
-
if (av[i] !== bv[i]) return false;
|
|
1581
|
-
}
|
|
1582
|
-
continue;
|
|
1583
|
-
}
|
|
1584
|
-
return false;
|
|
1585
|
-
}
|
|
1586
|
-
return true;
|
|
1587
|
-
}
|
|
1588
|
-
matchQueryPattern(pattern, query) {
|
|
1589
|
-
if (!pattern) {
|
|
1590
|
-
return true;
|
|
1591
|
-
}
|
|
1592
|
-
for (const [key, token] of Object.entries(pattern)) {
|
|
1593
|
-
const raw = query[key];
|
|
1594
|
-
if (raw == null) {
|
|
1595
|
-
this.log('matchQueryPattern: key missing', {
|
|
1596
|
-
key,
|
|
1597
|
-
token,
|
|
1598
|
-
query
|
|
1599
|
-
});
|
|
1600
|
-
return false;
|
|
1601
|
-
}
|
|
1602
|
-
if (typeof raw !== 'string') {
|
|
1603
|
-
this.log('matchQueryPattern: non-string value', {
|
|
1604
|
-
key,
|
|
1605
|
-
token,
|
|
1606
|
-
value: raw
|
|
1607
|
-
});
|
|
1608
|
-
return false;
|
|
1609
|
-
}
|
|
1610
|
-
if (token.type === 'const') {
|
|
1611
|
-
if (raw !== token.value) {
|
|
1612
|
-
this.log('matchQueryPattern: const mismatch', {
|
|
1613
|
-
key,
|
|
1614
|
-
expected: token.value,
|
|
1615
|
-
actual: raw
|
|
1616
|
-
});
|
|
1617
|
-
return false;
|
|
1618
|
-
}
|
|
1619
|
-
} else if (token.type === 'param') {
|
|
1620
|
-
this.log('matchQueryPattern: param ok', {
|
|
1621
|
-
key,
|
|
1622
|
-
value: raw,
|
|
1623
|
-
paramName: token.name
|
|
1624
|
-
});
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
this.log('matchQueryPattern: success', {
|
|
1628
|
-
pattern,
|
|
1629
|
-
query
|
|
1630
|
-
});
|
|
1631
|
-
return true;
|
|
1632
|
-
}
|
|
1633
|
-
isWebEnv() {
|
|
1634
|
-
const g = globalThis;
|
|
1635
|
-
return !!(g.addEventListener && g.history && g.location);
|
|
1636
|
-
}
|
|
1637
|
-
getCurrentUrl() {
|
|
1638
|
-
const g = globalThis;
|
|
1639
|
-
return g.location ? `${g.location.pathname}${g.location.search}` : '/';
|
|
1640
|
-
}
|
|
1641
|
-
readHistoryIndex(state) {
|
|
1642
|
-
if (state && typeof state === 'object' && '__srIndex' in state) {
|
|
1643
|
-
const idx = state.__srIndex;
|
|
1644
|
-
if (typeof idx === 'number') return idx;
|
|
1645
|
-
}
|
|
1646
|
-
return 0;
|
|
1647
|
-
}
|
|
1648
|
-
getHistoryIndexOrNull() {
|
|
1649
|
-
const g = globalThis;
|
|
1650
|
-
const st = g.history?.state;
|
|
1651
|
-
if (st && typeof st === 'object' && '__srIndex' in st) {
|
|
1652
|
-
const idx = st.__srIndex;
|
|
1653
|
-
return typeof idx === 'number' ? idx : null;
|
|
1654
|
-
}
|
|
1655
|
-
return null;
|
|
1656
|
-
}
|
|
1657
|
-
getHistoryIndex() {
|
|
1658
|
-
const g = globalThis;
|
|
1659
|
-
return this.readHistoryIndex(g.history?.state);
|
|
1660
|
-
}
|
|
1661
|
-
ensureHistoryIndex() {
|
|
1662
|
-
const g = globalThis;
|
|
1663
|
-
const st = g.history?.state ?? {};
|
|
1664
|
-
let next = st;
|
|
1665
|
-
let changed = false;
|
|
1666
|
-
if (!('__srIndex' in next)) {
|
|
1667
|
-
next = {
|
|
1668
|
-
...next,
|
|
1669
|
-
__srIndex: 0
|
|
1670
|
-
};
|
|
1671
|
-
changed = true;
|
|
1672
|
-
}
|
|
1673
|
-
if (!('__srPath' in next)) {
|
|
1674
|
-
next = {
|
|
1675
|
-
...next,
|
|
1676
|
-
__srPath: this.getCurrentUrl()
|
|
1677
|
-
};
|
|
1678
|
-
changed = true;
|
|
1679
|
-
}
|
|
1680
|
-
if (changed) {
|
|
1681
|
-
try {
|
|
1682
|
-
g.history?.replaceState(next, '', g.location?.href);
|
|
1683
|
-
} catch (e) {
|
|
1684
|
-
if (this.debugEnabled) console.warn('[Router] ensureHistoryIndex replaceState failed', e);
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
getRouterPathFromHistory() {
|
|
1689
|
-
const g = globalThis;
|
|
1690
|
-
const st = g.history?.state;
|
|
1691
|
-
if (st && typeof st === 'object' && '__srPath' in st) {
|
|
1692
|
-
const p = st.__srPath;
|
|
1693
|
-
if (typeof p === 'string' && p.length > 0) {
|
|
1694
|
-
return p;
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
return this.getCurrentUrl();
|
|
1698
|
-
}
|
|
1699
|
-
getReplaceDedupeFromHistory() {
|
|
1700
|
-
const g = globalThis;
|
|
1701
|
-
const st = g.history?.state;
|
|
1702
|
-
if (st && typeof st === 'object' && '__srReplaceDedupe' in st) {
|
|
1703
|
-
const v = st.__srReplaceDedupe;
|
|
1704
|
-
return typeof v === 'boolean' ? v : false;
|
|
1705
|
-
}
|
|
1706
|
-
return false;
|
|
1707
|
-
}
|
|
1708
|
-
shouldSyncPathWithUrl(path) {
|
|
1709
|
-
const {
|
|
1710
|
-
pathname,
|
|
1711
|
-
query
|
|
1712
|
-
} = this.parsePath(path);
|
|
1713
|
-
const base = this.matchBaseRoute(pathname, query);
|
|
1714
|
-
if (!base) return true;
|
|
1715
|
-
const mergedOptions = this.mergeOptions(base.options, base.stackId);
|
|
1716
|
-
const rawsyncWithUrl = mergedOptions?.syncWithUrl;
|
|
1717
|
-
if (typeof rawsyncWithUrl === 'boolean') {
|
|
1718
|
-
return rawsyncWithUrl;
|
|
1719
|
-
}
|
|
1720
|
-
return true;
|
|
1721
|
-
}
|
|
1722
|
-
pushUrl(to, opts) {
|
|
1723
|
-
const g = globalThis;
|
|
1724
|
-
const st = g.history?.state ?? {};
|
|
1725
|
-
const prev = this.readHistoryIndex(st);
|
|
1726
|
-
const base = st;
|
|
1727
|
-
const syncWithUrl = opts?.syncWithUrl ?? true;
|
|
1728
|
-
const routerPath = to;
|
|
1729
|
-
const visualUrl = syncWithUrl ? to : this.getCurrentUrl();
|
|
1730
|
-
const next = {
|
|
1731
|
-
...base,
|
|
1732
|
-
__srIndex: prev + 1,
|
|
1733
|
-
__srPath: routerPath
|
|
1734
|
-
};
|
|
1735
|
-
g.history?.pushState(next, '', visualUrl);
|
|
1736
|
-
}
|
|
1737
|
-
replaceUrl(to, opts) {
|
|
1738
|
-
const g = globalThis;
|
|
1739
|
-
const st = g.history?.state ?? {};
|
|
1740
|
-
const base = st;
|
|
1741
|
-
const syncWithUrl = opts?.syncWithUrl ?? true;
|
|
1742
|
-
const routerPath = to;
|
|
1743
|
-
const visualUrl = syncWithUrl ? to : this.getCurrentUrl();
|
|
1744
|
-
const next = {
|
|
1745
|
-
...base,
|
|
1746
|
-
__srPath: routerPath,
|
|
1747
|
-
__srReplaceDedupe: !!opts?.dedupe
|
|
1748
|
-
};
|
|
1749
|
-
g.history?.replaceState(next, '', visualUrl);
|
|
1750
|
-
}
|
|
1751
|
-
replaceUrlSilently(to) {
|
|
1752
|
-
if (!this.isWebEnv()) return;
|
|
1753
|
-
if (this.suppressHistorySyncCount > 10) {
|
|
1754
|
-
console.warn('[Router] suppressHistorySyncCount reset from', this.suppressHistorySyncCount);
|
|
1755
|
-
this.suppressHistorySyncCount = 0;
|
|
1756
|
-
}
|
|
1757
|
-
this.suppressHistorySyncCount += 1;
|
|
1758
|
-
this.replaceUrl(to, {
|
|
1759
|
-
syncWithUrl: true
|
|
1760
|
-
});
|
|
1761
|
-
}
|
|
1762
|
-
buildUrlFromActiveRoute() {
|
|
1763
|
-
const ar = this.activeRoute;
|
|
1764
|
-
if (!ar || !ar.path) {
|
|
1765
|
-
return null;
|
|
1766
|
-
}
|
|
1767
|
-
const path = ar.path;
|
|
1768
|
-
const query = ar.query;
|
|
1769
|
-
if (!query || Object.keys(query).length === 0) {
|
|
1770
|
-
return path;
|
|
1771
|
-
}
|
|
1772
|
-
const search = qs.stringify(query);
|
|
1773
|
-
return `${path}${search && search !== '' ? `?${search}` : ''}`;
|
|
1774
|
-
}
|
|
1775
|
-
patchBrowserHistoryOnce() {
|
|
1776
|
-
const g = globalThis;
|
|
1777
|
-
const key = Symbol.for('sigmela_router_history_patch');
|
|
1778
|
-
if (g[key]) return;
|
|
1779
|
-
const hist = g.history;
|
|
1780
|
-
if (hist && hist.pushState && hist.replaceState) {
|
|
1781
|
-
const originalPush = hist.pushState.bind(hist);
|
|
1782
|
-
const originalReplace = hist.replaceState.bind(hist);
|
|
1783
|
-
hist.pushState = (data, unused, url) => {
|
|
1784
|
-
const res = originalPush(data, unused, url);
|
|
1785
|
-
if (g.Event && g.dispatchEvent) {
|
|
1786
|
-
g.dispatchEvent(new g.Event('pushState'));
|
|
1787
|
-
}
|
|
1788
|
-
return res;
|
|
1789
|
-
};
|
|
1790
|
-
hist.replaceState = (data, unused, url) => {
|
|
1791
|
-
const res = originalReplace(data, unused, url);
|
|
1792
|
-
if (g.Event && g.dispatchEvent) {
|
|
1793
|
-
g.dispatchEvent(new g.Event('replaceState'));
|
|
1794
|
-
}
|
|
1795
|
-
return res;
|
|
1796
|
-
};
|
|
1797
|
-
}
|
|
1798
|
-
g[key] = true;
|
|
1799
|
-
}
|
|
1800
|
-
setupBrowserHistory() {
|
|
1801
|
-
// Abort previous listeners in case setupBrowserHistory is called twice
|
|
1802
|
-
this.abortController?.abort();
|
|
1803
|
-
this.abortController = new AbortController();
|
|
1804
|
-
const {
|
|
1805
|
-
signal
|
|
1806
|
-
} = this.abortController;
|
|
1807
|
-
const g = globalThis;
|
|
1808
|
-
const gAny = g;
|
|
1809
|
-
const activeKey = Symbol.for('sigmela_router_active_instance');
|
|
1810
|
-
this.patchBrowserHistoryOnce();
|
|
1811
|
-
this.ensureHistoryIndex();
|
|
1812
|
-
this.lastBrowserIndex = this.getHistoryIndex();
|
|
1813
|
-
|
|
1814
|
-
// If a new Router instance is created (e.g. Fast Refresh / HMR), ensure only the latest
|
|
1815
|
-
// instance reacts to browser history events to avoid duplicate navigation updates.
|
|
1816
|
-
gAny[activeKey] = this;
|
|
1817
|
-
const onHistory = ev => {
|
|
1818
|
-
if (gAny[activeKey] !== this) return;
|
|
1819
|
-
if (ev.type === 'pushState') {
|
|
1820
|
-
const path = this.getRouterPathFromHistory();
|
|
1821
|
-
const idx = this.getHistoryIndexOrNull();
|
|
1822
|
-
this.lastBrowserIndex = idx !== null ? idx : Math.max(0, this.lastBrowserIndex + 1);
|
|
1823
|
-
this.performNavigation(path, 'push');
|
|
1824
|
-
return;
|
|
1825
|
-
}
|
|
1826
|
-
if (ev.type === 'replaceState') {
|
|
1827
|
-
if (this.suppressHistorySyncCount > 0) {
|
|
1828
|
-
this.log('onHistory: replaceState suppressed (internal URL sync)');
|
|
1829
|
-
this.suppressHistorySyncCount -= 1;
|
|
1830
|
-
const idx = this.getHistoryIndexOrNull();
|
|
1831
|
-
this.lastBrowserIndex = idx !== null ? idx : this.lastBrowserIndex;
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
const path = this.getRouterPathFromHistory();
|
|
1835
|
-
const dedupe = this.getReplaceDedupeFromHistory();
|
|
1836
|
-
this.performNavigation(path, 'replace', {
|
|
1837
|
-
dedupe
|
|
1838
|
-
});
|
|
1839
|
-
const idx = this.getHistoryIndexOrNull();
|
|
1840
|
-
this.lastBrowserIndex = idx !== null ? idx : this.lastBrowserIndex;
|
|
1841
|
-
return;
|
|
1842
|
-
}
|
|
1843
|
-
if (ev.type === 'popstate') {
|
|
1844
|
-
const url = this.getRouterPathFromHistory();
|
|
1845
|
-
const idx = this.getHistoryIndexOrNull();
|
|
1846
|
-
const delta = idx !== null ? idx - this.lastBrowserIndex : 0;
|
|
1847
|
-
this.log('popstate event', {
|
|
1848
|
-
url,
|
|
1849
|
-
idx,
|
|
1850
|
-
prevIndex: this.lastBrowserIndex,
|
|
1851
|
-
delta
|
|
1852
|
-
});
|
|
1853
|
-
|
|
1854
|
-
// If browser history index isn't available (external history manipulations),
|
|
1855
|
-
// treat popstate as a soft replace. The route path still comes from __srPath when present.
|
|
1856
|
-
if (idx === null) {
|
|
1857
|
-
this.performNavigation(url, 'replace', {
|
|
1858
|
-
dedupe: true
|
|
1859
|
-
});
|
|
1860
|
-
return;
|
|
1861
|
-
}
|
|
1862
|
-
if (delta < 0) {
|
|
1863
|
-
this.performNavigation(url, 'replace', {
|
|
1864
|
-
dedupe: true
|
|
1865
|
-
});
|
|
1866
|
-
} else if (delta > 0) {
|
|
1867
|
-
this.log('popstate: forward history step, treat as push', {
|
|
1868
|
-
url
|
|
1869
|
-
});
|
|
1870
|
-
this.performNavigation(url, 'push');
|
|
1871
|
-
} else {
|
|
1872
|
-
this.log('popstate: same index, soft replace+dedupe', {
|
|
1873
|
-
url
|
|
1874
|
-
});
|
|
1875
|
-
this.performNavigation(url, 'replace', {
|
|
1876
|
-
dedupe: true
|
|
1877
|
-
});
|
|
1878
|
-
}
|
|
1879
|
-
this.lastBrowserIndex = idx;
|
|
1880
|
-
}
|
|
1881
|
-
};
|
|
1882
|
-
g.addEventListener?.('pushState', onHistory, {
|
|
1883
|
-
signal
|
|
1884
|
-
});
|
|
1885
|
-
g.addEventListener?.('replaceState', onHistory, {
|
|
1886
|
-
signal
|
|
1887
|
-
});
|
|
1888
|
-
g.addEventListener?.('popstate', onHistory, {
|
|
1889
|
-
signal
|
|
1890
|
-
});
|
|
1891
|
-
}
|
|
1892
|
-
destroyed = false;
|
|
1893
|
-
destroy() {
|
|
1894
|
-
this.destroyed = true;
|
|
1895
|
-
this.abortController?.abort();
|
|
1896
|
-
this.listeners.clear();
|
|
1897
|
-
this.stackListeners.clear();
|
|
1898
|
-
this.activeRouteListeners.clear();
|
|
1899
|
-
this.rootListeners.clear();
|
|
1900
|
-
}
|
|
1901
|
-
syncUrlAfterInternalPop(popped) {
|
|
1902
|
-
if (!this.isWebEnv()) return;
|
|
1903
|
-
const compiled = this.getCompiledRoute(popped.routeId);
|
|
1904
|
-
const isQueryRoute = compiled && compiled.queryPattern && Object.keys(compiled.queryPattern).length > 0;
|
|
1905
|
-
if (isQueryRoute) {
|
|
1906
|
-
const currentUrl = this.getCurrentUrl();
|
|
1907
|
-
const {
|
|
1908
|
-
pathname,
|
|
1909
|
-
query
|
|
1910
|
-
} = this.parsePath(currentUrl);
|
|
1911
|
-
const pattern = compiled.queryPattern;
|
|
1912
|
-
const nextQuery = {
|
|
1913
|
-
...query
|
|
1914
|
-
};
|
|
1915
|
-
for (const key of Object.keys(pattern)) {
|
|
1916
|
-
delete nextQuery[key];
|
|
1917
|
-
}
|
|
1918
|
-
const hasQuery = Object.keys(nextQuery).length > 0;
|
|
1919
|
-
const newSearch = hasQuery ? `?${qs.stringify(nextQuery)}` : '';
|
|
1920
|
-
const nextUrl = `${pathname}${newSearch}`;
|
|
1921
|
-
this.log('syncUrlWithStateAfterInternalPop (query)', {
|
|
1922
|
-
poppedRouteId: popped.routeId,
|
|
1923
|
-
from: currentUrl,
|
|
1924
|
-
to: nextUrl
|
|
1925
|
-
});
|
|
1926
|
-
this.replaceUrlSilently(nextUrl);
|
|
1927
|
-
return;
|
|
1928
|
-
}
|
|
1929
|
-
const from = this.getCurrentUrl();
|
|
1930
|
-
const nextUrl = this.buildUrlFromActiveRoute();
|
|
1931
|
-
if (!nextUrl) {
|
|
1932
|
-
this.log('syncUrlWithStateAfterInternalPop: no activeRoute, skip URL sync', {
|
|
1933
|
-
from
|
|
1934
|
-
});
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
this.log('syncUrlAfterInternalPop (base)', {
|
|
1938
|
-
poppedRouteId: popped.routeId,
|
|
1939
|
-
from,
|
|
1940
|
-
to: nextUrl
|
|
1941
|
-
});
|
|
1942
|
-
this.replaceUrlSilently(nextUrl);
|
|
1943
|
-
}
|
|
1944
|
-
popFromActiveStack() {
|
|
1945
|
-
const active = this.activeRoute;
|
|
1946
|
-
if (!active || !active.stackId) {
|
|
1947
|
-
if (this.root) {
|
|
1948
|
-
const sid = this.root.getId();
|
|
1949
|
-
if (sid) {
|
|
1950
|
-
const stackHistory = this.getStackHistory(sid);
|
|
1951
|
-
if (stackHistory.length > 0) {
|
|
1952
|
-
const top = stackHistory[stackHistory.length - 1];
|
|
1953
|
-
if (!top) return null;
|
|
1954
|
-
const isModalOrSheet = top.options?.stackPresentation === 'modal' || top.options?.stackPresentation === 'sheet';
|
|
1955
|
-
if (stackHistory.length > 1 || isModalOrSheet) {
|
|
1956
|
-
if (top.options?.stackPresentation === 'sheet') {
|
|
1957
|
-
const dismisser = this.sheetDismissers.get(top.key);
|
|
1958
|
-
if (dismisser) {
|
|
1959
|
-
this.unregisterSheetDismisser(top.key);
|
|
1960
|
-
dismisser();
|
|
1961
|
-
return top;
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
this.applyHistoryChange('pop', top);
|
|
1965
|
-
return top;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
return null;
|
|
1971
|
-
}
|
|
1972
|
-
let activeItem;
|
|
1973
|
-
for (let i = this.state.history.length - 1; i >= 0; i--) {
|
|
1974
|
-
const h = this.state.history[i];
|
|
1975
|
-
if (h && active && h.stackId === active.stackId) {
|
|
1976
|
-
activeItem = h;
|
|
1977
|
-
break;
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
if (!active || !activeItem || !activeItem.stackId) return null;
|
|
1981
|
-
const stackHistory = this.getStackHistory(activeItem.stackId);
|
|
1982
|
-
const isModalOrSheet = activeItem.options?.stackPresentation === 'modal' || activeItem.options?.stackPresentation === 'sheet';
|
|
1983
|
-
const allowRootPop = activeItem.options?.allowRootPop === true;
|
|
1984
|
-
if (stackHistory.length <= 1 && !isModalOrSheet && !allowRootPop) {
|
|
1985
|
-
return null;
|
|
1986
|
-
}
|
|
1987
|
-
if (activeItem.options?.stackPresentation === 'sheet') {
|
|
1988
|
-
const dismisser = this.sheetDismissers.get(activeItem.key);
|
|
1989
|
-
if (dismisser) {
|
|
1990
|
-
this.unregisterSheetDismisser(activeItem.key);
|
|
1991
|
-
dismisser();
|
|
1992
|
-
return activeItem;
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
const top = stackHistory[stackHistory.length - 1];
|
|
1996
|
-
if (!top) return null;
|
|
1997
|
-
this.applyHistoryChange('pop', top);
|
|
1998
|
-
return top;
|
|
1999
|
-
}
|
|
2000
|
-
buildHistoryFromUrl(url, reuseKeysFrom) {
|
|
2001
|
-
const {
|
|
2002
|
-
pathname,
|
|
2003
|
-
query
|
|
2004
|
-
} = this.parsePath(url);
|
|
2005
|
-
this.log('parse', {
|
|
2006
|
-
url,
|
|
2007
|
-
pathname,
|
|
2008
|
-
query
|
|
2009
|
-
});
|
|
2010
|
-
const baseRoute = this.matchBaseRoute(pathname, {});
|
|
2011
|
-
const deepest = this.matchBaseRoute(pathname, query);
|
|
2012
|
-
const hasDeepestQueryPattern = deepest && deepest.queryPattern && Object.keys(deepest.queryPattern).length > 0;
|
|
2013
|
-
const isDeepestOverlay = hasDeepestQueryPattern && deepest && baseRoute && deepest.routeId !== baseRoute.routeId;
|
|
2014
|
-
if (!deepest || isDeepestOverlay && !baseRoute) {
|
|
2015
|
-
if (__DEV__) {
|
|
2016
|
-
console.warn(`[Router] parse: no base route found for "${pathname}", seeding initial history`);
|
|
2017
|
-
} else {
|
|
2018
|
-
console.warn(`[Router] No base route found for "${pathname}"`);
|
|
2019
|
-
}
|
|
2020
|
-
this.seedInitialHistory();
|
|
2021
|
-
this.recomputeActiveRoute();
|
|
2022
|
-
this.emit(this.listeners);
|
|
2023
|
-
return;
|
|
2024
|
-
}
|
|
2025
|
-
const segments = pathname.split('/').filter(Boolean);
|
|
2026
|
-
const prefixes = segments.length > 0 ? segments.map((_, i) => '/' + segments.slice(0, i + 1).join('/')) : ['/'];
|
|
2027
|
-
if (!prefixes.includes('/')) {
|
|
2028
|
-
prefixes.unshift('/');
|
|
2029
|
-
}
|
|
2030
|
-
const items = [];
|
|
2031
|
-
prefixes.forEach((prefixPath, index) => {
|
|
2032
|
-
this.log('parse prefix', {
|
|
2033
|
-
index,
|
|
2034
|
-
prefixPath
|
|
2035
|
-
});
|
|
2036
|
-
let routeForPrefix;
|
|
2037
|
-
if (index === prefixes.length - 1) {
|
|
2038
|
-
routeForPrefix = baseRoute || deepest;
|
|
2039
|
-
} else {
|
|
2040
|
-
let best;
|
|
2041
|
-
for (const route of this.registry) {
|
|
2042
|
-
if (route.queryPattern) continue;
|
|
2043
|
-
const matchResult = route.matchPath(prefixPath);
|
|
2044
|
-
if (!matchResult) continue;
|
|
2045
|
-
const spec = route.baseSpecificity;
|
|
2046
|
-
this.log('parse candidate for prefix', {
|
|
2047
|
-
prefixPath,
|
|
2048
|
-
routeId: route.routeId,
|
|
2049
|
-
path: route.path,
|
|
2050
|
-
baseSpecificity: spec
|
|
2051
|
-
});
|
|
2052
|
-
if (!best || spec > best.spec) {
|
|
2053
|
-
best = {
|
|
2054
|
-
route,
|
|
2055
|
-
spec
|
|
2056
|
-
};
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
routeForPrefix = best?.route;
|
|
2060
|
-
}
|
|
2061
|
-
if (!routeForPrefix) {
|
|
2062
|
-
this.log('parse: no route for prefix', {
|
|
2063
|
-
index,
|
|
2064
|
-
prefixPath
|
|
2065
|
-
});
|
|
2066
|
-
return;
|
|
2067
|
-
}
|
|
2068
|
-
const matchResult = routeForPrefix.matchPath(prefixPath);
|
|
2069
|
-
const params = matchResult ? matchResult.params : undefined;
|
|
2070
|
-
const itemQuery = index === prefixes.length - 1 && !isDeepestOverlay ? query : {};
|
|
2071
|
-
const item = this.createHistoryItem(routeForPrefix, params, itemQuery, prefixPath);
|
|
2072
|
-
this.log('parse: push item', {
|
|
2073
|
-
index,
|
|
2074
|
-
routeId: routeForPrefix.routeId,
|
|
2075
|
-
path: routeForPrefix.path,
|
|
2076
|
-
prefixPath,
|
|
2077
|
-
params,
|
|
2078
|
-
itemQuery
|
|
2079
|
-
});
|
|
2080
|
-
items.push(item);
|
|
2081
|
-
});
|
|
2082
|
-
let overlayItem;
|
|
2083
|
-
if (isDeepestOverlay && deepest && baseRoute) {
|
|
2084
|
-
overlayItem = this.createHistoryItem(deepest, undefined, query, pathname);
|
|
2085
|
-
this.log('parse: push overlay item', {
|
|
2086
|
-
routeId: deepest.routeId,
|
|
2087
|
-
path: deepest.path,
|
|
2088
|
-
pathname,
|
|
2089
|
-
query
|
|
2090
|
-
});
|
|
2091
|
-
items.push(overlayItem);
|
|
2092
|
-
}
|
|
2093
|
-
if (items.length > 0) {
|
|
2094
|
-
const lastItem = items[items.length - 1];
|
|
2095
|
-
const lastItemCompiled = lastItem ? this.getCompiledRoute(lastItem.routeId) : undefined;
|
|
2096
|
-
const isLastItemOverlay = lastItem && lastItemCompiled && lastItemCompiled.queryPattern && Object.keys(lastItemCompiled.queryPattern).length > 0;
|
|
2097
|
-
const finalRouteId = isLastItemOverlay && items.length > 1 ? items[items.length - 2]?.routeId : lastItem?.routeId;
|
|
2098
|
-
const searchStartIndex = isLastItemOverlay ? items.length - 2 : items.length - 1;
|
|
2099
|
-
for (let i = searchStartIndex; i >= 0; i--) {
|
|
2100
|
-
const item = items[i];
|
|
2101
|
-
if (item && item.routeId) {
|
|
2102
|
-
const compiled = this.getCompiledRoute(item.routeId);
|
|
2103
|
-
if (compiled && compiled.childNode) {
|
|
2104
|
-
this.addChildNodeSeedsToItems(item.routeId, items, finalRouteId);
|
|
2105
|
-
// Don't break: we may have multiple nested containers (e.g. TabBar -> SplitView).
|
|
2106
|
-
// All of them must be activated/seeded to make the UI consistent on initial deep-link.
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
if (overlayItem) {
|
|
2111
|
-
const overlayIndex = items.findIndex(item => item.key === overlayItem.key);
|
|
2112
|
-
if (overlayIndex >= 0 && overlayIndex < items.length - 1) {
|
|
2113
|
-
items.splice(overlayIndex, 1);
|
|
2114
|
-
items.push(overlayItem);
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
if (!items.length) {
|
|
2119
|
-
if (__DEV__) {
|
|
2120
|
-
console.warn('[Router] parse: no items built for URL, seeding initial history');
|
|
2121
|
-
} else {
|
|
2122
|
-
console.warn('[Router] No items built for URL, seeding initial history');
|
|
2123
|
-
}
|
|
2124
|
-
this.seedInitialHistory();
|
|
2125
|
-
this.recomputeActiveRoute();
|
|
2126
|
-
this.emit(this.listeners);
|
|
2127
|
-
return;
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
// When rebuilding history (e.g. tab reset), preserve keys for matching items
|
|
2131
|
-
// so web animations don't replay for the whole root stack.
|
|
2132
|
-
this.reuseKeysFromPreviousHistory(items, reuseKeysFrom);
|
|
2133
|
-
this.setState({
|
|
2134
|
-
history: items
|
|
2135
|
-
});
|
|
2136
|
-
// setState -> updateStackHistories already emits to changed stacks
|
|
2137
|
-
|
|
2138
|
-
this.recomputeActiveRoute();
|
|
2139
|
-
this.emit(this.listeners);
|
|
2140
|
-
}
|
|
2141
|
-
reuseKeysFromPreviousHistory(next, prev) {
|
|
2142
|
-
if (!prev || prev.length === 0) return;
|
|
2143
|
-
const used = new Set();
|
|
2144
|
-
const sameItemIdentity = (a, b) => {
|
|
2145
|
-
if (a.routeId !== b.routeId) return false;
|
|
2146
|
-
if ((a.stackId ?? '') !== (b.stackId ?? '')) return false;
|
|
2147
|
-
if ((a.path ?? '') !== (b.path ?? '')) return false;
|
|
2148
|
-
const sameParams = this.areShallowEqual(a.params ?? {}, b.params ?? {});
|
|
2149
|
-
if (!sameParams) return false;
|
|
2150
|
-
const sameQuery = this.areShallowEqual(a.query ?? {}, b.query ?? {});
|
|
2151
|
-
if (!sameQuery) return false;
|
|
2152
|
-
|
|
2153
|
-
// Keep presentation stable: modal/sheet items should not steal keys from push items.
|
|
2154
|
-
const ap = a.options?.stackPresentation ?? null;
|
|
2155
|
-
const bp = b.options?.stackPresentation ?? null;
|
|
2156
|
-
if (ap !== bp) return false;
|
|
2157
|
-
return true;
|
|
2158
|
-
};
|
|
2159
|
-
for (const item of next) {
|
|
2160
|
-
for (let i = prev.length - 1; i >= 0; i--) {
|
|
2161
|
-
const candidate = prev[i];
|
|
2162
|
-
if (!candidate) continue;
|
|
2163
|
-
if (used.has(candidate.key)) continue;
|
|
2164
|
-
if (!sameItemIdentity(item, candidate)) continue;
|
|
2165
|
-
item.key = candidate.key;
|
|
2166
|
-
used.add(candidate.key);
|
|
2167
|
-
break;
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
}
|