2024-02-04 20:51:30 +00:00
import type {
AllowedComponentProps ,
AnchorHTMLAttributes ,
ComputedRef ,
DefineComponent ,
InjectionKey , PropType ,
VNodeProps
} from 'vue'
2023-09-28 10:08:20 +00:00
import { computed , defineComponent , h , inject , onBeforeUnmount , onMounted , provide , ref , resolveComponent } from 'vue'
2024-02-04 20:51:30 +00:00
import type { RouteLocation , RouteLocationRaw , Router , RouterLinkProps } from '#vue-router'
2023-10-20 15:33:45 +00:00
import { hasProtocol , joinURL , parseQuery , parseURL , withTrailingSlash , withoutTrailingSlash } from 'ufo'
2022-10-17 11:15:29 +00:00
import { preloadRouteComponents } from '../composables/preload'
2022-12-05 10:09:58 +00:00
import { onNuxtReady } from '../composables/ready'
2022-09-14 09:22:03 +00:00
import { navigateTo , useRouter } from '../composables/router'
2023-10-20 15:33:45 +00:00
import { useNuxtApp , useRuntimeConfig } from '../nuxt'
2022-12-05 10:09:58 +00:00
import { cancelIdleCallback , requestIdleCallback } from '../compat/idle-callback'
2022-03-14 13:36:32 +00:00
2023-10-18 12:43:42 +00:00
// @ts-expect-error virtual file
import { nuxtLinkDefaults } from '#build/nuxt.config.mjs'
2022-10-14 08:36:03 +00:00
const firstNonUndefined = < T > ( . . . args : ( T | undefined ) [ ] ) = > args . find ( arg = > arg !== undefined )
2022-03-14 13:36:32 +00:00
2023-09-28 10:08:20 +00:00
const NuxtLinkDevKeySymbol : InjectionKey < boolean > = Symbol ( 'nuxt-link-dev-key' )
2022-03-14 13:36:32 +00:00
2024-02-04 20:51:30 +00:00
/ * *
* Create a NuxtLink component with given options as defaults .
* @see https : //nuxt.com/docs/api/components/nuxt-link
* /
export interface NuxtLinkOptions extends
Pick < RouterLinkProps , ' activeClass ' | ' exactActiveClass ' > ,
Pick < NuxtLinkProps , ' prefetchedClass ' > {
/ * *
* The name of the component .
* @default "NuxtLink"
* /
2022-07-07 17:28:23 +00:00
componentName? : string
2024-02-04 20:51:30 +00:00
/ * *
* A default ` rel ` attribute value applied on external links . Defaults to ` "noopener noreferrer" ` . Set it to ` "" ` to disable .
* /
2022-07-07 17:28:23 +00:00
externalRelAttribute? : string | null
2024-02-04 20:51:30 +00:00
/ * *
* An option to either add or remove trailing slashes in the ` href ` .
* If unset or not matching the valid values ` append ` or ` remove ` , it will be ignored .
* /
2023-03-07 07:17:42 +00:00
trailingSlash ? : 'append' | 'remove'
2022-03-14 13:36:32 +00:00
}
2024-02-04 20:51:30 +00:00
/ * *
* < NuxtLink > is a drop - in replacement for both Vue Router 's <RouterLink> component and HTML' s < a > tag .
* @see https : //nuxt.com/docs/api/components/nuxt-link
* /
export interface NuxtLinkProps extends Omit < RouterLinkProps , ' to ' > {
/ * *
* Route Location the link should navigate to when clicked on .
* /
to? : RouteLocationRaw // need to manually type to avoid breaking typedPages
/ * *
* An alias for ` to ` . If used with ` to ` , ` href ` will be ignored
* /
href? : NuxtLinkProps [ 'to' ]
/ * *
* Forces the link to be considered as external ( true ) or internal ( false ) . This is helpful to handle edge - cases
* /
2022-07-07 17:28:23 +00:00
external? : boolean
2024-02-04 20:51:30 +00:00
/ * *
* Where to display the linked URL , as the name for a browsing context .
* /
2022-10-13 17:51:26 +00:00
target ? : '_blank' | '_parent' | '_self' | '_top' | ( string & { } ) | null
2024-02-04 20:51:30 +00:00
/ * *
* A rel attribute value to apply on the link . Defaults to "noopener noreferrer" for external links .
* /
rel ? : 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | ( string & { } ) | null
/ * *
* If set to true , no rel attribute will be added to the link
* /
2022-07-07 17:28:23 +00:00
noRel? : boolean
2024-02-04 20:51:30 +00:00
/ * *
* A class to apply to links that have been prefetched .
* /
prefetchedClass? : string
/ * *
* When enabled will prefetch middleware , layouts and payloads of links in the viewport .
* /
2022-09-13 20:20:23 +00:00
prefetch? : boolean
2024-02-04 20:51:30 +00:00
/ * *
* Escape hatch to disable ` prefetch ` attribute .
* /
2022-09-13 20:20:23 +00:00
noPrefetch? : boolean
}
2024-02-04 20:51:30 +00:00
/*@__NO_SIDE_EFFECTS__*/
2022-03-14 13:36:32 +00:00
export function defineNuxtLink ( options : NuxtLinkOptions ) {
const componentName = options . componentName || 'NuxtLink'
2024-02-04 20:51:30 +00:00
function checkPropConflicts ( props : NuxtLinkProps , main : keyof NuxtLinkProps , sub : keyof NuxtLinkProps ) : void {
2023-08-07 22:03:40 +00:00
if ( import . meta . dev && props [ main ] !== undefined && props [ sub ] !== undefined ) {
2022-03-14 13:36:32 +00:00
console . warn ( ` [ ${ componentName } ] \` ${ main } \` and \` ${ sub } \` cannot be used together. \` ${ sub } \` will be ignored. ` )
}
}
2024-02-04 20:51:30 +00:00
2024-02-22 11:55:14 +00:00
function resolveTrailingSlashBehavior ( to : string , resolve : Router [ 'resolve' ] ) : string
function resolveTrailingSlashBehavior ( to : RouteLocationRaw , resolve : Router [ 'resolve' ] ) : Exclude < RouteLocationRaw , string >
function resolveTrailingSlashBehavior ( to : RouteLocationRaw , resolve : Router [ 'resolve' ] ) : RouteLocationRaw | RouteLocation {
2023-03-07 07:17:42 +00:00
if ( ! to || ( options . trailingSlash !== 'append' && options . trailingSlash !== 'remove' ) ) {
return to
}
if ( typeof to === 'string' ) {
2023-12-12 12:52:55 +00:00
return applyTrailingSlashBehavior ( to , options . trailingSlash )
2023-03-07 07:17:42 +00:00
}
const path = 'path' in to ? to.path : resolve ( to ) . path
return {
. . . to ,
name : undefined , // named routes would otherwise always override trailing slash behavior
2023-12-12 12:52:55 +00:00
path : applyTrailingSlashBehavior ( path , options . trailingSlash )
2023-03-07 07:17:42 +00:00
}
}
2022-03-14 13:36:32 +00:00
return defineComponent ( {
name : componentName ,
props : {
// Routing
to : {
2023-05-09 17:08:07 +00:00
type : [ String , Object ] as PropType < RouteLocationRaw > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
href : {
2023-05-09 17:08:07 +00:00
type : [ String , Object ] as PropType < RouteLocationRaw > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
// Attributes
target : {
2024-02-04 20:51:30 +00:00
type : String as PropType < NuxtLinkProps [ ' target ' ] > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
rel : {
2024-02-04 20:51:30 +00:00
type : String as PropType < NuxtLinkProps [ ' rel ' ] > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
noRel : {
2024-02-04 20:51:30 +00:00
type : Boolean as PropType < NuxtLinkProps [ ' noRel ' ] > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
2022-09-13 20:20:23 +00:00
// Prefetching
prefetch : {
2024-02-04 20:51:30 +00:00
type : Boolean as PropType < NuxtLinkProps [ ' prefetch ' ] > ,
2022-09-13 20:20:23 +00:00
default : undefined ,
required : false
} ,
noPrefetch : {
2024-02-04 20:51:30 +00:00
type : Boolean as PropType < NuxtLinkProps [ ' noPrefetch ' ] > ,
2022-09-13 20:20:23 +00:00
default : undefined ,
required : false
} ,
2022-03-14 13:36:32 +00:00
// Styling
activeClass : {
2024-02-04 20:51:30 +00:00
type : String as PropType < NuxtLinkProps [ ' activeClass ' ] > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
exactActiveClass : {
2024-02-04 20:51:30 +00:00
type : String as PropType < NuxtLinkProps [ ' exactActiveClass ' ] > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
2022-09-13 20:20:23 +00:00
prefetchedClass : {
2024-02-04 20:51:30 +00:00
type : String as PropType < NuxtLinkProps [ ' prefetchedClass ' ] > ,
2022-09-13 20:20:23 +00:00
default : undefined ,
required : false
} ,
2022-03-14 13:36:32 +00:00
// Vue Router's `<RouterLink>` additional props
replace : {
2024-02-04 20:51:30 +00:00
type : Boolean as PropType < NuxtLinkProps [ ' replace ' ] > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
ariaCurrentValue : {
2024-02-04 20:51:30 +00:00
type : String as PropType < NuxtLinkProps [ ' ariaCurrentValue ' ] > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
// Edge cases handling
external : {
2024-02-04 20:51:30 +00:00
type : Boolean as PropType < NuxtLinkProps [ ' external ' ] > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
} ,
// Slot API
custom : {
2024-02-04 20:51:30 +00:00
type : Boolean as PropType < NuxtLinkProps [ ' custom ' ] > ,
2022-03-14 13:36:32 +00:00
default : undefined ,
required : false
}
} ,
setup ( props , { slots } ) {
2022-08-12 17:47:58 +00:00
const router = useRouter ( )
2023-10-20 15:33:45 +00:00
const config = useRuntimeConfig ( )
2022-03-14 13:36:32 +00:00
// Resolving `to` value from `to` and `href` props
2022-08-12 17:47:58 +00:00
const to : ComputedRef < string | RouteLocationRaw > = computed ( ( ) = > {
2022-03-14 13:36:32 +00:00
checkPropConflicts ( props , 'to' , 'href' )
2023-03-07 07:17:42 +00:00
const path = props . to || props . href || '' // Defaults to empty string (won't render any `href` attribute)
return resolveTrailingSlashBehavior ( path , router . resolve )
2022-03-14 13:36:32 +00:00
} )
2023-10-20 15:33:45 +00:00
// Lazily check whether to.value has a protocol
2024-02-04 22:21:39 +00:00
const isAbsoluteUrl = computed ( ( ) = > typeof to . value === 'string' && hasProtocol ( to . value , { acceptRelative : true } ) )
const hasTarget = computed ( ( ) = > props . target && props . target !== '_self' )
2023-10-20 15:33:45 +00:00
2022-03-14 13:36:32 +00:00
// Resolving link type
const isExternal = computed < boolean > ( ( ) = > {
2022-08-12 17:47:58 +00:00
// External prop is explicitly set
2022-03-14 13:36:32 +00:00
if ( props . external ) {
return true
}
// When `target` prop is set, link is external
2024-02-04 22:21:39 +00:00
if ( hasTarget . value ) {
2022-03-14 13:36:32 +00:00
return true
}
// When `to` is a route object then it's an internal link
if ( typeof to . value === 'object' ) {
return false
}
2024-02-04 22:21:39 +00:00
return to . value === '' || isAbsoluteUrl . value
2022-03-14 13:36:32 +00:00
} )
2022-09-13 20:20:23 +00:00
// Prefetching
const prefetched = ref ( false )
2023-08-07 22:03:40 +00:00
const el = import . meta . server ? undefined : ref < HTMLElement | null > ( null )
const elRef = import . meta . server ? undefined : ( ref : any ) = > { el ! . value = props . custom ? ref?.$el?.nextElementSibling : ref?.$el }
2023-03-20 21:46:12 +00:00
2023-08-07 22:03:40 +00:00
if ( import . meta . client ) {
2022-09-13 20:20:23 +00:00
checkPropConflicts ( props , 'prefetch' , 'noPrefetch' )
2023-02-20 20:31:27 +00:00
const shouldPrefetch = props . prefetch !== false && props . noPrefetch !== true && props . target !== '_blank' && ! isSlowConnection ( )
2022-09-13 20:20:23 +00:00
if ( shouldPrefetch ) {
const nuxtApp = useNuxtApp ( )
let idleId : number
2023-03-20 21:46:12 +00:00
let unobserve : ( ( ) = > void ) | null = null
2022-09-13 20:20:23 +00:00
onMounted ( ( ) = > {
2022-12-02 16:13:35 +00:00
const observer = useObserver ( )
2022-12-05 10:09:58 +00:00
onNuxtReady ( ( ) = > {
2022-12-02 16:13:35 +00:00
idleId = requestIdleCallback ( ( ) = > {
if ( el ? . value ? . tagName ) {
2023-05-11 08:37:32 +00:00
unobserve = observer ! . observe ( el . value as HTMLElement , async ( ) = > {
2022-12-02 16:13:35 +00:00
unobserve ? . ( )
unobserve = null
2023-02-20 20:31:27 +00:00
const path = typeof to . value === 'string' ? to.value : router.resolve ( to . value ) . fullPath
2022-12-02 16:13:35 +00:00
await Promise . all ( [
2023-02-20 20:31:27 +00:00
nuxtApp . hooks . callHook ( 'link:prefetch' , path ) . catch ( ( ) = > { } ) ,
2022-12-02 16:13:35 +00:00
! isExternal . value && preloadRouteComponents ( to . value as string , router ) . catch ( ( ) = > { } )
] )
prefetched . value = true
} )
}
} )
2022-12-05 10:09:58 +00:00
} )
2022-09-13 20:20:23 +00:00
} )
onBeforeUnmount ( ( ) = > {
if ( idleId ) { cancelIdleCallback ( idleId ) }
unobserve ? . ( )
unobserve = null
} )
}
}
2023-09-28 10:08:20 +00:00
if ( import . meta . dev && import . meta . server && ! props . custom ) {
const isNuxtLinkChild = inject ( NuxtLinkDevKeySymbol , false )
if ( isNuxtLinkChild ) {
console . log ( '[nuxt] [NuxtLink] You can\'t nest one <a> inside another <a>. This will cause a hydration error on client-side. You can pass the `custom` prop to take full control of the markup.' )
} else {
provide ( NuxtLinkDevKeySymbol , true )
}
}
2022-03-14 13:36:32 +00:00
return ( ) = > {
if ( ! isExternal . value ) {
2024-02-04 20:51:30 +00:00
const routerLinkProps : RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = {
2023-03-20 21:46:12 +00:00
ref : elRef ,
2023-03-02 09:53:46 +00:00
to : to.value ,
activeClass : props.activeClass || options . activeClass ,
exactActiveClass : props.exactActiveClass || options . exactActiveClass ,
replace : props.replace ,
ariaCurrentValue : props.ariaCurrentValue ,
custom : props.custom
}
// `custom` API cannot support fallthrough attributes as the slot
// may render fragment or text root nodes (#14897, #19375)
if ( ! props . custom ) {
if ( prefetched . value ) {
routerLinkProps . class = props . prefetchedClass || options . prefetchedClass
}
2024-02-04 20:51:30 +00:00
routerLinkProps . rel = props . rel || undefined
2023-03-02 09:53:46 +00:00
}
2022-03-14 13:36:32 +00:00
// Internal link
return h (
resolveComponent ( 'RouterLink' ) ,
2023-03-02 09:53:46 +00:00
routerLinkProps ,
2022-03-14 13:36:32 +00:00
slots . default
)
}
// Resolves `to` value if it's a route location object
2023-04-01 11:12:34 +00:00
// converts `""` to `null` to prevent the attribute from being added as empty (`href=""`)
2023-10-20 15:33:45 +00:00
const href = typeof to . value === 'object'
? router . resolve ( to . value ) ? . href ? ? null
2024-02-04 22:21:39 +00:00
: ( to . value && ! props . external && ! isAbsoluteUrl . value )
2023-10-20 15:33:45 +00:00
? resolveTrailingSlashBehavior ( joinURL ( config . app . baseURL , to . value ) , router . resolve ) as string
: to . value || null
2022-03-14 13:36:32 +00:00
// Resolves `target` value
const target = props . target || null
// Resolves `rel`
checkPropConflicts ( props , 'noRel' , 'rel' )
2024-02-04 22:21:39 +00:00
const rel = firstNonUndefined < string | null > (
2022-03-14 13:36:32 +00:00
// converts `""` to `null` to prevent the attribute from being added as empty (`rel=""`)
2024-02-04 22:21:39 +00:00
props . noRel ? '' : props . rel ,
options . externalRelAttribute ,
/ *
* A fallback rel of ` noopener noreferrer ` is applied for external links or links that open in a new tab .
* This solves a reverse tabnapping security flaw in browsers pre - 2021 as well as improving privacy .
* /
( isAbsoluteUrl . value || hasTarget . value ) ? 'noopener noreferrer' : ''
) || null
2022-03-14 13:36:32 +00:00
2022-07-07 17:28:23 +00:00
// https://router.vuejs.org/api/#custom
if ( props . custom ) {
2022-08-12 17:47:58 +00:00
if ( ! slots . default ) {
return null
}
2023-02-27 15:13:14 +00:00
2024-02-21 21:20:13 +00:00
const navigate = ( ) = > navigateTo ( href , { replace : props.replace , external : props.external } )
2022-07-07 17:28:23 +00:00
return slots . default ( {
href ,
navigate ,
2023-02-27 15:13:14 +00:00
get route ( ) {
if ( ! href ) { return undefined }
const url = parseURL ( href )
return {
path : url.pathname ,
fullPath : url.pathname ,
get query ( ) { return parseQuery ( url . search ) } ,
hash : url.hash ,
params : { } ,
name : undefined ,
matched : [ ] ,
redirectedFrom : undefined ,
meta : { } ,
href
2024-02-04 20:51:30 +00:00
} satisfies RouteLocation & { href : string }
2023-02-27 15:13:14 +00:00
} ,
2022-07-07 17:28:23 +00:00
rel ,
target ,
2022-11-09 09:02:11 +00:00
isExternal : isExternal.value ,
2022-07-07 17:28:23 +00:00
isActive : false ,
isExactActive : false
} )
}
2022-10-17 11:15:29 +00:00
return h ( 'a' , { ref : el , href , rel , target } , slots . default ? . ( ) )
2022-03-14 13:36:32 +00:00
}
}
} ) as unknown as DefineComponent < NuxtLinkProps >
}
2023-10-18 12:43:42 +00:00
export default defineNuxtLink ( nuxtLinkDefaults )
2022-09-13 20:20:23 +00:00
2023-12-12 12:52:55 +00:00
// -- NuxtLink utils --
function applyTrailingSlashBehavior ( to : string , trailingSlash : NuxtLinkOptions [ 'trailingSlash' ] ) : string {
const normalizeFn = trailingSlash === 'append' ? withTrailingSlash : withoutTrailingSlash
// Until https://github.com/unjs/ufo/issues/189 is resolved
const hasProtocolDifferentFromHttp = hasProtocol ( to ) && ! to . startsWith ( 'http' )
if ( hasProtocolDifferentFromHttp ) {
return to
}
return normalizeFn ( to , true )
}
2022-09-13 20:20:23 +00:00
// --- Prefetching utils ---
2023-01-10 14:33:21 +00:00
type CallbackFn = ( ) = > void
type ObserveFn = ( element : Element , callback : CallbackFn ) = > ( ) = > void
2022-09-13 20:20:23 +00:00
2023-01-10 14:33:21 +00:00
function useObserver ( ) : { observe : ObserveFn } | undefined {
2023-08-07 22:03:40 +00:00
if ( import . meta . server ) { return }
2022-09-13 20:20:23 +00:00
const nuxtApp = useNuxtApp ( )
if ( nuxtApp . _observer ) {
return nuxtApp . _observer
}
let observer : IntersectionObserver | null = null
2023-01-10 14:33:21 +00:00
2022-09-13 20:20:23 +00:00
const callbacks = new Map < Element , CallbackFn > ( )
2023-01-10 14:33:21 +00:00
const observe : ObserveFn = ( element , callback ) = > {
2022-09-13 20:20:23 +00:00
if ( ! observer ) {
observer = new IntersectionObserver ( ( entries ) = > {
for ( const entry of entries ) {
const callback = callbacks . get ( entry . target )
const isVisible = entry . isIntersecting || entry . intersectionRatio > 0
if ( isVisible && callback ) { callback ( ) }
}
} )
}
callbacks . set ( element , callback )
observer . observe ( element )
return ( ) = > {
callbacks . delete ( element )
observer ! . unobserve ( element )
if ( callbacks . size === 0 ) {
observer ! . disconnect ( )
observer = null
}
}
}
const _observer = nuxtApp . _observer = {
observe
}
return _observer
}
function isSlowConnection ( ) {
2023-08-07 22:03:40 +00:00
if ( import . meta . server ) { return }
2022-09-13 20:20:23 +00:00
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection
const cn = ( navigator as any ) . connection as { saveData : boolean , effectiveType : string } | null
if ( cn && ( cn . saveData || /2g/ . test ( cn . effectiveType ) ) ) { return true }
return false
}