@sigmela/router 0.0.11 → 0.0.13
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/lib/module/Navigation.js +1 -1
- package/lib/module/StackRenderer.js +4 -2
- package/lib/module/TabBar/RenderTabBar.js +3 -2
- package/lib/typescript/src/StackRenderer.d.ts +2 -0
- package/lib/typescript/src/TabBar/RenderTabBar.d.ts +1 -1
- package/package.json +1 -2
- package/src/Navigation.tsx +0 -102
- package/src/NavigationStack.ts +0 -106
- package/src/Router.ts +0 -684
- package/src/RouterContext.tsx +0 -58
- package/src/ScreenStackItem.tsx +0 -64
- package/src/StackRenderer.tsx +0 -41
- package/src/TabBar/RenderTabBar.tsx +0 -154
- package/src/TabBar/TabBar.ts +0 -106
- package/src/TabBar/TabBarContext.ts +0 -4
- package/src/TabBar/useTabBar.ts +0 -10
- package/src/createController.ts +0 -27
- package/src/index.ts +0 -24
- package/src/types.ts +0 -272
package/src/Router.ts
DELETED
|
@@ -1,684 +0,0 @@
|
|
|
1
|
-
import { NavigationStack } from './NavigationStack';
|
|
2
|
-
import { nanoid } from 'nanoid/non-secure';
|
|
3
|
-
import { TabBar } from './TabBar/TabBar';
|
|
4
|
-
import qs from 'query-string';
|
|
5
|
-
import type {
|
|
6
|
-
CompiledRoute,
|
|
7
|
-
HistoryItem,
|
|
8
|
-
Scope,
|
|
9
|
-
ScreenOptions,
|
|
10
|
-
VisibleRoute,
|
|
11
|
-
} from './types';
|
|
12
|
-
|
|
13
|
-
type Listener = () => void;
|
|
14
|
-
|
|
15
|
-
export interface RouterConfig {
|
|
16
|
-
root: TabBar | NavigationStack;
|
|
17
|
-
global?: NavigationStack;
|
|
18
|
-
screenOptions?: ScreenOptions; // global overrides
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Root transition option: allow string shorthand like 'fade'
|
|
22
|
-
export type RootTransition = ScreenOptions['stackAnimation'];
|
|
23
|
-
|
|
24
|
-
function isTabBarLike(obj: any): obj is TabBar {
|
|
25
|
-
return (
|
|
26
|
-
obj != null &&
|
|
27
|
-
typeof obj === 'object' &&
|
|
28
|
-
'onIndexChange' in obj &&
|
|
29
|
-
'getState' in obj &&
|
|
30
|
-
'subscribe' in obj &&
|
|
31
|
-
'stacks' in obj
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isNavigationStackLike(obj: any): obj is NavigationStack {
|
|
36
|
-
return (
|
|
37
|
-
obj != null &&
|
|
38
|
-
typeof obj === 'object' &&
|
|
39
|
-
'getRoutes' in obj &&
|
|
40
|
-
'getId' in obj
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type RouterState = { history: HistoryItem[]; activeTabIndex?: number };
|
|
45
|
-
|
|
46
|
-
const EMPTY_ARRAY: HistoryItem[] = [];
|
|
47
|
-
|
|
48
|
-
export class Router {
|
|
49
|
-
public tabBar: TabBar | null = null;
|
|
50
|
-
public root: NavigationStack | TabBar | null = null;
|
|
51
|
-
public global: NavigationStack | null = null;
|
|
52
|
-
|
|
53
|
-
private readonly listeners: Set<Listener> = new Set();
|
|
54
|
-
private readonly registry: CompiledRoute[] = [];
|
|
55
|
-
private state: RouterState = {
|
|
56
|
-
history: [],
|
|
57
|
-
activeTabIndex: undefined,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
private readonly routerScreenOptions: ScreenOptions | undefined;
|
|
61
|
-
|
|
62
|
-
// per-stack slices and listeners
|
|
63
|
-
private stackSlices = new Map<string, HistoryItem[]>();
|
|
64
|
-
private stackListeners = new Map<string, Set<Listener>>();
|
|
65
|
-
private activeTabListeners = new Set<Listener>();
|
|
66
|
-
private stackById = new Map<string, NavigationStack>();
|
|
67
|
-
private routeById = new Map<
|
|
68
|
-
string,
|
|
69
|
-
{ path: string; stackId: string; tabIndex?: number; scope: Scope }
|
|
70
|
-
>();
|
|
71
|
-
private visibleRoute: VisibleRoute = null;
|
|
72
|
-
// Root structure listeners (TabBar ↔ NavigationStack changes)
|
|
73
|
-
private rootListeners: Set<Listener> = new Set();
|
|
74
|
-
private rootTransition?: RootTransition = undefined;
|
|
75
|
-
|
|
76
|
-
constructor(config: RouterConfig) {
|
|
77
|
-
this.routerScreenOptions = config.screenOptions;
|
|
78
|
-
|
|
79
|
-
if (isTabBarLike(config.root)) {
|
|
80
|
-
this.tabBar = config.root as TabBar;
|
|
81
|
-
}
|
|
82
|
-
if (config.global) {
|
|
83
|
-
this.global = config.global;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
this.root = config.root;
|
|
87
|
-
|
|
88
|
-
if (this.tabBar) {
|
|
89
|
-
this.state = {
|
|
90
|
-
history: [],
|
|
91
|
-
activeTabIndex: this.tabBar.getState().index,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.buildRegistry();
|
|
96
|
-
this.seedInitialHistory();
|
|
97
|
-
this.recomputeVisibleRoute();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Public API
|
|
101
|
-
public navigate = (path: string): void => {
|
|
102
|
-
this.performNavigation(path, 'push');
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
public replace = (path: string): void => {
|
|
106
|
-
this.performNavigation(path, 'replace');
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
public goBack = (): void => {
|
|
110
|
-
// Global layer wins
|
|
111
|
-
if (this.global) {
|
|
112
|
-
const gid = this.global.getId();
|
|
113
|
-
const gslice = this.getStackHistory(gid);
|
|
114
|
-
const gtop = gslice.length ? gslice[gslice.length - 1] : undefined;
|
|
115
|
-
if (gtop) {
|
|
116
|
-
this.applyHistoryChange('pop', gtop);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Tab layer
|
|
122
|
-
if (this.tabBar) {
|
|
123
|
-
const idx = this.getActiveTabIndex();
|
|
124
|
-
const state = this.tabBar.getState();
|
|
125
|
-
const route = state.tabs[idx];
|
|
126
|
-
if (!route) return;
|
|
127
|
-
const stack = this.tabBar.stacks[route.tabKey];
|
|
128
|
-
if (!stack) return;
|
|
129
|
-
const sid = stack.getId();
|
|
130
|
-
const slice = this.getStackHistory(sid);
|
|
131
|
-
if (slice.length > 1) {
|
|
132
|
-
const top = slice[slice.length - 1];
|
|
133
|
-
if (top) {
|
|
134
|
-
this.applyHistoryChange('pop', top);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Root layer
|
|
141
|
-
if (isNavigationStackLike(this.root)) {
|
|
142
|
-
const sid = this.root.getId();
|
|
143
|
-
const slice = this.getStackHistory(sid);
|
|
144
|
-
if (slice.length > 1) {
|
|
145
|
-
const top = slice[slice.length - 1];
|
|
146
|
-
if (top) {
|
|
147
|
-
this.applyHistoryChange('pop', top);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
public onTabIndexChange = (index: number): void => {
|
|
154
|
-
if (this.tabBar) {
|
|
155
|
-
this.tabBar.onIndexChange(index);
|
|
156
|
-
this.setState({ activeTabIndex: index });
|
|
157
|
-
this.emit(this.activeTabListeners);
|
|
158
|
-
this.recomputeVisibleRoute();
|
|
159
|
-
this.emit(this.listeners);
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
public setActiveTabIndex = (index: number): void => {
|
|
164
|
-
this.onTabIndexChange(index);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
public ensureTabSeed = (index: number): void => {
|
|
168
|
-
if (!this.tabBar) return;
|
|
169
|
-
const state = this.tabBar.getState();
|
|
170
|
-
const route = state.tabs[index];
|
|
171
|
-
if (!route) return;
|
|
172
|
-
const key = route.tabKey;
|
|
173
|
-
const stack = this.tabBar.stacks[key];
|
|
174
|
-
if (!stack) return;
|
|
175
|
-
const hasAny = this.getStackHistory(stack.getId()).length > 0;
|
|
176
|
-
if (hasAny) return;
|
|
177
|
-
const first = stack.getFirstRoute();
|
|
178
|
-
if (!first) return;
|
|
179
|
-
|
|
180
|
-
const newItem: HistoryItem = {
|
|
181
|
-
key: this.generateKey(),
|
|
182
|
-
scope: 'tab',
|
|
183
|
-
routeId: first.routeId,
|
|
184
|
-
component: first.component,
|
|
185
|
-
options: this.mergeOptions(first.options, stack.getId()),
|
|
186
|
-
params: {},
|
|
187
|
-
tabIndex: index,
|
|
188
|
-
stackId: stack.getId(),
|
|
189
|
-
};
|
|
190
|
-
this.applyHistoryChange('push', newItem);
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
public getState = () => {
|
|
194
|
-
return this.state;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
public subscribe(listener: Listener): () => void {
|
|
198
|
-
this.listeners.add(listener);
|
|
199
|
-
return () => {
|
|
200
|
-
this.listeners.delete(listener);
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Per-stack subscriptions
|
|
205
|
-
public getStackHistory = (stackId: string): HistoryItem[] => {
|
|
206
|
-
return this.stackSlices.get(stackId) ?? EMPTY_ARRAY;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
public subscribeStack = (stackId: string, cb: Listener): (() => void) => {
|
|
210
|
-
if (!stackId) return () => {};
|
|
211
|
-
let set = this.stackListeners.get(stackId);
|
|
212
|
-
if (!set) {
|
|
213
|
-
set = new Set();
|
|
214
|
-
this.stackListeners.set(stackId, set);
|
|
215
|
-
}
|
|
216
|
-
set.add(cb);
|
|
217
|
-
return () => {
|
|
218
|
-
set!.delete(cb);
|
|
219
|
-
};
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
public getActiveTabIndex = (): number => {
|
|
223
|
-
return this.state.activeTabIndex ?? 0;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
public subscribeActiveTab = (cb: Listener): (() => void) => {
|
|
227
|
-
this.activeTabListeners.add(cb);
|
|
228
|
-
return () => this.activeTabListeners.delete(cb);
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
public getRootStackId(): string | undefined {
|
|
232
|
-
return isNavigationStackLike(this.root) ? this.root.getId() : undefined;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
public getGlobalStackId(): string | undefined {
|
|
236
|
-
return this.global?.getId();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
public hasTabBar(): boolean {
|
|
240
|
-
return !!this.tabBar;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
public subscribeRoot(listener: Listener): () => void {
|
|
244
|
-
this.rootListeners.add(listener);
|
|
245
|
-
return () => this.rootListeners.delete(listener);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private emitRootChange(): void {
|
|
249
|
-
this.rootListeners.forEach((l) => l());
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
public getRootTransition(): RootTransition | undefined {
|
|
253
|
-
return this.rootTransition;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
public setRoot(
|
|
257
|
-
nextRoot: TabBar | NavigationStack,
|
|
258
|
-
options?: { transition?: RootTransition }
|
|
259
|
-
): void {
|
|
260
|
-
// Update root/tabBar references
|
|
261
|
-
this.tabBar = isTabBarLike(nextRoot) ? (nextRoot as TabBar) : null;
|
|
262
|
-
this.root = nextRoot;
|
|
263
|
-
|
|
264
|
-
// Save requested transition (stackAnimation string)
|
|
265
|
-
this.rootTransition = options?.transition ?? undefined;
|
|
266
|
-
|
|
267
|
-
// If switching to TabBar, reset selected tab to the first one to avoid
|
|
268
|
-
// leaking previously selected tab across auth flow changes.
|
|
269
|
-
if (this.tabBar) {
|
|
270
|
-
this.tabBar.onIndexChange(0);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Reset core structures (keep global reference as-is)
|
|
274
|
-
this.registry.length = 0;
|
|
275
|
-
this.stackSlices.clear();
|
|
276
|
-
this.stackById.clear();
|
|
277
|
-
this.routeById.clear();
|
|
278
|
-
|
|
279
|
-
// Reset state (activeTabIndex from tabBar if present)
|
|
280
|
-
this.state = {
|
|
281
|
-
history: [],
|
|
282
|
-
activeTabIndex: this.tabBar ? this.tabBar.getState().index : undefined,
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
// Rebuild registry and seed new root
|
|
286
|
-
this.buildRegistry();
|
|
287
|
-
this.seedInitialHistory();
|
|
288
|
-
this.recomputeVisibleRoute();
|
|
289
|
-
this.emitRootChange();
|
|
290
|
-
this.emit(this.listeners);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Visible route (global top if present, else active tab/root top)
|
|
294
|
-
public getVisibleRoute = (): VisibleRoute => {
|
|
295
|
-
return this.visibleRoute;
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
private recomputeVisibleRoute(): void {
|
|
299
|
-
// Global top
|
|
300
|
-
if (this.global) {
|
|
301
|
-
const gid = this.global.getId();
|
|
302
|
-
const gslice = this.getStackHistory(gid);
|
|
303
|
-
const gtop = gslice.length ? gslice[gslice.length - 1] : undefined;
|
|
304
|
-
if (gtop) {
|
|
305
|
-
const meta = this.routeById.get(gtop.routeId);
|
|
306
|
-
this.visibleRoute = meta
|
|
307
|
-
? {
|
|
308
|
-
...meta,
|
|
309
|
-
routeId: gtop.routeId,
|
|
310
|
-
params: gtop.params,
|
|
311
|
-
query: gtop.query,
|
|
312
|
-
}
|
|
313
|
-
: {
|
|
314
|
-
routeId: gtop.routeId,
|
|
315
|
-
stackId: gtop.stackId,
|
|
316
|
-
params: gtop.params,
|
|
317
|
-
query: gtop.query,
|
|
318
|
-
scope: 'global',
|
|
319
|
-
};
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// TabBar
|
|
325
|
-
if (this.tabBar) {
|
|
326
|
-
const idx = this.getActiveTabIndex();
|
|
327
|
-
const state = this.tabBar.getState();
|
|
328
|
-
const route = state.tabs[idx];
|
|
329
|
-
if (route) {
|
|
330
|
-
const stack = this.tabBar.stacks[route.tabKey];
|
|
331
|
-
if (stack) {
|
|
332
|
-
const sid = stack.getId();
|
|
333
|
-
const slice = this.getStackHistory(sid);
|
|
334
|
-
const top = slice.length ? slice[slice.length - 1] : undefined;
|
|
335
|
-
if (top) {
|
|
336
|
-
const meta = this.routeById.get(top.routeId);
|
|
337
|
-
this.visibleRoute = meta
|
|
338
|
-
? {
|
|
339
|
-
...meta,
|
|
340
|
-
routeId: top.routeId,
|
|
341
|
-
params: top.params,
|
|
342
|
-
query: top.query,
|
|
343
|
-
}
|
|
344
|
-
: {
|
|
345
|
-
routeId: top.routeId,
|
|
346
|
-
stackId: sid,
|
|
347
|
-
tabIndex: idx,
|
|
348
|
-
params: top.params,
|
|
349
|
-
query: top.query,
|
|
350
|
-
scope: 'tab',
|
|
351
|
-
};
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
} else {
|
|
355
|
-
this.visibleRoute = {
|
|
356
|
-
routeId: `tab-screen-${idx}`,
|
|
357
|
-
tabIndex: idx,
|
|
358
|
-
scope: 'tab',
|
|
359
|
-
};
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Root stack
|
|
366
|
-
if (this.root && isNavigationStackLike(this.root)) {
|
|
367
|
-
const sid = this.root.getId();
|
|
368
|
-
const slice = this.getStackHistory(sid);
|
|
369
|
-
const top = slice.length ? slice[slice.length - 1] : undefined;
|
|
370
|
-
if (top) {
|
|
371
|
-
const meta = this.routeById.get(top.routeId);
|
|
372
|
-
this.visibleRoute = meta
|
|
373
|
-
? {
|
|
374
|
-
...meta,
|
|
375
|
-
routeId: top.routeId,
|
|
376
|
-
params: top.params,
|
|
377
|
-
query: top.query,
|
|
378
|
-
}
|
|
379
|
-
: {
|
|
380
|
-
routeId: top.routeId,
|
|
381
|
-
stackId: sid,
|
|
382
|
-
params: top.params,
|
|
383
|
-
query: top.query,
|
|
384
|
-
scope: 'root',
|
|
385
|
-
};
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
this.visibleRoute = null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Internal navigation logic
|
|
393
|
-
private performNavigation(path: string, action: 'push' | 'replace'): void {
|
|
394
|
-
const { pathname, query } = this.parsePath(path);
|
|
395
|
-
const matched = this.matchRoute(pathname);
|
|
396
|
-
if (!matched) {
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
matched.scope === 'tab' &&
|
|
402
|
-
this.tabBar &&
|
|
403
|
-
matched.tabIndex !== undefined
|
|
404
|
-
) {
|
|
405
|
-
this.onTabIndexChange(matched.tabIndex);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const matchResult = matched.match(pathname);
|
|
409
|
-
const params = matchResult ? matchResult.params : undefined;
|
|
410
|
-
|
|
411
|
-
// Prevent duplicate push when navigating to the same screen already on top of its stack
|
|
412
|
-
if (action === 'push') {
|
|
413
|
-
const top = this.getTopForTarget(matched.stackId);
|
|
414
|
-
if (top && top.routeId === matched.routeId) {
|
|
415
|
-
const prev = top.params ? JSON.stringify(top.params) : '';
|
|
416
|
-
const next = params ? JSON.stringify(params) : '';
|
|
417
|
-
if (prev === next) {
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// If there's a controller, execute it first
|
|
424
|
-
if (matched.controller) {
|
|
425
|
-
const controllerInput = { params, query };
|
|
426
|
-
const present = (passProps?: Record<string, unknown>) => {
|
|
427
|
-
const newItem = this.createHistoryItem(
|
|
428
|
-
matched,
|
|
429
|
-
params,
|
|
430
|
-
query,
|
|
431
|
-
pathname,
|
|
432
|
-
passProps
|
|
433
|
-
);
|
|
434
|
-
this.applyHistoryChange(action, newItem);
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
matched.controller(controllerInput, present);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const newItem = this.createHistoryItem(matched, params, query, pathname);
|
|
442
|
-
this.applyHistoryChange(action, newItem);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
private createHistoryItem(
|
|
446
|
-
matched: CompiledRoute,
|
|
447
|
-
params: Record<string, any> | undefined,
|
|
448
|
-
query: Record<string, unknown>,
|
|
449
|
-
pathname: string,
|
|
450
|
-
passProps?: any
|
|
451
|
-
): HistoryItem {
|
|
452
|
-
return {
|
|
453
|
-
key: this.generateKey(),
|
|
454
|
-
scope: matched.scope,
|
|
455
|
-
routeId: matched.routeId,
|
|
456
|
-
component: matched.component,
|
|
457
|
-
options: this.mergeOptions(matched.options, matched.stackId),
|
|
458
|
-
params,
|
|
459
|
-
query: query as any,
|
|
460
|
-
passProps,
|
|
461
|
-
tabIndex: matched.tabIndex,
|
|
462
|
-
stackId: matched.stackId,
|
|
463
|
-
pattern: matched.path,
|
|
464
|
-
path: pathname,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Internal helpers
|
|
469
|
-
private buildRegistry(): void {
|
|
470
|
-
this.registry.length = 0;
|
|
471
|
-
|
|
472
|
-
const addFromStack = (
|
|
473
|
-
stack: NavigationStack,
|
|
474
|
-
scope: Scope,
|
|
475
|
-
extras: { tabIndex?: number }
|
|
476
|
-
) => {
|
|
477
|
-
const stackId = stack.getId();
|
|
478
|
-
this.stackById.set(stackId, stack);
|
|
479
|
-
for (const r of stack.getRoutes()) {
|
|
480
|
-
this.registry.push({
|
|
481
|
-
routeId: r.routeId,
|
|
482
|
-
scope,
|
|
483
|
-
path: r.path,
|
|
484
|
-
match: r.match,
|
|
485
|
-
component: r.component,
|
|
486
|
-
controller: r.controller,
|
|
487
|
-
options: r.options,
|
|
488
|
-
tabIndex: extras.tabIndex,
|
|
489
|
-
stackId,
|
|
490
|
-
});
|
|
491
|
-
this.routeById.set(r.routeId, {
|
|
492
|
-
path: r.path,
|
|
493
|
-
stackId,
|
|
494
|
-
tabIndex: extras.tabIndex,
|
|
495
|
-
scope,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
// init empty slice
|
|
499
|
-
if (!this.stackSlices.has(stackId))
|
|
500
|
-
this.stackSlices.set(stackId, EMPTY_ARRAY);
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
if (isNavigationStackLike(this.root)) {
|
|
504
|
-
addFromStack(this.root, 'root', {});
|
|
505
|
-
} else if (this.tabBar) {
|
|
506
|
-
const state = this.tabBar.getState();
|
|
507
|
-
state.tabs.forEach((tab, idx) => {
|
|
508
|
-
const stack = this.tabBar!.stacks[tab.tabKey];
|
|
509
|
-
if (stack) {
|
|
510
|
-
addFromStack(stack, 'tab', { tabIndex: idx });
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (this.global) {
|
|
516
|
-
addFromStack(this.global, 'global', {});
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
private seedInitialHistory(): void {
|
|
521
|
-
if (this.state.history.length > 0) return;
|
|
522
|
-
|
|
523
|
-
if (this.tabBar) {
|
|
524
|
-
const state = this.tabBar.getState();
|
|
525
|
-
const activeIdx = state.index ?? 0;
|
|
526
|
-
const route = state.tabs[activeIdx];
|
|
527
|
-
if (!route) return;
|
|
528
|
-
const stack = this.tabBar.stacks[route.tabKey];
|
|
529
|
-
if (stack) {
|
|
530
|
-
const first = stack.getFirstRoute();
|
|
531
|
-
if (first) {
|
|
532
|
-
const newItem: HistoryItem = {
|
|
533
|
-
key: this.generateKey(),
|
|
534
|
-
scope: 'tab',
|
|
535
|
-
routeId: first.routeId,
|
|
536
|
-
component: first.component,
|
|
537
|
-
options: this.mergeOptions(first.options, stack.getId()),
|
|
538
|
-
params: {},
|
|
539
|
-
tabIndex: activeIdx,
|
|
540
|
-
stackId: stack.getId(),
|
|
541
|
-
};
|
|
542
|
-
this.applyHistoryChange('push', newItem);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (isNavigationStackLike(this.root)) {
|
|
549
|
-
const first = this.root.getFirstRoute();
|
|
550
|
-
if (first) {
|
|
551
|
-
const newItem: HistoryItem = {
|
|
552
|
-
key: this.generateKey(),
|
|
553
|
-
scope: 'root',
|
|
554
|
-
routeId: first.routeId,
|
|
555
|
-
component: first.component,
|
|
556
|
-
options: this.mergeOptions(first.options, this.root.getId()),
|
|
557
|
-
params: {},
|
|
558
|
-
stackId: this.root.getId(),
|
|
559
|
-
};
|
|
560
|
-
this.applyHistoryChange('push', newItem);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
private matchRoute(path: string): CompiledRoute | undefined {
|
|
566
|
-
for (const r of this.registry) {
|
|
567
|
-
if (r.match(path)) return r;
|
|
568
|
-
}
|
|
569
|
-
return undefined;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
private generateKey(): string {
|
|
573
|
-
return `route-${nanoid()}`;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
private parsePath(path: string): {
|
|
577
|
-
pathname: string;
|
|
578
|
-
query: Record<string, unknown>;
|
|
579
|
-
} {
|
|
580
|
-
const parsed = qs.parseUrl(path);
|
|
581
|
-
return { pathname: parsed.url, query: parsed.query };
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
private applyHistoryChange(
|
|
585
|
-
action: 'push' | 'replace' | 'pop',
|
|
586
|
-
item: HistoryItem
|
|
587
|
-
): void {
|
|
588
|
-
const sid = item.stackId!;
|
|
589
|
-
if (action === 'push') {
|
|
590
|
-
this.setState({ history: [...this.state.history, item] });
|
|
591
|
-
const prevSlice = this.stackSlices.get(sid) ?? EMPTY_ARRAY;
|
|
592
|
-
const nextSlice = [...prevSlice, item];
|
|
593
|
-
this.stackSlices.set(sid, nextSlice);
|
|
594
|
-
this.emit(this.stackListeners.get(sid));
|
|
595
|
-
this.recomputeVisibleRoute();
|
|
596
|
-
this.emit(this.listeners);
|
|
597
|
-
} else if (action === 'replace') {
|
|
598
|
-
const prevTop = this.state.history[this.state.history.length - 1];
|
|
599
|
-
const prevSid = prevTop?.stackId;
|
|
600
|
-
this.setState({ history: [...this.state.history.slice(0, -1), item] });
|
|
601
|
-
if (prevSid && prevSid !== sid) {
|
|
602
|
-
const prevSlice = this.stackSlices.get(prevSid) ?? EMPTY_ARRAY;
|
|
603
|
-
const trimmed = prevSlice.slice(0, -1);
|
|
604
|
-
this.stackSlices.set(prevSid, trimmed);
|
|
605
|
-
this.emit(this.stackListeners.get(prevSid));
|
|
606
|
-
const newSlice = this.stackSlices.get(sid) ?? EMPTY_ARRAY;
|
|
607
|
-
const appended = [...newSlice, item];
|
|
608
|
-
this.stackSlices.set(sid, appended);
|
|
609
|
-
this.emit(this.stackListeners.get(sid));
|
|
610
|
-
} else {
|
|
611
|
-
const prevSlice = this.stackSlices.get(sid) ?? EMPTY_ARRAY;
|
|
612
|
-
const nextSlice = prevSlice.length
|
|
613
|
-
? [...prevSlice.slice(0, -1), item]
|
|
614
|
-
: [item];
|
|
615
|
-
this.stackSlices.set(sid, nextSlice);
|
|
616
|
-
this.emit(this.stackListeners.get(sid));
|
|
617
|
-
}
|
|
618
|
-
this.recomputeVisibleRoute();
|
|
619
|
-
this.emit(this.listeners);
|
|
620
|
-
} else if (action === 'pop') {
|
|
621
|
-
// Remove specific item by key from global history
|
|
622
|
-
const nextHist = this.state.history.filter((h) => h.key !== item.key);
|
|
623
|
-
this.setState({ history: nextHist });
|
|
624
|
-
|
|
625
|
-
// Update slice only if the last item matches the popped one
|
|
626
|
-
const prevSlice = this.stackSlices.get(sid) ?? EMPTY_ARRAY;
|
|
627
|
-
const last = prevSlice.length
|
|
628
|
-
? prevSlice[prevSlice.length - 1]
|
|
629
|
-
: undefined;
|
|
630
|
-
if (last && last.key === item.key) {
|
|
631
|
-
const nextSlice = prevSlice.slice(0, -1);
|
|
632
|
-
this.stackSlices.set(sid, nextSlice);
|
|
633
|
-
this.emit(this.stackListeners.get(sid));
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
this.recomputeVisibleRoute();
|
|
637
|
-
this.emit(this.listeners);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
private setState(next: Partial<RouterState>): void {
|
|
642
|
-
const prev = this.state;
|
|
643
|
-
const nextState: RouterState = {
|
|
644
|
-
history: next.history ?? prev.history,
|
|
645
|
-
activeTabIndex: next.activeTabIndex ?? prev.activeTabIndex,
|
|
646
|
-
};
|
|
647
|
-
this.state = nextState;
|
|
648
|
-
// Callers will emit updates explicitly.
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
private emit(set?: Set<Listener> | null): void {
|
|
652
|
-
if (!set) return;
|
|
653
|
-
set.forEach((l) => l());
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
private getTopForTarget(stackId?: string): HistoryItem | undefined {
|
|
657
|
-
if (!stackId) return undefined;
|
|
658
|
-
const slice = this.stackSlices.get(stackId) ?? EMPTY_ARRAY;
|
|
659
|
-
return slice.length ? slice[slice.length - 1] : undefined;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
private mergeOptions(
|
|
663
|
-
routeOptions?: ScreenOptions,
|
|
664
|
-
stackId?: string
|
|
665
|
-
): ScreenOptions | undefined {
|
|
666
|
-
const stackDefaults = stackId
|
|
667
|
-
? (this.findStackById(stackId)?.getDefaultOptions() as any)
|
|
668
|
-
: undefined;
|
|
669
|
-
const routerDefaults = this.routerScreenOptions as any;
|
|
670
|
-
if (!routerDefaults && !stackDefaults && !routeOptions) return undefined;
|
|
671
|
-
|
|
672
|
-
const merged: any = {
|
|
673
|
-
...(stackDefaults ?? {}),
|
|
674
|
-
...(routeOptions ?? {}),
|
|
675
|
-
...(routerDefaults ?? {}),
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
return merged;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
private findStackById(stackId: string): NavigationStack | undefined {
|
|
682
|
-
return this.stackById.get(stackId);
|
|
683
|
-
}
|
|
684
|
-
}
|