@fictjs/router 0.2.2 → 0.3.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.cjs +2373 -3
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1136 -2
- package/dist/index.d.ts +1136 -2
- package/dist/index.js +2305 -3
- package/dist/index.js.map +1 -0
- package/package.json +33 -7
- package/src/components.tsx +926 -0
- package/src/context.ts +404 -0
- package/src/data.ts +545 -0
- package/src/history.ts +659 -0
- package/src/index.ts +217 -0
- package/src/lazy.tsx +242 -0
- package/src/link.tsx +601 -0
- package/src/scroll.ts +245 -0
- package/src/types.ts +447 -0
- package/src/utils.ts +570 -0
package/src/history.ts
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview History implementations for @fictjs/router
|
|
3
|
+
*
|
|
4
|
+
* Provides browser history, hash history, and memory history implementations.
|
|
5
|
+
* These handle the low-level navigation state management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { History, HistoryAction, HistoryListener, Location, To, Blocker } from './types'
|
|
9
|
+
import { createLocation, createURL, parseURL, createKey, normalizePath } from './utils'
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Shared Utilities
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a history state object
|
|
17
|
+
*/
|
|
18
|
+
function createHistoryState(
|
|
19
|
+
location: Location,
|
|
20
|
+
index: number,
|
|
21
|
+
): { usr: unknown; key: string; idx: number } {
|
|
22
|
+
return {
|
|
23
|
+
usr: location.state,
|
|
24
|
+
key: location.key,
|
|
25
|
+
idx: index,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read a location from window.history.state
|
|
31
|
+
*/
|
|
32
|
+
function readLocation(
|
|
33
|
+
state: { usr?: unknown; key?: string; idx?: number } | null,
|
|
34
|
+
url: string,
|
|
35
|
+
): Location {
|
|
36
|
+
const { pathname, search, hash } = parseURL(url)
|
|
37
|
+
return {
|
|
38
|
+
pathname,
|
|
39
|
+
search,
|
|
40
|
+
hash,
|
|
41
|
+
state: state?.usr ?? null,
|
|
42
|
+
key: state?.key ?? createKey(),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Browser History
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a browser history instance that uses the History API.
|
|
52
|
+
* This is the standard history for most web applications.
|
|
53
|
+
*
|
|
54
|
+
* @throws Error if called in a non-browser environment (SSR)
|
|
55
|
+
*/
|
|
56
|
+
export function createBrowserHistory(): History {
|
|
57
|
+
// SSR guard: throw clear error if window is not available
|
|
58
|
+
if (typeof window === 'undefined') {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'[fict-router] createBrowserHistory cannot be used in a server environment. ' +
|
|
61
|
+
'Use createMemoryHistory or createStaticHistory for SSR.',
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const listeners = new Set<HistoryListener>()
|
|
66
|
+
const blockers = new Set<Blocker>()
|
|
67
|
+
|
|
68
|
+
let action: HistoryAction = 'POP'
|
|
69
|
+
let location = readLocation(
|
|
70
|
+
window.history.state,
|
|
71
|
+
window.location.pathname + window.location.search + window.location.hash,
|
|
72
|
+
)
|
|
73
|
+
let index = (window.history.state?.idx as number) ?? 0
|
|
74
|
+
|
|
75
|
+
// Handle popstate (back/forward navigation)
|
|
76
|
+
function handlePopState(event: PopStateEvent) {
|
|
77
|
+
const nextLocation = readLocation(
|
|
78
|
+
event.state,
|
|
79
|
+
window.location.pathname + window.location.search + window.location.hash,
|
|
80
|
+
)
|
|
81
|
+
const nextAction: HistoryAction = 'POP'
|
|
82
|
+
const nextIndex = (event.state?.idx as number) ?? 0
|
|
83
|
+
|
|
84
|
+
// Check blockers
|
|
85
|
+
if (blockers.size > 0) {
|
|
86
|
+
let blocked = false
|
|
87
|
+
const retry = () => {
|
|
88
|
+
// Re-trigger the navigation
|
|
89
|
+
window.history.go(nextIndex - index)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const blocker of blockers) {
|
|
93
|
+
blocker({
|
|
94
|
+
action: nextAction,
|
|
95
|
+
location: nextLocation,
|
|
96
|
+
retry,
|
|
97
|
+
})
|
|
98
|
+
blocked = true
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (blocked) {
|
|
103
|
+
// Restore the previous state by going back
|
|
104
|
+
window.history.go(index - nextIndex)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
action = nextAction
|
|
110
|
+
location = nextLocation
|
|
111
|
+
index = nextIndex
|
|
112
|
+
|
|
113
|
+
notifyListeners()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
window.addEventListener('popstate', handlePopState)
|
|
117
|
+
|
|
118
|
+
function notifyListeners() {
|
|
119
|
+
for (const listener of listeners) {
|
|
120
|
+
listener({ action, location })
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function push(to: To, state?: unknown) {
|
|
125
|
+
const nextLocation = createLocation(to, state)
|
|
126
|
+
const nextAction: HistoryAction = 'PUSH'
|
|
127
|
+
|
|
128
|
+
// Check blockers
|
|
129
|
+
if (blockers.size > 0) {
|
|
130
|
+
let blocked = false
|
|
131
|
+
const retry = () => push(to, state)
|
|
132
|
+
|
|
133
|
+
for (const blocker of blockers) {
|
|
134
|
+
blocker({
|
|
135
|
+
action: nextAction,
|
|
136
|
+
location: nextLocation,
|
|
137
|
+
retry,
|
|
138
|
+
})
|
|
139
|
+
blocked = true
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (blocked) return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
action = nextAction
|
|
147
|
+
location = nextLocation
|
|
148
|
+
index++
|
|
149
|
+
|
|
150
|
+
const historyState = createHistoryState(location, index)
|
|
151
|
+
window.history.pushState(historyState, '', createURL(location))
|
|
152
|
+
|
|
153
|
+
notifyListeners()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function replace(to: To, state?: unknown) {
|
|
157
|
+
const nextLocation = createLocation(to, state)
|
|
158
|
+
const nextAction: HistoryAction = 'REPLACE'
|
|
159
|
+
|
|
160
|
+
// Check blockers
|
|
161
|
+
if (blockers.size > 0) {
|
|
162
|
+
let blocked = false
|
|
163
|
+
const retry = () => replace(to, state)
|
|
164
|
+
|
|
165
|
+
for (const blocker of blockers) {
|
|
166
|
+
blocker({
|
|
167
|
+
action: nextAction,
|
|
168
|
+
location: nextLocation,
|
|
169
|
+
retry,
|
|
170
|
+
})
|
|
171
|
+
blocked = true
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (blocked) return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
action = nextAction
|
|
179
|
+
location = nextLocation
|
|
180
|
+
|
|
181
|
+
const historyState = createHistoryState(location, index)
|
|
182
|
+
window.history.replaceState(historyState, '', createURL(location))
|
|
183
|
+
|
|
184
|
+
notifyListeners()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function go(delta: number) {
|
|
188
|
+
window.history.go(delta)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Initialize history state if not set
|
|
192
|
+
if (window.history.state === null) {
|
|
193
|
+
const historyState = createHistoryState(location, index)
|
|
194
|
+
window.history.replaceState(historyState, '', createURL(location))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
get action() {
|
|
199
|
+
return action
|
|
200
|
+
},
|
|
201
|
+
get location() {
|
|
202
|
+
return location
|
|
203
|
+
},
|
|
204
|
+
push,
|
|
205
|
+
replace,
|
|
206
|
+
go,
|
|
207
|
+
back() {
|
|
208
|
+
go(-1)
|
|
209
|
+
},
|
|
210
|
+
forward() {
|
|
211
|
+
go(1)
|
|
212
|
+
},
|
|
213
|
+
listen(listener: HistoryListener) {
|
|
214
|
+
listeners.add(listener)
|
|
215
|
+
return () => listeners.delete(listener)
|
|
216
|
+
},
|
|
217
|
+
createHref(to: To) {
|
|
218
|
+
const loc = typeof to === 'string' ? parseURL(to) : to
|
|
219
|
+
return createURL(loc as Location)
|
|
220
|
+
},
|
|
221
|
+
block(blocker: Blocker) {
|
|
222
|
+
blockers.add(blocker)
|
|
223
|
+
|
|
224
|
+
// Set up beforeunload handler if this is the first blocker
|
|
225
|
+
if (blockers.size === 1) {
|
|
226
|
+
window.addEventListener('beforeunload', handleBeforeUnload)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return () => {
|
|
230
|
+
blockers.delete(blocker)
|
|
231
|
+
if (blockers.size === 0) {
|
|
232
|
+
window.removeEventListener('beforeunload', handleBeforeUnload)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function handleBeforeUnload(event: BeforeUnloadEvent) {
|
|
240
|
+
event.preventDefault()
|
|
241
|
+
// Modern browsers ignore the return value, but we set it anyway
|
|
242
|
+
event.returnValue = ''
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ============================================================================
|
|
246
|
+
// Hash History
|
|
247
|
+
// ============================================================================
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create a hash history instance that uses the URL hash.
|
|
251
|
+
* Useful for static file hosting or when you can't configure server-side routing.
|
|
252
|
+
*
|
|
253
|
+
* @throws Error if called in a non-browser environment (SSR)
|
|
254
|
+
*/
|
|
255
|
+
export function createHashHistory(options: { hashType?: 'slash' | 'noslash' } = {}): History {
|
|
256
|
+
// SSR guard: throw clear error if window is not available
|
|
257
|
+
if (typeof window === 'undefined') {
|
|
258
|
+
throw new Error(
|
|
259
|
+
'[fict-router] createHashHistory cannot be used in a server environment. ' +
|
|
260
|
+
'Use createMemoryHistory or createStaticHistory for SSR.',
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const { hashType = 'slash' } = options
|
|
265
|
+
const listeners = new Set<HistoryListener>()
|
|
266
|
+
const blockers = new Set<Blocker>()
|
|
267
|
+
|
|
268
|
+
let action: HistoryAction = 'POP'
|
|
269
|
+
let location = readHashLocation()
|
|
270
|
+
let index = 0
|
|
271
|
+
|
|
272
|
+
function readHashLocation(): Location {
|
|
273
|
+
let hash = window.location.hash.slice(1) // Remove the #
|
|
274
|
+
|
|
275
|
+
// Handle hash type
|
|
276
|
+
if (hashType === 'slash' && !hash.startsWith('/')) {
|
|
277
|
+
hash = '/' + hash
|
|
278
|
+
} else if (hashType === 'noslash' && hash.startsWith('/')) {
|
|
279
|
+
hash = hash.slice(1)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const { pathname, search, hash: innerHash } = parseURL(hash || '/')
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
pathname: normalizePath(pathname),
|
|
286
|
+
search,
|
|
287
|
+
hash: innerHash,
|
|
288
|
+
state: window.history.state?.usr ?? null,
|
|
289
|
+
key: window.history.state?.key ?? createKey(),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function createHashHref(location: Location): string {
|
|
294
|
+
const url = createURL(location)
|
|
295
|
+
if (hashType === 'noslash') {
|
|
296
|
+
return '#' + url.slice(1) // Remove leading /
|
|
297
|
+
}
|
|
298
|
+
return '#' + url
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function handleHashChange() {
|
|
302
|
+
const nextLocation = readHashLocation()
|
|
303
|
+
const nextAction: HistoryAction = 'POP'
|
|
304
|
+
|
|
305
|
+
// Check blockers
|
|
306
|
+
if (blockers.size > 0) {
|
|
307
|
+
let blocked = false
|
|
308
|
+
const retry = () => {
|
|
309
|
+
window.location.hash = createHashHref(nextLocation)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const blocker of blockers) {
|
|
313
|
+
blocker({
|
|
314
|
+
action: nextAction,
|
|
315
|
+
location: nextLocation,
|
|
316
|
+
retry,
|
|
317
|
+
})
|
|
318
|
+
blocked = true
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (blocked) {
|
|
323
|
+
// Restore the previous hash
|
|
324
|
+
window.location.hash = createHashHref(location)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
action = nextAction
|
|
330
|
+
location = nextLocation
|
|
331
|
+
|
|
332
|
+
notifyListeners()
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
window.addEventListener('hashchange', handleHashChange)
|
|
336
|
+
|
|
337
|
+
function notifyListeners() {
|
|
338
|
+
for (const listener of listeners) {
|
|
339
|
+
listener({ action, location })
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function push(to: To, state?: unknown) {
|
|
344
|
+
const nextLocation = createLocation(to, state)
|
|
345
|
+
const nextAction: HistoryAction = 'PUSH'
|
|
346
|
+
|
|
347
|
+
// Check blockers
|
|
348
|
+
if (blockers.size > 0) {
|
|
349
|
+
let blocked = false
|
|
350
|
+
const retry = () => push(to, state)
|
|
351
|
+
|
|
352
|
+
for (const blocker of blockers) {
|
|
353
|
+
blocker({
|
|
354
|
+
action: nextAction,
|
|
355
|
+
location: nextLocation,
|
|
356
|
+
retry,
|
|
357
|
+
})
|
|
358
|
+
blocked = true
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (blocked) return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
action = nextAction
|
|
366
|
+
location = nextLocation
|
|
367
|
+
index++
|
|
368
|
+
|
|
369
|
+
const historyState = createHistoryState(location, index)
|
|
370
|
+
window.history.pushState(historyState, '', createHashHref(location))
|
|
371
|
+
|
|
372
|
+
notifyListeners()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function replace(to: To, state?: unknown) {
|
|
376
|
+
const nextLocation = createLocation(to, state)
|
|
377
|
+
const nextAction: HistoryAction = 'REPLACE'
|
|
378
|
+
|
|
379
|
+
// Check blockers
|
|
380
|
+
if (blockers.size > 0) {
|
|
381
|
+
let blocked = false
|
|
382
|
+
const retry = () => replace(to, state)
|
|
383
|
+
|
|
384
|
+
for (const blocker of blockers) {
|
|
385
|
+
blocker({
|
|
386
|
+
action: nextAction,
|
|
387
|
+
location: nextLocation,
|
|
388
|
+
retry,
|
|
389
|
+
})
|
|
390
|
+
blocked = true
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (blocked) return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
action = nextAction
|
|
398
|
+
location = nextLocation
|
|
399
|
+
|
|
400
|
+
const historyState = createHistoryState(location, index)
|
|
401
|
+
window.history.replaceState(historyState, '', createHashHref(location))
|
|
402
|
+
|
|
403
|
+
notifyListeners()
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function go(delta: number) {
|
|
407
|
+
window.history.go(delta)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
get action() {
|
|
412
|
+
return action
|
|
413
|
+
},
|
|
414
|
+
get location() {
|
|
415
|
+
return location
|
|
416
|
+
},
|
|
417
|
+
push,
|
|
418
|
+
replace,
|
|
419
|
+
go,
|
|
420
|
+
back() {
|
|
421
|
+
go(-1)
|
|
422
|
+
},
|
|
423
|
+
forward() {
|
|
424
|
+
go(1)
|
|
425
|
+
},
|
|
426
|
+
listen(listener: HistoryListener) {
|
|
427
|
+
listeners.add(listener)
|
|
428
|
+
return () => listeners.delete(listener)
|
|
429
|
+
},
|
|
430
|
+
createHref(to: To) {
|
|
431
|
+
const loc = createLocation(to)
|
|
432
|
+
return createHashHref(loc)
|
|
433
|
+
},
|
|
434
|
+
block(blocker: Blocker) {
|
|
435
|
+
blockers.add(blocker)
|
|
436
|
+
|
|
437
|
+
if (blockers.size === 1) {
|
|
438
|
+
window.addEventListener('beforeunload', handleBeforeUnload)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return () => {
|
|
442
|
+
blockers.delete(blocker)
|
|
443
|
+
if (blockers.size === 0) {
|
|
444
|
+
window.removeEventListener('beforeunload', handleBeforeUnload)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ============================================================================
|
|
452
|
+
// Memory History
|
|
453
|
+
// ============================================================================
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Create a memory history instance that keeps history in memory.
|
|
457
|
+
* Useful for testing and server-side rendering.
|
|
458
|
+
*/
|
|
459
|
+
export function createMemoryHistory(
|
|
460
|
+
options: {
|
|
461
|
+
initialEntries?: string[]
|
|
462
|
+
initialIndex?: number
|
|
463
|
+
} = {},
|
|
464
|
+
): History {
|
|
465
|
+
const { initialEntries = ['/'], initialIndex } = options
|
|
466
|
+
const listeners = new Set<HistoryListener>()
|
|
467
|
+
const blockers = new Set<Blocker>()
|
|
468
|
+
|
|
469
|
+
// Initialize entries
|
|
470
|
+
const entries: Location[] = initialEntries.map((entry, i) => createLocation(entry, null, `${i}`))
|
|
471
|
+
|
|
472
|
+
let index = initialIndex ?? entries.length - 1
|
|
473
|
+
let action: HistoryAction = 'POP'
|
|
474
|
+
|
|
475
|
+
// Clamp index to valid range
|
|
476
|
+
index = Math.max(0, Math.min(index, entries.length - 1))
|
|
477
|
+
|
|
478
|
+
function notifyListeners() {
|
|
479
|
+
const location = entries[index]!
|
|
480
|
+
for (const listener of listeners) {
|
|
481
|
+
listener({ action, location })
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function push(to: To, state?: unknown) {
|
|
486
|
+
const nextLocation = createLocation(to, state)
|
|
487
|
+
const nextAction: HistoryAction = 'PUSH'
|
|
488
|
+
|
|
489
|
+
// Check blockers
|
|
490
|
+
if (blockers.size > 0) {
|
|
491
|
+
let blocked = false
|
|
492
|
+
const retry = () => push(to, state)
|
|
493
|
+
|
|
494
|
+
for (const blocker of blockers) {
|
|
495
|
+
blocker({
|
|
496
|
+
action: nextAction,
|
|
497
|
+
location: nextLocation,
|
|
498
|
+
retry,
|
|
499
|
+
})
|
|
500
|
+
blocked = true
|
|
501
|
+
break
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (blocked) return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
action = nextAction
|
|
508
|
+
|
|
509
|
+
// Remove any entries after the current index
|
|
510
|
+
entries.splice(index + 1)
|
|
511
|
+
|
|
512
|
+
// Add the new entry
|
|
513
|
+
entries.push(nextLocation)
|
|
514
|
+
index = entries.length - 1
|
|
515
|
+
|
|
516
|
+
notifyListeners()
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function replace(to: To, state?: unknown) {
|
|
520
|
+
const nextLocation = createLocation(to, state)
|
|
521
|
+
const nextAction: HistoryAction = 'REPLACE'
|
|
522
|
+
|
|
523
|
+
// Check blockers
|
|
524
|
+
if (blockers.size > 0) {
|
|
525
|
+
let blocked = false
|
|
526
|
+
const retry = () => replace(to, state)
|
|
527
|
+
|
|
528
|
+
for (const blocker of blockers) {
|
|
529
|
+
blocker({
|
|
530
|
+
action: nextAction,
|
|
531
|
+
location: nextLocation,
|
|
532
|
+
retry,
|
|
533
|
+
})
|
|
534
|
+
blocked = true
|
|
535
|
+
break
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (blocked) return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
action = nextAction
|
|
542
|
+
entries[index] = nextLocation
|
|
543
|
+
|
|
544
|
+
notifyListeners()
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function go(delta: number) {
|
|
548
|
+
const nextIndex = Math.max(0, Math.min(index + delta, entries.length - 1))
|
|
549
|
+
|
|
550
|
+
if (nextIndex === index) return
|
|
551
|
+
|
|
552
|
+
const nextLocation = entries[nextIndex]!
|
|
553
|
+
const nextAction: HistoryAction = 'POP'
|
|
554
|
+
|
|
555
|
+
// Check blockers
|
|
556
|
+
if (blockers.size > 0) {
|
|
557
|
+
let blocked = false
|
|
558
|
+
const retry = () => go(delta)
|
|
559
|
+
|
|
560
|
+
for (const blocker of blockers) {
|
|
561
|
+
blocker({
|
|
562
|
+
action: nextAction,
|
|
563
|
+
location: nextLocation,
|
|
564
|
+
retry,
|
|
565
|
+
})
|
|
566
|
+
blocked = true
|
|
567
|
+
break
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (blocked) return
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
action = nextAction
|
|
574
|
+
index = nextIndex
|
|
575
|
+
|
|
576
|
+
notifyListeners()
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
get action() {
|
|
581
|
+
return action
|
|
582
|
+
},
|
|
583
|
+
get location() {
|
|
584
|
+
return entries[index]!
|
|
585
|
+
},
|
|
586
|
+
push,
|
|
587
|
+
replace,
|
|
588
|
+
go,
|
|
589
|
+
back() {
|
|
590
|
+
go(-1)
|
|
591
|
+
},
|
|
592
|
+
forward() {
|
|
593
|
+
go(1)
|
|
594
|
+
},
|
|
595
|
+
listen(listener: HistoryListener) {
|
|
596
|
+
listeners.add(listener)
|
|
597
|
+
return () => listeners.delete(listener)
|
|
598
|
+
},
|
|
599
|
+
createHref(to: To) {
|
|
600
|
+
const loc = typeof to === 'string' ? parseURL(to) : to
|
|
601
|
+
return createURL(loc as Location)
|
|
602
|
+
},
|
|
603
|
+
block(blocker: Blocker) {
|
|
604
|
+
blockers.add(blocker)
|
|
605
|
+
return () => blockers.delete(blocker)
|
|
606
|
+
},
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ============================================================================
|
|
611
|
+
// Static History (for SSR)
|
|
612
|
+
// ============================================================================
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Create a static history for server-side rendering.
|
|
616
|
+
* This history doesn't support navigation and always returns the initial location.
|
|
617
|
+
*/
|
|
618
|
+
export function createStaticHistory(url: string): History {
|
|
619
|
+
const location = createLocation(url)
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
get action(): HistoryAction {
|
|
623
|
+
return 'POP'
|
|
624
|
+
},
|
|
625
|
+
get location() {
|
|
626
|
+
return location
|
|
627
|
+
},
|
|
628
|
+
push() {
|
|
629
|
+
// No-op on server
|
|
630
|
+
console.warn('[fict-router] Cannot push on static history (SSR)')
|
|
631
|
+
},
|
|
632
|
+
replace() {
|
|
633
|
+
// No-op on server
|
|
634
|
+
console.warn('[fict-router] Cannot replace on static history (SSR)')
|
|
635
|
+
},
|
|
636
|
+
go() {
|
|
637
|
+
// No-op on server
|
|
638
|
+
console.warn('[fict-router] Cannot go on static history (SSR)')
|
|
639
|
+
},
|
|
640
|
+
back() {
|
|
641
|
+
// No-op on server
|
|
642
|
+
},
|
|
643
|
+
forward() {
|
|
644
|
+
// No-op on server
|
|
645
|
+
},
|
|
646
|
+
listen() {
|
|
647
|
+
// No-op on server
|
|
648
|
+
return () => {}
|
|
649
|
+
},
|
|
650
|
+
createHref(to: To) {
|
|
651
|
+
const loc = typeof to === 'string' ? parseURL(to) : to
|
|
652
|
+
return createURL(loc as Location)
|
|
653
|
+
},
|
|
654
|
+
block() {
|
|
655
|
+
// No-op on server
|
|
656
|
+
return () => {}
|
|
657
|
+
},
|
|
658
|
+
}
|
|
659
|
+
}
|