drab
Version:
Interactivity for You
128 lines (127 loc) • 5.58 kB
JavaScript
import { Lifecycle, Trigger } 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.
*
* ### Attributes
*
* `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 Lifecycle(Trigger()) {
#prefetchedUrls = new Set();
constructor() {
super();
}
/** When to prefetch the url. */
get #strategy() {
return this.getAttribute("strategy");
}
/** 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.
*/
prefetch(options) {
const { url } = options;
// if not the current page and not already prefetched
if (!(url === window.location.href) && !this.#prefetchedUrls.has(url)) {
this.#prefetchedUrls.add(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 (options.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);
}
}
}
mount() {
// immediately prefetch the `url` attribute if it exists
if (this.#url)
this.prefetch({ url: this.#url, prerender: this.#prerender });
// prefetch the `trigger` elements
const anchors = this.triggers(HTMLAnchorElement);
const prerender = this.#prerender;
const strategy = this.#strategy;
let prefetchTimer;
const listener = (delay = 200) => (e) => {
const { href } = e.currentTarget;
prefetchTimer = setTimeout(() => this.prefetch({ url: href, prerender }), delay);
};
const reset = () => clearTimeout(prefetchTimer);
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
this.prefetch({
url: entry.target.href,
prerender,
});
}
}
});
for (const anchor of anchors) {
if (strategy === "load") {
this.prefetch({ 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 });
}
}
}
}