@docusaurus/core
Version:
Easy to Maintain Open Source Documentation Websites
143 lines (142 loc) • 7.29 kB
JavaScript
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, { useEffect, useImperativeHandle, useRef, } from 'react';
import { NavLink, Link as RRLink } from 'react-router-dom';
import { applyTrailingSlash } from '@docusaurus/utils-common';
import useDocusaurusContext from './useDocusaurusContext';
import isInternalUrl from './isInternalUrl';
import ExecutionEnvironment from './ExecutionEnvironment';
import useBrokenLinks from './useBrokenLinks';
import { useBaseUrlUtils } from './useBaseUrl';
// TODO all this wouldn't be necessary if we used ReactRouter basename feature
// We don't automatically add base urls to all links,
// only the "safe" ones, starting with / (like /docs/introduction)
// this is because useBaseUrl() actually transforms relative links
// like "introduction" to "/baseUrl/introduction" => bad behavior to fix
const shouldAddBaseUrlAutomatically = (to) => to.startsWith('/');
function Link({ isNavLink, to, href, activeClassName, isActive, 'data-noBrokenLinkCheck': noBrokenLinkCheck, autoAddBaseUrl = true, ...props }, forwardedRef) {
const { siteConfig } = useDocusaurusContext();
const { trailingSlash, baseUrl } = siteConfig;
const router = siteConfig.future.experimental_router;
const { withBaseUrl } = useBaseUrlUtils();
const brokenLinks = useBrokenLinks();
const innerRef = useRef(null);
useImperativeHandle(forwardedRef, () => innerRef.current);
// IMPORTANT: using to or href should not change anything
// For example, MDX links will ALWAYS give us the href props
// Using one prop or the other should not be used to distinguish
// internal links (/docs/myDoc) from external links (https://github.com)
const targetLinkUnprefixed = to || href;
function maybeAddBaseUrl(str) {
return autoAddBaseUrl && shouldAddBaseUrlAutomatically(str)
? withBaseUrl(str)
: str;
}
const isInternal = isInternalUrl(targetLinkUnprefixed);
// pathname:// is a special "protocol" we use to tell Docusaurus link
// that a link is not "internal" and that we shouldn't use history.push()
// this is not ideal but a good enough escape hatch for now
// see https://github.com/facebook/docusaurus/issues/3309
// note: we want baseUrl to be appended (see issue for details)
// TODO read routes and automatically detect internal/external links?
const targetLinkWithoutPathnameProtocol = targetLinkUnprefixed?.replace('pathname://', '');
// TODO we should use ReactRouter basename feature instead!
// Automatically apply base url in links that start with /
let targetLink = typeof targetLinkWithoutPathnameProtocol !== 'undefined'
? maybeAddBaseUrl(targetLinkWithoutPathnameProtocol)
: undefined;
// TODO find a way to solve this problem properly
// Fix edge case when useBaseUrl is used on a link
// "./" is useful for images and other resources
// But we don't need it for <Link>
// unfortunately we can't really make the difference :/
if (router === 'hash' && targetLink?.startsWith('./')) {
targetLink = targetLink?.slice(1);
}
if (targetLink && isInternal) {
targetLink = applyTrailingSlash(targetLink, { trailingSlash, baseUrl });
}
const preloaded = useRef(false);
const LinkComponent = (isNavLink ? NavLink : RRLink);
const IOSupported = ExecutionEnvironment.canUseIntersectionObserver;
const ioRef = useRef();
const handleRef = (el) => {
innerRef.current = el;
if (IOSupported && el && isInternal) {
// If IO supported and element reference found, set up Observer.
ioRef.current = new window.IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (el === entry.target) {
// If element is in viewport, stop observing and run callback.
// https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
if (entry.isIntersecting || entry.intersectionRatio > 0) {
ioRef.current.unobserve(el);
ioRef.current.disconnect();
if (targetLink != null) {
window.docusaurus.prefetch(targetLink);
}
}
}
});
});
// Add element to the observer.
ioRef.current.observe(el);
}
};
const onInteractionEnter = () => {
if (!preloaded.current && targetLink != null) {
window.docusaurus.preload(targetLink);
preloaded.current = true;
}
};
useEffect(() => {
// If IO is not supported. We prefetch by default (only once).
if (!IOSupported && isInternal && ExecutionEnvironment.canUseDOM) {
if (targetLink != null) {
window.docusaurus.prefetch(targetLink);
}
}
// When unmounting, stop intersection observer from watching.
return () => {
if (IOSupported && ioRef.current) {
ioRef.current.disconnect();
}
};
}, [ioRef, targetLink, IOSupported, isInternal]);
// It is simple local anchor link targeting current page?
const isAnchorLink = targetLink?.startsWith('#') ?? false;
// See also RR logic:
// https://github.com/remix-run/react-router/blob/v5/packages/react-router-dom/modules/Link.js#L47
const hasInternalTarget = !props.target || props.target === '_self';
// Should we use a regular <a> tag instead of React-Router Link component?
const isRegularHtmlLink = !targetLink ||
!isInternal ||
!hasInternalTarget ||
// When using the hash router, we can't use the regular <a> link for anchors
// We need to use React Router to navigate to /#/pathname/#anchor
// And not /#anchor
// See also https://github.com/facebook/docusaurus/pull/10311
(isAnchorLink && router !== 'hash');
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
brokenLinks.collectLink(targetLink);
}
if (props.id) {
brokenLinks.collectAnchor(props.id);
}
// These props are only added in unit tests to assert/capture the type of link
const testOnlyProps = process.env.NODE_ENV === 'test'
? { 'data-test-link-type': isRegularHtmlLink ? 'regular' : 'react-router' }
: {};
return isRegularHtmlLink ? (
// eslint-disable-next-line jsx-a11y/anchor-has-content, @docusaurus/no-html-links
<a ref={innerRef} href={targetLink} {...(targetLinkUnprefixed &&
!isInternal && { target: '_blank', rel: 'noopener noreferrer' })} {...props} {...testOnlyProps}/>) : (<LinkComponent {...props} onMouseEnter={onInteractionEnter} onTouchStart={onInteractionEnter} innerRef={handleRef} to={targetLink}
// Avoid "React does not recognize the `activeClassName` prop on a DOM
// element"
{...(isNavLink && { isActive, activeClassName })} {...testOnlyProps}/>);
}
export default React.forwardRef(Link);