@tanstack/vue-router 1.167.4 → 1.168.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/esm/Match.js +55 -61
- package/dist/esm/Match.js.map +1 -1
- package/dist/esm/Matches.js +8 -15
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/Scripts.js +7 -6
- package/dist/esm/Scripts.js.map +1 -1
- package/dist/esm/Transitioner.js +18 -24
- package/dist/esm/Transitioner.js.map +1 -1
- package/dist/esm/headContentUtils.js +13 -15
- package/dist/esm/headContentUtils.js.map +1 -1
- package/dist/esm/index.dev.js +6 -6
- package/dist/esm/index.js +6 -6
- package/dist/esm/link.js +242 -178
- package/dist/esm/link.js.map +1 -1
- package/dist/esm/matchContext.d.ts +8 -14
- package/dist/esm/matchContext.js +11 -9
- package/dist/esm/matchContext.js.map +1 -1
- package/dist/esm/not-found.js +6 -3
- package/dist/esm/not-found.js.map +1 -1
- package/dist/esm/router.js +2 -1
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/routerStores.d.ts +13 -0
- package/dist/esm/routerStores.js +33 -0
- package/dist/esm/routerStores.js.map +1 -0
- package/dist/esm/ssr/RouterClient.js +1 -1
- package/dist/esm/ssr/RouterClient.js.map +1 -1
- package/dist/esm/ssr/renderRouterToStream.js +2 -2
- package/dist/esm/ssr/renderRouterToStream.js.map +1 -1
- package/dist/esm/ssr/renderRouterToString.js +1 -1
- package/dist/esm/ssr/renderRouterToString.js.map +1 -1
- package/dist/esm/useCanGoBack.d.ts +1 -1
- package/dist/esm/useCanGoBack.js +3 -2
- package/dist/esm/useCanGoBack.js.map +1 -1
- package/dist/esm/useLocation.js +3 -2
- package/dist/esm/useLocation.js.map +1 -1
- package/dist/esm/useMatch.js +29 -19
- package/dist/esm/useMatch.js.map +1 -1
- package/dist/esm/useRouterState.js +4 -4
- package/dist/esm/useRouterState.js.map +1 -1
- package/dist/source/Match.jsx +121 -159
- package/dist/source/Match.jsx.map +1 -1
- package/dist/source/Matches.jsx +11 -28
- package/dist/source/Matches.jsx.map +1 -1
- package/dist/source/Scripts.jsx +32 -35
- package/dist/source/Scripts.jsx.map +1 -1
- package/dist/source/Transitioner.jsx +19 -21
- package/dist/source/Transitioner.jsx.map +1 -1
- package/dist/source/headContentUtils.jsx +51 -61
- package/dist/source/headContentUtils.jsx.map +1 -1
- package/dist/source/link.jsx +298 -249
- package/dist/source/link.jsx.map +1 -1
- package/dist/source/matchContext.d.ts +8 -14
- package/dist/source/matchContext.jsx +17 -23
- package/dist/source/matchContext.jsx.map +1 -1
- package/dist/source/not-found.jsx +6 -5
- package/dist/source/not-found.jsx.map +1 -1
- package/dist/source/router.js +2 -1
- package/dist/source/router.js.map +1 -1
- package/dist/source/routerStores.d.ts +13 -0
- package/dist/source/routerStores.js +37 -0
- package/dist/source/routerStores.js.map +1 -0
- package/dist/source/ssr/RouterClient.jsx +1 -1
- package/dist/source/ssr/RouterClient.jsx.map +1 -1
- package/dist/source/ssr/renderRouterToStream.jsx +2 -2
- package/dist/source/ssr/renderRouterToStream.jsx.map +1 -1
- package/dist/source/ssr/renderRouterToString.jsx +1 -1
- package/dist/source/ssr/renderRouterToString.jsx.map +1 -1
- package/dist/source/useCanGoBack.d.ts +1 -1
- package/dist/source/useCanGoBack.js +4 -2
- package/dist/source/useCanGoBack.js.map +1 -1
- package/dist/source/useLocation.jsx +4 -4
- package/dist/source/useLocation.jsx.map +1 -1
- package/dist/source/useMatch.jsx +60 -38
- package/dist/source/useMatch.jsx.map +1 -1
- package/dist/source/useRouterState.jsx +4 -4
- package/dist/source/useRouterState.jsx.map +1 -1
- package/package.json +2 -2
- package/skills/vue-router/SKILL.md +3 -0
- package/src/Match.tsx +168 -180
- package/src/Matches.tsx +18 -31
- package/src/Scripts.tsx +40 -40
- package/src/Transitioner.tsx +35 -23
- package/src/headContentUtils.tsx +101 -107
- package/src/link.tsx +445 -300
- package/src/matchContext.tsx +23 -25
- package/src/not-found.tsx +9 -5
- package/src/router.ts +2 -1
- package/src/routerStores.ts +54 -0
- package/src/ssr/RouterClient.tsx +1 -1
- package/src/ssr/renderRouterToStream.tsx +2 -2
- package/src/ssr/renderRouterToString.tsx +1 -1
- package/src/useCanGoBack.ts +7 -2
- package/src/useLocation.tsx +8 -5
- package/src/useMatch.tsx +95 -49
- package/src/useRouterState.tsx +6 -4
package/src/link.tsx
CHANGED
|
@@ -6,16 +6,17 @@ import {
|
|
|
6
6
|
preloadWarning,
|
|
7
7
|
removeTrailingSlash,
|
|
8
8
|
} from '@tanstack/router-core'
|
|
9
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { useStore } from '@tanstack/vue-store'
|
|
11
12
|
import { useRouter } from './useRouter'
|
|
12
13
|
import { useIntersectionObserver } from './utils'
|
|
13
|
-
import { useMatches } from './Matches'
|
|
14
14
|
|
|
15
15
|
import type {
|
|
16
16
|
AnyRouter,
|
|
17
17
|
Constrain,
|
|
18
18
|
LinkOptions,
|
|
19
|
+
ParsedLocation,
|
|
19
20
|
RegisteredRouter,
|
|
20
21
|
RoutePaths,
|
|
21
22
|
} from '@tanstack/router-core'
|
|
@@ -48,6 +49,14 @@ type LinkHTMLAttributes = AnchorHTMLAttributes &
|
|
|
48
49
|
disabled?: boolean
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
type VueStyleLinkEventHandlers = {
|
|
53
|
+
onMouseenter?: EventHandler<MouseEvent>
|
|
54
|
+
onMouseleave?: EventHandler<MouseEvent>
|
|
55
|
+
onMouseover?: EventHandler<MouseEvent>
|
|
56
|
+
onMouseout?: EventHandler<MouseEvent>
|
|
57
|
+
onTouchstart?: EventHandler<TouchEvent>
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
interface StyledProps {
|
|
52
61
|
class?: LinkHTMLAttributes['class']
|
|
53
62
|
style?: LinkHTMLAttributes['style']
|
|
@@ -63,6 +72,9 @@ type PropsOfComponent<TComp> =
|
|
|
63
72
|
? P
|
|
64
73
|
: Record<string, unknown>
|
|
65
74
|
|
|
75
|
+
type AnyLinkPropsOptions = UseLinkPropsOptions<any, any, any, any, any>
|
|
76
|
+
type LinkEventOptions = AnyLinkPropsOptions & Partial<VueStyleLinkEventHandlers>
|
|
77
|
+
|
|
66
78
|
export function useLinkProps<
|
|
67
79
|
TRouter extends AnyRouter = RegisteredRouter,
|
|
68
80
|
TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
|
|
@@ -92,172 +104,8 @@ export function useLinkProps<
|
|
|
92
104
|
}
|
|
93
105
|
})
|
|
94
106
|
|
|
95
|
-
const buildLocationKey = useRouterState({
|
|
96
|
-
select: (s) => {
|
|
97
|
-
const leaf = s.matches[s.matches.length - 1]
|
|
98
|
-
return {
|
|
99
|
-
search: leaf?.search,
|
|
100
|
-
hash: s.location.hash,
|
|
101
|
-
path: leaf?.pathname, // path + params
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
// when `from` is not supplied, use the leaf route of the current matches as the `from` location
|
|
107
|
-
const from = useMatches({
|
|
108
|
-
select: (matches) => options.from ?? matches[matches.length - 1]?.fullPath,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
const _options = Vue.computed(() => ({
|
|
112
|
-
...options,
|
|
113
|
-
from: from.value,
|
|
114
|
-
}))
|
|
115
|
-
|
|
116
|
-
const next = Vue.computed(() => {
|
|
117
|
-
// Depend on search to rebuild when search changes
|
|
118
|
-
buildLocationKey.value
|
|
119
|
-
return router.buildLocation(_options.value as any)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const preload = Vue.computed(() => {
|
|
123
|
-
if (_options.value.reloadDocument) {
|
|
124
|
-
return false
|
|
125
|
-
}
|
|
126
|
-
return options.preload ?? router.options.defaultPreload
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
const preloadDelay = Vue.computed(
|
|
130
|
-
() => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0,
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
const isActive = useRouterState({
|
|
134
|
-
select: (s) => {
|
|
135
|
-
const activeOptions = options.activeOptions
|
|
136
|
-
if (activeOptions?.exact) {
|
|
137
|
-
const testExact = exactPathTest(
|
|
138
|
-
s.location.pathname,
|
|
139
|
-
next.value.pathname,
|
|
140
|
-
router.basepath,
|
|
141
|
-
)
|
|
142
|
-
if (!testExact) {
|
|
143
|
-
return false
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
const currentPathSplit = removeTrailingSlash(
|
|
147
|
-
s.location.pathname,
|
|
148
|
-
router.basepath,
|
|
149
|
-
).split('/')
|
|
150
|
-
const nextPathSplit = removeTrailingSlash(
|
|
151
|
-
next.value?.pathname,
|
|
152
|
-
router.basepath,
|
|
153
|
-
)?.split('/')
|
|
154
|
-
|
|
155
|
-
const pathIsFuzzyEqual = nextPathSplit?.every(
|
|
156
|
-
(d, i) => d === currentPathSplit[i],
|
|
157
|
-
)
|
|
158
|
-
if (!pathIsFuzzyEqual) {
|
|
159
|
-
return false
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (activeOptions?.includeSearch ?? true) {
|
|
164
|
-
const searchTest = deepEqual(s.location.search, next.value.search, {
|
|
165
|
-
partial: !activeOptions?.exact,
|
|
166
|
-
ignoreUndefined: !activeOptions?.explicitUndefined,
|
|
167
|
-
})
|
|
168
|
-
if (!searchTest) {
|
|
169
|
-
return false
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (activeOptions?.includeHash) {
|
|
174
|
-
return s.location.hash === next.value.hash
|
|
175
|
-
}
|
|
176
|
-
return true
|
|
177
|
-
},
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
const doPreload = () =>
|
|
181
|
-
router.preloadRoute(_options.value as any).catch((err: any) => {
|
|
182
|
-
console.warn(err)
|
|
183
|
-
console.warn(preloadWarning)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const preloadViewportIoCallback = (
|
|
187
|
-
entry: IntersectionObserverEntry | undefined,
|
|
188
|
-
) => {
|
|
189
|
-
if (entry?.isIntersecting) {
|
|
190
|
-
doPreload()
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
107
|
const ref = Vue.ref<Element | null>(null)
|
|
195
|
-
|
|
196
|
-
useIntersectionObserver(
|
|
197
|
-
ref,
|
|
198
|
-
preloadViewportIoCallback,
|
|
199
|
-
{ rootMargin: '100px' },
|
|
200
|
-
{ disabled: () => !!options.disabled || !(preload.value === 'viewport') },
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
Vue.effect(() => {
|
|
204
|
-
if (hasRenderFetched) {
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
if (!options.disabled && preload.value === 'render') {
|
|
208
|
-
doPreload()
|
|
209
|
-
hasRenderFetched = true
|
|
210
|
-
}
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
// Create safe props that can be spread
|
|
214
|
-
const getPropsSafeToSpread = () => {
|
|
215
|
-
const result: Record<string, any> = {}
|
|
216
|
-
const optionRecord = options as unknown as Record<string, unknown>
|
|
217
|
-
for (const key in options) {
|
|
218
|
-
if (
|
|
219
|
-
![
|
|
220
|
-
'activeProps',
|
|
221
|
-
'inactiveProps',
|
|
222
|
-
'activeOptions',
|
|
223
|
-
'to',
|
|
224
|
-
'preload',
|
|
225
|
-
'preloadDelay',
|
|
226
|
-
'hashScrollIntoView',
|
|
227
|
-
'replace',
|
|
228
|
-
'startTransition',
|
|
229
|
-
'resetScroll',
|
|
230
|
-
'viewTransition',
|
|
231
|
-
'children',
|
|
232
|
-
'target',
|
|
233
|
-
'disabled',
|
|
234
|
-
'style',
|
|
235
|
-
'class',
|
|
236
|
-
'onClick',
|
|
237
|
-
'onBlur',
|
|
238
|
-
'onFocus',
|
|
239
|
-
'onMouseEnter',
|
|
240
|
-
'onMouseLeave',
|
|
241
|
-
'onMouseOver',
|
|
242
|
-
'onMouseOut',
|
|
243
|
-
'onTouchStart',
|
|
244
|
-
'ignoreBlocker',
|
|
245
|
-
'params',
|
|
246
|
-
'search',
|
|
247
|
-
'hash',
|
|
248
|
-
'state',
|
|
249
|
-
'mask',
|
|
250
|
-
'reloadDocument',
|
|
251
|
-
'_asChild',
|
|
252
|
-
'from',
|
|
253
|
-
'additionalProps',
|
|
254
|
-
].includes(key)
|
|
255
|
-
) {
|
|
256
|
-
result[key] = optionRecord[key]
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return result
|
|
260
|
-
}
|
|
108
|
+
const eventHandlers = getLinkEventHandlers(options as LinkEventOptions)
|
|
261
109
|
|
|
262
110
|
if (type.value === 'external') {
|
|
263
111
|
// Block dangerous protocols like javascript:, blob:, data:
|
|
@@ -267,7 +115,7 @@ export function useLinkProps<
|
|
|
267
115
|
}
|
|
268
116
|
// Return props without href to prevent navigation
|
|
269
117
|
const safeProps: Record<string, unknown> = {
|
|
270
|
-
...getPropsSafeToSpread(),
|
|
118
|
+
...getPropsSafeToSpread(options as AnyLinkPropsOptions),
|
|
271
119
|
ref,
|
|
272
120
|
// No href attribute - blocks the dangerous protocol
|
|
273
121
|
target: options.target,
|
|
@@ -277,11 +125,11 @@ export function useLinkProps<
|
|
|
277
125
|
onClick: options.onClick,
|
|
278
126
|
onBlur: options.onBlur,
|
|
279
127
|
onFocus: options.onFocus,
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
128
|
+
onMouseenter: eventHandlers.onMouseenter,
|
|
129
|
+
onMouseleave: eventHandlers.onMouseleave,
|
|
130
|
+
onMouseover: eventHandlers.onMouseover,
|
|
131
|
+
onMouseout: eventHandlers.onMouseout,
|
|
132
|
+
onTouchstart: eventHandlers.onTouchstart,
|
|
285
133
|
}
|
|
286
134
|
|
|
287
135
|
// Remove undefined values
|
|
@@ -298,7 +146,7 @@ export function useLinkProps<
|
|
|
298
146
|
|
|
299
147
|
// External links just have simple props
|
|
300
148
|
const externalProps: Record<string, unknown> = {
|
|
301
|
-
...getPropsSafeToSpread(),
|
|
149
|
+
...getPropsSafeToSpread(options as AnyLinkPropsOptions),
|
|
302
150
|
ref,
|
|
303
151
|
href: options.to,
|
|
304
152
|
target: options.target,
|
|
@@ -308,11 +156,11 @@ export function useLinkProps<
|
|
|
308
156
|
onClick: options.onClick,
|
|
309
157
|
onBlur: options.onBlur,
|
|
310
158
|
onFocus: options.onFocus,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
159
|
+
onMouseenter: eventHandlers.onMouseenter,
|
|
160
|
+
onMouseleave: eventHandlers.onMouseleave,
|
|
161
|
+
onMouseover: eventHandlers.onMouseover,
|
|
162
|
+
onMouseout: eventHandlers.onMouseout,
|
|
163
|
+
onTouchstart: eventHandlers.onTouchstart,
|
|
316
164
|
}
|
|
317
165
|
|
|
318
166
|
// Remove undefined values
|
|
@@ -327,6 +175,113 @@ export function useLinkProps<
|
|
|
327
175
|
) as unknown as LinkHTMLAttributes
|
|
328
176
|
}
|
|
329
177
|
|
|
178
|
+
// During SSR we render exactly once and do not need reactivity.
|
|
179
|
+
// Avoid store subscriptions, effects and observers on the server.
|
|
180
|
+
if (isServer ?? router.isServer) {
|
|
181
|
+
const next = router.buildLocation(options as any)
|
|
182
|
+
const href = getHref({
|
|
183
|
+
options: options as AnyLinkPropsOptions,
|
|
184
|
+
router,
|
|
185
|
+
nextLocation: next,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const isActive = getIsActive({
|
|
189
|
+
loc: router.stores.location.state,
|
|
190
|
+
nextLoc: next,
|
|
191
|
+
activeOptions: options.activeOptions,
|
|
192
|
+
router,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const {
|
|
196
|
+
resolvedActiveProps,
|
|
197
|
+
resolvedInactiveProps,
|
|
198
|
+
resolvedClassName,
|
|
199
|
+
resolvedStyle,
|
|
200
|
+
} = resolveStyleProps({
|
|
201
|
+
options: options as AnyLinkPropsOptions,
|
|
202
|
+
isActive,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const result = combineResultProps({
|
|
206
|
+
href,
|
|
207
|
+
options: options as AnyLinkPropsOptions,
|
|
208
|
+
isActive,
|
|
209
|
+
isTransitioning: false,
|
|
210
|
+
resolvedActiveProps,
|
|
211
|
+
resolvedInactiveProps,
|
|
212
|
+
resolvedClassName,
|
|
213
|
+
resolvedStyle,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
return Vue.ref(
|
|
217
|
+
result as LinkHTMLAttributes,
|
|
218
|
+
) as unknown as LinkHTMLAttributes
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const currentLocation = useStore(router.stores.location, (l) => l, {
|
|
222
|
+
equal: (prev, next) => prev.href === next.href,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const next = Vue.computed(() => {
|
|
226
|
+
// Rebuild when inherited search/hash or the current route context changes.
|
|
227
|
+
|
|
228
|
+
const opts = { _fromLocation: currentLocation.value, ...options }
|
|
229
|
+
return router.buildLocation(opts as any)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const preload = Vue.computed(() => {
|
|
233
|
+
if (options.reloadDocument) {
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
return options.preload ?? router.options.defaultPreload
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const preloadDelay = Vue.computed(
|
|
240
|
+
() => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
const isActive = Vue.computed(() =>
|
|
244
|
+
getIsActive({
|
|
245
|
+
activeOptions: options.activeOptions,
|
|
246
|
+
loc: currentLocation.value,
|
|
247
|
+
nextLoc: next.value,
|
|
248
|
+
router,
|
|
249
|
+
}),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const doPreload = () =>
|
|
253
|
+
router
|
|
254
|
+
.preloadRoute({ ...options, _builtLocation: next.value } as any)
|
|
255
|
+
.catch((err: any) => {
|
|
256
|
+
console.warn(err)
|
|
257
|
+
console.warn(preloadWarning)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const preloadViewportIoCallback = (
|
|
261
|
+
entry: IntersectionObserverEntry | undefined,
|
|
262
|
+
) => {
|
|
263
|
+
if (entry?.isIntersecting) {
|
|
264
|
+
doPreload()
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
useIntersectionObserver(
|
|
269
|
+
ref,
|
|
270
|
+
preloadViewportIoCallback,
|
|
271
|
+
{ rootMargin: '100px' },
|
|
272
|
+
{ disabled: () => !!options.disabled || !(preload.value === 'viewport') },
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
Vue.effect(() => {
|
|
276
|
+
if (hasRenderFetched) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
if (!options.disabled && preload.value === 'render') {
|
|
280
|
+
doPreload()
|
|
281
|
+
hasRenderFetched = true
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
330
285
|
// The click handler
|
|
331
286
|
const handleClick = (e: PointerEvent): void => {
|
|
332
287
|
// Check actual element's target attribute as fallback
|
|
@@ -344,7 +299,7 @@ export function useLinkProps<
|
|
|
344
299
|
e.button === 0
|
|
345
300
|
) {
|
|
346
301
|
// Don't prevent default or handle navigation if reloadDocument is true
|
|
347
|
-
if (
|
|
302
|
+
if (options.reloadDocument) {
|
|
348
303
|
return
|
|
349
304
|
}
|
|
350
305
|
|
|
@@ -359,7 +314,7 @@ export function useLinkProps<
|
|
|
359
314
|
|
|
360
315
|
// All is well? Navigate!
|
|
361
316
|
router.navigate({
|
|
362
|
-
...
|
|
317
|
+
...options,
|
|
363
318
|
replace: options.replace,
|
|
364
319
|
resetScroll: options.resetScroll,
|
|
365
320
|
hashScrollIntoView: options.hashScrollIntoView,
|
|
@@ -421,75 +376,20 @@ export function useLinkProps<
|
|
|
421
376
|
}
|
|
422
377
|
|
|
423
378
|
// Get the active and inactive props
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
: {}
|
|
431
|
-
|
|
432
|
-
return props || { class: undefined, style: undefined }
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
const resolvedInactiveProps = Vue.computed<StyledProps>(() => {
|
|
436
|
-
const inactiveProps = options.inactiveProps || (() => ({}))
|
|
437
|
-
const props = isActive.value
|
|
438
|
-
? {}
|
|
439
|
-
: typeof inactiveProps === 'function'
|
|
440
|
-
? inactiveProps()
|
|
441
|
-
: inactiveProps
|
|
442
|
-
|
|
443
|
-
return props || { class: undefined, style: undefined }
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
const resolvedClassName = Vue.computed(() => {
|
|
447
|
-
const classes = [
|
|
448
|
-
options.class,
|
|
449
|
-
resolvedActiveProps.value?.class,
|
|
450
|
-
resolvedInactiveProps.value?.class,
|
|
451
|
-
].filter(Boolean)
|
|
452
|
-
return classes.length ? classes.join(' ') : undefined
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
const resolvedStyle = Vue.computed(() => {
|
|
456
|
-
const result: Record<string, string | number> = {}
|
|
457
|
-
|
|
458
|
-
// Merge styles from all sources
|
|
459
|
-
if (options.style) {
|
|
460
|
-
Object.assign(result, options.style)
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (resolvedActiveProps.value?.style) {
|
|
464
|
-
Object.assign(result, resolvedActiveProps.value.style)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (resolvedInactiveProps.value?.style) {
|
|
468
|
-
Object.assign(result, resolvedInactiveProps.value.style)
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
return Object.keys(result).length > 0 ? result : undefined
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
const href = Vue.computed(() => {
|
|
475
|
-
if (options.disabled) {
|
|
476
|
-
return undefined
|
|
477
|
-
}
|
|
478
|
-
const nextLocation = next.value
|
|
479
|
-
const location = nextLocation?.maskedLocation ?? nextLocation
|
|
480
|
-
|
|
481
|
-
// Use publicHref - it contains the correct href for display
|
|
482
|
-
// When a rewrite changes the origin, publicHref is the full URL
|
|
483
|
-
// Otherwise it's the origin-stripped path
|
|
484
|
-
// This avoids constructing URL objects in the hot path
|
|
485
|
-
const publicHref = location?.publicHref
|
|
486
|
-
if (!publicHref) return undefined
|
|
487
|
-
|
|
488
|
-
const external = location?.external
|
|
489
|
-
if (external) return publicHref
|
|
379
|
+
const resolvedStyleProps = Vue.computed(() =>
|
|
380
|
+
resolveStyleProps({
|
|
381
|
+
options: options as AnyLinkPropsOptions,
|
|
382
|
+
isActive: isActive.value,
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
490
385
|
|
|
491
|
-
|
|
492
|
-
|
|
386
|
+
const href = Vue.computed(() =>
|
|
387
|
+
getHref({
|
|
388
|
+
options: options as AnyLinkPropsOptions,
|
|
389
|
+
router,
|
|
390
|
+
nextLocation: next.value,
|
|
391
|
+
}),
|
|
392
|
+
)
|
|
493
393
|
|
|
494
394
|
// Create static event handlers that don't change between renders
|
|
495
395
|
const staticEventHandlers = {
|
|
@@ -506,23 +406,23 @@ export function useLinkProps<
|
|
|
506
406
|
enqueueIntentPreload,
|
|
507
407
|
]) as any,
|
|
508
408
|
onMouseenter: composeEventHandlers<MouseEvent>([
|
|
509
|
-
|
|
409
|
+
eventHandlers.onMouseenter,
|
|
510
410
|
enqueueIntentPreload,
|
|
511
411
|
]) as any,
|
|
512
412
|
onMouseover: composeEventHandlers<MouseEvent>([
|
|
513
|
-
|
|
413
|
+
eventHandlers.onMouseover,
|
|
514
414
|
enqueueIntentPreload,
|
|
515
415
|
]) as any,
|
|
516
416
|
onMouseleave: composeEventHandlers<MouseEvent>([
|
|
517
|
-
|
|
417
|
+
eventHandlers.onMouseleave,
|
|
518
418
|
handleLeave,
|
|
519
419
|
]) as any,
|
|
520
420
|
onMouseout: composeEventHandlers<MouseEvent>([
|
|
521
|
-
|
|
421
|
+
eventHandlers.onMouseout,
|
|
522
422
|
handleLeave,
|
|
523
423
|
]) as any,
|
|
524
424
|
onTouchstart: composeEventHandlers<TouchEvent>([
|
|
525
|
-
|
|
425
|
+
eventHandlers.onTouchstart,
|
|
526
426
|
handleTouchStart,
|
|
527
427
|
]) as any,
|
|
528
428
|
}
|
|
@@ -530,62 +430,309 @@ export function useLinkProps<
|
|
|
530
430
|
// Compute all props synchronously to avoid hydration mismatches
|
|
531
431
|
// Using Vue.computed ensures props are calculated at render time, not after
|
|
532
432
|
const computedProps = Vue.computed<LinkHTMLAttributes>(() => {
|
|
533
|
-
const
|
|
534
|
-
|
|
433
|
+
const {
|
|
434
|
+
resolvedActiveProps,
|
|
435
|
+
resolvedInactiveProps,
|
|
436
|
+
resolvedClassName,
|
|
437
|
+
resolvedStyle,
|
|
438
|
+
} = resolvedStyleProps.value
|
|
439
|
+
return combineResultProps({
|
|
535
440
|
href: href.value,
|
|
441
|
+
options: options as AnyLinkPropsOptions,
|
|
536
442
|
ref,
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
443
|
+
staticEventHandlers,
|
|
444
|
+
isActive: isActive.value,
|
|
445
|
+
isTransitioning: isTransitioning.value,
|
|
446
|
+
resolvedActiveProps,
|
|
447
|
+
resolvedInactiveProps,
|
|
448
|
+
resolvedClassName,
|
|
449
|
+
resolvedStyle,
|
|
450
|
+
})
|
|
451
|
+
})
|
|
541
452
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
453
|
+
// Return the computed ref itself - callers should access .value
|
|
454
|
+
return computedProps as unknown as LinkHTMLAttributes
|
|
455
|
+
}
|
|
546
456
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
457
|
+
function resolveStyleProps({
|
|
458
|
+
options,
|
|
459
|
+
isActive,
|
|
460
|
+
}: {
|
|
461
|
+
options: AnyLinkPropsOptions
|
|
462
|
+
isActive: boolean
|
|
463
|
+
}) {
|
|
464
|
+
const activeProps = options.activeProps || (() => ({ class: 'active' }))
|
|
465
|
+
const resolvedActiveProps: StyledProps = (isActive
|
|
466
|
+
? typeof activeProps === 'function'
|
|
467
|
+
? activeProps()
|
|
468
|
+
: activeProps
|
|
469
|
+
: {}) || { class: undefined, style: undefined }
|
|
470
|
+
|
|
471
|
+
const inactiveProps = options.inactiveProps || (() => ({}))
|
|
472
|
+
|
|
473
|
+
const resolvedInactiveProps: StyledProps = (isActive
|
|
474
|
+
? {}
|
|
475
|
+
: typeof inactiveProps === 'function'
|
|
476
|
+
? inactiveProps()
|
|
477
|
+
: inactiveProps) || { class: undefined, style: undefined }
|
|
478
|
+
|
|
479
|
+
const classes = [
|
|
480
|
+
options.class,
|
|
481
|
+
resolvedActiveProps?.class,
|
|
482
|
+
resolvedInactiveProps?.class,
|
|
483
|
+
].filter(Boolean)
|
|
484
|
+
const resolvedClassName = classes.length ? classes.join(' ') : undefined
|
|
485
|
+
|
|
486
|
+
const result: Record<string, string | number> = {}
|
|
487
|
+
|
|
488
|
+
// Merge styles from all sources
|
|
489
|
+
if (options.style) {
|
|
490
|
+
Object.assign(result, options.style)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (resolvedActiveProps?.style) {
|
|
494
|
+
Object.assign(result, resolvedActiveProps.style)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (resolvedInactiveProps?.style) {
|
|
498
|
+
Object.assign(result, resolvedInactiveProps.style)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const resolvedStyle = Object.keys(result).length > 0 ? result : undefined
|
|
502
|
+
return {
|
|
503
|
+
resolvedActiveProps,
|
|
504
|
+
resolvedInactiveProps,
|
|
505
|
+
resolvedClassName,
|
|
506
|
+
resolvedStyle,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function combineResultProps({
|
|
511
|
+
href,
|
|
512
|
+
options,
|
|
513
|
+
isActive,
|
|
514
|
+
isTransitioning,
|
|
515
|
+
resolvedActiveProps,
|
|
516
|
+
resolvedInactiveProps,
|
|
517
|
+
resolvedClassName,
|
|
518
|
+
resolvedStyle,
|
|
519
|
+
ref,
|
|
520
|
+
staticEventHandlers,
|
|
521
|
+
}: {
|
|
522
|
+
initial?: LinkHTMLAttributes
|
|
523
|
+
href: string | undefined
|
|
524
|
+
options: AnyLinkPropsOptions
|
|
525
|
+
isActive: boolean
|
|
526
|
+
isTransitioning: boolean
|
|
527
|
+
resolvedActiveProps: StyledProps
|
|
528
|
+
resolvedInactiveProps: StyledProps
|
|
529
|
+
resolvedClassName?: string
|
|
530
|
+
resolvedStyle?: Record<string, string | number>
|
|
531
|
+
ref?: Vue.VNodeRef | undefined
|
|
532
|
+
staticEventHandlers?: {
|
|
533
|
+
onClick: any
|
|
534
|
+
onBlur: any
|
|
535
|
+
onFocus: any
|
|
536
|
+
onMouseenter: any
|
|
537
|
+
onMouseover: any
|
|
538
|
+
onMouseleave: any
|
|
539
|
+
onMouseout: any
|
|
540
|
+
onTouchstart: any
|
|
541
|
+
}
|
|
542
|
+
}) {
|
|
543
|
+
const result: Record<string, unknown> = {
|
|
544
|
+
...getPropsSafeToSpread(options),
|
|
545
|
+
ref,
|
|
546
|
+
...staticEventHandlers,
|
|
547
|
+
href,
|
|
548
|
+
disabled: !!options.disabled,
|
|
549
|
+
target: options.target,
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (resolvedStyle) {
|
|
553
|
+
result.style = resolvedStyle
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (resolvedClassName) {
|
|
557
|
+
result.class = resolvedClassName
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (options.disabled) {
|
|
561
|
+
result.role = 'link'
|
|
562
|
+
result['aria-disabled'] = true
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (isActive) {
|
|
566
|
+
result['data-status'] = 'active'
|
|
567
|
+
result['aria-current'] = 'page'
|
|
568
|
+
}
|
|
551
569
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
570
|
+
if (isTransitioning) {
|
|
571
|
+
result['data-transitioning'] = 'transitioning'
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const key of Object.keys(resolvedActiveProps)) {
|
|
575
|
+
if (key !== 'class' && key !== 'style') {
|
|
576
|
+
result[key] = resolvedActiveProps[key]
|
|
556
577
|
}
|
|
578
|
+
}
|
|
557
579
|
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
result[
|
|
561
|
-
result['aria-current'] = 'page'
|
|
580
|
+
for (const key of Object.keys(resolvedInactiveProps)) {
|
|
581
|
+
if (key !== 'class' && key !== 'style') {
|
|
582
|
+
result[key] = resolvedInactiveProps[key]
|
|
562
583
|
}
|
|
584
|
+
}
|
|
585
|
+
return result
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function getLinkEventHandlers(
|
|
589
|
+
options: LinkEventOptions,
|
|
590
|
+
): VueStyleLinkEventHandlers {
|
|
591
|
+
return {
|
|
592
|
+
onMouseenter: options.onMouseEnter ?? options.onMouseenter,
|
|
593
|
+
onMouseleave: options.onMouseLeave ?? options.onMouseleave,
|
|
594
|
+
onMouseover: options.onMouseOver ?? options.onMouseover,
|
|
595
|
+
onMouseout: options.onMouseOut ?? options.onMouseout,
|
|
596
|
+
onTouchstart: options.onTouchStart ?? options.onTouchstart,
|
|
597
|
+
}
|
|
598
|
+
}
|
|
563
599
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
600
|
+
const propsUnsafeToSpread = new Set([
|
|
601
|
+
'activeProps',
|
|
602
|
+
'inactiveProps',
|
|
603
|
+
'activeOptions',
|
|
604
|
+
'to',
|
|
605
|
+
'preload',
|
|
606
|
+
'preloadDelay',
|
|
607
|
+
'hashScrollIntoView',
|
|
608
|
+
'replace',
|
|
609
|
+
'startTransition',
|
|
610
|
+
'resetScroll',
|
|
611
|
+
'viewTransition',
|
|
612
|
+
'children',
|
|
613
|
+
'target',
|
|
614
|
+
'disabled',
|
|
615
|
+
'style',
|
|
616
|
+
'class',
|
|
617
|
+
'onClick',
|
|
618
|
+
'onBlur',
|
|
619
|
+
'onFocus',
|
|
620
|
+
'onMouseEnter',
|
|
621
|
+
'onMouseenter',
|
|
622
|
+
'onMouseLeave',
|
|
623
|
+
'onMouseleave',
|
|
624
|
+
'onMouseOver',
|
|
625
|
+
'onMouseover',
|
|
626
|
+
'onMouseOut',
|
|
627
|
+
'onMouseout',
|
|
628
|
+
'onTouchStart',
|
|
629
|
+
'onTouchstart',
|
|
630
|
+
'ignoreBlocker',
|
|
631
|
+
'params',
|
|
632
|
+
'search',
|
|
633
|
+
'hash',
|
|
634
|
+
'state',
|
|
635
|
+
'mask',
|
|
636
|
+
'reloadDocument',
|
|
637
|
+
'_asChild',
|
|
638
|
+
'from',
|
|
639
|
+
'additionalProps',
|
|
640
|
+
])
|
|
641
|
+
|
|
642
|
+
// Create safe props that can be spread
|
|
643
|
+
const getPropsSafeToSpread = (options: AnyLinkPropsOptions) => {
|
|
644
|
+
const result: Record<string, unknown> = {}
|
|
645
|
+
for (const key in options) {
|
|
646
|
+
if (!propsUnsafeToSpread.has(key)) {
|
|
647
|
+
result[key] = (options as Record<string, unknown>)[key]
|
|
567
648
|
}
|
|
649
|
+
}
|
|
568
650
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const inactiveP = resolvedInactiveProps.value
|
|
651
|
+
return result
|
|
652
|
+
}
|
|
572
653
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
654
|
+
function getIsActive({
|
|
655
|
+
activeOptions,
|
|
656
|
+
loc,
|
|
657
|
+
nextLoc,
|
|
658
|
+
router,
|
|
659
|
+
}: {
|
|
660
|
+
activeOptions: LinkOptions['activeOptions']
|
|
661
|
+
loc: {
|
|
662
|
+
pathname: string
|
|
663
|
+
search: any
|
|
664
|
+
hash: string
|
|
665
|
+
}
|
|
666
|
+
nextLoc: {
|
|
667
|
+
pathname: string
|
|
668
|
+
search: any
|
|
669
|
+
hash: string
|
|
670
|
+
}
|
|
671
|
+
router: AnyRouter
|
|
672
|
+
}) {
|
|
673
|
+
if (activeOptions?.exact) {
|
|
674
|
+
const testExact = exactPathTest(
|
|
675
|
+
loc.pathname,
|
|
676
|
+
nextLoc.pathname,
|
|
677
|
+
router.basepath,
|
|
678
|
+
)
|
|
679
|
+
if (!testExact) {
|
|
680
|
+
return false
|
|
577
681
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
682
|
+
} else {
|
|
683
|
+
const currentPath = removeTrailingSlash(loc.pathname, router.basepath)
|
|
684
|
+
const nextPath = removeTrailingSlash(nextLoc.pathname, router.basepath)
|
|
685
|
+
|
|
686
|
+
const pathIsFuzzyEqual =
|
|
687
|
+
currentPath.startsWith(nextPath) &&
|
|
688
|
+
(currentPath.length === nextPath.length ||
|
|
689
|
+
currentPath[nextPath.length] === '/')
|
|
690
|
+
if (!pathIsFuzzyEqual) {
|
|
691
|
+
return false
|
|
582
692
|
}
|
|
693
|
+
}
|
|
583
694
|
|
|
584
|
-
|
|
585
|
-
|
|
695
|
+
if (activeOptions?.includeSearch ?? true) {
|
|
696
|
+
const searchTest = deepEqual(loc.search, nextLoc.search, {
|
|
697
|
+
partial: !activeOptions?.exact,
|
|
698
|
+
ignoreUndefined: !activeOptions?.explicitUndefined,
|
|
699
|
+
})
|
|
700
|
+
if (!searchTest) {
|
|
701
|
+
return false
|
|
702
|
+
}
|
|
703
|
+
}
|
|
586
704
|
|
|
587
|
-
|
|
588
|
-
|
|
705
|
+
if (activeOptions?.includeHash) {
|
|
706
|
+
return loc.hash === nextLoc.hash
|
|
707
|
+
}
|
|
708
|
+
return true
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function getHref({
|
|
712
|
+
options,
|
|
713
|
+
router,
|
|
714
|
+
nextLocation,
|
|
715
|
+
}: {
|
|
716
|
+
options: AnyLinkPropsOptions
|
|
717
|
+
router: AnyRouter
|
|
718
|
+
nextLocation?: ParsedLocation
|
|
719
|
+
}) {
|
|
720
|
+
if (options.disabled) {
|
|
721
|
+
return undefined
|
|
722
|
+
}
|
|
723
|
+
const location = nextLocation?.maskedLocation ?? nextLocation
|
|
724
|
+
|
|
725
|
+
// Use publicHref - it contains the correct href for display
|
|
726
|
+
// When a rewrite changes the origin, publicHref is the full URL
|
|
727
|
+
// Otherwise it's the origin-stripped path
|
|
728
|
+
// This avoids constructing URL objects in the hot path
|
|
729
|
+
const publicHref = location?.publicHref
|
|
730
|
+
if (!publicHref) return undefined
|
|
731
|
+
|
|
732
|
+
const external = location?.external
|
|
733
|
+
if (external) return publicHref
|
|
734
|
+
|
|
735
|
+
return router.history.createHref(publicHref) || '/'
|
|
589
736
|
}
|
|
590
737
|
|
|
591
738
|
// Type definitions
|
|
@@ -747,17 +894,15 @@ const LinkImpl = Vue.defineComponent({
|
|
|
747
894
|
],
|
|
748
895
|
setup(props, { attrs, slots }) {
|
|
749
896
|
// Call useLinkProps ONCE during setup with combined props and attrs
|
|
750
|
-
// The returned object is a computed ref that updates reactively
|
|
751
897
|
const allProps = { ...props, ...attrs }
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
898
|
+
const linkPropsSource = useLinkProps(allProps as any) as
|
|
899
|
+
| LinkHTMLAttributes
|
|
900
|
+
| Vue.ComputedRef<LinkHTMLAttributes>
|
|
755
901
|
|
|
756
902
|
return () => {
|
|
757
903
|
const Component = props._asChild || 'a'
|
|
758
904
|
|
|
759
|
-
|
|
760
|
-
const linkProps = linkPropsComputed.value
|
|
905
|
+
const linkProps = Vue.unref(linkPropsSource)
|
|
761
906
|
|
|
762
907
|
const isActive = linkProps['data-status'] === 'active'
|
|
763
908
|
const isTransitioning =
|