2020-07-02 06:53:17 +02:00
import React , { Children } from 'react'
2020-07-13 18:08:12 +02:00
import { UrlObject } from 'url'
2020-08-05 20:12:17 +02:00
import {
PrefetchOptions ,
NextRouter ,
isLocalURL ,
} from '../next-server/lib/router/router'
import { execOnce } from '../next-server/lib/utils'
2020-07-07 06:52:26 +02:00
import { useRouter } from './router'
2020-07-13 18:08:12 +02:00
import { addBasePath , resolveHref } from '../next-server/lib/router/router'
2019-05-30 03:19:32 +02:00
type Url = string | UrlObject
2018-08-07 07:44:18 +02:00
2019-07-11 20:52:21 +02:00
export type LinkProps = {
2019-05-30 03:19:32 +02:00
href : Url
2019-07-11 20:52:21 +02:00
as ? : Url
2019-05-30 03:19:32 +02:00
replace? : boolean
scroll? : boolean
shallow? : boolean
passHref? : boolean
prefetch? : boolean
2019-04-25 21:31:53 +02:00
}
2020-06-01 23:00:22 +02:00
let cachedObserver : IntersectionObserver
2020-05-04 22:59:38 +02:00
const listeners = new Map < Element , ( ) = > void > ( )
2019-05-30 03:19:32 +02:00
const IntersectionObserver =
2020-05-04 22:59:38 +02:00
typeof window !== 'undefined' ? window . IntersectionObserver : null
2020-03-01 00:06:18 +01:00
const prefetched : { [ cacheKey : string ] : boolean } = { }
2019-05-01 15:14:27 +02:00
2020-05-10 23:52:30 +02:00
function getObserver ( ) : IntersectionObserver | undefined {
2019-05-01 15:14:27 +02:00
// Return shared instance of IntersectionObserver if already created
2020-06-01 23:00:22 +02:00
if ( cachedObserver ) {
return cachedObserver
2019-05-01 15:14:27 +02:00
}
// Only create shared IntersectionObserver if supported in browser
if ( ! IntersectionObserver ) {
return undefined
}
2020-06-01 23:00:22 +02:00
return ( cachedObserver = new IntersectionObserver (
2020-05-18 21:24:37 +02:00
( entries ) = > {
entries . forEach ( ( entry ) = > {
2019-05-01 15:14:27 +02:00
if ( ! listeners . has ( entry . target ) ) {
return
}
2020-05-04 22:59:38 +02:00
const cb = listeners . get ( entry . target ) !
2019-05-01 15:14:27 +02:00
if ( entry . isIntersecting || entry . intersectionRatio > 0 ) {
2020-06-01 23:00:22 +02:00
cachedObserver . unobserve ( entry . target )
2019-05-30 03:19:32 +02:00
listeners . delete ( entry . target )
cb ( )
}
} )
} ,
{ rootMargin : '200px' }
2019-05-01 15:14:27 +02:00
) )
}
2020-05-04 22:59:38 +02:00
const listenToIntersections = ( el : Element , cb : ( ) = > void ) = > {
2019-05-01 15:14:27 +02:00
const observer = getObserver ( )
if ( ! observer ) {
return ( ) = > { }
}
observer . observe ( el )
listeners . set ( el , cb )
return ( ) = > {
2019-09-16 16:56:56 +02:00
try {
observer . unobserve ( el )
} catch ( err ) {
console . error ( err )
}
2019-05-01 15:14:27 +02:00
listeners . delete ( el )
}
}
2020-07-07 06:52:26 +02:00
function prefetch (
router : NextRouter ,
href : string ,
as : string ,
options? : PrefetchOptions
) : void {
2020-07-02 06:53:17 +02:00
if ( typeof window === 'undefined' ) return
2020-08-05 20:12:17 +02:00
if ( ! isLocalURL ( href ) ) return
2020-07-02 06:53:17 +02:00
// Prefetch the JSON page if asked (only in the client)
// We need to handle a prefetch error here since we may be
// loading with priority which can reject but we don't
// want to force navigation since this is only a prefetch
2020-07-07 06:52:26 +02:00
router . prefetch ( href , as , options ) . catch ( ( err ) = > {
2019-08-12 06:26:25 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
2020-07-02 06:53:17 +02:00
// rethrow to show invalid URL errors
throw err
2019-08-12 06:26:25 +02:00
}
2020-07-02 06:53:17 +02:00
} )
// Join on an invalid URI character
2020-07-07 06:52:26 +02:00
prefetched [ href + '%' + as ] = true
2020-07-02 06:53:17 +02:00
}
2019-05-01 15:14:27 +02:00
2020-08-05 20:12:17 +02:00
function isNewTabRequest ( event : React.MouseEvent ) {
const { target } = event . currentTarget as HTMLAnchorElement
return (
( target && target !== '_self' ) ||
event . metaKey ||
event . ctrlKey ||
event . shiftKey ||
( event . nativeEvent && event . nativeEvent . which === 2 )
)
}
2020-07-02 06:53:17 +02:00
function linkClicked (
e : React.MouseEvent ,
2020-07-07 06:52:26 +02:00
router : NextRouter ,
2020-07-02 06:53:17 +02:00
href : string ,
2020-07-07 06:52:26 +02:00
as : string ,
2020-07-02 06:53:17 +02:00
replace? : boolean ,
shallow? : boolean ,
scroll? : boolean
) : void {
2020-08-05 20:12:17 +02:00
const { nodeName } = e . currentTarget
2018-08-07 05:53:06 +02:00
2020-08-05 20:12:17 +02:00
if ( nodeName === 'A' && ( isNewTabRequest ( e ) || ! isLocalURL ( href ) ) ) {
// ignore click for new tab / new window behavior
2020-07-02 06:53:17 +02:00
return
2019-12-20 22:30:58 +01:00
}
2020-07-02 06:53:17 +02:00
e . preventDefault ( )
2018-08-07 05:53:06 +02:00
2020-07-02 06:53:17 +02:00
// avoid scroll for urls with anchor refs
if ( scroll == null ) {
scroll = as . indexOf ( '#' ) < 0
}
2018-08-07 05:53:06 +02:00
2020-07-02 06:53:17 +02:00
// replace state instead of push if prop is present
2020-07-07 06:52:26 +02:00
router [ replace ? 'replace' : 'push' ] ( href , as , { shallow } ) . then (
2020-07-02 06:53:17 +02:00
( success : boolean ) = > {
if ( ! success ) return
if ( scroll ) {
window . scrollTo ( 0 , 0 )
document . body . focus ( )
}
2016-10-06 09:07:41 +02:00
}
2020-07-02 06:53:17 +02:00
)
}
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +02:00
function Link ( props : React.PropsWithChildren < LinkProps > ) {
if ( process . env . NODE_ENV !== 'production' ) {
// This hook is in a conditional but that is ok because `process.env.NODE_ENV` never changes
// eslint-disable-next-line react-hooks/rules-of-hooks
const hasWarned = React . useRef ( false )
if ( props . prefetch && ! hasWarned . current ) {
hasWarned . current = true
console . warn (
'Next.js auto-prefetches automatically based on viewport. The prefetch attribute is no longer needed. More: https://err.sh/vercel/next.js/prefetch-true-deprecated'
)
2016-10-06 09:07:41 +02:00
}
2020-07-02 06:53:17 +02:00
}
const p = props . prefetch !== false
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +02:00
const [ childElm , setChildElm ] = React . useState < Element > ( )
2017-01-02 07:22:07 +01:00
2020-07-07 06:52:26 +02:00
const router = useRouter ( )
2020-07-29 08:56:33 +02:00
const pathname = ( router && router . pathname ) || '/'
2020-07-07 06:52:26 +02:00
const { href , as } = React . useMemo ( ( ) = > {
2020-07-29 08:56:33 +02:00
const resolvedHref = resolveHref ( pathname , props . href )
2020-07-07 06:52:26 +02:00
return {
href : resolvedHref ,
2020-07-29 08:56:33 +02:00
as : props . as ? resolveHref ( pathname , props . as ) : resolvedHref ,
2020-07-07 06:52:26 +02:00
}
2020-07-29 08:56:33 +02:00
} , [ pathname , props . href , props . as ] )
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +02:00
React . useEffect ( ( ) = > {
2020-08-05 20:12:17 +02:00
if (
p &&
IntersectionObserver &&
childElm &&
childElm . tagName &&
isLocalURL ( href )
) {
2020-07-07 06:52:26 +02:00
// Join on an invalid URI character
const isPrefetched = prefetched [ href + '%' + as ]
2020-07-02 06:53:17 +02:00
if ( ! isPrefetched ) {
return listenToIntersections ( childElm , ( ) = > {
2020-07-07 06:52:26 +02:00
prefetch ( router , href , as )
2020-07-02 06:53:17 +02:00
} )
}
2016-12-22 09:15:49 +01:00
}
2020-07-07 06:52:26 +02:00
} , [ p , childElm , href , as , router ] )
2016-12-22 09:15:49 +01:00
2020-07-02 06:53:17 +02:00
let { children , replace , shallow , scroll } = props
// Deprecated. Warning shown by propType check. If the children provided is a string (<Link>example</Link>) we wrap it in an <a> tag
if ( typeof children === 'string' ) {
children = < a > { children } < / a >
2019-05-30 03:19:32 +02:00
}
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +02:00
// This will return the first child, if multiple are provided it will throw an error
const child : any = Children . only ( children )
const childProps : {
onMouseEnter? : React.MouseEventHandler
onClick : React.MouseEventHandler
href? : string
ref? : any
} = {
ref : ( el : any ) = > {
2020-07-13 15:12:18 +02:00
if ( el ) setChildElm ( el )
2020-07-02 06:53:17 +02:00
if ( child && typeof child === 'object' && child . ref ) {
if ( typeof child . ref === 'function' ) child . ref ( el )
else if ( typeof child . ref === 'object' ) {
child . ref . current = el
2020-03-01 00:06:18 +01:00
}
2020-02-18 22:28:29 +01:00
}
2020-07-02 06:53:17 +02:00
} ,
onClick : ( e : React.MouseEvent ) = > {
if ( child . props && typeof child . props . onClick === 'function' ) {
child . props . onClick ( e )
}
if ( ! e . defaultPrevented ) {
2020-07-07 06:52:26 +02:00
linkClicked ( e , router , href , as , replace , shallow , scroll )
2020-07-02 06:53:17 +02:00
}
} ,
2017-02-15 15:01:03 +01:00
}
2020-07-02 06:53:17 +02:00
if ( p ) {
childProps . onMouseEnter = ( e : React.MouseEvent ) = > {
2020-08-05 20:12:17 +02:00
if ( ! isLocalURL ( href ) ) return
2020-07-02 06:53:17 +02:00
if ( child . props && typeof child . props . onMouseEnter === 'function' ) {
child . props . onMouseEnter ( e )
}
2020-07-07 06:52:26 +02:00
prefetch ( router , href , as , { priority : true } )
2017-02-03 21:27:12 +01:00
}
2020-07-02 06:53:17 +02:00
}
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +02:00
// If child is an <a> tag and doesn't have a href attribute, or if the 'passHref' property is
// defined, we specify the current 'href', so that repetition is not needed by the user
if ( props . passHref || ( child . type === 'a' && ! ( 'href' in child . props ) ) ) {
2020-07-07 06:52:26 +02:00
childProps . href = addBasePath ( as )
2020-07-02 06:53:17 +02:00
}
2016-10-06 09:07:41 +02:00
2020-07-02 06:53:17 +02:00
return React . cloneElement ( child , childProps )
2016-10-06 09:07:41 +02:00
}
2018-09-14 11:34:08 +02:00
if ( process . env . NODE_ENV === 'development' ) {
2018-09-27 21:10:53 +02:00
const warn = execOnce ( console . error )
2018-09-14 11:34:08 +02:00
// This module gets removed by webpack.IgnorePlugin
2020-01-04 14:12:11 +01:00
const PropTypes = require ( 'prop-types' )
2018-09-14 11:34:08 +02:00
const exact = require ( 'prop-types-exact' )
2020-01-04 14:12:11 +01:00
// @ts-ignore the property is supported, when declaring it on the class it outputs an extra bit of code which is not needed.
2018-09-14 11:34:08 +02:00
Link . propTypes = exact ( {
href : PropTypes.oneOfType ( [ PropTypes . string , PropTypes . object ] ) . isRequired ,
as : PropTypes . oneOfType ( [ PropTypes . string , PropTypes . object ] ) ,
prefetch : PropTypes.bool ,
replace : PropTypes.bool ,
shallow : PropTypes.bool ,
passHref : PropTypes.bool ,
scroll : PropTypes.bool ,
children : PropTypes.oneOfType ( [
PropTypes . element ,
2020-01-04 14:12:11 +01:00
( props : any , propName : string ) = > {
2018-09-14 11:34:08 +02:00
const value = props [ propName ]
if ( typeof value === 'string' ) {
2019-05-30 03:19:32 +02:00
warn (
` Warning: You're using a string directly inside <Link>. This usage has been deprecated. Please add an <a> tag as child of <Link> `
)
2018-09-14 11:34:08 +02:00
}
return null
2019-04-25 21:31:53 +02:00
} ,
] ) . isRequired ,
2018-09-14 11:34:08 +02:00
} )
}
2018-08-07 05:23:28 +02:00
export default Link