drab
Version:
Interactivity for You
150 lines (149 loc) • 6.2 kB
JavaScript
import { Base } from "../base/index.js";
/**
* The `Prefetch` element can prefetch a url, or enhance the `HTMLAnchorElement` by loading the HTML for a page before it is navigated to. This element speeds up the navigation for multi-page applications (MPAs).
*
* If you are using a framework that already has a prefetch feature or uses a client side router, it is best to use the framework's feature instead of this element to ensure prefetching is working in accordance with the router.
*
* `strategy`
*
* Set the `strategy` attribute to specify the when the prefetch will take place.
*
* - `"hover"` - (default) After `mouseover`, `focus`, or `touchstart` for > 200ms
* - `"visible"` - Uses an intersection observer to prefetch when the anchor is within the viewport.
* - `"load"` - Immediately prefetches when the element is loaded, use carefully.
*
* `prerender`
*
* Use the `prerender` attribute to use the Speculation Rules API when supported to prerender on the client. This allows you to run client side JavaScript in advance instead of only fetching the HTML.
*
* Browsers that do not support will still use `<link rel="prefetch">` instead.
*
* [Speculation Rules Reference](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API)
*
* `url`
*
* Add a `url` attribute to immediately prefetch a url without having to provide
* (or in addition to) `trigger` anchor elements.
*
* This element can be deprecated once the Speculation Rules API is supported across browsers. The API will be able to prefetch assets in a similar way with the `source: "document"` and `eagerness` features, and will work without JavaScript.
*/
export class Prefetch extends Base {
#prefetchedUrls = [];
constructor() {
super();
}
/** When to prefetch the url. */
get #strategy() {
return (this.getAttribute("strategy") ?? "hover");
}
/** Prerender with the Speculation Rules API. */
get #prerender() {
return this.hasAttribute("prerender");
}
/** `url` to prefetch on `mount`. */
get #url() {
return this.getAttribute("url");
}
/**
* Appends `<link rel="prefetch">` or `<script type="speculationrules">` to the head of the document.
*
* @param options Configuration options.
*/
appendTag(options) {
const { url, prerender } = options;
// if not the current page and not already prefetched
if (!(url === window.location.href) &&
!this.#prefetchedUrls.includes(url)) {
this.#prefetchedUrls.push(url);
if (HTMLScriptElement.supports &&
HTMLScriptElement.supports("speculationrules")) {
const rules = {
// Currently, adding `prefetch` is required to fallback if `prerender` fails.
// Possibly will be automatic in the future, in which case it can be removed.
// https://github.com/WICG/nav-speculation/issues/162#issuecomment-1977818473
prefetch: [
{
source: "list",
urls: [url],
},
],
};
if (prerender) {
rules.prerender = rules.prefetch;
}
const script = document.createElement("script");
script.type = "speculationrules";
script.textContent = JSON.stringify(rules);
document.head.append(script);
}
else {
const link = document.createElement("link");
link.rel = "prefetch";
link.as = "document";
link.href = url;
document.head.append(link);
}
}
}
/**
* Use to prefetch/prerender HTML.
*
* Can be used more than once with different options for different selectors.
*
* @param options Prefetch options.
*/
prefetch(options = {
anchors: this.getTrigger(),
prerender: this.#prerender,
strategy: this.#strategy,
}) {
// defaults if partially defined
const { anchors = this.getTrigger(), prerender = this.#prerender, strategy = this.#strategy, } = options;
let prefetchTimer;
/**
* @param delay ms delay - for `hover`
* @returns the event listener with delay
*/
const listener = (delay = 200) => (e) => {
const { href } = e.currentTarget;
prefetchTimer = setTimeout(() => this.appendTag({ url: href, prerender }), delay);
};
const reset = () => clearTimeout(prefetchTimer);
const observer = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
this.appendTag({
url: e.target.href,
prerender,
});
}
}
});
for (const anchor of anchors) {
if (strategy === "load") {
this.appendTag({ url: anchor.href, prerender });
}
else if (strategy === "visible") {
observer.observe(anchor);
}
else {
// "hover" - default
anchor.addEventListener("mouseover", listener());
anchor.addEventListener("mouseout", reset);
anchor.addEventListener("focus", listener());
anchor.addEventListener("focusout", reset);
// immediately append on touchstart, no delay
// passive: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners
anchor.addEventListener("touchstart", listener(0), { passive: true });
}
}
}
mount() {
// immediately prefetch the `url` attribute if it exists
if (this.#url) {
this.appendTag({ url: this.#url, prerender: this.#prerender });
}
// prefetch the `trigger` elements
this.prefetch();
}
}