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