@tanstack/router-core 0.0.1-beta.195 → 0.0.1-beta.197

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/src/history.ts DELETED
@@ -1,398 +0,0 @@
1
- // While the public API was clearly inspired by the "history" npm package,
2
- // This implementation attempts to be more lightweight by
3
- // making assumptions about the way TanStack Router works
4
-
5
- export interface RouterHistory {
6
- location: HistoryLocation
7
- subscribe: (cb: () => void) => () => void
8
- push: (path: string, state?: any) => void
9
- replace: (path: string, state?: any) => void
10
- go: (index: number) => void
11
- back: () => void
12
- forward: () => void
13
- createHref: (href: string) => string
14
- block: (blockerFn: BlockerFn) => () => void
15
- flush: () => void
16
- }
17
-
18
- export interface HistoryLocation extends ParsedPath {
19
- state: HistoryState
20
- }
21
-
22
- export interface ParsedPath {
23
- href: string
24
- pathname: string
25
- search: string
26
- hash: string
27
- }
28
-
29
- export interface HistoryState {
30
- key: string
31
- __tempLocation?: HistoryLocation
32
- __tempKey?: string
33
- }
34
-
35
- type BlockerFn = (retry: () => void, cancel: () => void) => void
36
-
37
- const pushStateEvent = 'pushstate'
38
- const popStateEvent = 'popstate'
39
- const beforeUnloadEvent = 'beforeunload'
40
-
41
- const beforeUnloadListener = (event: Event) => {
42
- event.preventDefault()
43
- // @ts-ignore
44
- return (event.returnValue = '')
45
- }
46
-
47
- const stopBlocking = () => {
48
- removeEventListener(beforeUnloadEvent, beforeUnloadListener, {
49
- capture: true,
50
- })
51
- }
52
-
53
- function createHistory(opts: {
54
- getLocation: () => HistoryLocation
55
- subscriber: false | ((onUpdate: () => void) => () => void)
56
- pushState: (path: string, state: any, onUpdate: () => void) => void
57
- replaceState: (path: string, state: any, onUpdate: () => void) => void
58
- go: (n: number) => void
59
- back: () => void
60
- forward: () => void
61
- createHref: (path: string) => string
62
- flush?: () => void
63
- }): RouterHistory {
64
- let location = opts.getLocation()
65
- let unsub = () => {}
66
- let subscribers = new Set<() => void>()
67
- let blockers: BlockerFn[] = []
68
- let queue: (() => void)[] = []
69
-
70
- const tryFlush = () => {
71
- if (blockers.length) {
72
- blockers[0]?.(tryFlush, () => {
73
- blockers = []
74
- stopBlocking()
75
- })
76
- return
77
- }
78
-
79
- while (queue.length) {
80
- queue.shift()?.()
81
- }
82
-
83
- if (!opts.subscriber) {
84
- onUpdate()
85
- }
86
- }
87
-
88
- const queueTask = (task: () => void) => {
89
- queue.push(task)
90
- tryFlush()
91
- }
92
-
93
- const onUpdate = () => {
94
- location = opts.getLocation()
95
- subscribers.forEach((subscriber) => subscriber())
96
- }
97
-
98
- return {
99
- get location() {
100
- return location
101
- },
102
- subscribe: (cb: () => void) => {
103
- if (subscribers.size === 0) {
104
- unsub =
105
- typeof opts.subscriber === 'function'
106
- ? opts.subscriber(onUpdate)
107
- : () => {}
108
- }
109
- subscribers.add(cb)
110
-
111
- return () => {
112
- subscribers.delete(cb)
113
- if (subscribers.size === 0) {
114
- unsub()
115
- }
116
- }
117
- },
118
- push: (path: string, state: any) => {
119
- assignKey(state)
120
- queueTask(() => {
121
- opts.pushState(path, state, onUpdate)
122
- })
123
- },
124
- replace: (path: string, state: any) => {
125
- assignKey(state)
126
- queueTask(() => {
127
- opts.replaceState(path, state, onUpdate)
128
- })
129
- },
130
- go: (index) => {
131
- queueTask(() => {
132
- opts.go(index)
133
- })
134
- },
135
- back: () => {
136
- queueTask(() => {
137
- opts.back()
138
- })
139
- },
140
- forward: () => {
141
- queueTask(() => {
142
- opts.forward()
143
- })
144
- },
145
- createHref: (str) => opts.createHref(str),
146
- block: (cb) => {
147
- blockers.push(cb)
148
-
149
- if (blockers.length === 1) {
150
- addEventListener(beforeUnloadEvent, beforeUnloadListener, {
151
- capture: true,
152
- })
153
- }
154
-
155
- return () => {
156
- blockers = blockers.filter((b) => b !== cb)
157
-
158
- if (!blockers.length) {
159
- stopBlocking()
160
- }
161
- }
162
- },
163
- flush: () => opts.flush?.(),
164
- }
165
- }
166
-
167
- function assignKey(state: HistoryState) {
168
- state.key = createRandomKey()
169
- // if (state.__actualLocation) {
170
- // state.__actualLocation.state = {
171
- // ...state.__actualLocation.state,
172
- // key,
173
- // }
174
- // }
175
- }
176
-
177
- /**
178
- * Creates a history object that can be used to interact with the browser's
179
- * navigation. This is a lightweight API wrapping the browser's native methods.
180
- * It is designed to work with TanStack Router, but could be used as a standalone API as well.
181
- * IMPORTANT: This API implements history throttling via a microtask to prevent
182
- * excessive calls to the history API. In some browsers, calling history.pushState or
183
- * history.replaceState in quick succession can cause the browser to ignore subsequent
184
- * calls. This API smooths out those differences and ensures that your application
185
- * state will *eventually* match the browser state. In most cases, this is not a problem,
186
- * but if you need to ensure that the browser state is up to date, you can use the
187
- * `history.flush` method to immediately flush all pending state changes to the browser URL.
188
- * @param opts
189
- * @param opts.getHref A function that returns the current href (path + search + hash)
190
- * @param opts.createHref A function that takes a path and returns a href (path + search + hash)
191
- * @returns A history instance
192
- */
193
- export function createBrowserHistory(opts?: {
194
- getHref?: () => string
195
- createHref?: (path: string) => string
196
- }): RouterHistory {
197
- const getHref =
198
- opts?.getHref ??
199
- (() =>
200
- `${window.location.pathname}${window.location.search}${window.location.hash}`)
201
-
202
- const createHref = opts?.createHref ?? ((path) => path)
203
-
204
- let currentLocation = parseLocation(getHref(), window.history.state)
205
-
206
- const getLocation = () => currentLocation
207
-
208
- let next:
209
- | undefined
210
- | {
211
- // This is the latest location that we were attempting to push/replace
212
- href: string
213
- // This is the latest state that we were attempting to push/replace
214
- state: any
215
- // This is the latest type that we were attempting to push/replace
216
- isPush: boolean
217
- }
218
-
219
- // Because we are proactively updating the location
220
- // in memory before actually updating the browser history,
221
- // we need to track when we are doing this so we don't
222
- // notify subscribers twice on the last update.
223
- let tracking = true
224
-
225
- // We need to track the current scheduled update to prevent
226
- // multiple updates from being scheduled at the same time.
227
- let scheduled: Promise<void> | undefined
228
-
229
- // This function is a wrapper to prevent any of the callback's
230
- // side effects from causing a subscriber notification
231
- const untrack = (fn: () => void) => {
232
- tracking = false
233
- fn()
234
- tracking = true
235
- }
236
-
237
- // This function flushes the next update to the browser history
238
- const flush = () => {
239
- // Do not notify subscribers about this push/replace call
240
- untrack(() => {
241
- if (!next) return
242
- window.history[next.isPush ? 'pushState' : 'replaceState'](
243
- next.state,
244
- '',
245
- next.href,
246
- )
247
- // Reset the nextIsPush flag and clear the scheduled update
248
- next = undefined
249
- scheduled = undefined
250
- })
251
- }
252
-
253
- // This function queues up a call to update the browser history
254
- const queueHistoryAction = (
255
- type: 'push' | 'replace',
256
- path: string,
257
- state: any,
258
- onUpdate: () => void,
259
- ) => {
260
- const href = createHref(path)
261
-
262
- // Update the location in memory
263
- currentLocation = parseLocation(href, state)
264
-
265
- // Keep track of the next location we need to flush to the URL
266
- next = {
267
- href,
268
- state,
269
- isPush: next?.isPush || type === 'push',
270
- }
271
- // Notify subscribers
272
- onUpdate()
273
-
274
- if (!scheduled) {
275
- // Schedule an update to the browser history
276
- scheduled = Promise.resolve().then(() => flush())
277
- }
278
- }
279
-
280
- return createHistory({
281
- getLocation,
282
- subscriber: (onUpdate) => {
283
- window.addEventListener(pushStateEvent, () => {
284
- currentLocation = parseLocation(getHref(), window.history.state)
285
- onUpdate()
286
- })
287
- window.addEventListener(popStateEvent, () => {
288
- currentLocation = parseLocation(getHref(), window.history.state)
289
- onUpdate()
290
- })
291
-
292
- var pushState = window.history.pushState
293
- window.history.pushState = function () {
294
- let res = pushState.apply(history, arguments as any)
295
- if (tracking) onUpdate()
296
- return res
297
- }
298
- var replaceState = window.history.replaceState
299
- window.history.replaceState = function () {
300
- let res = replaceState.apply(history, arguments as any)
301
- if (tracking) onUpdate()
302
- return res
303
- }
304
-
305
- return () => {
306
- window.history.pushState = pushState
307
- window.history.replaceState = replaceState
308
- window.removeEventListener(pushStateEvent, onUpdate)
309
- window.removeEventListener(popStateEvent, onUpdate)
310
- }
311
- },
312
- pushState: (path, state, onUpdate) =>
313
- queueHistoryAction('push', path, state, onUpdate),
314
- replaceState: (path, state, onUpdate) =>
315
- queueHistoryAction('replace', path, state, onUpdate),
316
- back: () => window.history.back(),
317
- forward: () => window.history.forward(),
318
- go: (n) => window.history.go(n),
319
- createHref: (path) => createHref(path),
320
- flush,
321
- })
322
- }
323
-
324
- export function createHashHistory(): RouterHistory {
325
- return createBrowserHistory({
326
- getHref: () => window.location.hash.substring(1),
327
- createHref: (path) => `#${path}`,
328
- })
329
- }
330
-
331
- export function createMemoryHistory(
332
- opts: {
333
- initialEntries: string[]
334
- initialIndex?: number
335
- } = {
336
- initialEntries: ['/'],
337
- },
338
- ): RouterHistory {
339
- const entries = opts.initialEntries
340
- let index = opts.initialIndex ?? entries.length - 1
341
- let currentState = {
342
- key: createRandomKey(),
343
- } as HistoryState
344
-
345
- const getLocation = () => parseLocation(entries[index]!, currentState)
346
-
347
- return createHistory({
348
- getLocation,
349
- subscriber: false,
350
- pushState: (path, state) => {
351
- currentState = state
352
- entries.push(path)
353
- index++
354
- },
355
- replaceState: (path, state) => {
356
- currentState = state
357
- entries[index] = path
358
- },
359
- back: () => {
360
- index--
361
- },
362
- forward: () => {
363
- index = Math.min(index + 1, entries.length - 1)
364
- },
365
- go: (n) => window.history.go(n),
366
- createHref: (path) => path,
367
- })
368
- }
369
-
370
- function parseLocation(href: string, state: HistoryState): HistoryLocation {
371
- let hashIndex = href.indexOf('#')
372
- let searchIndex = href.indexOf('?')
373
-
374
- return {
375
- href,
376
- pathname: href.substring(
377
- 0,
378
- hashIndex > 0
379
- ? searchIndex > 0
380
- ? Math.min(hashIndex, searchIndex)
381
- : hashIndex
382
- : searchIndex > 0
383
- ? searchIndex
384
- : href.length,
385
- ),
386
- hash: hashIndex > -1 ? href.substring(hashIndex) : '',
387
- search:
388
- searchIndex > -1
389
- ? href.slice(searchIndex, hashIndex === -1 ? undefined : hashIndex)
390
- : '',
391
- state: state || {},
392
- }
393
- }
394
-
395
- // Thanks co-pilot!
396
- function createRandomKey() {
397
- return (Math.random() + 1).toString(36).substring(7)
398
- }