UNPKG

drab

Version:

Interactivity for You

150 lines (149 loc) 6.2 kB
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(); } }