@tanstack/react-router 0.0.1-beta.22 → 0.0.1-beta.221

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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/build/cjs/CatchBoundary.js +126 -0
  3. package/build/cjs/CatchBoundary.js.map +1 -0
  4. package/build/cjs/Matches.js +235 -0
  5. package/build/cjs/Matches.js.map +1 -0
  6. package/build/cjs/RouterProvider.js +1085 -0
  7. package/build/cjs/RouterProvider.js.map +1 -0
  8. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +1 -19
  9. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -1
  10. package/build/cjs/awaited.js +45 -0
  11. package/build/cjs/awaited.js.map +1 -0
  12. package/build/cjs/defer.js +39 -0
  13. package/build/cjs/defer.js.map +1 -0
  14. package/build/cjs/fileRoute.js +29 -0
  15. package/build/cjs/fileRoute.js.map +1 -0
  16. package/build/cjs/index.js +135 -0
  17. package/build/cjs/index.js.map +1 -0
  18. package/build/cjs/lazyRouteComponent.js +57 -0
  19. package/build/cjs/lazyRouteComponent.js.map +1 -0
  20. package/build/cjs/link.js +151 -0
  21. package/build/cjs/link.js.map +1 -0
  22. package/build/cjs/path.js +211 -0
  23. package/build/cjs/path.js.map +1 -0
  24. package/build/cjs/qss.js +65 -0
  25. package/build/cjs/qss.js.map +1 -0
  26. package/build/cjs/redirects.js +27 -0
  27. package/build/cjs/redirects.js.map +1 -0
  28. package/build/cjs/route.js +139 -0
  29. package/build/cjs/route.js.map +1 -0
  30. package/build/cjs/router.js +160 -0
  31. package/build/cjs/router.js.map +1 -0
  32. package/build/cjs/scroll-restoration.js +186 -0
  33. package/build/cjs/scroll-restoration.js.map +1 -0
  34. package/build/cjs/searchParams.js +83 -0
  35. package/build/cjs/searchParams.js.map +1 -0
  36. package/build/cjs/useBlocker.js +64 -0
  37. package/build/cjs/useBlocker.js.map +1 -0
  38. package/build/cjs/useNavigate.js +78 -0
  39. package/build/cjs/useNavigate.js.map +1 -0
  40. package/build/cjs/useParams.js +28 -0
  41. package/build/cjs/useParams.js.map +1 -0
  42. package/build/cjs/useSearch.js +27 -0
  43. package/build/cjs/useSearch.js.map +1 -0
  44. package/build/cjs/utils.js +236 -0
  45. package/build/cjs/utils.js.map +1 -0
  46. package/build/esm/index.js +2136 -2564
  47. package/build/esm/index.js.map +1 -1
  48. package/build/stats-html.html +59 -49
  49. package/build/stats-react.json +922 -43
  50. package/build/types/CatchBoundary.d.ts +33 -0
  51. package/build/types/Matches.d.ts +34 -0
  52. package/build/types/RouterProvider.d.ts +87 -0
  53. package/build/types/awaited.d.ts +8 -0
  54. package/build/types/defer.d.ts +19 -0
  55. package/build/types/fileRoute.d.ts +32 -0
  56. package/build/types/history.d.ts +7 -0
  57. package/build/types/index.d.ts +27 -104
  58. package/build/types/injectHtml.d.ts +0 -0
  59. package/build/types/lazyRouteComponent.d.ts +2 -0
  60. package/build/types/link.d.ts +105 -0
  61. package/build/types/location.d.ts +14 -0
  62. package/build/types/path.d.ts +16 -0
  63. package/build/types/qss.d.ts +2 -0
  64. package/build/types/redirects.d.ts +10 -0
  65. package/build/types/route.d.ts +270 -0
  66. package/build/types/routeInfo.d.ts +22 -0
  67. package/build/types/router.d.ts +123 -0
  68. package/build/types/scroll-restoration.d.ts +6 -0
  69. package/build/types/searchParams.d.ts +7 -0
  70. package/build/types/useBlocker.d.ts +8 -0
  71. package/build/types/useNavigate.d.ts +20 -0
  72. package/build/types/useParams.d.ts +7 -0
  73. package/build/types/useSearch.d.ts +7 -0
  74. package/build/types/utils.d.ts +66 -0
  75. package/build/umd/index.development.js +2399 -2484
  76. package/build/umd/index.development.js.map +1 -1
  77. package/build/umd/index.production.js +4 -4
  78. package/build/umd/index.production.js.map +1 -1
  79. package/package.json +9 -10
  80. package/src/CatchBoundary.tsx +98 -0
  81. package/src/Matches.tsx +345 -0
  82. package/src/RouterProvider.tsx +1575 -0
  83. package/src/awaited.tsx +40 -0
  84. package/src/defer.ts +55 -0
  85. package/src/fileRoute.ts +153 -0
  86. package/src/history.ts +8 -0
  87. package/src/index.tsx +28 -693
  88. package/src/injectHtml.ts +28 -0
  89. package/src/lazyRouteComponent.tsx +33 -0
  90. package/src/link.tsx +508 -0
  91. package/src/location.ts +15 -0
  92. package/src/path.ts +256 -0
  93. package/src/qss.ts +53 -0
  94. package/src/redirects.ts +31 -0
  95. package/src/route.ts +837 -0
  96. package/src/routeInfo.ts +68 -0
  97. package/src/router.ts +330 -0
  98. package/src/scroll-restoration.tsx +192 -0
  99. package/src/searchParams.ts +79 -0
  100. package/src/useBlocker.tsx +34 -0
  101. package/src/useNavigate.tsx +109 -0
  102. package/src/useParams.tsx +25 -0
  103. package/src/useSearch.tsx +25 -0
  104. package/src/utils.ts +350 -0
  105. package/build/cjs/react-router/src/index.js +0 -466
  106. package/build/cjs/react-router/src/index.js.map +0 -1
  107. package/build/cjs/router-core/build/esm/index.js +0 -2523
  108. package/build/cjs/router-core/build/esm/index.js.map +0 -1
@@ -0,0 +1,68 @@
1
+ import { AnyRoute, Route } from './route'
2
+ import { Expand, UnionToIntersection } from './utils'
3
+
4
+ export type ParseRoute<TRouteTree extends AnyRoute> =
5
+ | TRouteTree
6
+ | ParseRouteChildren<TRouteTree>
7
+
8
+ export type ParseRouteChildren<TRouteTree extends AnyRoute> =
9
+ TRouteTree extends Route<
10
+ any,
11
+ any,
12
+ any,
13
+ any,
14
+ any,
15
+ any,
16
+ any,
17
+ any,
18
+ any,
19
+ any,
20
+ any,
21
+ any,
22
+ infer TChildren,
23
+ any
24
+ >
25
+ ? unknown extends TChildren
26
+ ? never
27
+ : TChildren extends AnyRoute[]
28
+ ? {
29
+ [TId in TChildren[number]['id'] as string]: ParseRoute<
30
+ TChildren[number]
31
+ >
32
+ }[string]
33
+ : never
34
+ : never
35
+
36
+ export type RoutesById<TRouteTree extends AnyRoute> = {
37
+ [K in ParseRoute<TRouteTree> as K['id']]: K
38
+ }
39
+
40
+ export type RouteById<TRouteTree extends AnyRoute, TId> = Extract<
41
+ ParseRoute<TRouteTree>,
42
+ { id: TId }
43
+ >
44
+
45
+ export type RouteIds<TRouteTree extends AnyRoute> = ParseRoute<TRouteTree>['id']
46
+
47
+ export type RoutesByPath<TRouteTree extends AnyRoute> = {
48
+ [K in ParseRoute<TRouteTree> as K['fullPath']]: K
49
+ }
50
+
51
+ export type RouteByPath<TRouteTree extends AnyRoute, TPath> = Extract<
52
+ ParseRoute<TRouteTree>,
53
+ { fullPath: TPath }
54
+ >
55
+
56
+ export type RoutePaths<TRouteTree extends AnyRoute> =
57
+ | ParseRoute<TRouteTree>['fullPath']
58
+ | '/'
59
+
60
+ export type FullSearchSchema<TRouteTree extends AnyRoute> = Partial<
61
+ Expand<
62
+ UnionToIntersection<ParseRoute<TRouteTree>['types']['fullSearchSchema']>
63
+ >
64
+ >
65
+
66
+ export type AllParams<TRouteTree extends AnyRoute> = Expand<
67
+ UnionToIntersection<ParseRoute<TRouteTree>['types']['allParams']>
68
+ >
package/src/router.ts ADDED
@@ -0,0 +1,330 @@
1
+ import { RouterHistory } from '@tanstack/history'
2
+
3
+ //
4
+
5
+ import {
6
+ AnySearchSchema,
7
+ AnyRoute,
8
+ AnyContext,
9
+ AnyPathParams,
10
+ RouteMask,
11
+ } from './route'
12
+ import { FullSearchSchema } from './routeInfo'
13
+ import { defaultParseSearch, defaultStringifySearch } from './searchParams'
14
+ import { PickAsRequired, Updater, NonNullableUpdater } from './utils'
15
+ import {
16
+ ErrorRouteComponent,
17
+ PendingRouteComponent,
18
+ RouteComponent,
19
+ } from './route'
20
+ import { RouteMatch } from './RouterProvider'
21
+ import { ParsedLocation } from './location'
22
+ import { LocationState } from './location'
23
+ import { SearchSerializer, SearchParser } from './searchParams'
24
+ import { RouterContext } from './RouterProvider'
25
+
26
+ //
27
+
28
+ declare global {
29
+ interface Window {
30
+ __TSR_DEHYDRATED__?: HydrationCtx
31
+ __TSR_ROUTER_CONTEXT__?: React.Context<RouterContext<any>>
32
+ }
33
+ }
34
+
35
+ export interface Register {
36
+ // router: Router
37
+ }
38
+
39
+ export type AnyRouter = Router<AnyRoute, any>
40
+
41
+ export type RegisteredRouter = Register extends {
42
+ router: infer TRouter extends AnyRouter
43
+ }
44
+ ? TRouter
45
+ : AnyRouter
46
+
47
+ export type HydrationCtx = {
48
+ router: DehydratedRouter
49
+ payload: Record<string, any>
50
+ }
51
+
52
+ export type RouterContextOptions<TRouteTree extends AnyRoute> =
53
+ AnyContext extends TRouteTree['types']['routerContext']
54
+ ? {
55
+ context?: TRouteTree['types']['routerContext']
56
+ }
57
+ : {
58
+ context: TRouteTree['types']['routerContext']
59
+ }
60
+
61
+ export interface RouterOptions<
62
+ TRouteTree extends AnyRoute,
63
+ TDehydrated extends Record<string, any> = Record<string, any>,
64
+ > {
65
+ history?: RouterHistory
66
+ stringifySearch?: SearchSerializer
67
+ parseSearch?: SearchParser
68
+ defaultPreload?: false | 'intent'
69
+ defaultPreloadDelay?: number
70
+ defaultComponent?: RouteComponent<AnySearchSchema, AnyPathParams, AnyContext>
71
+ defaultErrorComponent?: ErrorRouteComponent<
72
+ AnySearchSchema,
73
+ AnyPathParams,
74
+ AnyContext
75
+ >
76
+ defaultPendingComponent?: PendingRouteComponent<
77
+ AnySearchSchema,
78
+ AnyPathParams,
79
+ AnyContext
80
+ >
81
+ defaultMaxAge?: number
82
+ defaultGcMaxAge?: number
83
+ defaultPreloadMaxAge?: number
84
+ caseSensitive?: boolean
85
+ routeTree?: TRouteTree
86
+ basepath?: string
87
+ createRoute?: (opts: { route: AnyRoute; router: AnyRouter }) => void
88
+ context?: TRouteTree['types']['routerContext']
89
+ // dehydrate?: () => TDehydrated
90
+ // hydrate?: (dehydrated: TDehydrated) => void
91
+ routeMasks?: RouteMask<TRouteTree>[]
92
+ unmaskOnReload?: boolean
93
+ }
94
+
95
+ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
96
+ status: 'pending' | 'idle'
97
+ matches: RouteMatch<TRouteTree>[]
98
+ pendingMatches: RouteMatch<TRouteTree>[]
99
+ location: ParsedLocation<FullSearchSchema<TRouteTree>>
100
+ resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
101
+ lastUpdated: number
102
+ }
103
+
104
+ export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
105
+
106
+ export interface BuildNextOptions {
107
+ to?: string | number | null
108
+ params?: true | Updater<unknown>
109
+ search?: true | Updater<unknown>
110
+ hash?: true | Updater<string>
111
+ state?: true | NonNullableUpdater<LocationState>
112
+ mask?: {
113
+ to?: string | number | null
114
+ params?: true | Updater<unknown>
115
+ search?: true | Updater<unknown>
116
+ hash?: true | Updater<string>
117
+ state?: true | NonNullableUpdater<LocationState>
118
+ unmaskOnReload?: boolean
119
+ }
120
+ from?: string
121
+ }
122
+
123
+ export interface DehydratedRouterState {
124
+ dehydratedMatches: DehydratedRouteMatch[]
125
+ }
126
+
127
+ export type DehydratedRouteMatch = Pick<
128
+ RouteMatch,
129
+ 'fetchedAt' | 'invalid' | 'id' | 'status' | 'updatedAt'
130
+ >
131
+
132
+ export interface DehydratedRouter {
133
+ state: DehydratedRouterState
134
+ }
135
+
136
+ export type RouterConstructorOptions<
137
+ TRouteTree extends AnyRoute,
138
+ TDehydrated extends Record<string, any>,
139
+ > = Omit<RouterOptions<TRouteTree, TDehydrated>, 'context'> &
140
+ RouterContextOptions<TRouteTree>
141
+
142
+ export const componentTypes = [
143
+ 'component',
144
+ 'errorComponent',
145
+ 'pendingComponent',
146
+ ] as const
147
+
148
+ export type RouterEvents = {
149
+ onBeforeLoad: {
150
+ type: 'onBeforeLoad'
151
+ fromLocation: ParsedLocation
152
+ toLocation: ParsedLocation
153
+ pathChanged: boolean
154
+ }
155
+ onLoad: {
156
+ type: 'onLoad'
157
+ fromLocation: ParsedLocation
158
+ toLocation: ParsedLocation
159
+ pathChanged: boolean
160
+ }
161
+ onResolved: {
162
+ type: 'onResolved'
163
+ fromLocation: ParsedLocation
164
+ toLocation: ParsedLocation
165
+ pathChanged: boolean
166
+ }
167
+ }
168
+
169
+ export type RouterEvent = RouterEvents[keyof RouterEvents]
170
+
171
+ export type RouterListener<TRouterEvent extends RouterEvent> = {
172
+ eventType: TRouterEvent['type']
173
+ fn: ListenerFn<TRouterEvent>
174
+ }
175
+
176
+ export class Router<
177
+ TRouteTree extends AnyRoute = AnyRoute,
178
+ TDehydrated extends Record<string, any> = Record<string, any>,
179
+ > {
180
+ options: PickAsRequired<
181
+ RouterOptions<TRouteTree, TDehydrated>,
182
+ 'stringifySearch' | 'parseSearch' | 'context'
183
+ >
184
+ routeTree: TRouteTree
185
+ // dehydratedData?: TDehydrated
186
+ // resetNextScroll = false
187
+ // tempLocationKey = `${Math.round(Math.random() * 10000000)}`
188
+
189
+ constructor(options: RouterConstructorOptions<TRouteTree, TDehydrated>) {
190
+ this.options = {
191
+ defaultPreloadDelay: 50,
192
+ context: undefined!,
193
+ ...options,
194
+ stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
195
+ parseSearch: options?.parseSearch ?? defaultParseSearch,
196
+ }
197
+
198
+ this.routeTree = this.options.routeTree as TRouteTree
199
+ }
200
+
201
+ subscribers = new Set<RouterListener<RouterEvent>>()
202
+
203
+ subscribe = <TType extends keyof RouterEvents>(
204
+ eventType: TType,
205
+ fn: ListenerFn<RouterEvents[TType]>,
206
+ ) => {
207
+ const listener: RouterListener<any> = {
208
+ eventType,
209
+ fn,
210
+ }
211
+
212
+ this.subscribers.add(listener)
213
+
214
+ return () => {
215
+ this.subscribers.delete(listener)
216
+ }
217
+ }
218
+
219
+ emit = (routerEvent: RouterEvent) => {
220
+ this.subscribers.forEach((listener) => {
221
+ if (listener.eventType === routerEvent.type) {
222
+ listener.fn(routerEvent)
223
+ }
224
+ })
225
+ }
226
+
227
+ // dehydrate = (): DehydratedRouter => {
228
+ // return {
229
+ // state: {
230
+ // dehydratedMatches: state.matches.map((d) =>
231
+ // pick(d, ['fetchedAt', 'invalid', 'id', 'status', 'updatedAt']),
232
+ // ),
233
+ // },
234
+ // }
235
+ // }
236
+
237
+ // hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
238
+ // let _ctx = __do_not_use_server_ctx
239
+ // // Client hydrates from window
240
+ // if (typeof document !== 'undefined') {
241
+ // _ctx = window.__TSR_DEHYDRATED__
242
+ // }
243
+
244
+ // invariant(
245
+ // _ctx,
246
+ // 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
247
+ // )
248
+
249
+ // const ctx = _ctx
250
+ // this.dehydratedData = ctx.payload as any
251
+ // this.options.hydrate?.(ctx.payload as any)
252
+ // const dehydratedState = ctx.router.state
253
+
254
+ // let matches = this.matchRoutes(
255
+ // state.location.pathname,
256
+ // state.location.search,
257
+ // ).map((match) => {
258
+ // const dehydratedMatch = dehydratedState.dehydratedMatches.find(
259
+ // (d) => d.id === match.id,
260
+ // )
261
+
262
+ // invariant(
263
+ // dehydratedMatch,
264
+ // `Could not find a client-side match for dehydrated match with id: ${match.id}!`,
265
+ // )
266
+
267
+ // if (dehydratedMatch) {
268
+ // return {
269
+ // ...match,
270
+ // ...dehydratedMatch,
271
+ // }
272
+ // }
273
+ // return match
274
+ // })
275
+
276
+ // this.setState((s) => {
277
+ // return {
278
+ // ...s,
279
+ // matches: dehydratedState.dehydratedMatches as any,
280
+ // }
281
+ // })
282
+ // }
283
+
284
+ // resolveMatchPromise = (matchId: string, key: string, value: any) => {
285
+ // state.matches
286
+ // .find((d) => d.id === matchId)
287
+ // ?.__promisesByKey[key]?.resolve(value)
288
+ // }
289
+
290
+ // setRouteMatch = (
291
+ // id: string,
292
+ // pending: boolean,
293
+ // updater: NonNullableUpdater<RouteMatch<TRouteTree>>,
294
+ // ) => {
295
+ // const key = pending ? 'pendingMatches' : 'matches'
296
+
297
+ // this.setState((prev) => {
298
+ // return {
299
+ // ...prev,
300
+ // [key]: prev[key].map((d) => {
301
+ // if (d.id === id) {
302
+ // return functionalUpdate(updater, d)
303
+ // }
304
+
305
+ // return d
306
+ // }),
307
+ // }
308
+ // })
309
+ // }
310
+
311
+ // setPendingRouteMatch = (
312
+ // id: string,
313
+ // updater: NonNullableUpdater<RouteMatch<TRouteTree>>,
314
+ // ) => {
315
+ // this.setRouteMatch(id, true, updater)
316
+ // }
317
+ }
318
+
319
+ // A function that takes an import() argument which is a function and returns a new function that will
320
+ // proxy arguments from the caller to the imported function, retaining all type
321
+ // information along the way
322
+ export function lazyFn<
323
+ T extends Record<string, (...args: any[]) => any>,
324
+ TKey extends keyof T = 'default',
325
+ >(fn: () => Promise<T>, key?: TKey) {
326
+ return async (...args: Parameters<T[TKey]>): Promise<ReturnType<T[TKey]>> => {
327
+ const imported = await fn()
328
+ return imported[key || 'default'](...args)
329
+ }
330
+ }
@@ -0,0 +1,192 @@
1
+ import * as React from 'react'
2
+
3
+ const useLayoutEffect =
4
+ typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
5
+
6
+ import { ParsedLocation } from './location'
7
+ import { useRouter } from './RouterProvider'
8
+ import { NonNullableUpdater, functionalUpdate } from './utils'
9
+
10
+ const windowKey = 'window'
11
+ const delimiter = '___'
12
+
13
+ let weakScrolledElements = new WeakSet<any>()
14
+
15
+ type CacheValue = Record<string, { scrollX: number; scrollY: number }>
16
+ type CacheState = {
17
+ cached: CacheValue
18
+ next: CacheValue
19
+ }
20
+
21
+ type Cache = {
22
+ state: CacheState
23
+ set: (updater: NonNullableUpdater<CacheState>) => void
24
+ }
25
+
26
+ let cache: Cache
27
+
28
+ const sessionsStorage = typeof window !== 'undefined' && window.sessionStorage
29
+
30
+ export type ScrollRestorationOptions = {
31
+ getKey?: (location: ParsedLocation) => string
32
+ }
33
+
34
+ const defaultGetKey = (location: ParsedLocation) => location.state.key!
35
+
36
+ export function useScrollRestoration(options?: ScrollRestorationOptions) {
37
+ const { state, subscribe, resetNextScrollRef } = useRouter()
38
+
39
+ useLayoutEffect(() => {
40
+ const getKey = options?.getKey || defaultGetKey
41
+
42
+ if (sessionsStorage) {
43
+ if (!cache) {
44
+ cache = (() => {
45
+ const storageKey = 'tsr-scroll-restoration-v2'
46
+
47
+ const state: CacheState = JSON.parse(
48
+ window.sessionStorage.getItem(storageKey) || 'null',
49
+ ) || { cached: {}, next: {} }
50
+
51
+ return {
52
+ state,
53
+ set: (updater) => {
54
+ cache.state = functionalUpdate(updater, cache.state)
55
+ window.sessionStorage.setItem(
56
+ storageKey,
57
+ JSON.stringify(cache.state),
58
+ )
59
+ },
60
+ }
61
+ })()
62
+ }
63
+ }
64
+
65
+ const { history } = window
66
+ if (history.scrollRestoration) {
67
+ history.scrollRestoration = 'manual'
68
+ }
69
+
70
+ const onScroll = (event: Event) => {
71
+ if (weakScrolledElements.has(event.target)) return
72
+ weakScrolledElements.add(event.target)
73
+
74
+ const elementSelector =
75
+ event.target === document || event.target === window
76
+ ? windowKey
77
+ : getCssSelector(event.target)
78
+
79
+ if (!cache.state.next[elementSelector]) {
80
+ cache.set((c) => ({
81
+ ...c,
82
+ next: {
83
+ ...c.next,
84
+ [elementSelector]: {
85
+ scrollX: NaN,
86
+ scrollY: NaN,
87
+ },
88
+ },
89
+ }))
90
+ }
91
+ }
92
+
93
+ const getCssSelector = (el: any): string => {
94
+ let path = [],
95
+ parent
96
+ while ((parent = el.parentNode)) {
97
+ path.unshift(
98
+ `${el.tagName}:nth-child(${
99
+ ([].indexOf as any).call(parent.children, el) + 1
100
+ })`,
101
+ )
102
+ el = parent
103
+ }
104
+ return `${path.join(' > ')}`.toLowerCase()
105
+ }
106
+
107
+ if (typeof document !== 'undefined') {
108
+ document.addEventListener('scroll', onScroll, true)
109
+ }
110
+
111
+ const unsubOnBeforeLoad = subscribe('onBeforeLoad', (event) => {
112
+ if (event.pathChanged) {
113
+ const restoreKey = getKey(event.fromLocation)
114
+ for (const elementSelector in cache.state.next) {
115
+ const entry = cache.state.next[elementSelector]!
116
+ if (elementSelector === windowKey) {
117
+ entry.scrollX = window.scrollX || 0
118
+ entry.scrollY = window.scrollY || 0
119
+ } else if (elementSelector) {
120
+ const element = document.querySelector(elementSelector)
121
+ entry.scrollX = element?.scrollLeft || 0
122
+ entry.scrollY = element?.scrollTop || 0
123
+ }
124
+
125
+ cache.set((c) => {
126
+ const next = { ...c.next }
127
+ delete next[elementSelector]
128
+
129
+ return {
130
+ ...c,
131
+ next,
132
+ cached: {
133
+ ...c.cached,
134
+ [[restoreKey, elementSelector].join(delimiter)]: entry,
135
+ },
136
+ }
137
+ })
138
+ }
139
+ }
140
+ })
141
+
142
+ const unsubOnResolved = subscribe('onResolved', (event) => {
143
+ if (event.pathChanged) {
144
+ if (!resetNextScrollRef.current) {
145
+ return
146
+ }
147
+
148
+ resetNextScrollRef.current = true
149
+
150
+ const getKey = options?.getKey || defaultGetKey
151
+
152
+ const restoreKey = getKey(event.toLocation)
153
+ let windowRestored = false
154
+
155
+ for (const cacheKey in cache.state.cached) {
156
+ const entry = cache.state.cached[cacheKey]!
157
+ const [key, elementSelector] = cacheKey.split(delimiter)
158
+ if (key === restoreKey) {
159
+ if (elementSelector === windowKey) {
160
+ windowRestored = true
161
+ window.scrollTo(entry.scrollX, entry.scrollY)
162
+ } else if (elementSelector) {
163
+ const element = document.querySelector(elementSelector)
164
+ if (element) {
165
+ element.scrollLeft = entry.scrollX
166
+ element.scrollTop = entry.scrollY
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ if (!windowRestored) {
173
+ window.scrollTo(0, 0)
174
+ }
175
+
176
+ cache.set((c) => ({ ...c, next: {} }))
177
+ weakScrolledElements = new WeakSet<any>()
178
+ }
179
+ })
180
+
181
+ return () => {
182
+ document.removeEventListener('scroll', onScroll)
183
+ unsubOnBeforeLoad()
184
+ unsubOnResolved()
185
+ }
186
+ }, [])
187
+ }
188
+
189
+ export function ScrollRestoration(props: ScrollRestorationOptions) {
190
+ useScrollRestoration(props)
191
+ return null
192
+ }
@@ -0,0 +1,79 @@
1
+ import { decode, encode } from './qss'
2
+ import { AnySearchSchema } from './route'
3
+
4
+ export const defaultParseSearch = parseSearchWith(JSON.parse)
5
+ export const defaultStringifySearch = stringifySearchWith(
6
+ JSON.stringify,
7
+ JSON.parse,
8
+ )
9
+
10
+ export function parseSearchWith(parser: (str: string) => any) {
11
+ return (searchStr: string): AnySearchSchema => {
12
+ if (searchStr.substring(0, 1) === '?') {
13
+ searchStr = searchStr.substring(1)
14
+ }
15
+
16
+ let query: Record<string, unknown> = decode(searchStr)
17
+
18
+ // Try to parse any query params that might be json
19
+ for (let key in query) {
20
+ const value = query[key]
21
+ if (typeof value === 'string') {
22
+ try {
23
+ query[key] = parser(value)
24
+ } catch (err) {
25
+ //
26
+ }
27
+ }
28
+ }
29
+
30
+ return query
31
+ }
32
+ }
33
+
34
+ export function stringifySearchWith(
35
+ stringify: (search: any) => string,
36
+ parser?: (str: string) => any,
37
+ ) {
38
+ function stringifyValue(val: any) {
39
+ if (typeof val === 'object' && val !== null) {
40
+ try {
41
+ return stringify(val)
42
+ } catch (err) {
43
+ // silent
44
+ }
45
+ } else if (typeof val === 'string' && typeof parser === 'function') {
46
+ try {
47
+ // Check if it's a valid parseable string.
48
+ // If it is, then stringify it again.
49
+ parser(val)
50
+ return stringify(val)
51
+ } catch (err) {
52
+ // silent
53
+ }
54
+ }
55
+ return val
56
+ }
57
+
58
+ return (search: Record<string, any>) => {
59
+ search = { ...search }
60
+
61
+ if (search) {
62
+ Object.keys(search).forEach((key) => {
63
+ const val = search[key]
64
+ if (typeof val === 'undefined' || val === undefined) {
65
+ delete search[key]
66
+ } else {
67
+ search[key] = stringifyValue(val)
68
+ }
69
+ })
70
+ }
71
+
72
+ const searchStr = encode(search as Record<string, string>).toString()
73
+
74
+ return searchStr ? `?${searchStr}` : ''
75
+ }
76
+ }
77
+
78
+ export type SearchSerializer = (searchObj: Record<string, any>) => string
79
+ export type SearchParser = (searchStr: string) => Record<string, any>
@@ -0,0 +1,34 @@
1
+ import * as React from 'react'
2
+ import { ReactNode } from './route'
3
+ import { useRouter } from './RouterProvider'
4
+
5
+ export function useBlocker(
6
+ message: string,
7
+ condition: boolean | any = true,
8
+ ): void {
9
+ const { history } = useRouter()
10
+
11
+ React.useEffect(() => {
12
+ if (!condition) return
13
+
14
+ let unblock = history.block((retry, cancel) => {
15
+ if (window.confirm(message)) {
16
+ unblock()
17
+ retry()
18
+ }
19
+ })
20
+
21
+ return unblock
22
+ })
23
+ }
24
+
25
+ export function Block({ message, condition, children }: PromptProps) {
26
+ useBlocker(message, condition)
27
+ return (children ?? null) as ReactNode
28
+ }
29
+
30
+ export type PromptProps = {
31
+ message: string
32
+ condition?: boolean | any
33
+ children?: ReactNode
34
+ }