@tanstack/vue-router 1.167.5 → 1.168.1
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 +4 -4
- 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/dist/source/link.jsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as Vue from 'vue';
|
|
2
2
|
import { deepEqual, exactPathTest, isDangerousProtocol, preloadWarning, removeTrailingSlash, } from '@tanstack/router-core';
|
|
3
|
-
import {
|
|
3
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
4
|
+
import { useStore } from '@tanstack/vue-store';
|
|
4
5
|
import { useRouter } from './useRouter';
|
|
5
6
|
import { useIntersectionObserver } from './utils';
|
|
6
|
-
import { useMatches } from './Matches';
|
|
7
7
|
const timeoutMap = new WeakMap();
|
|
8
8
|
export function useLinkProps(options) {
|
|
9
9
|
const router = useRouter();
|
|
@@ -24,134 +24,8 @@ export function useLinkProps(options) {
|
|
|
24
24
|
return 'internal';
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
|
-
const buildLocationKey = useRouterState({
|
|
28
|
-
select: (s) => {
|
|
29
|
-
const leaf = s.matches[s.matches.length - 1];
|
|
30
|
-
return {
|
|
31
|
-
search: leaf?.search,
|
|
32
|
-
hash: s.location.hash,
|
|
33
|
-
path: leaf?.pathname, // path + params
|
|
34
|
-
};
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
// when `from` is not supplied, use the leaf route of the current matches as the `from` location
|
|
38
|
-
const from = useMatches({
|
|
39
|
-
select: (matches) => options.from ?? matches[matches.length - 1]?.fullPath,
|
|
40
|
-
});
|
|
41
|
-
const _options = Vue.computed(() => ({
|
|
42
|
-
...options,
|
|
43
|
-
from: from.value,
|
|
44
|
-
}));
|
|
45
|
-
const next = Vue.computed(() => {
|
|
46
|
-
// Depend on search to rebuild when search changes
|
|
47
|
-
buildLocationKey.value;
|
|
48
|
-
return router.buildLocation(_options.value);
|
|
49
|
-
});
|
|
50
|
-
const preload = Vue.computed(() => {
|
|
51
|
-
if (_options.value.reloadDocument) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
return options.preload ?? router.options.defaultPreload;
|
|
55
|
-
});
|
|
56
|
-
const preloadDelay = Vue.computed(() => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0);
|
|
57
|
-
const isActive = useRouterState({
|
|
58
|
-
select: (s) => {
|
|
59
|
-
const activeOptions = options.activeOptions;
|
|
60
|
-
if (activeOptions?.exact) {
|
|
61
|
-
const testExact = exactPathTest(s.location.pathname, next.value.pathname, router.basepath);
|
|
62
|
-
if (!testExact) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
const currentPathSplit = removeTrailingSlash(s.location.pathname, router.basepath).split('/');
|
|
68
|
-
const nextPathSplit = removeTrailingSlash(next.value?.pathname, router.basepath)?.split('/');
|
|
69
|
-
const pathIsFuzzyEqual = nextPathSplit?.every((d, i) => d === currentPathSplit[i]);
|
|
70
|
-
if (!pathIsFuzzyEqual) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (activeOptions?.includeSearch ?? true) {
|
|
75
|
-
const searchTest = deepEqual(s.location.search, next.value.search, {
|
|
76
|
-
partial: !activeOptions?.exact,
|
|
77
|
-
ignoreUndefined: !activeOptions?.explicitUndefined,
|
|
78
|
-
});
|
|
79
|
-
if (!searchTest) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (activeOptions?.includeHash) {
|
|
84
|
-
return s.location.hash === next.value.hash;
|
|
85
|
-
}
|
|
86
|
-
return true;
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
const doPreload = () => router.preloadRoute(_options.value).catch((err) => {
|
|
90
|
-
console.warn(err);
|
|
91
|
-
console.warn(preloadWarning);
|
|
92
|
-
});
|
|
93
|
-
const preloadViewportIoCallback = (entry) => {
|
|
94
|
-
if (entry?.isIntersecting) {
|
|
95
|
-
doPreload();
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
27
|
const ref = Vue.ref(null);
|
|
99
|
-
|
|
100
|
-
Vue.effect(() => {
|
|
101
|
-
if (hasRenderFetched) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (!options.disabled && preload.value === 'render') {
|
|
105
|
-
doPreload();
|
|
106
|
-
hasRenderFetched = true;
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
// Create safe props that can be spread
|
|
110
|
-
const getPropsSafeToSpread = () => {
|
|
111
|
-
const result = {};
|
|
112
|
-
const optionRecord = options;
|
|
113
|
-
for (const key in options) {
|
|
114
|
-
if (![
|
|
115
|
-
'activeProps',
|
|
116
|
-
'inactiveProps',
|
|
117
|
-
'activeOptions',
|
|
118
|
-
'to',
|
|
119
|
-
'preload',
|
|
120
|
-
'preloadDelay',
|
|
121
|
-
'hashScrollIntoView',
|
|
122
|
-
'replace',
|
|
123
|
-
'startTransition',
|
|
124
|
-
'resetScroll',
|
|
125
|
-
'viewTransition',
|
|
126
|
-
'children',
|
|
127
|
-
'target',
|
|
128
|
-
'disabled',
|
|
129
|
-
'style',
|
|
130
|
-
'class',
|
|
131
|
-
'onClick',
|
|
132
|
-
'onBlur',
|
|
133
|
-
'onFocus',
|
|
134
|
-
'onMouseEnter',
|
|
135
|
-
'onMouseLeave',
|
|
136
|
-
'onMouseOver',
|
|
137
|
-
'onMouseOut',
|
|
138
|
-
'onTouchStart',
|
|
139
|
-
'ignoreBlocker',
|
|
140
|
-
'params',
|
|
141
|
-
'search',
|
|
142
|
-
'hash',
|
|
143
|
-
'state',
|
|
144
|
-
'mask',
|
|
145
|
-
'reloadDocument',
|
|
146
|
-
'_asChild',
|
|
147
|
-
'from',
|
|
148
|
-
'additionalProps',
|
|
149
|
-
].includes(key)) {
|
|
150
|
-
result[key] = optionRecord[key];
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return result;
|
|
154
|
-
};
|
|
28
|
+
const eventHandlers = getLinkEventHandlers(options);
|
|
155
29
|
if (type.value === 'external') {
|
|
156
30
|
// Block dangerous protocols like javascript:, blob:, data:
|
|
157
31
|
if (isDangerousProtocol(options.to, router.protocolAllowlist)) {
|
|
@@ -160,7 +34,7 @@ export function useLinkProps(options) {
|
|
|
160
34
|
}
|
|
161
35
|
// Return props without href to prevent navigation
|
|
162
36
|
const safeProps = {
|
|
163
|
-
...getPropsSafeToSpread(),
|
|
37
|
+
...getPropsSafeToSpread(options),
|
|
164
38
|
ref,
|
|
165
39
|
// No href attribute - blocks the dangerous protocol
|
|
166
40
|
target: options.target,
|
|
@@ -170,11 +44,11 @@ export function useLinkProps(options) {
|
|
|
170
44
|
onClick: options.onClick,
|
|
171
45
|
onBlur: options.onBlur,
|
|
172
46
|
onFocus: options.onFocus,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
47
|
+
onMouseenter: eventHandlers.onMouseenter,
|
|
48
|
+
onMouseleave: eventHandlers.onMouseleave,
|
|
49
|
+
onMouseover: eventHandlers.onMouseover,
|
|
50
|
+
onMouseout: eventHandlers.onMouseout,
|
|
51
|
+
onTouchstart: eventHandlers.onTouchstart,
|
|
178
52
|
};
|
|
179
53
|
// Remove undefined values
|
|
180
54
|
Object.keys(safeProps).forEach((key) => {
|
|
@@ -186,7 +60,7 @@ export function useLinkProps(options) {
|
|
|
186
60
|
}
|
|
187
61
|
// External links just have simple props
|
|
188
62
|
const externalProps = {
|
|
189
|
-
...getPropsSafeToSpread(),
|
|
63
|
+
...getPropsSafeToSpread(options),
|
|
190
64
|
ref,
|
|
191
65
|
href: options.to,
|
|
192
66
|
target: options.target,
|
|
@@ -196,11 +70,11 @@ export function useLinkProps(options) {
|
|
|
196
70
|
onClick: options.onClick,
|
|
197
71
|
onBlur: options.onBlur,
|
|
198
72
|
onFocus: options.onFocus,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
73
|
+
onMouseenter: eventHandlers.onMouseenter,
|
|
74
|
+
onMouseleave: eventHandlers.onMouseleave,
|
|
75
|
+
onMouseover: eventHandlers.onMouseover,
|
|
76
|
+
onMouseout: eventHandlers.onMouseout,
|
|
77
|
+
onTouchstart: eventHandlers.onTouchstart,
|
|
204
78
|
};
|
|
205
79
|
// Remove undefined values
|
|
206
80
|
Object.keys(externalProps).forEach((key) => {
|
|
@@ -210,6 +84,79 @@ export function useLinkProps(options) {
|
|
|
210
84
|
});
|
|
211
85
|
return Vue.computed(() => externalProps);
|
|
212
86
|
}
|
|
87
|
+
// During SSR we render exactly once and do not need reactivity.
|
|
88
|
+
// Avoid store subscriptions, effects and observers on the server.
|
|
89
|
+
if (isServer ?? router.isServer) {
|
|
90
|
+
const next = router.buildLocation(options);
|
|
91
|
+
const href = getHref({
|
|
92
|
+
options: options,
|
|
93
|
+
router,
|
|
94
|
+
nextLocation: next,
|
|
95
|
+
});
|
|
96
|
+
const isActive = getIsActive({
|
|
97
|
+
loc: router.stores.location.state,
|
|
98
|
+
nextLoc: next,
|
|
99
|
+
activeOptions: options.activeOptions,
|
|
100
|
+
router,
|
|
101
|
+
});
|
|
102
|
+
const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, } = resolveStyleProps({
|
|
103
|
+
options: options,
|
|
104
|
+
isActive,
|
|
105
|
+
});
|
|
106
|
+
const result = combineResultProps({
|
|
107
|
+
href,
|
|
108
|
+
options: options,
|
|
109
|
+
isActive,
|
|
110
|
+
isTransitioning: false,
|
|
111
|
+
resolvedActiveProps,
|
|
112
|
+
resolvedInactiveProps,
|
|
113
|
+
resolvedClassName,
|
|
114
|
+
resolvedStyle,
|
|
115
|
+
});
|
|
116
|
+
return Vue.ref(result);
|
|
117
|
+
}
|
|
118
|
+
const currentLocation = useStore(router.stores.location, (l) => l, {
|
|
119
|
+
equal: (prev, next) => prev.href === next.href,
|
|
120
|
+
});
|
|
121
|
+
const next = Vue.computed(() => {
|
|
122
|
+
// Rebuild when inherited search/hash or the current route context changes.
|
|
123
|
+
const opts = { _fromLocation: currentLocation.value, ...options };
|
|
124
|
+
return router.buildLocation(opts);
|
|
125
|
+
});
|
|
126
|
+
const preload = Vue.computed(() => {
|
|
127
|
+
if (options.reloadDocument) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return options.preload ?? router.options.defaultPreload;
|
|
131
|
+
});
|
|
132
|
+
const preloadDelay = Vue.computed(() => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0);
|
|
133
|
+
const isActive = Vue.computed(() => getIsActive({
|
|
134
|
+
activeOptions: options.activeOptions,
|
|
135
|
+
loc: currentLocation.value,
|
|
136
|
+
nextLoc: next.value,
|
|
137
|
+
router,
|
|
138
|
+
}));
|
|
139
|
+
const doPreload = () => router
|
|
140
|
+
.preloadRoute({ ...options, _builtLocation: next.value })
|
|
141
|
+
.catch((err) => {
|
|
142
|
+
console.warn(err);
|
|
143
|
+
console.warn(preloadWarning);
|
|
144
|
+
});
|
|
145
|
+
const preloadViewportIoCallback = (entry) => {
|
|
146
|
+
if (entry?.isIntersecting) {
|
|
147
|
+
doPreload();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
useIntersectionObserver(ref, preloadViewportIoCallback, { rootMargin: '100px' }, { disabled: () => !!options.disabled || !(preload.value === 'viewport') });
|
|
151
|
+
Vue.effect(() => {
|
|
152
|
+
if (hasRenderFetched) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!options.disabled && preload.value === 'render') {
|
|
156
|
+
doPreload();
|
|
157
|
+
hasRenderFetched = true;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
213
160
|
// The click handler
|
|
214
161
|
const handleClick = (e) => {
|
|
215
162
|
// Check actual element's target attribute as fallback
|
|
@@ -221,7 +168,7 @@ export function useLinkProps(options) {
|
|
|
221
168
|
(!effectiveTarget || effectiveTarget === '_self') &&
|
|
222
169
|
e.button === 0) {
|
|
223
170
|
// Don't prevent default or handle navigation if reloadDocument is true
|
|
224
|
-
if (
|
|
171
|
+
if (options.reloadDocument) {
|
|
225
172
|
return;
|
|
226
173
|
}
|
|
227
174
|
e.preventDefault();
|
|
@@ -232,7 +179,7 @@ export function useLinkProps(options) {
|
|
|
232
179
|
});
|
|
233
180
|
// All is well? Navigate!
|
|
234
181
|
router.navigate({
|
|
235
|
-
...
|
|
182
|
+
...options,
|
|
236
183
|
replace: options.replace,
|
|
237
184
|
resetScroll: options.resetScroll,
|
|
238
185
|
hashScrollIntoView: options.hashScrollIntoView,
|
|
@@ -283,64 +230,15 @@ export function useLinkProps(options) {
|
|
|
283
230
|
};
|
|
284
231
|
}
|
|
285
232
|
// Get the active and inactive props
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
});
|
|
295
|
-
const resolvedInactiveProps = Vue.computed(() => {
|
|
296
|
-
const inactiveProps = options.inactiveProps || (() => ({}));
|
|
297
|
-
const props = isActive.value
|
|
298
|
-
? {}
|
|
299
|
-
: typeof inactiveProps === 'function'
|
|
300
|
-
? inactiveProps()
|
|
301
|
-
: inactiveProps;
|
|
302
|
-
return props || { class: undefined, style: undefined };
|
|
303
|
-
});
|
|
304
|
-
const resolvedClassName = Vue.computed(() => {
|
|
305
|
-
const classes = [
|
|
306
|
-
options.class,
|
|
307
|
-
resolvedActiveProps.value?.class,
|
|
308
|
-
resolvedInactiveProps.value?.class,
|
|
309
|
-
].filter(Boolean);
|
|
310
|
-
return classes.length ? classes.join(' ') : undefined;
|
|
311
|
-
});
|
|
312
|
-
const resolvedStyle = Vue.computed(() => {
|
|
313
|
-
const result = {};
|
|
314
|
-
// Merge styles from all sources
|
|
315
|
-
if (options.style) {
|
|
316
|
-
Object.assign(result, options.style);
|
|
317
|
-
}
|
|
318
|
-
if (resolvedActiveProps.value?.style) {
|
|
319
|
-
Object.assign(result, resolvedActiveProps.value.style);
|
|
320
|
-
}
|
|
321
|
-
if (resolvedInactiveProps.value?.style) {
|
|
322
|
-
Object.assign(result, resolvedInactiveProps.value.style);
|
|
323
|
-
}
|
|
324
|
-
return Object.keys(result).length > 0 ? result : undefined;
|
|
325
|
-
});
|
|
326
|
-
const href = Vue.computed(() => {
|
|
327
|
-
if (options.disabled) {
|
|
328
|
-
return undefined;
|
|
329
|
-
}
|
|
330
|
-
const nextLocation = next.value;
|
|
331
|
-
const location = nextLocation?.maskedLocation ?? nextLocation;
|
|
332
|
-
// Use publicHref - it contains the correct href for display
|
|
333
|
-
// When a rewrite changes the origin, publicHref is the full URL
|
|
334
|
-
// Otherwise it's the origin-stripped path
|
|
335
|
-
// This avoids constructing URL objects in the hot path
|
|
336
|
-
const publicHref = location?.publicHref;
|
|
337
|
-
if (!publicHref)
|
|
338
|
-
return undefined;
|
|
339
|
-
const external = location?.external;
|
|
340
|
-
if (external)
|
|
341
|
-
return publicHref;
|
|
342
|
-
return router.history.createHref(publicHref) || '/';
|
|
343
|
-
});
|
|
233
|
+
const resolvedStyleProps = Vue.computed(() => resolveStyleProps({
|
|
234
|
+
options: options,
|
|
235
|
+
isActive: isActive.value,
|
|
236
|
+
}));
|
|
237
|
+
const href = Vue.computed(() => getHref({
|
|
238
|
+
options: options,
|
|
239
|
+
router,
|
|
240
|
+
nextLocation: next.value,
|
|
241
|
+
}));
|
|
344
242
|
// Create static event handlers that don't change between renders
|
|
345
243
|
const staticEventHandlers = {
|
|
346
244
|
onClick: composeEventHandlers([
|
|
@@ -356,76 +254,229 @@ export function useLinkProps(options) {
|
|
|
356
254
|
enqueueIntentPreload,
|
|
357
255
|
]),
|
|
358
256
|
onMouseenter: composeEventHandlers([
|
|
359
|
-
|
|
257
|
+
eventHandlers.onMouseenter,
|
|
360
258
|
enqueueIntentPreload,
|
|
361
259
|
]),
|
|
362
260
|
onMouseover: composeEventHandlers([
|
|
363
|
-
|
|
261
|
+
eventHandlers.onMouseover,
|
|
364
262
|
enqueueIntentPreload,
|
|
365
263
|
]),
|
|
366
264
|
onMouseleave: composeEventHandlers([
|
|
367
|
-
|
|
265
|
+
eventHandlers.onMouseleave,
|
|
368
266
|
handleLeave,
|
|
369
267
|
]),
|
|
370
268
|
onMouseout: composeEventHandlers([
|
|
371
|
-
|
|
269
|
+
eventHandlers.onMouseout,
|
|
372
270
|
handleLeave,
|
|
373
271
|
]),
|
|
374
272
|
onTouchstart: composeEventHandlers([
|
|
375
|
-
|
|
273
|
+
eventHandlers.onTouchstart,
|
|
376
274
|
handleTouchStart,
|
|
377
275
|
]),
|
|
378
276
|
};
|
|
379
277
|
// Compute all props synchronously to avoid hydration mismatches
|
|
380
278
|
// Using Vue.computed ensures props are calculated at render time, not after
|
|
381
279
|
const computedProps = Vue.computed(() => {
|
|
382
|
-
const
|
|
383
|
-
|
|
280
|
+
const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, } = resolvedStyleProps.value;
|
|
281
|
+
return combineResultProps({
|
|
384
282
|
href: href.value,
|
|
283
|
+
options: options,
|
|
385
284
|
ref,
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
285
|
+
staticEventHandlers,
|
|
286
|
+
isActive: isActive.value,
|
|
287
|
+
isTransitioning: isTransitioning.value,
|
|
288
|
+
resolvedActiveProps,
|
|
289
|
+
resolvedInactiveProps,
|
|
290
|
+
resolvedClassName,
|
|
291
|
+
resolvedStyle,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
// Return the computed ref itself - callers should access .value
|
|
295
|
+
return computedProps;
|
|
296
|
+
}
|
|
297
|
+
function resolveStyleProps({ options, isActive, }) {
|
|
298
|
+
const activeProps = options.activeProps || (() => ({ class: 'active' }));
|
|
299
|
+
const resolvedActiveProps = (isActive
|
|
300
|
+
? typeof activeProps === 'function'
|
|
301
|
+
? activeProps()
|
|
302
|
+
: activeProps
|
|
303
|
+
: {}) || { class: undefined, style: undefined };
|
|
304
|
+
const inactiveProps = options.inactiveProps || (() => ({}));
|
|
305
|
+
const resolvedInactiveProps = (isActive
|
|
306
|
+
? {}
|
|
307
|
+
: typeof inactiveProps === 'function'
|
|
308
|
+
? inactiveProps()
|
|
309
|
+
: inactiveProps) || { class: undefined, style: undefined };
|
|
310
|
+
const classes = [
|
|
311
|
+
options.class,
|
|
312
|
+
resolvedActiveProps?.class,
|
|
313
|
+
resolvedInactiveProps?.class,
|
|
314
|
+
].filter(Boolean);
|
|
315
|
+
const resolvedClassName = classes.length ? classes.join(' ') : undefined;
|
|
316
|
+
const result = {};
|
|
317
|
+
// Merge styles from all sources
|
|
318
|
+
if (options.style) {
|
|
319
|
+
Object.assign(result, options.style);
|
|
320
|
+
}
|
|
321
|
+
if (resolvedActiveProps?.style) {
|
|
322
|
+
Object.assign(result, resolvedActiveProps.style);
|
|
323
|
+
}
|
|
324
|
+
if (resolvedInactiveProps?.style) {
|
|
325
|
+
Object.assign(result, resolvedInactiveProps.style);
|
|
326
|
+
}
|
|
327
|
+
const resolvedStyle = Object.keys(result).length > 0 ? result : undefined;
|
|
328
|
+
return {
|
|
329
|
+
resolvedActiveProps,
|
|
330
|
+
resolvedInactiveProps,
|
|
331
|
+
resolvedClassName,
|
|
332
|
+
resolvedStyle,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function combineResultProps({ href, options, isActive, isTransitioning, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, ref, staticEventHandlers, }) {
|
|
336
|
+
const result = {
|
|
337
|
+
...getPropsSafeToSpread(options),
|
|
338
|
+
ref,
|
|
339
|
+
...staticEventHandlers,
|
|
340
|
+
href,
|
|
341
|
+
disabled: !!options.disabled,
|
|
342
|
+
target: options.target,
|
|
343
|
+
};
|
|
344
|
+
if (resolvedStyle) {
|
|
345
|
+
result.style = resolvedStyle;
|
|
346
|
+
}
|
|
347
|
+
if (resolvedClassName) {
|
|
348
|
+
result.class = resolvedClassName;
|
|
349
|
+
}
|
|
350
|
+
if (options.disabled) {
|
|
351
|
+
result.role = 'link';
|
|
352
|
+
result['aria-disabled'] = true;
|
|
353
|
+
}
|
|
354
|
+
if (isActive) {
|
|
355
|
+
result['data-status'] = 'active';
|
|
356
|
+
result['aria-current'] = 'page';
|
|
357
|
+
}
|
|
358
|
+
if (isTransitioning) {
|
|
359
|
+
result['data-transitioning'] = 'transitioning';
|
|
360
|
+
}
|
|
361
|
+
for (const key of Object.keys(resolvedActiveProps)) {
|
|
362
|
+
if (key !== 'class' && key !== 'style') {
|
|
363
|
+
result[key] = resolvedActiveProps[key];
|
|
397
364
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
result[
|
|
365
|
+
}
|
|
366
|
+
for (const key of Object.keys(resolvedInactiveProps)) {
|
|
367
|
+
if (key !== 'class' && key !== 'style') {
|
|
368
|
+
result[key] = resolvedInactiveProps[key];
|
|
402
369
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
function getLinkEventHandlers(options) {
|
|
374
|
+
return {
|
|
375
|
+
onMouseenter: options.onMouseEnter ?? options.onMouseenter,
|
|
376
|
+
onMouseleave: options.onMouseLeave ?? options.onMouseleave,
|
|
377
|
+
onMouseover: options.onMouseOver ?? options.onMouseover,
|
|
378
|
+
onMouseout: options.onMouseOut ?? options.onMouseout,
|
|
379
|
+
onTouchstart: options.onTouchStart ?? options.onTouchstart,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const propsUnsafeToSpread = new Set([
|
|
383
|
+
'activeProps',
|
|
384
|
+
'inactiveProps',
|
|
385
|
+
'activeOptions',
|
|
386
|
+
'to',
|
|
387
|
+
'preload',
|
|
388
|
+
'preloadDelay',
|
|
389
|
+
'hashScrollIntoView',
|
|
390
|
+
'replace',
|
|
391
|
+
'startTransition',
|
|
392
|
+
'resetScroll',
|
|
393
|
+
'viewTransition',
|
|
394
|
+
'children',
|
|
395
|
+
'target',
|
|
396
|
+
'disabled',
|
|
397
|
+
'style',
|
|
398
|
+
'class',
|
|
399
|
+
'onClick',
|
|
400
|
+
'onBlur',
|
|
401
|
+
'onFocus',
|
|
402
|
+
'onMouseEnter',
|
|
403
|
+
'onMouseenter',
|
|
404
|
+
'onMouseLeave',
|
|
405
|
+
'onMouseleave',
|
|
406
|
+
'onMouseOver',
|
|
407
|
+
'onMouseover',
|
|
408
|
+
'onMouseOut',
|
|
409
|
+
'onMouseout',
|
|
410
|
+
'onTouchStart',
|
|
411
|
+
'onTouchstart',
|
|
412
|
+
'ignoreBlocker',
|
|
413
|
+
'params',
|
|
414
|
+
'search',
|
|
415
|
+
'hash',
|
|
416
|
+
'state',
|
|
417
|
+
'mask',
|
|
418
|
+
'reloadDocument',
|
|
419
|
+
'_asChild',
|
|
420
|
+
'from',
|
|
421
|
+
'additionalProps',
|
|
422
|
+
]);
|
|
423
|
+
// Create safe props that can be spread
|
|
424
|
+
const getPropsSafeToSpread = (options) => {
|
|
425
|
+
const result = {};
|
|
426
|
+
for (const key in options) {
|
|
427
|
+
if (!propsUnsafeToSpread.has(key)) {
|
|
428
|
+
result[key] = options[key];
|
|
407
429
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
};
|
|
433
|
+
function getIsActive({ activeOptions, loc, nextLoc, router, }) {
|
|
434
|
+
if (activeOptions?.exact) {
|
|
435
|
+
const testExact = exactPathTest(loc.pathname, nextLoc.pathname, router.basepath);
|
|
436
|
+
if (!testExact) {
|
|
437
|
+
return false;
|
|
411
438
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const currentPath = removeTrailingSlash(loc.pathname, router.basepath);
|
|
442
|
+
const nextPath = removeTrailingSlash(nextLoc.pathname, router.basepath);
|
|
443
|
+
const pathIsFuzzyEqual = currentPath.startsWith(nextPath) &&
|
|
444
|
+
(currentPath.length === nextPath.length ||
|
|
445
|
+
currentPath[nextPath.length] === '/');
|
|
446
|
+
if (!pathIsFuzzyEqual) {
|
|
447
|
+
return false;
|
|
419
448
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
449
|
+
}
|
|
450
|
+
if (activeOptions?.includeSearch ?? true) {
|
|
451
|
+
const searchTest = deepEqual(loc.search, nextLoc.search, {
|
|
452
|
+
partial: !activeOptions?.exact,
|
|
453
|
+
ignoreUndefined: !activeOptions?.explicitUndefined,
|
|
454
|
+
});
|
|
455
|
+
if (!searchTest) {
|
|
456
|
+
return false;
|
|
424
457
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
458
|
+
}
|
|
459
|
+
if (activeOptions?.includeHash) {
|
|
460
|
+
return loc.hash === nextLoc.hash;
|
|
461
|
+
}
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
function getHref({ options, router, nextLocation, }) {
|
|
465
|
+
if (options.disabled) {
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
const location = nextLocation?.maskedLocation ?? nextLocation;
|
|
469
|
+
// Use publicHref - it contains the correct href for display
|
|
470
|
+
// When a rewrite changes the origin, publicHref is the full URL
|
|
471
|
+
// Otherwise it's the origin-stripped path
|
|
472
|
+
// This avoids constructing URL objects in the hot path
|
|
473
|
+
const publicHref = location?.publicHref;
|
|
474
|
+
if (!publicHref)
|
|
475
|
+
return undefined;
|
|
476
|
+
const external = location?.external;
|
|
477
|
+
if (external)
|
|
478
|
+
return publicHref;
|
|
479
|
+
return router.history.createHref(publicHref) || '/';
|
|
429
480
|
}
|
|
430
481
|
export function createLink(Comp) {
|
|
431
482
|
return Vue.defineComponent({
|
|
@@ -466,13 +517,11 @@ const LinkImpl = Vue.defineComponent({
|
|
|
466
517
|
],
|
|
467
518
|
setup(props, { attrs, slots }) {
|
|
468
519
|
// Call useLinkProps ONCE during setup with combined props and attrs
|
|
469
|
-
// The returned object is a computed ref that updates reactively
|
|
470
520
|
const allProps = { ...props, ...attrs };
|
|
471
|
-
const
|
|
521
|
+
const linkPropsSource = useLinkProps(allProps);
|
|
472
522
|
return () => {
|
|
473
523
|
const Component = props._asChild || 'a';
|
|
474
|
-
|
|
475
|
-
const linkProps = linkPropsComputed.value;
|
|
524
|
+
const linkProps = Vue.unref(linkPropsSource);
|
|
476
525
|
const isActive = linkProps['data-status'] === 'active';
|
|
477
526
|
const isTransitioning = linkProps['data-transitioning'] === 'transitioning';
|
|
478
527
|
// Create the slot content or empty array if no default slot
|