@openwebf/react-router 0.23.7 → 0.24.1
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 +19 -4
- package/dist/index.esm.js +275 -74
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +275 -73
- 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/dist/utils/RouterLink.d.ts +4 -0
- package/dist/utils/RouterLink.d.ts.map +1 -1
- package/package.json +2 -2
- 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
|
|
@@ -420,17 +431,21 @@ interface HybridRouterChangeEvent extends SyntheticEvent {
|
|
|
420
431
|
readonly path: string;
|
|
421
432
|
}
|
|
422
433
|
type HybridRouterChangeEventHandler = EventHandler<HybridRouterChangeEvent>;
|
|
434
|
+
interface HybridRouterPrerenderingEvent extends SyntheticEvent {
|
|
435
|
+
}
|
|
436
|
+
type HybridRouterPrerenderingEventHandler = EventHandler<HybridRouterPrerenderingEvent>;
|
|
423
437
|
interface WebFHybridRouterProps {
|
|
424
438
|
path: string;
|
|
425
439
|
title?: string;
|
|
426
440
|
theme?: 'material' | 'cupertino';
|
|
427
441
|
onScreen?: HybridRouterChangeEventHandler;
|
|
428
442
|
offScreen?: HybridRouterChangeEventHandler;
|
|
443
|
+
onPrerendering?: HybridRouterPrerenderingEventHandler;
|
|
429
444
|
children?: ReactNode;
|
|
430
445
|
}
|
|
431
446
|
interface WebFRouterLinkElement extends WebFElementWithMethods<{}> {
|
|
432
447
|
}
|
|
433
448
|
declare const WebFRouterLink: FC<WebFHybridRouterProps>;
|
|
434
449
|
|
|
435
|
-
export { Route, Routes, WebFRouter, WebFRouterLink, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
|
|
436
|
-
export type { HybridRouteStackEntry, HybridRouterChangeEvent, HybridRouterChangeEventHandler, Location, NavigateFunction, NavigateOptions, NavigationMethods, RouteMatch, RouteObject, RouteParams, RouteProps, RoutesProps, WebFHybridRouterProps, WebFRouterLinkElement };
|
|
450
|
+
export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
|
|
451
|
+
export type { HybridRouteStackEntry, HybridRouterChangeEvent, HybridRouterChangeEventHandler, HybridRouterPrerenderingEvent, HybridRouterPrerenderingEventHandler, 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 {
|
|
@@ -262,6 +341,13 @@ const RawWebFRouterLink = createWebFComponent({
|
|
|
262
341
|
callback(event);
|
|
263
342
|
},
|
|
264
343
|
},
|
|
344
|
+
{
|
|
345
|
+
propName: 'onPrerendering',
|
|
346
|
+
eventName: 'prerendering',
|
|
347
|
+
handler: (callback) => (event) => {
|
|
348
|
+
callback(event);
|
|
349
|
+
},
|
|
350
|
+
},
|
|
265
351
|
],
|
|
266
352
|
});
|
|
267
353
|
const WebFRouterLink = function (props) {
|
|
@@ -272,7 +358,12 @@ const WebFRouterLink = function (props) {
|
|
|
272
358
|
props.onScreen(event);
|
|
273
359
|
}
|
|
274
360
|
};
|
|
275
|
-
|
|
361
|
+
const handlePrerendering = (event) => {
|
|
362
|
+
var _a;
|
|
363
|
+
enableRender(true);
|
|
364
|
+
(_a = props.onPrerendering) === null || _a === void 0 ? void 0 : _a.call(props, event);
|
|
365
|
+
};
|
|
366
|
+
return (React.createElement(RawWebFRouterLink, { title: props.title, path: props.path, theme: props.theme, onScreen: handleOnScreen, offScreen: props.offScreen, onPrerendering: handlePrerendering }, isRender ? props.children : null));
|
|
276
367
|
};
|
|
277
368
|
|
|
278
369
|
/**
|
|
@@ -289,7 +380,7 @@ const WebFRouterLink = function (props) {
|
|
|
289
380
|
*
|
|
290
381
|
* Responsible for managing page rendering, lifecycle and navigation bar
|
|
291
382
|
*/
|
|
292
|
-
function Route({ path, prerender = false, element, title, theme }) {
|
|
383
|
+
function Route({ path, mountedPath, prerender = false, element, title, theme }) {
|
|
293
384
|
// Mark whether the page has been rendered
|
|
294
385
|
const [hasRendered, updateRender] = useState(false);
|
|
295
386
|
/**
|
|
@@ -308,7 +399,7 @@ function Route({ path, prerender = false, element, title, theme }) {
|
|
|
308
399
|
*/
|
|
309
400
|
const handleOffScreen = useMemoizedFn(() => {
|
|
310
401
|
});
|
|
311
|
-
return (React.createElement(WebFRouterLink, { path: path, title: title, theme: theme, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
|
|
402
|
+
return (React.createElement(WebFRouterLink, { path: mountedPath !== null && mountedPath !== void 0 ? mountedPath : path, title: title, theme: theme, onScreen: handleOnScreen, offScreen: handleOffScreen }, shouldRenderChildren ? element : null));
|
|
312
403
|
}
|
|
313
404
|
|
|
314
405
|
/**
|
|
@@ -316,6 +407,7 @@ function Route({ path, prerender = false, element, title, theme }) {
|
|
|
316
407
|
*/
|
|
317
408
|
const RouteContext = createContext({
|
|
318
409
|
path: undefined,
|
|
410
|
+
mountedPath: undefined,
|
|
319
411
|
params: undefined,
|
|
320
412
|
routeParams: undefined,
|
|
321
413
|
activePath: undefined,
|
|
@@ -336,9 +428,9 @@ const RouteContext = createContext({
|
|
|
336
428
|
*/
|
|
337
429
|
function useRouteContext() {
|
|
338
430
|
const context = useContext(RouteContext);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
&& context.
|
|
431
|
+
const isActive = context.activePath !== undefined
|
|
432
|
+
&& context.mountedPath !== undefined
|
|
433
|
+
&& context.activePath === context.mountedPath;
|
|
342
434
|
return Object.assign(Object.assign({}, context), { isActive });
|
|
343
435
|
}
|
|
344
436
|
/**
|
|
@@ -364,12 +456,9 @@ function useLocation() {
|
|
|
364
456
|
const context = useRouteContext();
|
|
365
457
|
// Create location object from context
|
|
366
458
|
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;
|
|
459
|
+
const pathname = context.activePath || WebFRouter.path;
|
|
371
460
|
// Get state - prioritize context params, fallback to WebFRouter.state
|
|
372
|
-
const state =
|
|
461
|
+
const state = context.isActive
|
|
373
462
|
? (context.params || WebFRouter.state)
|
|
374
463
|
: WebFRouter.state;
|
|
375
464
|
return {
|
|
@@ -378,7 +467,7 @@ function useLocation() {
|
|
|
378
467
|
isActive: context.isActive,
|
|
379
468
|
key: `${pathname}-${Date.now()}`
|
|
380
469
|
};
|
|
381
|
-
}, [context.isActive, context.
|
|
470
|
+
}, [context.isActive, context.activePath, context.params]);
|
|
382
471
|
return location;
|
|
383
472
|
}
|
|
384
473
|
/**
|
|
@@ -419,75 +508,110 @@ function useParams() {
|
|
|
419
508
|
/**
|
|
420
509
|
* Route-specific context provider that only updates when the route is active
|
|
421
510
|
*/
|
|
422
|
-
function RouteContextProvider({
|
|
511
|
+
function RouteContextProvider({ patternPath, mountedPath, children, }) {
|
|
423
512
|
const globalContext = useContext(RouteContext);
|
|
424
513
|
// Create a route-specific context that only updates when this route is active
|
|
425
514
|
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
|
|
515
|
+
const isActive = globalContext.activePath !== undefined && globalContext.activePath === mountedPath;
|
|
516
|
+
const match = isActive ? matchPath(patternPath, mountedPath) : null;
|
|
517
|
+
if (isActive && match) {
|
|
432
518
|
const effectiveParams = globalContext.params !== undefined ? globalContext.params : WebFRouter.state;
|
|
433
519
|
return {
|
|
434
|
-
path,
|
|
520
|
+
path: patternPath,
|
|
521
|
+
mountedPath,
|
|
435
522
|
params: effectiveParams,
|
|
436
|
-
routeParams:
|
|
523
|
+
routeParams: match.params,
|
|
437
524
|
activePath: globalContext.activePath,
|
|
438
525
|
routeEventKind: globalContext.routeEventKind
|
|
439
526
|
};
|
|
440
527
|
}
|
|
441
|
-
// Return previous values if not active
|
|
442
528
|
return {
|
|
443
|
-
path,
|
|
529
|
+
path: patternPath,
|
|
530
|
+
mountedPath,
|
|
444
531
|
params: undefined,
|
|
445
532
|
routeParams: undefined,
|
|
446
533
|
activePath: globalContext.activePath,
|
|
447
534
|
routeEventKind: undefined
|
|
448
535
|
};
|
|
449
|
-
}, [
|
|
536
|
+
}, [patternPath, mountedPath, globalContext.activePath, globalContext.params, globalContext.routeEventKind]);
|
|
450
537
|
return (React.createElement(RouteContext.Provider, { value: routeSpecificContext }, children));
|
|
451
538
|
}
|
|
539
|
+
function patternScore(pattern) {
|
|
540
|
+
if (pattern === '*')
|
|
541
|
+
return 0;
|
|
542
|
+
const segments = pattern.split('/').filter(Boolean);
|
|
543
|
+
let score = 0;
|
|
544
|
+
for (const segment of segments) {
|
|
545
|
+
if (segment === '*')
|
|
546
|
+
score += 1;
|
|
547
|
+
else if (segment.startsWith(':'))
|
|
548
|
+
score += 2;
|
|
549
|
+
else
|
|
550
|
+
score += 3;
|
|
551
|
+
}
|
|
552
|
+
return score * 100 + segments.length;
|
|
553
|
+
}
|
|
554
|
+
function findBestMatch(patterns, pathname) {
|
|
555
|
+
var _a;
|
|
556
|
+
let best = null;
|
|
557
|
+
for (const pattern of patterns) {
|
|
558
|
+
const match = matchPath(pattern, pathname);
|
|
559
|
+
if (!match)
|
|
560
|
+
continue;
|
|
561
|
+
const score = patternScore(pattern);
|
|
562
|
+
if (!best || score > best.score)
|
|
563
|
+
best = { match, score };
|
|
564
|
+
}
|
|
565
|
+
return (_a = best === null || best === void 0 ? void 0 : best.match) !== null && _a !== void 0 ? _a : null;
|
|
566
|
+
}
|
|
567
|
+
function escapeAttributeValue(value) {
|
|
568
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
569
|
+
}
|
|
452
570
|
function Routes({ children }) {
|
|
453
571
|
// State to track current route information
|
|
454
572
|
const [routeState, setRouteState] = useState({
|
|
455
573
|
path: undefined,
|
|
574
|
+
mountedPath: undefined,
|
|
456
575
|
activePath: WebFRouter.path, // Initialize with current path
|
|
457
576
|
params: undefined,
|
|
458
577
|
routeParams: undefined,
|
|
459
578
|
routeEventKind: undefined
|
|
460
579
|
});
|
|
580
|
+
const [stack, setStack] = useState(() => WebFRouter.stack);
|
|
581
|
+
const [preMountedPaths, setPreMountedPaths] = useState([]);
|
|
582
|
+
const routePatternsRef = useRef([]);
|
|
583
|
+
const pendingEnsureResolversRef = useRef(new Map());
|
|
584
|
+
// Keep a stable view of declared route patterns for event handlers.
|
|
585
|
+
useEffect(() => {
|
|
586
|
+
const patterns = [];
|
|
587
|
+
Children.forEach(children, (child) => {
|
|
588
|
+
if (!isValidElement(child))
|
|
589
|
+
return;
|
|
590
|
+
if (child.type !== Route)
|
|
591
|
+
return;
|
|
592
|
+
patterns.push(child.props.path);
|
|
593
|
+
});
|
|
594
|
+
routePatternsRef.current = patterns;
|
|
595
|
+
}, [children]);
|
|
461
596
|
// Listen to hybridrouterchange event
|
|
462
597
|
useEffect(() => {
|
|
463
598
|
const handleRouteChange = (event) => {
|
|
599
|
+
var _a, _b, _c;
|
|
464
600
|
const routeEvent = event;
|
|
465
601
|
// Check for new event detail structure with params
|
|
466
602
|
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;
|
|
603
|
+
const newActivePath = WebFRouter.path;
|
|
604
|
+
const newStack = WebFRouter.stack;
|
|
605
|
+
setStack(newStack);
|
|
606
|
+
setPreMountedPaths((prev) => prev.filter((p) => newStack.some((entry) => entry.path === p)));
|
|
607
|
+
const bestMatch = newActivePath ? findBestMatch(routePatternsRef.current, newActivePath) : null;
|
|
608
|
+
const routeParams = (eventDetail === null || eventDetail === void 0 ? void 0 : eventDetail.params) || (bestMatch === null || bestMatch === void 0 ? void 0 : bestMatch.params) || undefined;
|
|
609
|
+
const activeEntry = [...newStack].reverse().find((entry) => entry.path === newActivePath);
|
|
610
|
+
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
611
|
// Update state based on event kind
|
|
489
612
|
setRouteState({
|
|
490
613
|
path: routeEvent.path,
|
|
614
|
+
mountedPath: routeEvent.path,
|
|
491
615
|
activePath: newActivePath,
|
|
492
616
|
params: eventState,
|
|
493
617
|
routeParams: routeParams, // Use params from Flutter if available
|
|
@@ -500,31 +624,108 @@ function Routes({ children }) {
|
|
|
500
624
|
return () => {
|
|
501
625
|
document.removeEventListener('hybridrouterchange', handleRouteChange);
|
|
502
626
|
};
|
|
503
|
-
}, [
|
|
627
|
+
}, []);
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
__unstable_setEnsureRouteMountedCallback((pathname) => {
|
|
630
|
+
var _a;
|
|
631
|
+
if (!pathname)
|
|
632
|
+
return;
|
|
633
|
+
const bestMatch = findBestMatch(routePatternsRef.current, pathname);
|
|
634
|
+
if (!bestMatch)
|
|
635
|
+
return;
|
|
636
|
+
const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
|
|
637
|
+
if (document.querySelector(selector))
|
|
638
|
+
return;
|
|
639
|
+
let resolveFn;
|
|
640
|
+
const promise = new Promise((resolve) => {
|
|
641
|
+
resolveFn = resolve;
|
|
642
|
+
});
|
|
643
|
+
pendingEnsureResolversRef.current.set(pathname, [
|
|
644
|
+
...((_a = pendingEnsureResolversRef.current.get(pathname)) !== null && _a !== void 0 ? _a : []),
|
|
645
|
+
resolveFn,
|
|
646
|
+
]);
|
|
647
|
+
setPreMountedPaths((prev) => (prev.includes(pathname) ? prev : [...prev, pathname]));
|
|
648
|
+
return promise;
|
|
649
|
+
});
|
|
650
|
+
return () => {
|
|
651
|
+
__unstable_setEnsureRouteMountedCallback(null);
|
|
652
|
+
};
|
|
653
|
+
}, []);
|
|
654
|
+
useEffect(() => {
|
|
655
|
+
const pending = pendingEnsureResolversRef.current;
|
|
656
|
+
for (const [pathname, resolvers] of pending.entries()) {
|
|
657
|
+
const selector = `webf-router-link[path="${escapeAttributeValue(pathname)}"]`;
|
|
658
|
+
if (!document.querySelector(selector))
|
|
659
|
+
continue;
|
|
660
|
+
for (const resolve of resolvers)
|
|
661
|
+
resolve();
|
|
662
|
+
pending.delete(pathname);
|
|
663
|
+
}
|
|
664
|
+
}, [children, stack, preMountedPaths, routeState.activePath]);
|
|
504
665
|
// Global context value
|
|
505
666
|
const globalContextValue = useMemo(() => ({
|
|
506
667
|
path: undefined,
|
|
668
|
+
mountedPath: undefined,
|
|
507
669
|
params: routeState.params,
|
|
508
670
|
routeParams: routeState.routeParams, // Pass through route params from Flutter
|
|
509
671
|
activePath: routeState.activePath,
|
|
510
672
|
routeEventKind: routeState.routeEventKind
|
|
511
673
|
}), [routeState.activePath, routeState.params, routeState.routeParams, routeState.routeEventKind]);
|
|
512
|
-
// Wrap each Route component with its own context provider
|
|
513
674
|
const wrappedChildren = useMemo(() => {
|
|
514
|
-
|
|
675
|
+
const declaredRoutes = [];
|
|
676
|
+
const patterns = [];
|
|
677
|
+
const declaredPaths = new Set();
|
|
678
|
+
Children.forEach(children, (child) => {
|
|
679
|
+
var _a;
|
|
515
680
|
if (!isValidElement(child)) {
|
|
516
|
-
|
|
681
|
+
declaredRoutes.push(child);
|
|
682
|
+
return;
|
|
517
683
|
}
|
|
518
|
-
// Ensure only Route components are direct children
|
|
519
684
|
if (child.type !== Route) {
|
|
520
685
|
console.warn('Routes component should only contain Route components as direct children');
|
|
521
|
-
|
|
686
|
+
declaredRoutes.push(child);
|
|
687
|
+
return;
|
|
522
688
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
689
|
+
const patternPath = child.props.path;
|
|
690
|
+
patterns.push(patternPath);
|
|
691
|
+
declaredPaths.add(patternPath);
|
|
692
|
+
const mountedPath = (_a = child.props.mountedPath) !== null && _a !== void 0 ? _a : patternPath;
|
|
693
|
+
declaredRoutes.push(React.createElement(RouteContextProvider, { key: `declared:${patternPath}`, patternPath: patternPath, mountedPath: mountedPath }, child));
|
|
526
694
|
});
|
|
527
|
-
|
|
695
|
+
const mountedPaths = [];
|
|
696
|
+
for (const entry of stack)
|
|
697
|
+
mountedPaths.push(entry.path);
|
|
698
|
+
for (const path of preMountedPaths)
|
|
699
|
+
mountedPaths.push(path);
|
|
700
|
+
if (routeState.activePath && !mountedPaths.includes(routeState.activePath))
|
|
701
|
+
mountedPaths.push(routeState.activePath);
|
|
702
|
+
const dynamicRoutes = [];
|
|
703
|
+
const seenMountedPaths = new Set();
|
|
704
|
+
for (const mountedPath of mountedPaths) {
|
|
705
|
+
if (seenMountedPaths.has(mountedPath))
|
|
706
|
+
continue;
|
|
707
|
+
seenMountedPaths.add(mountedPath);
|
|
708
|
+
if (declaredPaths.has(mountedPath))
|
|
709
|
+
continue;
|
|
710
|
+
const bestMatch = findBestMatch(patterns, mountedPath);
|
|
711
|
+
if (!bestMatch)
|
|
712
|
+
continue;
|
|
713
|
+
const matchingRouteElement = Children.toArray(children).find((node) => {
|
|
714
|
+
if (!isValidElement(node))
|
|
715
|
+
return false;
|
|
716
|
+
if (node.type !== Route)
|
|
717
|
+
return false;
|
|
718
|
+
return node.props.path === bestMatch.path;
|
|
719
|
+
});
|
|
720
|
+
if (!matchingRouteElement)
|
|
721
|
+
continue;
|
|
722
|
+
const routeInstance = React.cloneElement(matchingRouteElement, {
|
|
723
|
+
mountedPath,
|
|
724
|
+
});
|
|
725
|
+
dynamicRoutes.push(React.createElement(RouteContextProvider, { key: `dynamic:${mountedPath}`, patternPath: bestMatch.path, mountedPath: mountedPath }, routeInstance));
|
|
726
|
+
}
|
|
727
|
+
return [...declaredRoutes, ...dynamicRoutes];
|
|
728
|
+
}, [children, stack, preMountedPaths, routeState.activePath]);
|
|
528
729
|
return (React.createElement(RouteContext.Provider, { value: globalContextValue }, wrappedChildren));
|
|
529
730
|
}
|
|
530
731
|
/**
|
|
@@ -624,5 +825,5 @@ function useNavigate() {
|
|
|
624
825
|
}, []);
|
|
625
826
|
}
|
|
626
827
|
|
|
627
|
-
export { Route, Routes, WebFRouter, WebFRouterLink, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
|
|
828
|
+
export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
|
|
628
829
|
//# sourceMappingURL=index.esm.js.map
|