@aminnairi/react-router 2.2.0 → 3.0.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/README.md +283 -193
- package/index.tsx +416 -278
- package/package.json +4 -1
- package/.eslintrc.cjs +0 -13
- package/tsconfig.json +0 -16
package/index.tsx
CHANGED
|
@@ -1,389 +1,527 @@
|
|
|
1
|
-
|
|
2
|
-
import { useEffect, useState, FunctionComponent, useMemo, Component, PropsWithChildren, createContext, SetStateAction, Dispatch, ReactNode, useContext, useCallback, memo, MouseEvent, ComponentProps, JSXElementConstructor, ElementType, ComponentPropsWithoutRef, MouseEventHandler } from "react";
|
|
3
|
-
|
|
4
|
-
export type AbsolutePath<Path extends string> =
|
|
5
|
-
Path extends `${infer Start}:${string}/${infer Rest}`
|
|
6
|
-
? `${Start}${string}/${AbsolutePath<Rest>}`
|
|
7
|
-
: Path extends `${infer Start}:${string}`
|
|
8
|
-
? `${Start}${string}`
|
|
9
|
-
: Path;
|
|
10
|
-
|
|
11
|
-
export type Parameters<Path extends string> =
|
|
12
|
-
Path extends `${string}/:${infer Segment}/${infer Rest}`
|
|
13
|
-
? { [K in Segment]: string } & Parameters<`/${Rest}`>
|
|
14
|
-
: Path extends `${string}/:${infer Segment}`
|
|
15
|
-
? { [K in Segment]: string }
|
|
16
|
-
: object;
|
|
17
|
-
|
|
18
|
-
export type GoToPageFunction<Path extends string> = (path: AbsolutePath<Path>) => void;
|
|
19
|
-
|
|
20
|
-
export interface PageComponentProps<Path extends string> {
|
|
21
|
-
parameters: Parameters<Path>,
|
|
22
|
-
}
|
|
1
|
+
import { Component, createContext, type Dispatch, type FunctionComponent, type ReactNode, type SetStateAction, useCallback, useContext, useEffect, useEffectEvent, useMemo, useState } from "react";
|
|
23
2
|
|
|
24
|
-
export interface
|
|
25
|
-
|
|
26
|
-
|
|
3
|
+
export interface IssueProps {
|
|
4
|
+
error: Error | null,
|
|
5
|
+
resetError: () => void
|
|
27
6
|
}
|
|
28
7
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
transition?: Transition
|
|
33
|
-
prefix?: string,
|
|
34
|
-
pages: Array<Page<Path>>
|
|
8
|
+
export interface Router<Path extends string, Locale> {
|
|
9
|
+
prefix?: string
|
|
10
|
+
locales?: Locale[]
|
|
11
|
+
transition?: Transition
|
|
35
12
|
fallback: FunctionComponent
|
|
36
13
|
issue: FunctionComponent<IssueProps>
|
|
14
|
+
pages: Array<Page<Path>>
|
|
37
15
|
}
|
|
38
16
|
|
|
39
|
-
export interface
|
|
40
|
-
|
|
41
|
-
path: string
|
|
17
|
+
export interface RouterProviderProps {
|
|
18
|
+
children: ReactNode
|
|
42
19
|
}
|
|
43
20
|
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
21
|
+
export interface RouterContextInterface<Locale> {
|
|
22
|
+
locale: Locale | null
|
|
23
|
+
prefix: string | null
|
|
24
|
+
path: string
|
|
25
|
+
setLocale: Dispatch<SetStateAction<Locale | null>>
|
|
47
26
|
}
|
|
48
27
|
|
|
28
|
+
export type NavigationDirection = "forward" | "backward";
|
|
49
29
|
|
|
50
|
-
|
|
51
|
-
return
|
|
30
|
+
function normalize(uri: string) {
|
|
31
|
+
return uri
|
|
32
|
+
.trim()
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/\/+/g, "/")
|
|
35
|
+
.replace(/^\/+|\/$/g, "");
|
|
52
36
|
}
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
const pathParts =
|
|
56
|
-
const
|
|
38
|
+
function matchPath(path: string, pathname: string) {
|
|
39
|
+
const pathParts = normalize(path).split("/").filter(Boolean);
|
|
40
|
+
const pathnameParts = normalize(pathname).split("/").filter(Boolean);
|
|
57
41
|
|
|
58
|
-
return (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
42
|
+
return pathParts.length === pathnameParts.length && pathParts.every((pathPart, index) => {
|
|
43
|
+
return pathPart.startsWith(":") || pathPart === pathnameParts.at(index);
|
|
44
|
+
});
|
|
62
45
|
}
|
|
63
46
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const pathParts = sanitizePath(`${prefix ?? ""}/${path}`).split("/").filter(Boolean);
|
|
70
|
-
const routeParts = sanitizePath(route).split("/").filter(Boolean);
|
|
47
|
+
function matchParameters(path: string, pathname: string): Record<string, string> {
|
|
48
|
+
const pathParts = normalize(path).split("/").filter(Boolean);
|
|
49
|
+
const pathnameParts = normalize(pathname).split("/").filter(Boolean);
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!routePart) {
|
|
76
|
-
return parameters;
|
|
77
|
-
}
|
|
51
|
+
if (pathParts.length !== pathnameParts.length) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
78
54
|
|
|
55
|
+
return pathParts.reduce((parameters, pathPart, index) => {
|
|
79
56
|
if (!pathPart.startsWith(":")) {
|
|
80
57
|
return parameters;
|
|
81
58
|
}
|
|
82
59
|
|
|
83
60
|
return {
|
|
84
61
|
...parameters,
|
|
85
|
-
[
|
|
62
|
+
[pathPart.slice(1)]: pathnameParts.at(index) ?? ""
|
|
86
63
|
}
|
|
87
|
-
}, {}
|
|
64
|
+
}, {});
|
|
88
65
|
}
|
|
89
66
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return doesRouteMatchPath(sanitizePath(`${prefix ?? ""}/${route.path}`), sanitizePath(path));
|
|
93
|
-
});
|
|
67
|
+
export class Uri<Locale> {
|
|
68
|
+
private constructor(public readonly path: string, public readonly prefix: string | null, public readonly locale: Locale | null) { }
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
|
|
70
|
+
public static from<Locale>(uri: string, expectedPrefix?: string, expectedLocales?: Locale[]): Uri<Locale> {
|
|
71
|
+
const [prefixOrLocale, localeOrNothing, ...parts] = normalize(uri).split("/");
|
|
72
|
+
const locales = expectedLocales ?? [];
|
|
97
73
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
74
|
+
if (expectedPrefix && prefixOrLocale && prefixOrLocale === normalize(expectedPrefix)) {
|
|
75
|
+
const locale = locales.find(expectedLocale => expectedLocale === localeOrNothing);
|
|
76
|
+
|
|
77
|
+
if (locale) {
|
|
78
|
+
return new Uri<Locale>(
|
|
79
|
+
parts.join("/"),
|
|
80
|
+
prefixOrLocale,
|
|
81
|
+
locale,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return new Uri<Locale>(
|
|
86
|
+
[localeOrNothing, ...parts].join("/"),
|
|
87
|
+
prefixOrLocale,
|
|
88
|
+
null
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const locale = locales.find(expectedLocale => expectedLocale === prefixOrLocale);
|
|
93
|
+
|
|
94
|
+
if (locale) {
|
|
95
|
+
return new Uri<Locale>(
|
|
96
|
+
[localeOrNothing, ...parts].join("/"),
|
|
97
|
+
null,
|
|
98
|
+
locale
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new Uri<Locale>(
|
|
103
|
+
[prefixOrLocale, localeOrNothing, ...parts].join("/"),
|
|
104
|
+
null,
|
|
105
|
+
null
|
|
106
|
+
);
|
|
107
|
+
}
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
export type ExtractParams<Path extends string> =
|
|
111
|
+
Path extends `${string}:${infer Param}/${infer Rest}`
|
|
112
|
+
? Param | ExtractParams<Rest>
|
|
113
|
+
: Path extends `${string}:${infer Param}`
|
|
114
|
+
? Param
|
|
115
|
+
: never
|
|
116
|
+
|
|
117
|
+
export type Params<Path extends string> = {
|
|
118
|
+
[Key in ExtractParams<Path>]: string
|
|
106
119
|
}
|
|
107
120
|
|
|
108
|
-
export interface
|
|
109
|
-
|
|
121
|
+
export interface PageParams<Path extends string> {
|
|
122
|
+
parameters: Params<Path>
|
|
110
123
|
}
|
|
111
124
|
|
|
112
|
-
export
|
|
113
|
-
|
|
125
|
+
export interface Page<Path extends string> {
|
|
126
|
+
path: Path
|
|
127
|
+
element: FunctionComponent<PageParams<Path>>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface ErrorBoundaryProps {
|
|
131
|
+
children: ReactNode;
|
|
132
|
+
issue: FunctionComponent<IssueProps>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, IssueProps> {
|
|
136
|
+
public constructor(props: ErrorBoundaryProps) {
|
|
114
137
|
super(props);
|
|
115
138
|
|
|
116
|
-
this.state = {
|
|
139
|
+
this.state = {
|
|
140
|
+
error: null,
|
|
141
|
+
resetError: this.resetError.bind(this)
|
|
142
|
+
};
|
|
117
143
|
}
|
|
118
144
|
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
private resetError() {
|
|
146
|
+
this.setState({ error: null });
|
|
147
|
+
}
|
|
121
148
|
|
|
122
|
-
|
|
149
|
+
public static getDerivedStateFromError(error: Error) {
|
|
150
|
+
return {
|
|
151
|
+
error
|
|
152
|
+
};
|
|
123
153
|
}
|
|
124
154
|
|
|
125
|
-
public override
|
|
126
|
-
|
|
155
|
+
public override componentDidCatch(error: unknown) {
|
|
156
|
+
this.setState({
|
|
157
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
158
|
+
});
|
|
159
|
+
}
|
|
127
160
|
|
|
161
|
+
public override render() {
|
|
128
162
|
if (this.state.error) {
|
|
129
|
-
const
|
|
130
|
-
if (this.props.transition && viewTransitionSupported) {
|
|
131
|
-
document.startViewTransition(() => {
|
|
132
|
-
this.setState({
|
|
133
|
-
error: null
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
163
|
+
const Issue = this.props.issue;
|
|
139
164
|
|
|
140
|
-
|
|
141
|
-
error: null
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return this.props.fallback({
|
|
146
|
-
error: this.state.error,
|
|
147
|
-
reset
|
|
148
|
-
});
|
|
165
|
+
return <Issue error={this.state.error} resetError={this.state.resetError} />;
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
return this.props.children;
|
|
152
169
|
}
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
export
|
|
156
|
-
return
|
|
172
|
+
export function createPage<Path extends string>(page: Page<Path>): Page<Path> {
|
|
173
|
+
return {
|
|
174
|
+
...page,
|
|
175
|
+
path: normalize(page.path) as Path
|
|
176
|
+
};
|
|
157
177
|
}
|
|
158
178
|
|
|
159
|
-
export
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
179
|
+
export type Transition = (direction: "forward" | "backward", next: () => void) => void;
|
|
180
|
+
|
|
181
|
+
export const scaleFadeTransition: Transition = async (direction: NavigationDirection, next) => {
|
|
182
|
+
try {
|
|
183
|
+
const transition = document.startViewTransition(() => {
|
|
184
|
+
next();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await transition.ready;
|
|
188
|
+
|
|
189
|
+
document.documentElement.animate(
|
|
190
|
+
[
|
|
191
|
+
{
|
|
192
|
+
transform: 'scale(1)',
|
|
193
|
+
opacity: 1
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
transform: direction === "forward" ? "scale(1.04)" : "scale(0.96)",
|
|
197
|
+
opacity: 0
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
{
|
|
201
|
+
duration: 200,
|
|
202
|
+
easing: "ease-in-out",
|
|
203
|
+
fill: "both",
|
|
204
|
+
pseudoElement: `::view-transition-old(root)`,
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
document.documentElement.animate(
|
|
209
|
+
[
|
|
210
|
+
{
|
|
211
|
+
transform: direction === "forward" ? "scale(0.96)" : "scale(1.04)",
|
|
212
|
+
opacity: 0
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
transform: 'scale(1)',
|
|
216
|
+
opacity: 1
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
{
|
|
220
|
+
duration: 200,
|
|
221
|
+
easing: "ease-in-out",
|
|
222
|
+
fill: "both",
|
|
223
|
+
pseudoElement: `::view-transition-new(root)`,
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error(error);
|
|
228
|
+
}
|
|
167
229
|
}
|
|
168
230
|
|
|
169
|
-
export
|
|
170
|
-
|
|
231
|
+
export const crossFadeTransition: Transition = async (direction: NavigationDirection, next) => {
|
|
232
|
+
try {
|
|
233
|
+
const transition = document.startViewTransition(() => {
|
|
234
|
+
next();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await transition.ready;
|
|
238
|
+
|
|
239
|
+
document.documentElement.animate(
|
|
240
|
+
[
|
|
241
|
+
{ opacity: 1 },
|
|
242
|
+
{ opacity: 0 }
|
|
243
|
+
],
|
|
244
|
+
{
|
|
245
|
+
duration: 200,
|
|
246
|
+
easing: "ease-in-out",
|
|
247
|
+
fill: "both",
|
|
248
|
+
pseudoElement: `::view-transition-old(root)`,
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
document.documentElement.animate(
|
|
253
|
+
[
|
|
254
|
+
{ opacity: 0 },
|
|
255
|
+
{ opacity: 1 }
|
|
256
|
+
],
|
|
257
|
+
{
|
|
258
|
+
duration: 200,
|
|
259
|
+
easing: "ease-in-out",
|
|
260
|
+
fill: "both",
|
|
261
|
+
pseudoElement: `::view-transition-new(root)`,
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error(error);
|
|
266
|
+
}
|
|
171
267
|
}
|
|
172
268
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
269
|
+
export const slideHorizontalTransition: Transition = async (direction: NavigationDirection, next) => {
|
|
270
|
+
try {
|
|
271
|
+
const transition = document.startViewTransition(() => {
|
|
272
|
+
next();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await transition.ready;
|
|
276
|
+
|
|
277
|
+
document.documentElement.animate(
|
|
278
|
+
[
|
|
279
|
+
{ transform: 'translateX(0)' },
|
|
280
|
+
{ transform: direction === "forward" ? 'translateX(-100%)' : 'translateX(100%)' }
|
|
281
|
+
],
|
|
282
|
+
{
|
|
283
|
+
duration: 200,
|
|
284
|
+
easing: "ease-in-out",
|
|
285
|
+
fill: "both",
|
|
286
|
+
pseudoElement: `::view-transition-old(root)`,
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
document.documentElement.animate(
|
|
291
|
+
[
|
|
292
|
+
{ transform: direction === "forward" ? 'translateX(100%)' : 'translateX(-100%)' },
|
|
293
|
+
{ transform: 'translateX(0)' }
|
|
294
|
+
],
|
|
295
|
+
{
|
|
296
|
+
duration: 200,
|
|
297
|
+
easing: "ease-in-out",
|
|
298
|
+
fill: "both",
|
|
299
|
+
pseudoElement: `::view-transition-new(root)`,
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(error);
|
|
304
|
+
}
|
|
186
305
|
}
|
|
187
306
|
|
|
188
|
-
export const
|
|
189
|
-
|
|
307
|
+
export const slideVerticalTransition: Transition = async (direction: NavigationDirection, next) => {
|
|
308
|
+
try {
|
|
309
|
+
const transition = document.startViewTransition(() => {
|
|
310
|
+
next();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await transition.ready;
|
|
314
|
+
|
|
315
|
+
document.documentElement.animate(
|
|
316
|
+
[
|
|
317
|
+
{ transform: 'translateY(0)' },
|
|
318
|
+
{ transform: direction === "forward" ? 'translateY(-100%)' : 'translateY(100%)' }
|
|
319
|
+
],
|
|
320
|
+
{
|
|
321
|
+
duration: 200,
|
|
322
|
+
easing: "ease-in-out",
|
|
323
|
+
fill: "both",
|
|
324
|
+
pseudoElement: `::view-transition-old(root)`,
|
|
325
|
+
}
|
|
326
|
+
);
|
|
190
327
|
|
|
191
|
-
|
|
192
|
-
|
|
328
|
+
document.documentElement.animate(
|
|
329
|
+
[
|
|
330
|
+
{ transform: direction === "forward" ? 'translateY(100%)' : 'translateY(-100%)' },
|
|
331
|
+
{ transform: 'translateY(0)' }
|
|
332
|
+
],
|
|
333
|
+
{
|
|
334
|
+
duration: 200,
|
|
335
|
+
easing: "ease-in-out",
|
|
336
|
+
fill: "both",
|
|
337
|
+
pseudoElement: `::view-transition-new(root)`,
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error(error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
193
344
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
345
|
+
export function createRouter<Locale extends string = never, Path extends string = never>({ prefix: expectedPrefix, locales, pages, fallback: Fallback, issue: Issue, transition }: Router<Path, Locale>) {
|
|
346
|
+
const RouterContext = createContext<RouterContextInterface<Locale>>({
|
|
347
|
+
locale: null,
|
|
348
|
+
prefix: null,
|
|
349
|
+
path: "/",
|
|
350
|
+
setLocale: () => { },
|
|
351
|
+
});
|
|
197
352
|
|
|
198
|
-
if (replace) {
|
|
199
|
-
window.history.replaceState(null, pathWithParameters, pathWithParameters);
|
|
200
|
-
} else {
|
|
201
|
-
window.history.pushState(null, pathWithParameters, pathWithParameters);
|
|
202
|
-
}
|
|
203
353
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
354
|
+
function useIsActivePage<P extends Path>(page: Page<P>): boolean {
|
|
355
|
+
const { path } = usePath();
|
|
356
|
+
return matchPath(page.path, path);
|
|
357
|
+
}
|
|
207
358
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
window.dispatchEvent(new CustomEvent(NavigationDirection.Backward));
|
|
211
|
-
}, []);
|
|
212
|
-
}
|
|
359
|
+
function useLocale() {
|
|
360
|
+
const context = useContext(RouterContext);
|
|
213
361
|
|
|
214
|
-
|
|
215
|
-
|
|
362
|
+
if (!context) {
|
|
363
|
+
throw new Error("component using the useLocale hook has not been wrapped inside RouterProvider.");
|
|
364
|
+
}
|
|
216
365
|
|
|
217
|
-
|
|
218
|
-
|
|
366
|
+
return {
|
|
367
|
+
locale: context.locale,
|
|
368
|
+
setLocale: (locale: Locale) => {
|
|
369
|
+
if (!locales?.includes(locale)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
219
372
|
|
|
220
|
-
|
|
221
|
-
|
|
373
|
+
window.history.pushState(null, "", `/${normalize(`${context.prefix ?? ""}/${locale}/${context.path}`)}`);
|
|
374
|
+
window.dispatchEvent(new Event("pushstate"));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
222
378
|
|
|
223
|
-
|
|
224
|
-
|
|
379
|
+
function usePrefix() {
|
|
380
|
+
const context = useContext(RouterContext);
|
|
225
381
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
};
|
|
382
|
+
if (!context) {
|
|
383
|
+
throw new Error("Component using the usePrefix hook has not been wrapped inside RouterProvider");
|
|
384
|
+
}
|
|
230
385
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
386
|
+
return {
|
|
387
|
+
prefix: context.prefix
|
|
388
|
+
};
|
|
389
|
+
}
|
|
235
390
|
|
|
236
|
-
|
|
391
|
+
function usePath() {
|
|
392
|
+
const context = useContext(RouterContext);
|
|
237
393
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const navigateToPage = useNavigateToPage(page);
|
|
394
|
+
if (!context) {
|
|
395
|
+
throw new Error("Component using the usePath hook has not been wrapped inside RouterProvider");
|
|
396
|
+
}
|
|
242
397
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}, [prefix, page, parameters]);
|
|
398
|
+
return {
|
|
399
|
+
path: context.path,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
248
402
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}, [navigateToPage, parameters]);
|
|
403
|
+
function useNavigateToPage<P extends Path>(page: Page<P>) {
|
|
404
|
+
const { locale } = useLocale();
|
|
405
|
+
const { prefix } = usePrefix();
|
|
253
406
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
407
|
+
return useCallback((...[params]: ExtractParams<P> extends never ? [] : [Params<P>]) => {
|
|
408
|
+
const path = Object.entries(params ?? {}).reduce<string>((oldParams, [name, value]) => {
|
|
409
|
+
return oldParams.replace(`:${name}`, String(value));
|
|
410
|
+
}, page.path);
|
|
257
411
|
|
|
258
|
-
|
|
259
|
-
<a href={path} onClick={onClick}>
|
|
260
|
-
{children}
|
|
261
|
-
</a>
|
|
262
|
-
);
|
|
263
|
-
});
|
|
412
|
+
const pathname = `/${normalize(`${prefix ?? ""}/${locale ?? ""}/${path}`)}`
|
|
264
413
|
|
|
265
|
-
|
|
266
|
-
};
|
|
414
|
+
console.log({ pathname });
|
|
267
415
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
416
|
+
window.history.pushState(null, "", pathname);
|
|
417
|
+
window.dispatchEvent(new Event("pushstate"));
|
|
418
|
+
}, [page, locale, prefix]);
|
|
419
|
+
}
|
|
272
420
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
[
|
|
277
|
-
|
|
278
|
-
{ transform: `translateX(${direction === NavigationDirection.Forward ? '100%' : '-100%'})`, opacity: 0 }
|
|
279
|
-
],
|
|
280
|
-
{
|
|
281
|
-
duration: 250,
|
|
282
|
-
easing: "ease-in-out",
|
|
283
|
-
fill: "both",
|
|
284
|
-
pseudoElement: "::view-transition-old(root)",
|
|
285
|
-
}
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
document.documentElement.animate(
|
|
289
|
-
[
|
|
290
|
-
{ transform: `translateX(${direction === NavigationDirection.Forward ? '-100%' : '100%'})`, opacity: 0 },
|
|
291
|
-
{ transform: 'translateX(0)', opacity: 1 }
|
|
292
|
-
],
|
|
293
|
-
{
|
|
294
|
-
duration: 250,
|
|
295
|
-
easing: "ease-in-out",
|
|
296
|
-
fill: "both",
|
|
297
|
-
pseudoElement: "::view-transition-new(root)",
|
|
298
|
-
}
|
|
299
|
-
);
|
|
300
|
-
}
|
|
421
|
+
function RouterProvider({ children }: RouterProviderProps) {
|
|
422
|
+
const uri = useMemo(() => Uri.from(window.location.pathname, expectedPrefix, locales), [expectedPrefix, locales]);
|
|
423
|
+
const [locale, setLocale] = useState(uri.locale);
|
|
424
|
+
const [path, setPath] = useState(uri.path);
|
|
425
|
+
const [prefix, setPrefix] = useState(uri.prefix);
|
|
301
426
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const [search, setSearch] = useState(new URLSearchParams(sanitizePath(window.location.search)));
|
|
306
|
-
const [hash, setHash] = useState(window.location.hash);
|
|
427
|
+
function setHash(newHash: string) {
|
|
428
|
+
window.location.hash = newHash;
|
|
429
|
+
}
|
|
307
430
|
|
|
308
431
|
const value = useMemo(() => {
|
|
309
432
|
return {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
setSearch,
|
|
316
|
-
setHash
|
|
433
|
+
locale,
|
|
434
|
+
path,
|
|
435
|
+
prefix: prefix ?? null,
|
|
436
|
+
setLocale,
|
|
437
|
+
setHash,
|
|
317
438
|
};
|
|
318
|
-
}, [
|
|
439
|
+
}, [locale, prefix, path]);
|
|
319
440
|
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
transition(direction, () => {
|
|
324
|
-
setPathname(sanitizePath(window.location.pathname));
|
|
325
|
-
});
|
|
441
|
+
const onNavigation = useEffectEvent((direction: NavigationDirection) => {
|
|
442
|
+
const pathname = normalize(window.location.pathname);
|
|
443
|
+
const uri = Uri.from(pathname, expectedPrefix, locales);
|
|
326
444
|
|
|
327
|
-
|
|
328
|
-
|
|
445
|
+
const defaultTransition: Transition = (_, next) => {
|
|
446
|
+
next();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const currentTransition: Transition = transition ?? defaultTransition;
|
|
450
|
+
|
|
451
|
+
currentTransition(direction, () => {
|
|
452
|
+
setLocale(uri.locale);
|
|
453
|
+
setPath(uri.path);
|
|
454
|
+
setPrefix(uri.prefix);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const onMount = useEffectEvent(() => {
|
|
459
|
+
const pathname = `/${normalize(window.location.pathname)}`;
|
|
460
|
+
const uri = Uri.from(pathname, expectedPrefix, [locale, ...locales ?? []]);
|
|
461
|
+
const newPathname = `/${normalize(`${uri.prefix ?? expectedPrefix ?? ""}/${uri.locale ?? locales?.at(0) ?? ""}/${uri.path}`)}`;
|
|
329
462
|
|
|
330
|
-
|
|
463
|
+
if (newPathname !== pathname) {
|
|
464
|
+
window.history.pushState(null, "", newPathname);
|
|
465
|
+
window.dispatchEvent(new Event("pushstate"));
|
|
331
466
|
}
|
|
467
|
+
});
|
|
332
468
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
};
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
const abortController = new AbortController();
|
|
336
471
|
|
|
337
|
-
|
|
338
|
-
onNavigation(
|
|
339
|
-
};
|
|
472
|
+
window.addEventListener("pushstate", () => {
|
|
473
|
+
onNavigation("forward");
|
|
474
|
+
}, abortController);
|
|
475
|
+
|
|
476
|
+
window.addEventListener("popstate", () => {
|
|
477
|
+
onNavigation("backward");
|
|
478
|
+
}, abortController);
|
|
340
479
|
|
|
341
|
-
|
|
342
|
-
window.addEventListener(NavigationDirection.Backward, onNavigationBackward);
|
|
480
|
+
onMount();
|
|
343
481
|
|
|
344
482
|
return () => {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
483
|
+
abortController.abort();
|
|
484
|
+
};
|
|
348
485
|
}, []);
|
|
349
486
|
|
|
350
487
|
return (
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
</ErrorBoundary>
|
|
355
|
-
</Context.Provider>
|
|
488
|
+
<RouterContext.Provider value={value}>
|
|
489
|
+
{children}
|
|
490
|
+
</RouterContext.Provider>
|
|
356
491
|
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const View = () => {
|
|
360
|
-
const Fallback = useMemo(() => fallback, []);
|
|
361
|
-
const { pathname } = useContext(Context);
|
|
362
|
-
const page = useMemo(() => findPage(pages, pathname, prefix), [pathname]);
|
|
492
|
+
}
|
|
363
493
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return getParameters(sanitizePath(page.path), sanitizePath(window.location.pathname), prefix);
|
|
367
|
-
}
|
|
494
|
+
function RouterView() {
|
|
495
|
+
const { path } = usePath();
|
|
368
496
|
|
|
369
|
-
|
|
370
|
-
|
|
497
|
+
const foundPage = useMemo(() => {
|
|
498
|
+
return pages.find(page => {
|
|
499
|
+
return matchPath(page.path, path);
|
|
500
|
+
});
|
|
501
|
+
}, [path, pages]);
|
|
371
502
|
|
|
372
|
-
if (
|
|
503
|
+
if (foundPage) {
|
|
373
504
|
return (
|
|
374
|
-
<
|
|
375
|
-
<
|
|
376
|
-
</
|
|
505
|
+
<ErrorBoundary issue={Issue}>
|
|
506
|
+
<foundPage.element parameters={matchParameters(foundPage.path, path) as Params<Path>} />
|
|
507
|
+
</ErrorBoundary>
|
|
377
508
|
);
|
|
378
509
|
}
|
|
379
510
|
|
|
380
511
|
return (
|
|
381
|
-
<
|
|
512
|
+
<ErrorBoundary issue={Issue}>
|
|
513
|
+
<Fallback />
|
|
514
|
+
</ErrorBoundary>
|
|
382
515
|
);
|
|
383
|
-
}
|
|
516
|
+
}
|
|
384
517
|
|
|
385
518
|
return {
|
|
386
|
-
|
|
387
|
-
|
|
519
|
+
RouterProvider,
|
|
520
|
+
RouterView,
|
|
521
|
+
useLocale,
|
|
522
|
+
usePrefix,
|
|
523
|
+
usePath,
|
|
524
|
+
useNavigateToPage,
|
|
525
|
+
useIsActivePage,
|
|
388
526
|
};
|
|
389
527
|
}
|