2019-04-25 21:31:53 +02:00
declare const __NEXT_DATA__ : any
2017-05-09 03:20:50 +02:00
2020-02-17 21:46:15 +01:00
import React , { Children , Component } from 'react'
import { parse , resolve , UrlObject } from 'url'
import { PrefetchOptions } from '../next-server/lib/router/router'
2019-05-30 03:19:32 +02:00
import {
execOnce ,
formatWithValidation ,
getLocationOrigin ,
2019-09-04 16:00:54 +02:00
} from '../next-server/lib/utils'
2020-02-17 21:46:15 +01:00
import Router from './router'
2020-04-14 09:50:39 +02:00
import { addBasePath } from '../next-server/lib/router/router'
2016-10-06 09:07:41 +02:00
2020-05-10 23:52:30 +02:00
function isLocal ( href : string ) : boolean {
2018-08-07 05:23:28 +02:00
const url = parse ( href , false , true )
const origin = parse ( getLocationOrigin ( ) , false , true )
2019-05-30 03:19:32 +02:00
return (
! url . host || ( url . protocol === origin . protocol && url . host === origin . host )
)
2018-08-07 05:23:28 +02:00
}
2019-05-30 03:19:32 +02:00
type Url = string | UrlObject
type FormatResult = { href : string ; as ? : string }
2019-04-25 21:31:53 +02:00
function memoizedFormatUrl ( formatFunc : ( href : Url , as ? : Url ) = > FormatResult ) {
2019-05-30 03:19:32 +02:00
let lastHref : null | Url = null
let lastAs : undefined | null | Url = null
let lastResult : null | FormatResult = null
2019-04-25 21:31:53 +02:00
return ( href : Url , as ? : Url ) = > {
if ( lastResult && href === lastHref && as === lastAs ) {
2018-08-07 07:44:18 +02:00
return lastResult
}
2019-03-17 00:54:58 +01:00
const result = formatFunc ( href , as )
2018-08-07 07:44:18 +02:00
lastHref = href
lastAs = as
lastResult = result
return result
}
}
2020-05-10 23:52:30 +02:00
function formatUrl ( url : Url ) : string {
2019-05-30 03:19:32 +02:00
return url && typeof url === 'object' ? formatWithValidation ( url ) : url
2019-03-17 00:54:58 +01: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 )
}
}
2019-04-25 21:31:53 +02:00
class Link extends Component < LinkProps > {
2019-08-12 06:26:25 +02:00
p : boolean
2019-12-20 22:30:58 +01:00
2019-08-12 06:26:25 +02:00
constructor ( props : LinkProps ) {
super ( props )
if ( process . env . NODE_ENV !== 'production' ) {
if ( props . prefetch ) {
console . warn (
2020-05-27 23:51:11 +02:00
'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'
2019-08-12 06:26:25 +02:00
)
}
}
this . p = props . prefetch !== false
2019-06-24 20:22:27 +02:00
}
2019-05-01 15:14:27 +02:00
cleanUpListeners = ( ) = > { }
2020-05-10 23:52:30 +02:00
componentWillUnmount ( ) : void {
2019-05-01 15:14:27 +02:00
this . cleanUpListeners ( )
2018-08-07 05:53:06 +02:00
}
2020-05-10 23:52:30 +02:00
getPaths ( ) : string [ ] {
2019-12-20 22:30:58 +01:00
const { pathname } = window . location
2020-02-17 21:46:15 +01:00
const { href : parsedHref , as : parsedAs } = this . formatUrls (
this . props . href ,
this . props . as
)
const resolvedHref = resolve ( pathname , parsedHref )
return [ resolvedHref , parsedAs ? resolve ( pathname , parsedAs ) : resolvedHref ]
2019-12-20 22:30:58 +01:00
}
2020-05-10 23:52:30 +02:00
handleRef ( ref : Element ) : void {
2019-08-12 06:26:25 +02:00
if ( this . p && IntersectionObserver && ref && ref . tagName ) {
2019-07-14 19:23:13 +02:00
this . cleanUpListeners ( )
2019-12-20 22:30:58 +01:00
2020-03-01 00:06:18 +01:00
const isPrefetched =
prefetched [
this . getPaths ( ) . join (
// Join on an invalid URI character
'%'
)
]
2019-12-20 22:30:58 +01:00
if ( ! isPrefetched ) {
this . cleanUpListeners = listenToIntersections ( ref , ( ) = > {
this . prefetch ( )
} )
}
2018-08-07 05:53:06 +02:00
}
}
2018-08-07 07:44:18 +02:00
// The function is memoized so that no extra lifecycles are needed
// as per https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
formatUrls = memoizedFormatUrl ( ( href , asHref ) = > {
return {
2020-06-08 17:59:50 +02:00
href : formatUrl ( href ) ,
as : asHref ? formatUrl ( asHref ) : asHref ,
2018-08-07 07:44:18 +02:00
}
} )
2018-08-07 05:53:06 +02:00
2020-05-10 23:52:30 +02:00
linkClicked = ( e : React.MouseEvent ) : void = > {
2020-02-17 22:16:19 +01:00
const { nodeName , target } = e . currentTarget as HTMLAnchorElement
2019-05-30 03:19:32 +02:00
if (
nodeName === 'A' &&
( ( target && target !== '_self' ) ||
e . metaKey ||
e . ctrlKey ||
e . shiftKey ||
( e . nativeEvent && e . nativeEvent . which === 2 ) )
) {
2016-10-06 09:07:41 +02:00
// ignore click for new tab / new window behavior
return
}
2018-08-07 07:44:18 +02:00
let { href , as } = this . formatUrls ( this . props . href , this . props . as )
2016-10-06 09:07:41 +02:00
if ( ! isLocal ( href ) ) {
2019-08-07 16:47:13 +02:00
// ignore click if it's outside our scope (e.g. https://google.com)
2016-10-06 09:07:41 +02:00
return
}
2017-01-02 07:22:07 +01:00
const { pathname } = window . location
href = resolve ( pathname , href )
as = as ? resolve ( pathname , as ) : href
2016-10-06 09:07:41 +02:00
e . preventDefault ( )
2016-12-22 09:15:49 +01:00
// avoid scroll for urls with anchor refs
let { scroll } = this . props
if ( scroll == null ) {
2016-12-28 06:27:52 +01:00
scroll = as . indexOf ( '#' ) < 0
2016-12-22 09:15:49 +01:00
}
2017-03-15 00:06:34 +01:00
// replace state instead of push if prop is present
2019-05-30 03:19:32 +02:00
Router [ this . props . replace ? 'replace' : 'push' ] ( href , as , {
shallow : this.props.shallow ,
2019-08-07 16:47:13 +02:00
} ) . then ( ( success : boolean ) = > {
if ( ! success ) return
if ( scroll ) {
window . scrollTo ( 0 , 0 )
document . body . focus ( )
}
2019-05-30 03:19:32 +02:00
} )
}
2016-10-06 09:07:41 +02:00
2020-05-10 23:52:30 +02:00
prefetch ( options? : PrefetchOptions ) : void {
2019-08-12 06:26:25 +02:00
if ( ! this . p || typeof window === 'undefined' ) return
2017-02-15 15:01:03 +01:00
// Prefetch the JSON page if asked (only in the client)
2020-03-01 00:06:18 +01:00
const paths = this . getPaths ( )
2020-02-18 22:28:29 +01:00
// 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-03-01 00:06:18 +01:00
Router . prefetch ( paths [ /* href */ 0 ] , paths [ /* asPath */ 1 ] , options ) . catch (
2020-05-18 21:24:37 +02:00
( err ) = > {
2020-03-01 00:06:18 +01:00
if ( process . env . NODE_ENV !== 'production' ) {
// rethrow to show invalid URL errors
throw err
}
2020-02-18 22:28:29 +01:00
}
2020-03-01 00:06:18 +01:00
)
prefetched [
paths . join (
// Join on an invalid URI character
'%'
)
] = true
2017-02-15 15:01:03 +01:00
}
2019-04-25 21:31:53 +02:00
render() {
2017-02-15 15:01:03 +01:00
let { children } = this . props
2020-06-08 17:59:50 +02:00
let { href , as } = this . formatUrls ( this . props . href , this . props . as )
as = as ? addBasePath ( as ) : as
href = addBasePath ( href )
2019-08-08 20:11:17 +02:00
// Deprecated. Warning shown by propType check. If the children provided is a string (<Link>example</Link>) we wrap it in an <a> tag
2017-02-03 21:27:12 +01:00
if ( typeof children === 'string' ) {
children = < a > { children } < / a >
}
2016-10-06 09:07:41 +02:00
2017-02-03 21:27:12 +01:00
// This will return the first child, if multiple are provided it will throw an error
2019-04-25 21:31:53 +02:00
const child : any = Children . only ( children )
const props : {
2019-05-30 03:19:32 +02:00
onMouseEnter : React.MouseEventHandler
onClick : React.MouseEventHandler
href? : string
ref? : any
2019-04-25 21:31:53 +02:00
} = {
2019-08-08 20:11:17 +02:00
ref : ( el : any ) = > {
this . handleRef ( el )
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
}
}
} ,
2019-06-07 17:43:27 +02:00
onMouseEnter : ( e : React.MouseEvent ) = > {
if ( child . props && typeof child . props . onMouseEnter === 'function' ) {
child . props . onMouseEnter ( e )
}
2020-02-17 21:46:15 +01:00
this . prefetch ( { priority : true } )
2019-05-06 15:44:18 +02:00
} ,
2019-04-25 21:31:53 +02:00
onClick : ( e : React.MouseEvent ) = > {
2018-05-27 20:47:02 +02:00
if ( child . props && typeof child . props . onClick === 'function' ) {
child . props . onClick ( e )
}
if ( ! e . defaultPrevented ) {
this . linkClicked ( e )
}
2019-04-25 21:31:53 +02:00
} ,
2017-02-03 21:27:12 +01:00
}
2016-10-06 09:07:41 +02:00
2017-07-09 07:09:02 +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
2019-05-30 03:19:32 +02:00
if (
this . props . passHref ||
( child . type === 'a' && ! ( 'href' in child . props ) )
) {
2017-03-12 04:57:51 +01:00
props . href = as || href
2017-02-03 21:27:12 +01:00
}
2016-10-06 09:07:41 +02:00
2017-05-09 03:20:50 +02:00
// Add the ending slash to the paths. So, we can serve the
// "<page>/index.html" directly.
2019-03-17 00:54:58 +01:00
if ( process . env . __NEXT_EXPORT_TRAILING_SLASH ) {
2020-01-04 18:16:57 +01:00
const rewriteUrlForNextExport = require ( '../next-server/lib/router/rewrite-url-for-export' )
. rewriteUrlForNextExport
2019-03-17 00:54:58 +01:00
if (
props . href &&
typeof __NEXT_DATA__ !== 'undefined' &&
__NEXT_DATA__ . nextExport
) {
2019-04-24 16:47:50 +02:00
props . href = rewriteUrlForNextExport ( props . href )
2019-03-17 00:54:58 +01:00
}
2017-05-09 03:20:50 +02:00
}
2017-02-03 21:27:12 +01:00
return React . cloneElement ( child , props )
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