import { MouseEvent, useEffect, useRef, useCallback } from "react";
import * as React from "react";
import { warn, connect, useConnect } from "frontity";
import useInView from "@frontity/hooks/use-in-view";
import {
Queue,
onHover,
removeSourceUrl,
shouldFetchLink,
isExternalUrl,
} from "./utils";
import { Packages, LinkProps } from "./types";
const queue = new Queue();
/**
* The Link component that enables linking to internal pages in a frontity app.
*
* Under the hood, this component uses the `actions.router.set(link)` method
* from `@frontity/tiny-router` and creates an `` tag.
*
* All "unknown" props passed to the Link are passed down to an anchor ``
* tag.
*
* @example
* ```js
*
*
Some Post
*
* ```
*
* @param props - Defined by {@link LinkProps}.
*
* @returns An HTML anchor element.
*/
const Link: React.FC = ({
link: rawLink,
children,
onClick,
target = "_self",
scroll = true,
prefetch = true,
replaceSourceUrls = true,
"aria-current": ariaCurrent,
...anchorProps
}) => {
const { state, actions } = useConnect();
const { match } = state.frontity;
const { ref: inViewRef, inView } = useInView({
triggerOnce: true,
rootMargin: "200px",
});
const ref = useRef(null);
// we need to handle multiple refs, one for useInView and one for tracking the hover events.
const setRefs = useCallback(
(node) => {
ref.current = node;
if (typeof inViewRef === "function") {
inViewRef(node);
}
},
[inViewRef]
);
if (!rawLink || typeof rawLink !== "string") {
warn("link prop is required and must be a string");
}
const link = replaceSourceUrls
? removeSourceUrl({
link: rawLink,
sourceUrl: state.source.url,
frontityUrl: state.frontity.url,
match,
})
: rawLink;
const autoPrefetch = state.theme?.autoPrefetch;
const isExternal = isExternalUrl(link);
useEffect(() => {
if (!prefetch || !link || !shouldFetchLink(link)) {
return;
}
/**
* Prefetches the link only if necessary.
*
* @param link - The link to prefetch.
* @param runNow - Whether the prefetch should be executed immediately.
*/
const maybePrefetch = (link: string, runNow = false) => {
if (queue.toPrefetch.has(link)) {
return;
}
const data = state.source.get(link);
if (data.isReady || data.isFetching) {
return;
}
// when prefetch mode is "hover" we want to prefetch without batching.
if (runNow) {
actions.source.fetch(link);
} else {
queue.toPrefetch.add(link);
}
// if the queue is still running this link will be picked up automatically.
if (!queue.isProcessing) {
queue.process(actions.source.fetch);
queue.isProcessing = true;
}
};
if (autoPrefetch === "all") {
maybePrefetch(link);
} else if (ref.current && autoPrefetch === "hover") {
return onHover(ref.current, () => {
maybePrefetch(link, true);
});
} else if (inView && autoPrefetch === "in-view") {
maybePrefetch(link);
}
}, [
prefetch,
link,
ref,
inView,
autoPrefetch,
actions.source,
isExternal,
state.source,
]);
/**
* The event handler for the click event. It will try to do client-side
* rendering but bail out in certain situations, like when the link is
* external or the user is trying to open a new tab.
*
* @param event - The mouse click event.
*/
const onClickHandler = (event: MouseEvent) => {
// Do nothing if it's an external link
if (isExternal) return;
// Do nothing if this is supposed to open in a new tab
if (target === "_blank") return;
// Allow the user to open the link in a new tab
if (
event.ctrlKey ||
event.shiftKey ||
event.metaKey ||
(event.button && event.button === 1)
) {
return;
}
// Prevent the server-side rendering.
event.preventDefault();
// Set the router to the new url.
actions.router.set(link);
// Scroll the page to the top
if (scroll) {
window.scrollTo(0, 0);
document.body.focus();
}
// If there's an additional handler, execute it.
if (typeof onClick === "function") {
onClick(event);
}
};
return (
{children}
);
};
export default connect(Link, { injectProps: false });