@openwebf/react-router 0.22.18 → 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 +89 -4
- package/dist/index.esm.js +367 -67
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +371 -66
- package/dist/index.js.map +1 -1
- package/dist/routes/Route.d.ts +13 -1
- package/dist/routes/Route.d.ts.map +1 -1
- package/dist/routes/Routes.d.ts +34 -0
- package/dist/routes/Routes.d.ts.map +1 -1
- package/dist/routes/utils.d.ts +40 -1
- package/dist/routes/utils.d.ts.map +1 -1
- package/dist/utils/RouterLink.d.ts +1 -0
- package/dist/utils/RouterLink.d.ts.map +1 -1
- package/package.json +2 -2
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,112 +62,231 @@ 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
|
};
|
|
222
|
+
/**
|
|
223
|
+
* Convert a route pattern to a regular expression
|
|
224
|
+
* @param pattern Route pattern like "/user/:userId" or "/category/:catId/product/:prodId"
|
|
225
|
+
* @returns Object with regex and parameter names
|
|
226
|
+
*/
|
|
227
|
+
function pathToRegex(pattern) {
|
|
228
|
+
const paramNames = [];
|
|
229
|
+
if (pattern === '*') {
|
|
230
|
+
paramNames.push('*');
|
|
231
|
+
return { regex: /^(.*)$/, paramNames };
|
|
232
|
+
}
|
|
233
|
+
// Escape special regex characters except : and *
|
|
234
|
+
let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
235
|
+
// Replace :param with named capture groups
|
|
236
|
+
regexPattern = regexPattern.replace(/:([^\/]+)/g, (_, paramName) => {
|
|
237
|
+
paramNames.push(paramName);
|
|
238
|
+
return '([^/]+)';
|
|
239
|
+
});
|
|
240
|
+
// Replace * with a splat capture group (matches across segments)
|
|
241
|
+
regexPattern = regexPattern.replace(/\*/g, () => {
|
|
242
|
+
paramNames.push('*');
|
|
243
|
+
return '(.*)';
|
|
244
|
+
});
|
|
245
|
+
// Add anchors for exact matching
|
|
246
|
+
regexPattern = `^${regexPattern}$`;
|
|
247
|
+
return {
|
|
248
|
+
regex: new RegExp(regexPattern),
|
|
249
|
+
paramNames
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Match a pathname against a route pattern and extract parameters
|
|
254
|
+
* @param pattern Route pattern like "/user/:userId"
|
|
255
|
+
* @param pathname Actual pathname like "/user/123"
|
|
256
|
+
* @returns Match result with extracted parameters or null if no match
|
|
257
|
+
*/
|
|
258
|
+
function matchPath(pattern, pathname) {
|
|
259
|
+
const { regex, paramNames } = pathToRegex(pattern);
|
|
260
|
+
const match = pathname.match(regex);
|
|
261
|
+
if (!match) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
// Extract parameters from capture groups
|
|
265
|
+
const params = {};
|
|
266
|
+
paramNames.forEach((paramName, index) => {
|
|
267
|
+
params[paramName] = match[index + 1]; // +1 because match[0] is the full match
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
path: pattern,
|
|
271
|
+
params,
|
|
272
|
+
isExact: true
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Find the best matching route from a list of route patterns
|
|
277
|
+
* @param routes Array of route patterns
|
|
278
|
+
* @param pathname Current pathname
|
|
279
|
+
* @returns Best match or null if no routes match
|
|
280
|
+
*/
|
|
281
|
+
function matchRoutes(routes, pathname) {
|
|
282
|
+
for (const route of routes) {
|
|
283
|
+
const match = matchPath(route, pathname);
|
|
284
|
+
if (match) {
|
|
285
|
+
return match;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
152
290
|
|
|
153
291
|
var isFunction = function (value) {
|
|
154
292
|
return typeof value === 'function';
|
|
@@ -184,7 +322,7 @@ const RawWebFRouterLink = createWebFComponent({
|
|
|
184
322
|
tagName: 'webf-router-link',
|
|
185
323
|
displayName: 'WebFRouterLink',
|
|
186
324
|
// Map props to attributes
|
|
187
|
-
attributeProps: ['path', 'title'],
|
|
325
|
+
attributeProps: ['path', 'title', 'theme'],
|
|
188
326
|
// Event handlers
|
|
189
327
|
events: [
|
|
190
328
|
{
|
|
@@ -213,7 +351,7 @@ const WebFRouterLink = function (props) {
|
|
|
213
351
|
props.onScreen(event);
|
|
214
352
|
}
|
|
215
353
|
};
|
|
216
|
-
return (React.createElement(RawWebFRouterLink, { title: props.title, path: props.path, onScreen: handleOnScreen, offScreen: props.offScreen }, isRender ? props.children : null));
|
|
354
|
+
return (React.createElement(RawWebFRouterLink, { title: props.title, path: props.path, theme: props.theme, onScreen: handleOnScreen, offScreen: props.offScreen }, isRender ? props.children : null));
|
|
217
355
|
};
|
|
218
356
|
|
|
219
357
|
/**
|
|
@@ -230,7 +368,7 @@ const WebFRouterLink = function (props) {
|
|
|
230
368
|
*
|
|
231
369
|
* Responsible for managing page rendering, lifecycle and navigation bar
|
|
232
370
|
*/
|
|
233
|
-
function Route({ path, prerender = false, element, title }) {
|
|
371
|
+
function Route({ path, mountedPath, prerender = false, element, title, theme }) {
|
|
234
372
|
// Mark whether the page has been rendered
|
|
235
373
|
const [hasRendered, updateRender] = useState(false);
|
|
236
374
|
/**
|
|
@@ -249,7 +387,7 @@ function Route({ path, prerender = false, element, title }) {
|
|
|
249
387
|
*/
|
|
250
388
|
const handleOffScreen = useMemoizedFn(() => {
|
|
251
389
|
});
|
|
252
|
-
return (React.createElement(WebFRouterLink, { path: path, title: title, 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));
|
|
253
391
|
}
|
|
254
392
|
|
|
255
393
|
/**
|
|
@@ -257,7 +395,9 @@ function Route({ path, prerender = false, element, title }) {
|
|
|
257
395
|
*/
|
|
258
396
|
const RouteContext = createContext({
|
|
259
397
|
path: undefined,
|
|
398
|
+
mountedPath: undefined,
|
|
260
399
|
params: undefined,
|
|
400
|
+
routeParams: undefined,
|
|
261
401
|
activePath: undefined,
|
|
262
402
|
routeEventKind: undefined
|
|
263
403
|
});
|
|
@@ -276,9 +416,9 @@ const RouteContext = createContext({
|
|
|
276
416
|
*/
|
|
277
417
|
function useRouteContext() {
|
|
278
418
|
const context = useContext(RouteContext);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
&& context.
|
|
419
|
+
const isActive = context.activePath !== undefined
|
|
420
|
+
&& context.mountedPath !== undefined
|
|
421
|
+
&& context.activePath === context.mountedPath;
|
|
282
422
|
return Object.assign(Object.assign({}, context), { isActive });
|
|
283
423
|
}
|
|
284
424
|
/**
|
|
@@ -292,6 +432,7 @@ function useRouteContext() {
|
|
|
292
432
|
* const location = useLocation();
|
|
293
433
|
*
|
|
294
434
|
* console.log('Current path:', location.pathname);
|
|
435
|
+
|
|
295
436
|
* console.log('Location state:', location.state);
|
|
296
437
|
* console.log('Is active:', location.isActive);
|
|
297
438
|
*
|
|
@@ -303,25 +444,43 @@ function useLocation() {
|
|
|
303
444
|
const context = useRouteContext();
|
|
304
445
|
// Create location object from context
|
|
305
446
|
const location = useMemo(() => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
isActive: true,
|
|
312
|
-
key: `${context.path}-active-${Date.now()}`
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
// For inactive routes, return the global location without state
|
|
447
|
+
const pathname = context.activePath || WebFRouter.path;
|
|
448
|
+
// Get state - prioritize context params, fallback to WebFRouter.state
|
|
449
|
+
const state = context.isActive
|
|
450
|
+
? (context.params || WebFRouter.state)
|
|
451
|
+
: WebFRouter.state;
|
|
316
452
|
return {
|
|
317
|
-
pathname
|
|
318
|
-
state
|
|
319
|
-
isActive:
|
|
320
|
-
key: `${
|
|
453
|
+
pathname,
|
|
454
|
+
state,
|
|
455
|
+
isActive: context.isActive,
|
|
456
|
+
key: `${pathname}-${Date.now()}`
|
|
321
457
|
};
|
|
322
|
-
}, [context.isActive, context.
|
|
458
|
+
}, [context.isActive, context.activePath, context.params]);
|
|
323
459
|
return location;
|
|
324
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Hook to get route parameters from dynamic routes
|
|
463
|
+
*
|
|
464
|
+
* @returns Route parameters object with parameter names as keys and values as strings
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* ```tsx
|
|
468
|
+
* // For route pattern "/user/:userId" and actual path "/user/123"
|
|
469
|
+
* function UserPage() {
|
|
470
|
+
* const params = useParams();
|
|
471
|
+
*
|
|
472
|
+
* console.log(params.userId); // "123"
|
|
473
|
+
*
|
|
474
|
+
* return <div>User ID: {params.userId}</div>;
|
|
475
|
+
* }
|
|
476
|
+
* ```
|
|
477
|
+
*/
|
|
478
|
+
function useParams() {
|
|
479
|
+
const context = useRouteContext();
|
|
480
|
+
return useMemo(() => {
|
|
481
|
+
return context.routeParams || {};
|
|
482
|
+
}, [context.routeParams]);
|
|
483
|
+
}
|
|
325
484
|
/**
|
|
326
485
|
* Routes component that wraps multiple Route components and provides shared context
|
|
327
486
|
*
|
|
@@ -337,50 +496,113 @@ function useLocation() {
|
|
|
337
496
|
/**
|
|
338
497
|
* Route-specific context provider that only updates when the route is active
|
|
339
498
|
*/
|
|
340
|
-
function RouteContextProvider({
|
|
499
|
+
function RouteContextProvider({ patternPath, mountedPath, children, }) {
|
|
341
500
|
const globalContext = useContext(RouteContext);
|
|
342
501
|
// Create a route-specific context that only updates when this route is active
|
|
343
502
|
const routeSpecificContext = useMemo(() => {
|
|
344
|
-
|
|
345
|
-
|
|
503
|
+
const isActive = globalContext.activePath !== undefined && globalContext.activePath === mountedPath;
|
|
504
|
+
const match = isActive ? matchPath(patternPath, mountedPath) : null;
|
|
505
|
+
if (isActive && match) {
|
|
506
|
+
const effectiveParams = globalContext.params !== undefined ? globalContext.params : WebFRouter.state;
|
|
346
507
|
return {
|
|
347
|
-
path,
|
|
348
|
-
|
|
508
|
+
path: patternPath,
|
|
509
|
+
mountedPath,
|
|
510
|
+
params: effectiveParams,
|
|
511
|
+
routeParams: match.params,
|
|
349
512
|
activePath: globalContext.activePath,
|
|
350
513
|
routeEventKind: globalContext.routeEventKind
|
|
351
514
|
};
|
|
352
515
|
}
|
|
353
|
-
// Return previous values if not active
|
|
354
516
|
return {
|
|
355
|
-
path,
|
|
517
|
+
path: patternPath,
|
|
518
|
+
mountedPath,
|
|
356
519
|
params: undefined,
|
|
520
|
+
routeParams: undefined,
|
|
357
521
|
activePath: globalContext.activePath,
|
|
358
522
|
routeEventKind: undefined
|
|
359
523
|
};
|
|
360
|
-
}, [
|
|
524
|
+
}, [patternPath, mountedPath, globalContext.activePath, globalContext.params, globalContext.routeEventKind]);
|
|
361
525
|
return (React.createElement(RouteContext.Provider, { value: routeSpecificContext }, children));
|
|
362
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
|
+
}
|
|
363
558
|
function Routes({ children }) {
|
|
364
559
|
// State to track current route information
|
|
365
560
|
const [routeState, setRouteState] = useState({
|
|
366
561
|
path: undefined,
|
|
367
|
-
|
|
562
|
+
mountedPath: undefined,
|
|
563
|
+
activePath: WebFRouter.path, // Initialize with current path
|
|
368
564
|
params: undefined,
|
|
565
|
+
routeParams: undefined,
|
|
369
566
|
routeEventKind: undefined
|
|
370
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]);
|
|
371
584
|
// Listen to hybridrouterchange event
|
|
372
585
|
useEffect(() => {
|
|
373
586
|
const handleRouteChange = (event) => {
|
|
587
|
+
var _a, _b, _c;
|
|
374
588
|
const routeEvent = event;
|
|
375
|
-
//
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
589
|
+
// Check for new event detail structure with params
|
|
590
|
+
const eventDetail = event.detail;
|
|
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;
|
|
379
599
|
// Update state based on event kind
|
|
380
600
|
setRouteState({
|
|
381
601
|
path: routeEvent.path,
|
|
602
|
+
mountedPath: routeEvent.path,
|
|
382
603
|
activePath: newActivePath,
|
|
383
|
-
params:
|
|
604
|
+
params: eventState,
|
|
605
|
+
routeParams: routeParams, // Use params from Flutter if available
|
|
384
606
|
routeEventKind: routeEvent.kind
|
|
385
607
|
});
|
|
386
608
|
};
|
|
@@ -390,30 +612,108 @@ function Routes({ children }) {
|
|
|
390
612
|
return () => {
|
|
391
613
|
document.removeEventListener('hybridrouterchange', handleRouteChange);
|
|
392
614
|
};
|
|
393
|
-
}, [
|
|
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]);
|
|
394
653
|
// Global context value
|
|
395
654
|
const globalContextValue = useMemo(() => ({
|
|
396
655
|
path: undefined,
|
|
656
|
+
mountedPath: undefined,
|
|
397
657
|
params: routeState.params,
|
|
658
|
+
routeParams: routeState.routeParams, // Pass through route params from Flutter
|
|
398
659
|
activePath: routeState.activePath,
|
|
399
660
|
routeEventKind: routeState.routeEventKind
|
|
400
|
-
}), [routeState.activePath, routeState.params, routeState.routeEventKind]);
|
|
401
|
-
// Wrap each Route component with its own context provider
|
|
661
|
+
}), [routeState.activePath, routeState.params, routeState.routeParams, routeState.routeEventKind]);
|
|
402
662
|
const wrappedChildren = useMemo(() => {
|
|
403
|
-
|
|
663
|
+
const declaredRoutes = [];
|
|
664
|
+
const patterns = [];
|
|
665
|
+
const declaredPaths = new Set();
|
|
666
|
+
Children.forEach(children, (child) => {
|
|
667
|
+
var _a;
|
|
404
668
|
if (!isValidElement(child)) {
|
|
405
|
-
|
|
669
|
+
declaredRoutes.push(child);
|
|
670
|
+
return;
|
|
406
671
|
}
|
|
407
|
-
// Ensure only Route components are direct children
|
|
408
672
|
if (child.type !== Route) {
|
|
409
673
|
console.warn('Routes component should only contain Route components as direct children');
|
|
410
|
-
|
|
674
|
+
declaredRoutes.push(child);
|
|
675
|
+
return;
|
|
411
676
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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));
|
|
415
682
|
});
|
|
416
|
-
|
|
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]);
|
|
417
717
|
return (React.createElement(RouteContext.Provider, { value: globalContextValue }, wrappedChildren));
|
|
418
718
|
}
|
|
419
719
|
/**
|
|
@@ -443,7 +743,7 @@ function useRoutes(routes) {
|
|
|
443
743
|
if (route.children && route.children.length > 0) {
|
|
444
744
|
console.warn('Nested routes are not supported yet');
|
|
445
745
|
}
|
|
446
|
-
return (React.createElement(Route, { key: route.path, path: route.path, element: route.element, prerender: route.prerender }));
|
|
746
|
+
return (React.createElement(Route, { key: route.path, path: route.path, element: route.element, prerender: route.prerender, theme: route.theme }));
|
|
447
747
|
});
|
|
448
748
|
}, [routes]);
|
|
449
749
|
// Return Routes component with Route children
|
|
@@ -513,5 +813,5 @@ function useNavigate() {
|
|
|
513
813
|
}, []);
|
|
514
814
|
}
|
|
515
815
|
|
|
516
|
-
export { Route, Routes, WebFRouter, WebFRouterLink, useLocation, useNavigate, useRouteContext, useRoutes };
|
|
816
|
+
export { Route, Routes, WebFRouter, WebFRouterLink, __unstable_setEnsureRouteMountedCallback, matchPath, matchRoutes, pathToRegex, useLocation, useNavigate, useParams, useRouteContext, useRoutes };
|
|
517
817
|
//# sourceMappingURL=index.esm.js.map
|