@openwebf/react-router 0.23.7 → 0.24.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/dist/index.d.ts +14 -3
- package/dist/index.esm.js +262 -73
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +262 -72
- package/dist/index.js.map +1 -1
- package/dist/routes/Route.d.ts +6 -1
- package/dist/routes/Route.d.ts.map +1 -1
- package/dist/routes/Routes.d.ts +5 -1
- package/dist/routes/Routes.d.ts.map +1 -1
- package/dist/routes/utils.d.ts +3 -1
- package/dist/routes/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/browser/index.d.ts +0 -88
- package/dist/browser/index.d.ts.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import React, { SyntheticEvent, EventHandler, ReactNode, FC } from 'react';
|
|
|
2
2
|
import { WebFElementWithMethods } from '@openwebf/react-core-ui';
|
|
3
3
|
|
|
4
4
|
type RoutePath = string;
|
|
5
|
+
type EnsureRouteMountedCallback = (pathname: string) => Promise<void> | void;
|
|
6
|
+
declare function __unstable_setEnsureRouteMountedCallback(callback: EnsureRouteMountedCallback | null): void;
|
|
5
7
|
/**
|
|
6
8
|
* Single entry in the hybrid router stack.
|
|
7
9
|
* Mirrors the data returned from webf.hybridHistory.buildContextStack.
|
|
@@ -27,7 +29,7 @@ declare const WebFRouter: {
|
|
|
27
29
|
/**
|
|
28
30
|
* Get the current route path
|
|
29
31
|
*/
|
|
30
|
-
readonly path:
|
|
32
|
+
readonly path: string;
|
|
31
33
|
/**
|
|
32
34
|
* Navigate to a specified route
|
|
33
35
|
* Applies route guards for permission checks before navigation
|
|
@@ -153,6 +155,11 @@ interface RouteProps {
|
|
|
153
155
|
* Must be a member of the RoutePath enum
|
|
154
156
|
*/
|
|
155
157
|
path: string;
|
|
158
|
+
/**
|
|
159
|
+
* The concrete path to mount for this route instance.
|
|
160
|
+
* Used internally to support dynamic routes like `/users/:id` mounting at `/users/123`.
|
|
161
|
+
*/
|
|
162
|
+
mountedPath?: string;
|
|
156
163
|
/**
|
|
157
164
|
* Whether to pre-render
|
|
158
165
|
* If true, the page will be rendered when the app starts, rather than waiting for route navigation
|
|
@@ -179,7 +186,7 @@ interface RouteProps {
|
|
|
179
186
|
*
|
|
180
187
|
* Responsible for managing page rendering, lifecycle and navigation bar
|
|
181
188
|
*/
|
|
182
|
-
declare function Route({ path, prerender, element, title, theme }: RouteProps): React.JSX.Element;
|
|
189
|
+
declare function Route({ path, mountedPath, prerender, element, title, theme }: RouteProps): React.JSX.Element;
|
|
183
190
|
|
|
184
191
|
/**
|
|
185
192
|
* Hook to get route context
|
|
@@ -201,6 +208,10 @@ declare function useRouteContext(): {
|
|
|
201
208
|
* Current route path, corresponds to RoutePath enum
|
|
202
209
|
*/
|
|
203
210
|
path: string | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* The concrete mounted path for this route instance (e.g. `/users/123`).
|
|
213
|
+
*/
|
|
214
|
+
mountedPath: string | undefined;
|
|
204
215
|
/**
|
|
205
216
|
* Page state
|
|
206
217
|
* State data passed during route navigation
|
|
@@ -432,5 +443,5 @@ interface WebFRouterLinkElement extends WebFElementWithMethods<{}> {
|
|
|
432
443
|
}
|
|
433
444
|
declare const WebFRouterLink: FC<WebFHybridRouterProps>;
|
|
434
445
|
|
|
435
|
-
export { Route, Routes, WebFRouter, WebFRouterLink, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
|
|
446
|
+
export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
|
|
436
447
|
export type { HybridRouteStackEntry, HybridRouterChangeEvent, HybridRouterChangeEventHandler, Location, NavigateFunction, NavigateOptions, NavigationMethods, RouteMatch, RouteObject, RouteParams, RouteProps, RoutesProps, WebFHybridRouterProps, WebFRouterLinkElement };
|
package/dist/index.esm.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { webf } from '@openwebf/webf-enterprise-typings';
|
|
2
1
|
import React, { useRef, useMemo, useState, createContext, useContext, useEffect, Children, isValidElement } from 'react';
|
|
3
2
|
import { createWebFComponent } from '@openwebf/react-core-ui';
|
|
4
3
|
|
|
@@ -34,6 +33,26 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
34
33
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
35
34
|
};
|
|
36
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Router management module
|
|
38
|
+
*
|
|
39
|
+
* Encapsulates routing navigation functionality with route guard mechanism for permission checks
|
|
40
|
+
*/
|
|
41
|
+
function getHybridHistory() {
|
|
42
|
+
var _a;
|
|
43
|
+
return (_a = globalThis === null || globalThis === void 0 ? void 0 : globalThis.webf) === null || _a === void 0 ? void 0 : _a.hybridHistory;
|
|
44
|
+
}
|
|
45
|
+
let ensureRouteMountedCallback = null;
|
|
46
|
+
function __unstable_setEnsureRouteMountedCallback(callback) {
|
|
47
|
+
ensureRouteMountedCallback = callback;
|
|
48
|
+
}
|
|
49
|
+
function ensureRouteMounted(pathname) {
|
|
50
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
51
|
+
if (!ensureRouteMountedCallback)
|
|
52
|
+
return;
|
|
53
|
+
yield ensureRouteMountedCallback(pathname);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
37
56
|
/**
|
|
38
57
|
* WebF Router object - provides comprehensive navigation APIs
|
|
39
58
|
* Combines web-like history management with Flutter-like navigation patterns
|
|
@@ -43,110 +62,161 @@ const WebFRouter = {
|
|
|
43
62
|
* Get the current state object associated with the history entry
|
|
44
63
|
*/
|
|
45
64
|
get state() {
|
|
46
|
-
|
|
65
|
+
var _a;
|
|
66
|
+
return (_a = getHybridHistory()) === null || _a === void 0 ? void 0 : _a.state;
|
|
47
67
|
},
|
|
48
68
|
/**
|
|
49
69
|
* Get the full hybrid router build context stack.
|
|
50
70
|
* The stack is ordered from root route (index 0) to the current top route (last element).
|
|
51
71
|
*/
|
|
52
72
|
get stack() {
|
|
53
|
-
|
|
73
|
+
var _a, _b;
|
|
74
|
+
return (_b = (_a = getHybridHistory()) === null || _a === void 0 ? void 0 : _a.buildContextStack) !== null && _b !== void 0 ? _b : [];
|
|
54
75
|
},
|
|
55
76
|
/**
|
|
56
77
|
* Get the current route path
|
|
57
78
|
*/
|
|
58
79
|
get path() {
|
|
59
|
-
|
|
80
|
+
var _a, _b;
|
|
81
|
+
return (_b = (_a = getHybridHistory()) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : '/';
|
|
60
82
|
},
|
|
61
83
|
/**
|
|
62
84
|
* Navigate to a specified route
|
|
63
85
|
* Applies route guards for permission checks before navigation
|
|
64
86
|
*/
|
|
65
87
|
push: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
|
|
66
|
-
|
|
88
|
+
const hybridHistory = getHybridHistory();
|
|
89
|
+
if (!hybridHistory)
|
|
90
|
+
throw new Error('WebF hybridHistory is not available');
|
|
91
|
+
yield ensureRouteMounted(path);
|
|
92
|
+
hybridHistory.pushNamed(path, { arguments: state });
|
|
67
93
|
}),
|
|
68
94
|
/**
|
|
69
95
|
* Replace the current route without adding to history
|
|
70
96
|
* Applies route guards for permission checks before navigation
|
|
71
97
|
*/
|
|
72
98
|
replace: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
|
-
|
|
99
|
+
const hybridHistory = getHybridHistory();
|
|
100
|
+
if (!hybridHistory)
|
|
101
|
+
throw new Error('WebF hybridHistory is not available');
|
|
102
|
+
yield ensureRouteMounted(path);
|
|
103
|
+
hybridHistory.pushReplacementNamed(path, { arguments: state });
|
|
74
104
|
}),
|
|
75
105
|
/**
|
|
76
106
|
* Navigate back to the previous route
|
|
77
107
|
*/
|
|
78
108
|
back: () => {
|
|
79
|
-
|
|
109
|
+
const hybridHistory = getHybridHistory();
|
|
110
|
+
if (!hybridHistory)
|
|
111
|
+
throw new Error('WebF hybridHistory is not available');
|
|
112
|
+
hybridHistory.back();
|
|
80
113
|
},
|
|
81
114
|
/**
|
|
82
115
|
* Close the current screen and return to the previous one
|
|
83
116
|
* Flutter-style navigation method
|
|
84
117
|
*/
|
|
85
118
|
pop: (result) => {
|
|
86
|
-
|
|
119
|
+
const hybridHistory = getHybridHistory();
|
|
120
|
+
if (!hybridHistory)
|
|
121
|
+
throw new Error('WebF hybridHistory is not available');
|
|
122
|
+
hybridHistory.pop(result);
|
|
87
123
|
},
|
|
88
124
|
/**
|
|
89
125
|
* Pop routes until reaching a specific route
|
|
90
126
|
*/
|
|
91
127
|
popUntil: (path) => {
|
|
92
|
-
|
|
128
|
+
const hybridHistory = getHybridHistory();
|
|
129
|
+
if (!hybridHistory)
|
|
130
|
+
throw new Error('WebF hybridHistory is not available');
|
|
131
|
+
hybridHistory.popUntil(path);
|
|
93
132
|
},
|
|
94
133
|
/**
|
|
95
134
|
* Pop the current route and push a new named route
|
|
96
135
|
*/
|
|
97
136
|
popAndPushNamed: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
|
|
98
|
-
|
|
137
|
+
const hybridHistory = getHybridHistory();
|
|
138
|
+
if (!hybridHistory)
|
|
139
|
+
throw new Error('WebF hybridHistory is not available');
|
|
140
|
+
yield ensureRouteMounted(path);
|
|
141
|
+
hybridHistory.popAndPushNamed(path, { arguments: state });
|
|
99
142
|
}),
|
|
100
143
|
/**
|
|
101
144
|
* Push a new route and remove routes until reaching a specific route
|
|
102
145
|
*/
|
|
103
146
|
pushNamedAndRemoveUntil: (path, state, untilPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
-
|
|
147
|
+
const hybridHistory = getHybridHistory();
|
|
148
|
+
if (!hybridHistory)
|
|
149
|
+
throw new Error('WebF hybridHistory is not available');
|
|
150
|
+
yield ensureRouteMounted(path);
|
|
151
|
+
hybridHistory.pushNamedAndRemoveUntil(state, path, untilPath);
|
|
105
152
|
}),
|
|
106
153
|
/**
|
|
107
154
|
* Push a new route and remove all routes until a specific route (Flutter-style)
|
|
108
155
|
*/
|
|
109
156
|
pushNamedAndRemoveUntilRoute: (newPath, untilPath, state) => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
-
|
|
157
|
+
const hybridHistory = getHybridHistory();
|
|
158
|
+
if (!hybridHistory)
|
|
159
|
+
throw new Error('WebF hybridHistory is not available');
|
|
160
|
+
yield ensureRouteMounted(newPath);
|
|
161
|
+
hybridHistory.pushNamedAndRemoveUntilRoute(newPath, untilPath, { arguments: state });
|
|
111
162
|
}),
|
|
112
163
|
/**
|
|
113
164
|
* Check if the navigator can go back
|
|
114
165
|
*/
|
|
115
166
|
canPop: () => {
|
|
116
|
-
|
|
167
|
+
const hybridHistory = getHybridHistory();
|
|
168
|
+
if (!hybridHistory)
|
|
169
|
+
return false;
|
|
170
|
+
return hybridHistory.canPop();
|
|
117
171
|
},
|
|
118
172
|
/**
|
|
119
173
|
* Pop the current route if possible
|
|
120
174
|
* Returns true if the pop was successful, false otherwise
|
|
121
175
|
*/
|
|
122
176
|
maybePop: (result) => {
|
|
123
|
-
|
|
177
|
+
const hybridHistory = getHybridHistory();
|
|
178
|
+
if (!hybridHistory)
|
|
179
|
+
return false;
|
|
180
|
+
return hybridHistory.maybePop(result);
|
|
124
181
|
},
|
|
125
182
|
/**
|
|
126
183
|
* Push a new state to the history stack (web-style navigation)
|
|
127
184
|
*/
|
|
128
185
|
pushState: (state, name) => {
|
|
129
|
-
|
|
186
|
+
const hybridHistory = getHybridHistory();
|
|
187
|
+
if (!hybridHistory)
|
|
188
|
+
throw new Error('WebF hybridHistory is not available');
|
|
189
|
+
hybridHistory.pushState(state, name);
|
|
130
190
|
},
|
|
131
191
|
/**
|
|
132
192
|
* Replace the current history entry with a new one (web-style navigation)
|
|
133
193
|
*/
|
|
134
194
|
replaceState: (state, name) => {
|
|
135
|
-
|
|
195
|
+
const hybridHistory = getHybridHistory();
|
|
196
|
+
if (!hybridHistory)
|
|
197
|
+
throw new Error('WebF hybridHistory is not available');
|
|
198
|
+
hybridHistory.replaceState(state, name);
|
|
136
199
|
},
|
|
137
200
|
/**
|
|
138
201
|
* Pop and push with restoration capability
|
|
139
202
|
* Returns a restoration ID string
|
|
140
203
|
*/
|
|
141
204
|
restorablePopAndPushState: (state, name) => {
|
|
142
|
-
|
|
205
|
+
const hybridHistory = getHybridHistory();
|
|
206
|
+
if (!hybridHistory)
|
|
207
|
+
throw new Error('WebF hybridHistory is not available');
|
|
208
|
+
return hybridHistory.restorablePopAndPushState(state, name);
|
|
143
209
|
},
|
|
144
210
|
/**
|
|
145
211
|
* Pop and push named route with restoration capability
|
|
146
212
|
* Returns a restoration ID string
|
|
147
213
|
*/
|
|
148
214
|
restorablePopAndPushNamed: (path, state) => __awaiter(void 0, void 0, void 0, function* () {
|
|
149
|
-
|
|
215
|
+
const hybridHistory = getHybridHistory();
|
|
216
|
+
if (!hybridHistory)
|
|
217
|
+
throw new Error('WebF hybridHistory is not available');
|
|
218
|
+
yield ensureRouteMounted(path);
|
|
219
|
+
return hybridHistory.restorablePopAndPushNamed(path, { arguments: state });
|
|
150
220
|
})
|
|
151
221
|
};
|
|
152
222
|
/**
|
|
@@ -156,6 +226,10 @@ const WebFRouter = {
|
|
|
156
226
|
*/
|
|
157
227
|
function pathToRegex(pattern) {
|
|
158
228
|
const paramNames = [];
|
|
229
|
+
if (pattern === '*') {
|
|
230
|
+
paramNames.push('*');
|
|
231
|
+
return { regex: /^(.*)$/, paramNames };
|
|
232
|
+
}
|
|
159
233
|
// Escape special regex characters except : and *
|
|
160
234
|
let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
161
235
|
// Replace :param with named capture groups
|
|
@@ -163,6 +237,11 @@ function pathToRegex(pattern) {
|
|
|
163
237
|
paramNames.push(paramName);
|
|
164
238
|
return '([^/]+)';
|
|
165
239
|
});
|
|
240
|
+
// Replace * with a splat capture group (matches across segments)
|
|
241
|
+
regexPattern = regexPattern.replace(/\*/g, () => {
|
|
242
|
+
paramNames.push('*');
|
|
243
|
+
return '(.*)';
|
|
244
|
+
});
|
|
166
245
|
// Add anchors for exact matching
|
|
167
246
|
regexPattern = `^${regexPattern}$`;
|
|
168
247
|
return {
|
|
@@ -289,7 +368,7 @@ const WebFRouterLink = function (props) {
|
|
|
289
368
|
*
|
|
290
369
|
* Responsible for managing page rendering, lifecycle and navigation bar
|
|
291
370
|
*/
|
|
292
|
-
function Route({ path, prerender = false, element, title, theme }) {
|
|
371
|
+
function Route({ path, mountedPath, prerender = false, element, title, theme }) {
|
|
293
372
|
// Mark whether the page has been rendered
|
|
294
373
|
const [hasRendered, updateRender] = useState(false);
|
|
295
374
|
/**
|
|
@@ -308,7 +387,7 @@ function Route({ path, prerender = false, element, title, theme }) {
|
|
|
308
387
|
*/
|
|
309
388
|
const handleOffScreen = useMemoizedFn(() => {
|
|
310
389
|
});
|
|
311
|
-
return (React.createElement(WebFRouterLink, { path: path, title: title, theme: theme, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
|
|
390
|
+
return (React.createElement(WebFRouterLink, { path: mountedPath !== null && mountedPath !== void 0 ? mountedPath : path, title: title, theme: theme, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
|
|
312
391
|
}
|
|
313
392
|
|
|
314
393
|
/**
|
|
@@ -316,6 +395,7 @@ function Route({ path, prerender = false, element, title, theme }) {
|
|
|
316
395
|
*/
|
|
317
396
|
const RouteContext = createContext({
|
|
318
397
|
path: undefined,
|
|
398
|
+
mountedPath: undefined,
|
|
319
399
|
params: undefined,
|
|
320
400
|
routeParams: undefined,
|
|
321
401
|
activePath: undefined,
|
|
@@ -336,9 +416,9 @@ const RouteContext = createContext({
|
|
|
336
416
|
*/
|
|
337
417
|
function useRouteContext() {
|
|
338
418
|
const context = useContext(RouteContext);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
&& context.
|
|
419
|
+
const isActive = context.activePath !== undefined
|
|
420
|
+
&& context.mountedPath !== undefined
|
|
421
|
+
&& context.activePath === context.mountedPath;
|
|
342
422
|
return Object.assign(Object.assign({}, context), { isActive });
|
|
343
423
|
}
|
|
344
424
|
/**
|
|
@@ -364,12 +444,9 @@ function useLocation() {
|
|
|
364
444
|
const context = useRouteContext();
|
|
365
445
|
// Create location object from context
|
|
366
446
|
const location = useMemo(() => {
|
|
367
|
-
const
|
|
368
|
-
let pathname = currentPath;
|
|
369
|
-
// Check if the current component's route matches the active path
|
|
370
|
-
const isCurrentRoute = context.path === context.activePath;
|
|
447
|
+
const pathname = context.activePath || WebFRouter.path;
|
|
371
448
|
// Get state - prioritize context params, fallback to WebFRouter.state
|
|
372
|
-
const state =
|
|
449
|
+
const state = context.isActive
|
|
373
450
|
? (context.params || WebFRouter.state)
|
|
374
451
|
: WebFRouter.state;
|
|
375
452
|
return {
|
|
@@ -378,7 +455,7 @@ function useLocation() {
|
|
|
378
455
|
isActive: context.isActive,
|
|
379
456
|
key: `${pathname}-${Date.now()}`
|
|
380
457
|
};
|
|
381
|
-
}, [context.isActive, context.
|
|
458
|
+
}, [context.isActive, context.activePath, context.params]);
|
|
382
459
|
return location;
|
|
383
460
|
}
|
|
384
461
|
/**
|
|
@@ -419,75 +496,110 @@ function useParams() {
|
|
|
419
496
|
/**
|
|
420
497
|
* Route-specific context provider that only updates when the route is active
|
|
421
498
|
*/
|
|
422
|
-
function RouteContextProvider({
|
|
499
|
+
function RouteContextProvider({ patternPath, mountedPath, children, }) {
|
|
423
500
|
const globalContext = useContext(RouteContext);
|
|
424
501
|
// Create a route-specific context that only updates when this route is active
|
|
425
502
|
const routeSpecificContext = useMemo(() => {
|
|
426
|
-
|
|
427
|
-
const match =
|
|
428
|
-
if (match) {
|
|
429
|
-
// Use route params from Flutter event if available, otherwise from local matching
|
|
430
|
-
const effectiveRouteParams = globalContext.routeParams || match.params;
|
|
431
|
-
// For matching routes, always try to get state from WebFRouter if params is undefined
|
|
503
|
+
const isActive = globalContext.activePath !== undefined && globalContext.activePath === mountedPath;
|
|
504
|
+
const match = isActive ? matchPath(patternPath, mountedPath) : null;
|
|
505
|
+
if (isActive && match) {
|
|
432
506
|
const effectiveParams = globalContext.params !== undefined ? globalContext.params : WebFRouter.state;
|
|
433
507
|
return {
|
|
434
|
-
path,
|
|
508
|
+
path: patternPath,
|
|
509
|
+
mountedPath,
|
|
435
510
|
params: effectiveParams,
|
|
436
|
-
routeParams:
|
|
511
|
+
routeParams: match.params,
|
|
437
512
|
activePath: globalContext.activePath,
|
|
438
513
|
routeEventKind: globalContext.routeEventKind
|
|
439
514
|
};
|
|
440
515
|
}
|
|
441
|
-
// Return previous values if not active
|
|
442
516
|
return {
|
|
443
|
-
path,
|
|
517
|
+
path: patternPath,
|
|
518
|
+
mountedPath,
|
|
444
519
|
params: undefined,
|
|
445
520
|
routeParams: undefined,
|
|
446
521
|
activePath: globalContext.activePath,
|
|
447
522
|
routeEventKind: undefined
|
|
448
523
|
};
|
|
449
|
-
}, [
|
|
524
|
+
}, [patternPath, mountedPath, globalContext.activePath, globalContext.params, globalContext.routeEventKind]);
|
|
450
525
|
return (React.createElement(RouteContext.Provider, { value: routeSpecificContext }, children));
|
|
451
526
|
}
|
|
527
|
+
function patternScore(pattern) {
|
|
528
|
+
if (pattern === '*')
|
|
529
|
+
return 0;
|
|
530
|
+
const segments = pattern.split('/').filter(Boolean);
|
|
531
|
+
let score = 0;
|
|
532
|
+
for (const segment of segments) {
|
|
533
|
+
if (segment === '*')
|
|
534
|
+
score += 1;
|
|
535
|
+
else if (segment.startsWith(':'))
|
|
536
|
+
score += 2;
|
|
537
|
+
else
|
|
538
|
+
score += 3;
|
|
539
|
+
}
|
|
540
|
+
return score * 100 + segments.length;
|
|
541
|
+
}
|
|
542
|
+
function findBestMatch(patterns, pathname) {
|
|
543
|
+
var _a;
|
|
544
|
+
let best = null;
|
|
545
|
+
for (const pattern of patterns) {
|
|
546
|
+
const match = matchPath(pattern, pathname);
|
|
547
|
+
if (!match)
|
|
548
|
+
continue;
|
|
549
|
+
const score = patternScore(pattern);
|
|
550
|
+
if (!best || score > best.score)
|
|
551
|
+
best = { match, score };
|
|
552
|
+
}
|
|
553
|
+
return (_a = best === null || best === void 0 ? void 0 : best.match) !== null && _a !== void 0 ? _a : null;
|
|
554
|
+
}
|
|
555
|
+
function escapeAttributeValue(value) {
|
|
556
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
557
|
+
}
|
|
452
558
|
function Routes({ children }) {
|
|
453
559
|
// State to track current route information
|
|
454
560
|
const [routeState, setRouteState] = useState({
|
|
455
561
|
path: undefined,
|
|
562
|
+
mountedPath: undefined,
|
|
456
563
|
activePath: WebFRouter.path, // Initialize with current path
|
|
457
564
|
params: undefined,
|
|
458
565
|
routeParams: undefined,
|
|
459
566
|
routeEventKind: undefined
|
|
460
567
|
});
|
|
568
|
+
const [stack, setStack] = useState(() => WebFRouter.stack);
|
|
569
|
+
const [preMountedPaths, setPreMountedPaths] = useState([]);
|
|
570
|
+
const routePatternsRef = useRef([]);
|
|
571
|
+
const pendingEnsureResolversRef = useRef(new Map());
|
|
572
|
+
// Keep a stable view of declared route patterns for event handlers.
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
const patterns = [];
|
|
575
|
+
Children.forEach(children, (child) => {
|
|
576
|
+
if (!isValidElement(child))
|
|
577
|
+
return;
|
|
578
|
+
if (child.type !== Route)
|
|
579
|
+
return;
|
|
580
|
+
patterns.push(child.props.path);
|
|
581
|
+
});
|
|
582
|
+
routePatternsRef.current = patterns;
|
|
583
|
+
}, [children]);
|
|
461
584
|
// Listen to hybridrouterchange event
|
|
462
585
|
useEffect(() => {
|
|
463
586
|
const handleRouteChange = (event) => {
|
|
587
|
+
var _a, _b, _c;
|
|
464
588
|
const routeEvent = event;
|
|
465
589
|
// Check for new event detail structure with params
|
|
466
590
|
const eventDetail = event.detail;
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const registeredRoutes = Array.from(document.querySelectorAll('webf-router-link'));
|
|
476
|
-
for (const routeElement of registeredRoutes) {
|
|
477
|
-
const routePath = routeElement.getAttribute('path');
|
|
478
|
-
if (routePath && routePath.includes(':')) {
|
|
479
|
-
const match = matchPath(routePath, newActivePath);
|
|
480
|
-
if (match) {
|
|
481
|
-
routeParams = match.params;
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
const eventState = (eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.state) || routeEvent.state;
|
|
591
|
+
const newActivePath = WebFRouter.path;
|
|
592
|
+
const newStack = WebFRouter.stack;
|
|
593
|
+
setStack(newStack);
|
|
594
|
+
setPreMountedPaths((prev) => prev.filter((p) => newStack.some((entry) => entry.path === p)));
|
|
595
|
+
const bestMatch = newActivePath ? findBestMatch(routePatternsRef.current, newActivePath) : null;
|
|
596
|
+
const routeParams = (eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.params) || (bestMatch === null || bestMatch === void 0 ? void 0 : bestMatch.params) || undefined;
|
|
597
|
+
const activeEntry = [...newStack].reverse().find((entry) => entry.path === newActivePath);
|
|
598
|
+
const eventState = (_c = (_b = (_a = activeEntry === null || activeEntry === void 0 ? void 0 : activeEntry.state) !== null && _a !== void 0 ? _a : eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.state) !== null && _b !== void 0 ? _b : routeEvent.state) !== null && _c !== void 0 ? _c : WebFRouter.state;
|
|
488
599
|
// Update state based on event kind
|
|
489
600
|
setRouteState({
|
|
490
601
|
path: routeEvent.path,
|
|
602
|
+
mountedPath: routeEvent.path,
|
|
491
603
|
activePath: newActivePath,
|
|
492
604
|
params: eventState,
|
|
493
605
|
routeParams: routeParams, // Use params from Flutter if available
|
|
@@ -500,31 +612,108 @@ function Routes({ children }) {
|
|
|
500
612
|
return () => {
|
|
501
613
|
document.removeEventListener('hybridrouterchange', handleRouteChange);
|
|
502
614
|
};
|
|
503
|
-
}, [
|
|
615
|
+
}, []);
|
|
616
|
+
useEffect(() => {
|
|
617
|
+
__unstable_setEnsureRouteMountedCallback((pathname) => {
|
|
618
|
+
var _a;
|
|
619
|
+
if (!pathname)
|
|
620
|
+
return;
|
|
621
|
+
const bestMatch = findBestMatch(routePatternsRef.current, pathname);
|
|
622
|
+
if (!bestMatch)
|
|
623
|
+
return;
|
|
624
|
+
const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
|
|
625
|
+
if (document.querySelector(selector))
|
|
626
|
+
return;
|
|
627
|
+
let resolveFn;
|
|
628
|
+
const promise = new Promise((resolve) => {
|
|
629
|
+
resolveFn = resolve;
|
|
630
|
+
});
|
|
631
|
+
pendingEnsureResolversRef.current.set(pathname, [
|
|
632
|
+
...((_a = pendingEnsureResolversRef.current.get(pathname)) !== null && _a !== void 0 ? _a : []),
|
|
633
|
+
resolveFn,
|
|
634
|
+
]);
|
|
635
|
+
setPreMountedPaths((prev) => (prev.includes(pathname) ? prev : [...prev, pathname]));
|
|
636
|
+
return promise;
|
|
637
|
+
});
|
|
638
|
+
return () => {
|
|
639
|
+
__unstable_setEnsureRouteMountedCallback(null);
|
|
640
|
+
};
|
|
641
|
+
}, []);
|
|
642
|
+
useEffect(() => {
|
|
643
|
+
const pending = pendingEnsureResolversRef.current;
|
|
644
|
+
for (const [pathname, resolvers] of pending.entries()) {
|
|
645
|
+
const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
|
|
646
|
+
if (!document.querySelector(selector))
|
|
647
|
+
continue;
|
|
648
|
+
for (const resolve of resolvers)
|
|
649
|
+
resolve();
|
|
650
|
+
pending.delete(pathname);
|
|
651
|
+
}
|
|
652
|
+
}, [children, stack, preMountedPaths, routeState.activePath]);
|
|
504
653
|
// Global context value
|
|
505
654
|
const globalContextValue = useMemo(() => ({
|
|
506
655
|
path: undefined,
|
|
656
|
+
mountedPath: undefined,
|
|
507
657
|
params: routeState.params,
|
|
508
658
|
routeParams: routeState.routeParams, // Pass through route params from Flutter
|
|
509
659
|
activePath: routeState.activePath,
|
|
510
660
|
routeEventKind: routeState.routeEventKind
|
|
511
661
|
}), [routeState.activePath, routeState.params, routeState.routeParams, routeState.routeEventKind]);
|
|
512
|
-
// Wrap each Route component with its own context provider
|
|
513
662
|
const wrappedChildren = useMemo(() => {
|
|
514
|
-
|
|
663
|
+
const declaredRoutes = [];
|
|
664
|
+
const patterns = [];
|
|
665
|
+
const declaredPaths = new Set();
|
|
666
|
+
Children.forEach(children, (child) => {
|
|
667
|
+
var _a;
|
|
515
668
|
if (!isValidElement(child)) {
|
|
516
|
-
|
|
669
|
+
declaredRoutes.push(child);
|
|
670
|
+
return;
|
|
517
671
|
}
|
|
518
|
-
// Ensure only Route components are direct children
|
|
519
672
|
if (child.type !== Route) {
|
|
520
673
|
console.warn('Routes component should only contain Route components as direct children');
|
|
521
|
-
|
|
674
|
+
declaredRoutes.push(child);
|
|
675
|
+
return;
|
|
522
676
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
677
|
+
const patternPath = child.props.path;
|
|
678
|
+
patterns.push(patternPath);
|
|
679
|
+
declaredPaths.add(patternPath);
|
|
680
|
+
const mountedPath = (_a = child.props.mountedPath) !== null && _a !== void 0 ? _a : patternPath;
|
|
681
|
+
declaredRoutes.push(React.createElement(RouteContextProvider, { key: `declared:${patternPath}`, patternPath: patternPath, mountedPath: mountedPath }, child));
|
|
526
682
|
});
|
|
527
|
-
|
|
683
|
+
const mountedPaths = [];
|
|
684
|
+
for (const entry of stack)
|
|
685
|
+
mountedPaths.push(entry.path);
|
|
686
|
+
for (const path of preMountedPaths)
|
|
687
|
+
mountedPaths.push(path);
|
|
688
|
+
if (routeState.activePath && !mountedPaths.includes(routeState.activePath))
|
|
689
|
+
mountedPaths.push(routeState.activePath);
|
|
690
|
+
const dynamicRoutes = [];
|
|
691
|
+
const seenMountedPaths = new Set();
|
|
692
|
+
for (const mountedPath of mountedPaths) {
|
|
693
|
+
if (seenMountedPaths.has(mountedPath))
|
|
694
|
+
continue;
|
|
695
|
+
seenMountedPaths.add(mountedPath);
|
|
696
|
+
if (declaredPaths.has(mountedPath))
|
|
697
|
+
continue;
|
|
698
|
+
const bestMatch = findBestMatch(patterns, mountedPath);
|
|
699
|
+
if (!bestMatch)
|
|
700
|
+
continue;
|
|
701
|
+
const matchingRouteElement = Children.toArray(children).find((node) => {
|
|
702
|
+
if (!isValidElement(node))
|
|
703
|
+
return false;
|
|
704
|
+
if (node.type !== Route)
|
|
705
|
+
return false;
|
|
706
|
+
return node.props.path === bestMatch.path;
|
|
707
|
+
});
|
|
708
|
+
if (!matchingRouteElement)
|
|
709
|
+
continue;
|
|
710
|
+
const routeInstance = React.cloneElement(matchingRouteElement, {
|
|
711
|
+
mountedPath,
|
|
712
|
+
});
|
|
713
|
+
dynamicRoutes.push(React.createElement(RouteContextProvider, { key: `dynamic:${mountedPath}`, patternPath: bestMatch.path, mountedPath: mountedPath }, routeInstance));
|
|
714
|
+
}
|
|
715
|
+
return [...declaredRoutes, ...dynamicRoutes];
|
|
716
|
+
}, [children, stack, preMountedPaths, routeState.activePath]);
|
|
528
717
|
return (React.createElement(RouteContext.Provider, { value: globalContextValue }, wrappedChildren));
|
|
529
718
|
}
|
|
530
719
|
/**
|
|
@@ -624,5 +813,5 @@ function useNavigate() {
|
|
|
624
813
|
}, []);
|
|
625
814
|
}
|
|
626
815
|
|
|
627
|
-
export { Route, Routes, WebFRouter, WebFRouterLink, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
|
|
816
|
+
export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
|
|
628
817
|
//# sourceMappingURL=index.esm.js.map
|