@benjavicente/angular-router-experimental 1.142.11
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/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/fesm2022/tanstack-angular-router-experimental-experimental.mjs +920 -0
- package/dist/fesm2022/tanstack-angular-router-experimental.mjs +4131 -0
- package/dist/types/tanstack-angular-router-experimental-experimental.d.ts +110 -0
- package/dist/types/tanstack-angular-router-experimental.d.ts +733 -0
- package/experimental/injectRouteErrorHandler.ts +51 -0
- package/experimental/public_api.ts +8 -0
- package/package.json +98 -0
- package/src/DefaultNotFound.ts +9 -0
- package/src/Link.ts +352 -0
- package/src/Match.ts +338 -0
- package/src/Matches.ts +37 -0
- package/src/RouterProvider.ts +162 -0
- package/src/document/build-match-managed-document.ts +308 -0
- package/src/document/document-dehydration.ts +27 -0
- package/src/document/document-equality.ts +29 -0
- package/src/document/document-router-token.ts +6 -0
- package/src/document/index.ts +33 -0
- package/src/document/install-unified-document-sync.ts +108 -0
- package/src/document/managed-document-types.ts +36 -0
- package/src/document/managed-dom.ts +307 -0
- package/src/document/provide-tanstack-body-managed-tags.ts +78 -0
- package/src/document/provide-tanstack-document-title.ts +59 -0
- package/src/document/provide-tanstack-document.ts +62 -0
- package/src/document/provide-tanstack-head-managed-tags.ts +63 -0
- package/src/fileRoute.ts +232 -0
- package/src/index.ts +173 -0
- package/src/injectBlocker.ts +196 -0
- package/src/injectCanGoBack.ts +11 -0
- package/src/injectErrorState.ts +21 -0
- package/src/injectIntersectionObserver.ts +28 -0
- package/src/injectLoaderData.ts +49 -0
- package/src/injectLoaderDeps.ts +45 -0
- package/src/injectLocation.ts +38 -0
- package/src/injectMatch.ts +122 -0
- package/src/injectMatchRoute.ts +58 -0
- package/src/injectMatches.ts +79 -0
- package/src/injectNavigate.ts +24 -0
- package/src/injectParams.ts +71 -0
- package/src/injectRouteContext.ts +31 -0
- package/src/injectRouter.ts +17 -0
- package/src/injectRouterState.ts +53 -0
- package/src/injectSearch.ts +71 -0
- package/src/injectStore.ts +87 -0
- package/src/matchInjectorToken.ts +23 -0
- package/src/renderer/injectIsCatchingError.ts +40 -0
- package/src/renderer/injectRender.ts +69 -0
- package/src/route.ts +641 -0
- package/src/router.ts +141 -0
- package/src/routerInjectionToken.ts +24 -0
- package/src/routerStores.ts +107 -0
- package/src/ssr-scroll-restoration.ts +48 -0
- package/src/transitioner.ts +255 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DestroyRef, inject, untracked } from '@angular/core'
|
|
2
|
+
import { injectMatch, injectRouter } from '@benjavicente/angular-router-experimental'
|
|
3
|
+
import type { AnyRouter, FromPathOption, RegisteredRouter } from '@benjavicente/router-core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EXPERIMENTAL
|
|
7
|
+
*
|
|
8
|
+
* While in other adapters you can use build-in error boundaries,
|
|
9
|
+
* Angular does not provide any. As an workarraound, we export a function
|
|
10
|
+
* to simulate an error boundary by changing the router state to show
|
|
11
|
+
* the error component.
|
|
12
|
+
*
|
|
13
|
+
* Note that an equivalent for suspense can't exist since we can't restore
|
|
14
|
+
* the component state when the promise is resolved as is with other adapters.
|
|
15
|
+
*/
|
|
16
|
+
export function injectRouteErrorHandler<
|
|
17
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
18
|
+
TDefaultFrom extends string = string,
|
|
19
|
+
>(options: { from?: FromPathOption<TRouter, TDefaultFrom> }) {
|
|
20
|
+
const router = injectRouter()
|
|
21
|
+
const match = injectMatch({ from: options.from })
|
|
22
|
+
|
|
23
|
+
let destroyed = false
|
|
24
|
+
|
|
25
|
+
inject(DestroyRef).onDestroy(() => {
|
|
26
|
+
destroyed = true
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
throw: (error: Error) => {
|
|
31
|
+
if (destroyed) {
|
|
32
|
+
console.warn(
|
|
33
|
+
'Attempted to throw error to route after it has been destroyed',
|
|
34
|
+
)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const matchId = untracked(match).id
|
|
39
|
+
|
|
40
|
+
router.updateMatch(matchId, (match) => {
|
|
41
|
+
return {
|
|
42
|
+
...match,
|
|
43
|
+
error,
|
|
44
|
+
status: 'error',
|
|
45
|
+
isFetching: false,
|
|
46
|
+
updatedAt: Date.now(),
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@benjavicente/angular-router-experimental",
|
|
3
|
+
"version": "1.142.11",
|
|
4
|
+
"description": "Modern and scalable routing for Angular applications",
|
|
5
|
+
"author": "Tanner Linsley",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TanStack/router.git",
|
|
10
|
+
"directory": "packages/angular-router-experimental"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://tanstack.com/router",
|
|
13
|
+
"funding": {
|
|
14
|
+
"type": "github",
|
|
15
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"angular",
|
|
19
|
+
"location",
|
|
20
|
+
"router",
|
|
21
|
+
"routing",
|
|
22
|
+
"async",
|
|
23
|
+
"async router",
|
|
24
|
+
"typescript"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"types": "./dist/types/tanstack-angular-router-experimental.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": {
|
|
31
|
+
"default": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/types/tanstack-angular-router-experimental.d.ts",
|
|
35
|
+
"require": null,
|
|
36
|
+
"default": "./dist/fesm2022/tanstack-angular-router-experimental.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./experimental": {
|
|
39
|
+
"types": "./dist/types/tanstack-angular-router-experimental-experimental.d.ts",
|
|
40
|
+
"require": null,
|
|
41
|
+
"default": "./dist/fesm2022/tanstack-angular-router-experimental-experimental.mjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"src",
|
|
47
|
+
"experimental"
|
|
48
|
+
],
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20.19"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"isbot": "^5.1.22",
|
|
54
|
+
"tslib": "^2.3.0",
|
|
55
|
+
"@benjavicente/history": "1.161.6",
|
|
56
|
+
"@benjavicente/router-core": "1.168.9"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@analogjs/vitest-angular": "^2.2.1",
|
|
60
|
+
"@angular/common": "^21.2.6",
|
|
61
|
+
"@angular/compiler": "^21.2.6",
|
|
62
|
+
"@angular/compiler-cli": "^21.2.6",
|
|
63
|
+
"@angular/core": "^21.2.6",
|
|
64
|
+
"@angular/platform-browser": "^21.2.6",
|
|
65
|
+
"@oxc-angular/vite": "^0.0.22",
|
|
66
|
+
"@testing-library/angular": "^19.0.0",
|
|
67
|
+
"@tanstack/vite-config": "0.5.2",
|
|
68
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
69
|
+
"@types/node": ">=20",
|
|
70
|
+
"combinate": "^1.1.11",
|
|
71
|
+
"jsdom": "^27.4.0",
|
|
72
|
+
"rxjs": "~7.8.0",
|
|
73
|
+
"vibe-rules": "^0.2.57",
|
|
74
|
+
"vite": "^8.0.0",
|
|
75
|
+
"vite-plugin-dts": "^4.5.4",
|
|
76
|
+
"vitest": "^4.0.17",
|
|
77
|
+
"zod": "^3.24.2"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"@angular/common": "^21.2.6",
|
|
81
|
+
"@angular/core": "^21.2.6"
|
|
82
|
+
},
|
|
83
|
+
"scripts": {
|
|
84
|
+
"clean": "rimraf ./dist && rimraf ./coverage",
|
|
85
|
+
"test:eslint": "eslint",
|
|
86
|
+
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
|
|
87
|
+
"test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js -p tsconfig.legacy.json",
|
|
88
|
+
"test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js -p tsconfig.legacy.json",
|
|
89
|
+
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js -p tsconfig.legacy.json",
|
|
90
|
+
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js -p tsconfig.legacy.json",
|
|
91
|
+
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js -p tsconfig.legacy.json",
|
|
92
|
+
"test:types:ts59": "tsc -p tsconfig.legacy.json",
|
|
93
|
+
"test:unit": "vitest",
|
|
94
|
+
"test:unit:dev": "vitest --watch",
|
|
95
|
+
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
|
|
96
|
+
"build": "vite build --config vite.lib.config.ts"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'tanstack-angular-router-default-no-found',
|
|
5
|
+
template: `<p>Not found</p>`,
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
7
|
+
host: { style: 'display: contents;' },
|
|
8
|
+
})
|
|
9
|
+
export class DefaultNotFoundComponent {}
|
package/src/Link.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DestroyRef,
|
|
3
|
+
Directive,
|
|
4
|
+
ElementRef,
|
|
5
|
+
Renderer2,
|
|
6
|
+
afterNextRender,
|
|
7
|
+
computed,
|
|
8
|
+
effect,
|
|
9
|
+
inject,
|
|
10
|
+
input,
|
|
11
|
+
signal,
|
|
12
|
+
untracked,
|
|
13
|
+
} from '@angular/core'
|
|
14
|
+
import {
|
|
15
|
+
AnyRouter,
|
|
16
|
+
LinkOptions as CoreLinkOptions,
|
|
17
|
+
RegisteredRouter,
|
|
18
|
+
RoutePaths,
|
|
19
|
+
deepEqual,
|
|
20
|
+
exactPathTest,
|
|
21
|
+
preloadWarning,
|
|
22
|
+
removeTrailingSlash,
|
|
23
|
+
} from '@benjavicente/router-core'
|
|
24
|
+
import { injectLocation } from './injectLocation'
|
|
25
|
+
import { injectRouter } from './injectRouter'
|
|
26
|
+
import { injectIntersectionObserver } from './injectIntersectionObserver'
|
|
27
|
+
|
|
28
|
+
@Directive({
|
|
29
|
+
selector: 'a[link]',
|
|
30
|
+
exportAs: 'link',
|
|
31
|
+
standalone: true,
|
|
32
|
+
host: {
|
|
33
|
+
'[href]': 'hrefOption()?.href',
|
|
34
|
+
'(click)': 'handleClick($event)',
|
|
35
|
+
'(focus)': 'handleFocus()',
|
|
36
|
+
'(mouseenter)': 'handleEnter($event)',
|
|
37
|
+
'(mouseover)': 'handleEnter($event)',
|
|
38
|
+
'(mouseleave)': 'handleLeave($event)',
|
|
39
|
+
'[attr.target]': 'target()',
|
|
40
|
+
'[attr.role]': 'disabled() ? "link" : undefined',
|
|
41
|
+
'[attr.aria-disabled]': 'disabled()',
|
|
42
|
+
'[attr.data-status]': 'isActive() ? "active" : undefined',
|
|
43
|
+
'[attr.aria-current]': 'isActive() ? "page" : undefined',
|
|
44
|
+
'[attr.data-transitioning]':
|
|
45
|
+
'isTransitioning() ? "transitioning" : undefined',
|
|
46
|
+
'[class]': 'isActiveProps()?.class',
|
|
47
|
+
'[style]': 'isActiveProps()?.style',
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
export class Link<
|
|
51
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
52
|
+
TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
|
|
53
|
+
TTo extends string | undefined = '.',
|
|
54
|
+
TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
|
|
55
|
+
TMaskTo extends string = '.',
|
|
56
|
+
> {
|
|
57
|
+
passiveEvents = injectPasiveEvents(() => ({
|
|
58
|
+
touchstart: this.handleTouchStart,
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
options = input.required<
|
|
62
|
+
LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
|
|
63
|
+
>({ alias: 'link' })
|
|
64
|
+
|
|
65
|
+
protected router = injectRouter()
|
|
66
|
+
protected isTransitioning = signal(false)
|
|
67
|
+
|
|
68
|
+
protected from = computed(() =>
|
|
69
|
+
untracked(() => this.options().from),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
protected disabled = computed(() => this._options().disabled ?? false)
|
|
73
|
+
protected target = computed(() => this._options().target)
|
|
74
|
+
|
|
75
|
+
protected _options = computed<
|
|
76
|
+
LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
|
|
77
|
+
>(() => {
|
|
78
|
+
return {
|
|
79
|
+
...this.options(),
|
|
80
|
+
from: this.from(),
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
protected nextLocation = computed(() => {
|
|
85
|
+
const currentLocation = this.location()
|
|
86
|
+
return this.router.buildLocation({
|
|
87
|
+
_fromLocation: currentLocation,
|
|
88
|
+
...this._options(),
|
|
89
|
+
} as any)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
protected hrefOption = computed(() => {
|
|
93
|
+
if (this._options().disabled) {
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const location = this.nextLocation().maskedLocation ?? this.nextLocation()
|
|
98
|
+
const external = location.external
|
|
99
|
+
const href = external
|
|
100
|
+
? location.publicHref
|
|
101
|
+
: this.router.history.createHref(location.publicHref) || '/'
|
|
102
|
+
|
|
103
|
+
return { href, external }
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
protected externalLink = computed(() => {
|
|
107
|
+
const hrefOption = this.hrefOption()
|
|
108
|
+
if (hrefOption?.external) {
|
|
109
|
+
return hrefOption.href
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
new URL(this.options()['to'] as any)
|
|
113
|
+
return this.options()['to']
|
|
114
|
+
} catch { }
|
|
115
|
+
return undefined
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
protected preload = computed(() => {
|
|
119
|
+
if (this.options()['reloadDocument']) {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
return this.options()['preload'] ?? this.router.options.defaultPreload
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
protected preloadDelay = computed(() => {
|
|
126
|
+
return (
|
|
127
|
+
this.options()['preloadDelay'] ??
|
|
128
|
+
this.router.options.defaultPreloadDelay ??
|
|
129
|
+
0
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
protected location = injectLocation<TRouter>()
|
|
134
|
+
|
|
135
|
+
protected isActiveProps = computed(() => {
|
|
136
|
+
const opts = this.options()
|
|
137
|
+
const isActive = this.isActive()
|
|
138
|
+
const props = isActive ? opts.activeProps : opts.inactiveProps
|
|
139
|
+
if (!props || typeof props !== 'object') return undefined
|
|
140
|
+
return props
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
protected isActive = computed(() => {
|
|
144
|
+
if (this.externalLink()) return false
|
|
145
|
+
|
|
146
|
+
const options = this.options()
|
|
147
|
+
const activeOptions = options.activeOptions
|
|
148
|
+
|
|
149
|
+
if (activeOptions?.exact) {
|
|
150
|
+
const testExact = exactPathTest(
|
|
151
|
+
this.location().pathname,
|
|
152
|
+
this.nextLocation().pathname,
|
|
153
|
+
this.router.basepath,
|
|
154
|
+
)
|
|
155
|
+
if (!testExact) {
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
const currentPathSplit = removeTrailingSlash(
|
|
160
|
+
this.location().pathname,
|
|
161
|
+
this.router.basepath,
|
|
162
|
+
)
|
|
163
|
+
const nextPathSplit = removeTrailingSlash(
|
|
164
|
+
this.nextLocation().pathname,
|
|
165
|
+
this.router.basepath,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const pathIsFuzzyEqual =
|
|
169
|
+
currentPathSplit.startsWith(nextPathSplit) &&
|
|
170
|
+
(currentPathSplit.length === nextPathSplit.length ||
|
|
171
|
+
currentPathSplit[nextPathSplit.length] === '/')
|
|
172
|
+
|
|
173
|
+
if (!pathIsFuzzyEqual) {
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (activeOptions?.includeSearch ?? true) {
|
|
179
|
+
const searchTest = deepEqual(
|
|
180
|
+
this.location().search,
|
|
181
|
+
this.nextLocation().search,
|
|
182
|
+
{
|
|
183
|
+
partial: !activeOptions?.exact,
|
|
184
|
+
ignoreUndefined: !activeOptions?.explicitUndefined,
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
if (!searchTest) {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (activeOptions?.includeHash) {
|
|
193
|
+
return this.location().hash === this.nextLocation().hash
|
|
194
|
+
}
|
|
195
|
+
return true
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
protected doPreload = () => {
|
|
199
|
+
this.router.preloadRoute(this.options() as any).catch((err: any) => {
|
|
200
|
+
console.warn(err)
|
|
201
|
+
console.warn(preloadWarning)
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
protected preloadViewportIoCallback = (
|
|
206
|
+
entry: IntersectionObserverEntry | undefined,
|
|
207
|
+
) => {
|
|
208
|
+
if (entry?.isIntersecting) {
|
|
209
|
+
this.doPreload()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private viewportPreloader = injectIntersectionObserver(
|
|
214
|
+
this.preloadViewportIoCallback,
|
|
215
|
+
{ rootMargin: '100px' },
|
|
216
|
+
() => !!this._options().disabled || !(this.preload() === 'viewport'),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
private hasRenderFetched = false
|
|
220
|
+
private rendererPreloader = effect(() => {
|
|
221
|
+
if (this.hasRenderFetched) return
|
|
222
|
+
|
|
223
|
+
if (!this._options().disabled && this.preload() === 'render') {
|
|
224
|
+
this.doPreload()
|
|
225
|
+
this.hasRenderFetched = true
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
protected handleClick = (event: MouseEvent) => {
|
|
230
|
+
const elementTarget = (
|
|
231
|
+
event.currentTarget as HTMLAnchorElement | SVGAElement
|
|
232
|
+
).getAttribute('target')
|
|
233
|
+
const target = this._options().target
|
|
234
|
+
const effectiveTarget = target !== undefined ? target : elementTarget
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
!this._options().disabled &&
|
|
238
|
+
!isCtrlEvent(event) &&
|
|
239
|
+
!event.defaultPrevented &&
|
|
240
|
+
(!effectiveTarget || effectiveTarget === '_self') &&
|
|
241
|
+
event.button === 0
|
|
242
|
+
) {
|
|
243
|
+
event.preventDefault()
|
|
244
|
+
|
|
245
|
+
this.isTransitioning.set(true)
|
|
246
|
+
|
|
247
|
+
const unsub = this.router.subscribe('onResolved', () => {
|
|
248
|
+
unsub()
|
|
249
|
+
this.isTransitioning.set(false)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
this.router.navigate(this._options())
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
protected handleFocus = () => {
|
|
257
|
+
if (this._options().disabled) return
|
|
258
|
+
if (this.preload()) {
|
|
259
|
+
this.doPreload()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
protected handleTouchStart = () => {
|
|
264
|
+
if (this._options().disabled) return
|
|
265
|
+
if (this.preload()) {
|
|
266
|
+
this.doPreload()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
protected handleEnter = (event: MouseEvent) => {
|
|
271
|
+
if (this._options().disabled) return
|
|
272
|
+
const eventTarget = (event.currentTarget || {}) as EventTargetWithPreloadTimeout
|
|
273
|
+
|
|
274
|
+
if (this.preload()) {
|
|
275
|
+
if (eventTarget.preloadTimeout) {
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
eventTarget.preloadTimeout = setTimeout(() => {
|
|
280
|
+
eventTarget.preloadTimeout = null
|
|
281
|
+
this.doPreload()
|
|
282
|
+
}, this.preloadDelay())
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
protected handleLeave = (event: MouseEvent) => {
|
|
287
|
+
if (this._options().disabled) return
|
|
288
|
+
const eventTarget = (event.currentTarget || {}) as EventTargetWithPreloadTimeout
|
|
289
|
+
|
|
290
|
+
if (eventTarget.preloadTimeout) {
|
|
291
|
+
clearTimeout(eventTarget.preloadTimeout)
|
|
292
|
+
eventTarget.preloadTimeout = null
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
interface ActiveLinkProps {
|
|
298
|
+
class?: string
|
|
299
|
+
style?: string
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface ActiveLinkOptionProps {
|
|
303
|
+
activeProps?: ActiveLinkProps
|
|
304
|
+
inactiveProps?: ActiveLinkProps
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
type EventTargetWithPreloadTimeout = EventTarget & {
|
|
308
|
+
preloadTimeout?: ReturnType<typeof setTimeout> | null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export type LinkOptions<
|
|
312
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
313
|
+
TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
|
|
314
|
+
TTo extends string | undefined = '.',
|
|
315
|
+
TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
|
|
316
|
+
TMaskTo extends string = '.',
|
|
317
|
+
> = CoreLinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
|
|
318
|
+
ActiveLinkOptionProps
|
|
319
|
+
|
|
320
|
+
function isCtrlEvent(e: MouseEvent) {
|
|
321
|
+
return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Angular does not provide by default passive events listeners
|
|
325
|
+
// to some events like React, and does not support a pasive options
|
|
326
|
+
// in the template, so we attach the pasive events manually here
|
|
327
|
+
|
|
328
|
+
type PassiveEvents = {
|
|
329
|
+
touchstart: (event: TouchEvent) => void
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function injectPasiveEvents(passiveEvents: () => PassiveEvents) {
|
|
333
|
+
const element = inject(ElementRef).nativeElement
|
|
334
|
+
const destroyRef = inject(DestroyRef)
|
|
335
|
+
const renderer = inject(Renderer2)
|
|
336
|
+
const cleanups: Array<() => void> = []
|
|
337
|
+
|
|
338
|
+
afterNextRender(() => {
|
|
339
|
+
for (const [event, handler] of Object.entries(passiveEvents())) {
|
|
340
|
+
const cleanup = renderer.listen(element, event, handler, {
|
|
341
|
+
passive: true,
|
|
342
|
+
})
|
|
343
|
+
cleanups.push(cleanup)
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
destroyRef.onDestroy(() => {
|
|
348
|
+
while (cleanups.length) {
|
|
349
|
+
cleanups.pop()?.()
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
}
|