@sigmela/router 0.0.14 → 0.0.15
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 +10 -16
- package/lib/module/Router.js +299 -52
- package/lib/module/ScreenStack/ScreenStack.native.js +3 -0
- package/lib/module/ScreenStack/ScreenStack.web.js +139 -0
- package/lib/module/ScreenStack/index.js +3 -0
- package/lib/module/{ScreenStackItem.js → ScreenStackItem/ScreenStackItem.js} +2 -2
- package/lib/module/ScreenStackItem/ScreenStackItem.types.js +3 -0
- package/lib/module/ScreenStackItem/ScreenStackItem.web.js +38 -0
- package/lib/module/ScreenStackItem/index.js +3 -0
- package/lib/module/StackRenderer.js +5 -5
- package/lib/module/TabBar/RenderTabBar.native.js +224 -0
- package/lib/module/TabBar/RenderTabBar.web.js +130 -0
- package/lib/module/TabBar/TabIcon.web.js +42 -0
- package/lib/module/TabBar/index.js +3 -0
- package/lib/module/styles.css +139 -0
- package/lib/module/web/TransitionStack.js +227 -0
- package/lib/typescript/src/Navigation.d.ts +2 -1
- package/lib/typescript/src/Router.d.ts +15 -1
- package/lib/typescript/src/ScreenStack/ScreenStack.native.d.ts +2 -0
- package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +11 -0
- package/lib/typescript/src/ScreenStack/index.d.ts +2 -0
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.d.ts +3 -0
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +10 -0
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +3 -0
- package/lib/typescript/src/ScreenStackItem/index.d.ts +3 -0
- package/lib/typescript/src/TabBar/RenderTabBar.native.d.ts +8 -0
- package/lib/typescript/src/TabBar/{RenderTabBar.d.ts → RenderTabBar.web.d.ts} +1 -1
- package/lib/typescript/src/TabBar/TabBar.d.ts +11 -3
- package/lib/typescript/src/TabBar/TabIcon.web.d.ts +7 -0
- package/lib/typescript/src/TabBar/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/types.d.ts +24 -165
- package/lib/typescript/src/web/TransitionStack.d.ts +21 -0
- package/package.json +3 -2
- package/lib/module/TabBar/RenderTabBar.js +0 -123
- package/lib/typescript/src/ScreenStackItem.d.ts +0 -10
package/lib/module/Navigation.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
// import { ScreenStackItem as RNNScreenStackItem } from 'react-native-screens';
|
|
4
|
+
|
|
5
|
+
import { RenderTabBar } from './TabBar/RenderTabBar';
|
|
6
|
+
import { ScreenStackItem } from "./ScreenStackItem/index.js";
|
|
5
7
|
import { RouterContext } from "./RouterContext.js";
|
|
8
|
+
import { ScreenStack } from "./ScreenStack/index.js";
|
|
6
9
|
import { StyleSheet } from 'react-native';
|
|
7
|
-
import { ScreenStackItem as RNNScreenStackItem, ScreenStack } from 'react-native-screens';
|
|
8
10
|
import { useSyncExternalStore, memo, useCallback, useEffect, useState } from 'react';
|
|
9
11
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
12
|
const EMPTY_HISTORY = [];
|
|
@@ -41,27 +43,19 @@ export const Navigation = /*#__PURE__*/memo(({
|
|
|
41
43
|
value: router,
|
|
42
44
|
children: /*#__PURE__*/_jsxs(ScreenStack, {
|
|
43
45
|
style: styles.flex,
|
|
44
|
-
children: [hasTabBar && /*#__PURE__*/_jsx(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
hidden: true
|
|
48
|
-
},
|
|
49
|
-
style: styles.flex,
|
|
50
|
-
stackAnimation: rootTransition,
|
|
51
|
-
children: /*#__PURE__*/_jsx(RenderTabBar, {
|
|
52
|
-
tabBar: router.tabBar,
|
|
53
|
-
appearance: appearance
|
|
54
|
-
})
|
|
46
|
+
children: [hasTabBar && /*#__PURE__*/_jsx(RenderTabBar, {
|
|
47
|
+
tabBar: router.tabBar,
|
|
48
|
+
appearance: appearance
|
|
55
49
|
}), rootItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
56
50
|
stackId: rootId,
|
|
57
51
|
item: item,
|
|
58
52
|
stackAnimation: rootTransition,
|
|
59
53
|
appearance: appearance
|
|
60
|
-
}, item.key)), globalItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
54
|
+
}, `root-${item.key}`)), globalItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
61
55
|
appearance: appearance,
|
|
62
56
|
stackId: globalId,
|
|
63
57
|
item: item
|
|
64
|
-
}, item.key))]
|
|
58
|
+
}, `global-${item.key}`))]
|
|
65
59
|
})
|
|
66
60
|
});
|
|
67
61
|
});
|
package/lib/module/Router.js
CHANGED
|
@@ -48,59 +48,45 @@ export class Router {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
this.buildRegistry();
|
|
51
|
-
this.
|
|
51
|
+
if (this.isWebEnv()) {
|
|
52
|
+
this.setupBrowserHistory();
|
|
53
|
+
this.parse(this.getCurrentUrl());
|
|
54
|
+
} else {
|
|
55
|
+
this.seedInitialHistory();
|
|
56
|
+
}
|
|
52
57
|
this.recomputeVisibleRoute();
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
// Public API
|
|
56
61
|
navigate = path => {
|
|
62
|
+
if (this.isWebEnv()) {
|
|
63
|
+
this.pushUrl(path);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
57
66
|
this.performNavigation(path, 'push');
|
|
58
67
|
};
|
|
59
|
-
replace = path => {
|
|
60
|
-
this.
|
|
68
|
+
replace = (path, dedupe) => {
|
|
69
|
+
if (this.isWebEnv()) {
|
|
70
|
+
this.pendingReplaceDedupe = !!dedupe;
|
|
71
|
+
this.replaceUrl(path);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.performNavigation(path, 'replace', {
|
|
75
|
+
dedupe: !!dedupe
|
|
76
|
+
});
|
|
61
77
|
};
|
|
62
78
|
goBack = () => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.
|
|
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
|
-
}
|
|
79
|
+
if (this.isWebEnv()) {
|
|
80
|
+
// Web: only go back within the current active stack.
|
|
81
|
+
const didPop = this.tryPopActiveStack();
|
|
82
|
+
if (didPop) {
|
|
83
|
+
// Keep URL in sync with the new visible route without adding history entries
|
|
84
|
+
const path = this.getVisibleRoute()?.path;
|
|
85
|
+
if (path) this.replaceUrl(path);
|
|
89
86
|
}
|
|
90
87
|
return;
|
|
91
88
|
}
|
|
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
|
-
}
|
|
89
|
+
this.popOnce();
|
|
104
90
|
};
|
|
105
91
|
onTabIndexChange = index => {
|
|
106
92
|
if (this.tabBar) {
|
|
@@ -242,7 +228,8 @@ export class Router {
|
|
|
242
228
|
...meta,
|
|
243
229
|
routeId: gtop.routeId,
|
|
244
230
|
params: gtop.params,
|
|
245
|
-
query: gtop.query
|
|
231
|
+
query: gtop.query,
|
|
232
|
+
path: gtop.path
|
|
246
233
|
} : {
|
|
247
234
|
routeId: gtop.routeId,
|
|
248
235
|
stackId: gtop.stackId,
|
|
@@ -271,7 +258,8 @@ export class Router {
|
|
|
271
258
|
...meta,
|
|
272
259
|
routeId: top.routeId,
|
|
273
260
|
params: top.params,
|
|
274
|
-
query: top.query
|
|
261
|
+
query: top.query,
|
|
262
|
+
path: top.path
|
|
275
263
|
} : {
|
|
276
264
|
routeId: top.routeId,
|
|
277
265
|
stackId: sid,
|
|
@@ -304,7 +292,8 @@ export class Router {
|
|
|
304
292
|
...meta,
|
|
305
293
|
routeId: top.routeId,
|
|
306
294
|
params: top.params,
|
|
307
|
-
query: top.query
|
|
295
|
+
query: top.query,
|
|
296
|
+
path: top.path
|
|
308
297
|
} : {
|
|
309
298
|
routeId: top.routeId,
|
|
310
299
|
stackId: sid,
|
|
@@ -319,7 +308,7 @@ export class Router {
|
|
|
319
308
|
}
|
|
320
309
|
|
|
321
310
|
// Internal navigation logic
|
|
322
|
-
performNavigation(path, action) {
|
|
311
|
+
performNavigation(path, action, opts) {
|
|
323
312
|
const {
|
|
324
313
|
pathname,
|
|
325
314
|
query
|
|
@@ -346,6 +335,19 @@ export class Router {
|
|
|
346
335
|
}
|
|
347
336
|
}
|
|
348
337
|
|
|
338
|
+
// Optional dedupe for replace: no-op when nothing changes at the top
|
|
339
|
+
if (action === 'replace' && opts?.dedupe) {
|
|
340
|
+
const top = this.getTopForTarget(matched.stackId);
|
|
341
|
+
if (top && top.routeId === matched.routeId) {
|
|
342
|
+
const sameParams = JSON.stringify(top.params ?? {}) === JSON.stringify(params ?? {});
|
|
343
|
+
const sameQuery = JSON.stringify(top.query ?? {}) === JSON.stringify(query ?? {});
|
|
344
|
+
const samePath = (top.path ?? '') === pathname;
|
|
345
|
+
if (sameParams && sameQuery && samePath) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
349
351
|
// If there's a controller, execute it first
|
|
350
352
|
if (matched.controller) {
|
|
351
353
|
const controllerInput = {
|
|
@@ -501,15 +503,27 @@ export class Router {
|
|
|
501
503
|
history: [...this.state.history.slice(0, -1), item]
|
|
502
504
|
});
|
|
503
505
|
if (prevSid && prevSid !== sid) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
this.stackSlices.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
506
|
+
// Cross-stack replace: do not modify the source stack.
|
|
507
|
+
// Update target stack without growing it unnecessarily.
|
|
508
|
+
const targetSlice = this.stackSlices.get(sid) ?? EMPTY_ARRAY;
|
|
509
|
+
if (targetSlice.length === 0) {
|
|
510
|
+
this.stackSlices.set(sid, [item]);
|
|
511
|
+
} else {
|
|
512
|
+
const targetTop = targetSlice[targetSlice.length - 1];
|
|
513
|
+
const sameRoute = targetTop?.routeId === item.routeId;
|
|
514
|
+
const sameParams = JSON.stringify(targetTop?.params ?? {}) === JSON.stringify(item.params ?? {});
|
|
515
|
+
if (sameRoute && sameParams) {
|
|
516
|
+
// No change needed for target stack
|
|
517
|
+
this.stackSlices.set(sid, targetSlice);
|
|
518
|
+
} else {
|
|
519
|
+
// Replace top of target stack
|
|
520
|
+
const replaced = [...targetSlice.slice(0, -1), item];
|
|
521
|
+
this.stackSlices.set(sid, replaced);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
511
524
|
this.emit(this.stackListeners.get(sid));
|
|
512
525
|
} else {
|
|
526
|
+
// Same-stack replace: replace top element
|
|
513
527
|
const prevSlice = this.stackSlices.get(sid) ?? EMPTY_ARRAY;
|
|
514
528
|
const nextSlice = prevSlice.length ? [...prevSlice.slice(0, -1), item] : [item];
|
|
515
529
|
this.stackSlices.set(sid, nextSlice);
|
|
@@ -568,4 +582,237 @@ export class Router {
|
|
|
568
582
|
findStackById(stackId) {
|
|
569
583
|
return this.stackById.get(stackId);
|
|
570
584
|
}
|
|
585
|
+
|
|
586
|
+
// ==== Web integration (History API) ====
|
|
587
|
+
isWebEnv() {
|
|
588
|
+
const g = globalThis;
|
|
589
|
+
return !!(g.addEventListener && g.history && g.location);
|
|
590
|
+
}
|
|
591
|
+
getCurrentUrl() {
|
|
592
|
+
const g = globalThis;
|
|
593
|
+
return g.location ? `${g.location.pathname}${g.location.search}` : '/';
|
|
594
|
+
}
|
|
595
|
+
readIndex(state) {
|
|
596
|
+
if (state && typeof state === 'object' && '__srIndex' in state) {
|
|
597
|
+
const idx = state.__srIndex;
|
|
598
|
+
if (typeof idx === 'number') return idx;
|
|
599
|
+
}
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
602
|
+
getHistoryIndex() {
|
|
603
|
+
const g = globalThis;
|
|
604
|
+
return this.readIndex(g.history?.state);
|
|
605
|
+
}
|
|
606
|
+
ensureHistoryIndex() {
|
|
607
|
+
const g = globalThis;
|
|
608
|
+
const st = g.history?.state ?? {};
|
|
609
|
+
if (this.readIndex(st) === 0 && !(st && typeof st === 'object' && '__srIndex' in st)) {
|
|
610
|
+
const next = {
|
|
611
|
+
...st,
|
|
612
|
+
__srIndex: 0
|
|
613
|
+
};
|
|
614
|
+
try {
|
|
615
|
+
g.history?.replaceState(next, '', g.location?.href);
|
|
616
|
+
} catch {
|
|
617
|
+
// ignore
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
pushUrl(to) {
|
|
622
|
+
const g = globalThis;
|
|
623
|
+
const st = g.history?.state ?? {};
|
|
624
|
+
const prev = this.readIndex(st);
|
|
625
|
+
const next = {
|
|
626
|
+
...st,
|
|
627
|
+
__srIndex: prev + 1
|
|
628
|
+
};
|
|
629
|
+
g.history?.pushState(next, '', to);
|
|
630
|
+
if (g.Event && g.dispatchEvent) {
|
|
631
|
+
g.dispatchEvent(new g.Event('pushState'));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
replaceUrl(to) {
|
|
635
|
+
const g = globalThis;
|
|
636
|
+
const st = g.history?.state ?? {};
|
|
637
|
+
g.history?.replaceState(st, '', to);
|
|
638
|
+
if (g.Event && g.dispatchEvent) {
|
|
639
|
+
g.dispatchEvent(new g.Event('replaceState'));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
patchHistoryOnce() {
|
|
643
|
+
const g = globalThis;
|
|
644
|
+
const key = Symbol.for('sigmela_router_history_patch');
|
|
645
|
+
if (g[key]) return;
|
|
646
|
+
const hist = g.history;
|
|
647
|
+
if (hist && hist.pushState && hist.replaceState) {
|
|
648
|
+
const originalPush = hist.pushState.bind(hist);
|
|
649
|
+
const originalReplace = hist.replaceState.bind(hist);
|
|
650
|
+
hist.pushState = (data, unused, url) => {
|
|
651
|
+
const res = originalPush(data, unused, url);
|
|
652
|
+
if (g.Event && g.dispatchEvent) {
|
|
653
|
+
g.dispatchEvent(new g.Event('pushState'));
|
|
654
|
+
}
|
|
655
|
+
return res;
|
|
656
|
+
};
|
|
657
|
+
hist.replaceState = (data, unused, url) => {
|
|
658
|
+
const res = originalReplace(data, unused, url);
|
|
659
|
+
if (g.Event && g.dispatchEvent) {
|
|
660
|
+
g.dispatchEvent(new g.Event('replaceState'));
|
|
661
|
+
}
|
|
662
|
+
return res;
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
g[key] = true;
|
|
666
|
+
}
|
|
667
|
+
lastBrowserIndex = 0;
|
|
668
|
+
pendingReplaceDedupe = false;
|
|
669
|
+
setupBrowserHistory() {
|
|
670
|
+
const g = globalThis;
|
|
671
|
+
this.patchHistoryOnce();
|
|
672
|
+
this.ensureHistoryIndex();
|
|
673
|
+
this.lastBrowserIndex = this.getHistoryIndex();
|
|
674
|
+
const onHistory = ev => {
|
|
675
|
+
const url = this.getCurrentUrl();
|
|
676
|
+
if (ev.type === 'pushState') {
|
|
677
|
+
this.lastBrowserIndex = this.getHistoryIndex();
|
|
678
|
+
this.performNavigation(url, 'push');
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (ev.type === 'replaceState') {
|
|
682
|
+
const dedupe = this.pendingReplaceDedupe === true;
|
|
683
|
+
this.performNavigation(url, 'replace', {
|
|
684
|
+
dedupe
|
|
685
|
+
});
|
|
686
|
+
this.lastBrowserIndex = this.getHistoryIndex();
|
|
687
|
+
this.pendingReplaceDedupe = false;
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (ev.type === 'popstate') {
|
|
691
|
+
const idx = this.getHistoryIndex();
|
|
692
|
+
const delta = idx - this.lastBrowserIndex;
|
|
693
|
+
if (delta < 0) {
|
|
694
|
+
let steps = -delta;
|
|
695
|
+
while (steps-- > 0) this.popOnce();
|
|
696
|
+
const target = this.parsePath(url).pathname;
|
|
697
|
+
const current = this.getVisibleRoute()?.path;
|
|
698
|
+
if (current !== target) this.performNavigation(url, 'replace');
|
|
699
|
+
} else if (delta > 0) {
|
|
700
|
+
this.performNavigation(url, 'push');
|
|
701
|
+
} else {
|
|
702
|
+
this.performNavigation(url, 'replace');
|
|
703
|
+
}
|
|
704
|
+
this.lastBrowserIndex = idx;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
g.addEventListener?.('pushState', onHistory);
|
|
708
|
+
g.addEventListener?.('replaceState', onHistory);
|
|
709
|
+
g.addEventListener?.('popstate', onHistory);
|
|
710
|
+
}
|
|
711
|
+
popOnce() {
|
|
712
|
+
if (this.tryPopActiveStack()) return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Attempts to pop exactly one screen within the active stack only.
|
|
716
|
+
// Returns true if a pop occurred; false otherwise.
|
|
717
|
+
tryPopActiveStack() {
|
|
718
|
+
if (this.global) {
|
|
719
|
+
const gid = this.global.getId();
|
|
720
|
+
const gslice = this.getStackHistory(gid);
|
|
721
|
+
const gtop = gslice.length ? gslice[gslice.length - 1] : undefined;
|
|
722
|
+
if (gtop) {
|
|
723
|
+
this.applyHistoryChange('pop', gtop);
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (this.tabBar) {
|
|
728
|
+
const idx = this.getActiveTabIndex();
|
|
729
|
+
const state = this.tabBar.getState();
|
|
730
|
+
const route = state.tabs[idx];
|
|
731
|
+
if (!route) return false;
|
|
732
|
+
const stack = this.tabBar.stacks[route.tabKey];
|
|
733
|
+
if (!stack) return false;
|
|
734
|
+
const sid = stack.getId();
|
|
735
|
+
const slice = this.getStackHistory(sid);
|
|
736
|
+
if (slice.length > 1) {
|
|
737
|
+
const top = slice[slice.length - 1];
|
|
738
|
+
if (top) {
|
|
739
|
+
this.applyHistoryChange('pop', top);
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
if (isNavigationStackLike(this.root)) {
|
|
746
|
+
const sid = this.root.getId();
|
|
747
|
+
const slice = this.getStackHistory(sid);
|
|
748
|
+
if (slice.length > 1) {
|
|
749
|
+
const top = slice[slice.length - 1];
|
|
750
|
+
if (top) {
|
|
751
|
+
this.applyHistoryChange('pop', top);
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Expand deep URL into a stack chain on initial load
|
|
760
|
+
parse(url) {
|
|
761
|
+
const {
|
|
762
|
+
pathname,
|
|
763
|
+
query
|
|
764
|
+
} = this.parsePath(url);
|
|
765
|
+
const deepest = this.matchRoute(pathname);
|
|
766
|
+
if (!deepest) {
|
|
767
|
+
this.seedInitialHistory();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (deepest.scope === 'tab' && this.tabBar && deepest.tabIndex !== undefined) {
|
|
771
|
+
this.onTabIndexChange(deepest.tabIndex);
|
|
772
|
+
}
|
|
773
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
774
|
+
const prefixes = new Array(parts.length + 1);
|
|
775
|
+
let acc = '';
|
|
776
|
+
prefixes[0] = '/';
|
|
777
|
+
for (let i = 0; i < parts.length; i++) {
|
|
778
|
+
acc += `/${parts[i]}`;
|
|
779
|
+
prefixes[i + 1] = acc;
|
|
780
|
+
}
|
|
781
|
+
const candidates = [];
|
|
782
|
+
for (const route of this.registry) {
|
|
783
|
+
if (route.stackId !== deepest.stackId || route.scope !== deepest.scope) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const segmentsCount = route.path.split('/').filter(Boolean).length;
|
|
787
|
+
const candidateUrl = prefixes[segmentsCount];
|
|
788
|
+
if (!candidateUrl) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const matchResult = route.match(candidateUrl);
|
|
792
|
+
if (!matchResult) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
candidates.push({
|
|
796
|
+
params: matchResult.params,
|
|
797
|
+
segmentCount: segmentsCount,
|
|
798
|
+
candidateUrl,
|
|
799
|
+
route
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
if (!candidates.length) {
|
|
803
|
+
this.seedInitialHistory();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
candidates.sort((a, b) => a.segmentCount - b.segmentCount);
|
|
807
|
+
const items = candidates.map((c, i) => this.createHistoryItem(c.route, c.params, i === candidates.length - 1 ? query : {}, c.candidateUrl));
|
|
808
|
+
const first = items[0];
|
|
809
|
+
const sid = first?.stackId ?? deepest.stackId;
|
|
810
|
+
this.setState({
|
|
811
|
+
history: items
|
|
812
|
+
});
|
|
813
|
+
if (sid) {
|
|
814
|
+
this.stackSlices.set(sid, items);
|
|
815
|
+
this.emit(this.stackListeners.get(sid));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
571
818
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Fragment, memo, useRef } from 'react';
|
|
4
|
+
import TransitionStack from "../web/TransitionStack.js";
|
|
5
|
+
import { Children, cloneElement, useLayoutEffect, useMemo, useState } from 'react';
|
|
6
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
7
|
+
export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
8
|
+
const {
|
|
9
|
+
children,
|
|
10
|
+
transitionTime = 250,
|
|
11
|
+
type,
|
|
12
|
+
animated = true
|
|
13
|
+
} = props;
|
|
14
|
+
const [records, setRecords] = useState([]);
|
|
15
|
+
const containerRef = useRef(null);
|
|
16
|
+
const transitionRef = useRef(null);
|
|
17
|
+
const prevSelectedRef = useRef(-1);
|
|
18
|
+
const lastExitingIndexRef = useRef(null);
|
|
19
|
+
const prevSignatureRef = useRef(null);
|
|
20
|
+
const childArray = useMemo(() => Children.toArray(children).filter(Boolean), [children]);
|
|
21
|
+
const nextKeys = useMemo(() => childArray.map(c => c.key), [childArray]);
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
setRecords(prev => {
|
|
24
|
+
const prevKeys = prev.map(r => r.key);
|
|
25
|
+
if (nextKeys.length === prevKeys.length + 1 && prevKeys.every((k, i) => k === nextKeys[i])) {
|
|
26
|
+
const child = childArray[childArray.length - 1];
|
|
27
|
+
if (!child) return prev;
|
|
28
|
+
const key = child.key;
|
|
29
|
+
const element = /*#__PURE__*/cloneElement(child, {
|
|
30
|
+
phase: 'active'
|
|
31
|
+
});
|
|
32
|
+
return [...prev, {
|
|
33
|
+
key,
|
|
34
|
+
phase: 'active',
|
|
35
|
+
element
|
|
36
|
+
}];
|
|
37
|
+
}
|
|
38
|
+
if (prevKeys.length === nextKeys.length + 1 && nextKeys.every((k, i) => k === prevKeys[i])) {
|
|
39
|
+
const last = prev[prev.length - 1];
|
|
40
|
+
if (!last) return prev;
|
|
41
|
+
if (last.phase === 'exiting') return prev;
|
|
42
|
+
return [...prev.slice(0, -1), {
|
|
43
|
+
key: last.key,
|
|
44
|
+
phase: 'exiting',
|
|
45
|
+
element: /*#__PURE__*/cloneElement(last.element, {
|
|
46
|
+
phase: 'exiting'
|
|
47
|
+
})
|
|
48
|
+
}];
|
|
49
|
+
}
|
|
50
|
+
const nextByKey = new Map();
|
|
51
|
+
for (const ch of childArray) nextByKey.set(ch.key, ch);
|
|
52
|
+
const prevByKey = new Map();
|
|
53
|
+
for (const r of prev) prevByKey.set(r.key, r);
|
|
54
|
+
const result = [];
|
|
55
|
+
for (const ch of childArray) {
|
|
56
|
+
const key = ch.key;
|
|
57
|
+
const existed = prevByKey.get(key);
|
|
58
|
+
const nextEl = /*#__PURE__*/cloneElement(ch, {
|
|
59
|
+
phase: 'active'
|
|
60
|
+
});
|
|
61
|
+
if (existed) {
|
|
62
|
+
const sameActive = existed.phase === 'active';
|
|
63
|
+
result.push(sameActive ? {
|
|
64
|
+
key,
|
|
65
|
+
phase: 'active',
|
|
66
|
+
element: nextEl
|
|
67
|
+
} : {
|
|
68
|
+
key,
|
|
69
|
+
phase: 'active',
|
|
70
|
+
element: nextEl
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
result.push({
|
|
74
|
+
key,
|
|
75
|
+
phase: 'active',
|
|
76
|
+
element: nextEl
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const r of prev) {
|
|
81
|
+
if (!nextByKey.has(r.key)) {
|
|
82
|
+
const exitingEl = /*#__PURE__*/cloneElement(r.element, {
|
|
83
|
+
phase: 'exiting'
|
|
84
|
+
});
|
|
85
|
+
result.push({
|
|
86
|
+
key: r.key,
|
|
87
|
+
phase: 'exiting',
|
|
88
|
+
element: exitingEl
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
});
|
|
94
|
+
}, [childArray, nextKeys]);
|
|
95
|
+
useLayoutEffect(() => {
|
|
96
|
+
if (!containerRef.current || transitionRef.current) return;
|
|
97
|
+
transitionRef.current = TransitionStack({
|
|
98
|
+
content: containerRef.current,
|
|
99
|
+
type: type,
|
|
100
|
+
transitionTime,
|
|
101
|
+
withAnimationListener: animated,
|
|
102
|
+
onTransitionEnd: () => {
|
|
103
|
+
setRecords(prev => {
|
|
104
|
+
const idx = lastExitingIndexRef.current ?? -1;
|
|
105
|
+
if (idx < 0 || idx >= prev.length) return prev;
|
|
106
|
+
const candidate = prev[idx];
|
|
107
|
+
if (candidate?.phase !== 'exiting') return prev;
|
|
108
|
+
return [...prev.slice(0, idx), ...prev.slice(idx + 1)];
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}, [type, transitionTime, animated]);
|
|
113
|
+
useLayoutEffect(() => {
|
|
114
|
+
if (!transitionRef.current) return;
|
|
115
|
+
const targetIndex = (() => {
|
|
116
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
117
|
+
const rec = records[i];
|
|
118
|
+
if (rec && rec.phase === 'active') return i;
|
|
119
|
+
}
|
|
120
|
+
return -1;
|
|
121
|
+
})();
|
|
122
|
+
if (targetIndex === -1) return;
|
|
123
|
+
const outgoingIndex = records.findIndex(it => it.phase === 'exiting');
|
|
124
|
+
lastExitingIndexRef.current = outgoingIndex !== -1 ? outgoingIndex : null;
|
|
125
|
+
const signature = records.map(r => `${String(r.key)}:${r.phase}`).join('|');
|
|
126
|
+
const stackChanged = prevSignatureRef.current !== null && prevSignatureRef.current !== signature;
|
|
127
|
+
const animate = animated && stackChanged;
|
|
128
|
+
transitionRef.current(targetIndex, animate);
|
|
129
|
+
prevSignatureRef.current = signature;
|
|
130
|
+
prevSelectedRef.current = targetIndex;
|
|
131
|
+
}, [records, animated]);
|
|
132
|
+
return /*#__PURE__*/_jsx("div", {
|
|
133
|
+
ref: containerRef,
|
|
134
|
+
className: `screen-stack${type ? ` ${type}` : ''}`,
|
|
135
|
+
children: records.map(r => /*#__PURE__*/_jsx(Fragment, {
|
|
136
|
+
children: r.element
|
|
137
|
+
}, r.key))
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { ScreenStackItem as RNSScreenStackItem } from 'react-native-screens';
|
|
4
|
-
import { RouteLocalContext, useRouter } from "
|
|
4
|
+
import { RouteLocalContext, useRouter } from "../RouterContext.js";
|
|
5
5
|
import { StyleSheet } from 'react-native';
|
|
6
6
|
import { memo } from 'react';
|
|
7
7
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
@@ -41,7 +41,7 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
41
41
|
screenId: item.key,
|
|
42
42
|
onDismissed: onDismissed,
|
|
43
43
|
style: StyleSheet.absoluteFill,
|
|
44
|
-
contentStyle: appearance?.
|
|
44
|
+
contentStyle: appearance?.screen,
|
|
45
45
|
headerConfig: headerConfig,
|
|
46
46
|
...screenProps,
|
|
47
47
|
stackAnimation: stackAnimation ?? item.options?.stackAnimation,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { RouteLocalContext } from "../RouterContext.js";
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
import { StyleSheet, View } from 'react-native';
|
|
6
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
7
|
+
export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
8
|
+
phase = 'active',
|
|
9
|
+
item,
|
|
10
|
+
appearance
|
|
11
|
+
}) => {
|
|
12
|
+
const value = {
|
|
13
|
+
presentation: item.options?.stackPresentation ?? 'push',
|
|
14
|
+
params: item.params,
|
|
15
|
+
query: item.query,
|
|
16
|
+
pattern: item.pattern,
|
|
17
|
+
path: item.path
|
|
18
|
+
};
|
|
19
|
+
return /*#__PURE__*/_jsx("div", {
|
|
20
|
+
"data-presentation": value.presentation,
|
|
21
|
+
className: "screen-stack-item",
|
|
22
|
+
"data-phase": phase,
|
|
23
|
+
children: /*#__PURE__*/_jsx(RouteLocalContext.Provider, {
|
|
24
|
+
value: value,
|
|
25
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
26
|
+
style: [styles.flex, appearance?.screen],
|
|
27
|
+
children: /*#__PURE__*/_jsx(item.component, {
|
|
28
|
+
...(item.passProps || {})
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
const styles = StyleSheet.create({
|
|
35
|
+
flex: {
|
|
36
|
+
flex: 1
|
|
37
|
+
}
|
|
38
|
+
});
|