diff --git a/package.json b/package.json index c058de50a..ea072314c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hotwired/turbo", - "version": "8.0.0-rc.2", + "version": "8.0.0-rc.3", "description": "The speed of a single-page web application without having to write any JavaScript", "module": "dist/turbo.es2017-esm.js", "main": "dist/turbo.es2017-umd.js", diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index ceedeb551..5f5450fff 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -1,4 +1,5 @@ import { + dispatch, doesNotTargetIFrame, getLocationForLink, getMetaContent, @@ -11,8 +12,7 @@ import { prefetchCache, cacheTtl } from "../core/drive/prefetch_cache" export class LinkPrefetchObserver { started = false - hoverTriggerEvent = "mouseenter" - touchTriggerEvent = "touchstart" + #prefetchedLink = null constructor(delegate, eventTarget) { this.delegate = delegate @@ -32,33 +32,35 @@ export class LinkPrefetchObserver { stop() { if (!this.started) return - this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, { + this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, { capture: true, passive: true }) - this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, { + this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, { capture: true, passive: true }) + this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true) this.started = false } #enable = () => { - this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, { + this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, { capture: true, passive: true }) - this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, { + this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, { capture: true, passive: true }) + this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true) this.started = true } #tryToPrefetchRequest = (event) => { - if (getMetaContent("turbo-prefetch") !== "true") return + if (getMetaContent("turbo-prefetch") === "false") return const target = event.target const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])") @@ -68,6 +70,8 @@ export class LinkPrefetchObserver { const location = getLocationForLink(link) if (this.delegate.canPrefetchRequestToLocation(link, location)) { + this.#prefetchedLink = link + const fetchRequest = new FetchRequest( this, FetchMethod.get, @@ -79,12 +83,19 @@ export class LinkPrefetchObserver { const delay = link.dataset.turboPrefetchDelay || getMetaContent("turbo-prefetch-delay") prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl, delay) - - link.addEventListener("mouseleave", () => prefetchCache.clear(), { once: true }) } } } + #cancelRequestIfObsolete = (event) => { + if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest() + } + + #cancelPrefetchRequest = () => { + prefetchCache.clear() + this.#prefetchedLink = null + } + #tryToUsePrefetchedRequest = (event) => { if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") { const cached = prefetchCache.get(event.detail.url.toString()) @@ -136,7 +147,16 @@ export class LinkPrefetchObserver { #isPrefetchable(link) { const href = link.getAttribute("href") - if (!href || href === "#" || link.getAttribute("data-turbo") === "false" || link.getAttribute("data-turbo-prefetch") === "false") { + if (!href || href.startsWith("#") || link.getAttribute("data-turbo") === "false" || link.getAttribute("data-turbo-prefetch") === "false") { + return false + } + + const event = dispatch("turbo:before-prefetch", { + target: link, + cancelable: true + }) + + if (event.defaultPrevented) { return false } diff --git a/src/tests/fixtures/hover_to_prefetch.html b/src/tests/fixtures/hover_to_prefetch.html index 9306aba3b..a39cd8456 100644 --- a/src/tests/fixtures/hover_to_prefetch.html +++ b/src/tests/fixtures/hover_to_prefetch.html @@ -30,6 +30,8 @@ >Won't prefetch when hovering me Won't prefetch when hovering me + Won't prefetch when hovering me Won't prefetch when hovering me { + await goTo({ page, path: "/hover_to_prefetch.html" }) + + await assertPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" }) + + await page.evaluate(() => { + document.body.addEventListener("turbo:before-prefetch", (event) => { + if (event.target.hasAttribute("data-remote")) { + event.preventDefault() + } + }) + }) + + await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" }) +}) + test("it doesn't prefetch the page when link has the same location", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) await assertNotPrefetchedOnHover({ page, selector: "#anchor_for_same_location" }) @@ -278,11 +294,6 @@ test("it resets the cache when a link is hovered", async ({ page }) => { assert.equal(requestCount, 2) }) -test("it prefetches page on touchstart", async ({ page }) => { - await goTo({ page, path: "/hover_to_prefetch.html" }) - await assertPrefetchedOnTouchstart({ page, selector: "#anchor_for_prefetch" }) -}) - test("it does not make a network request when clicking on a link that has been prefetched", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) await hoverSelector({ page, selector: "#anchor_for_prefetch" }) @@ -302,26 +313,6 @@ test("it follows the link using the cached response when clicking on a link that assert.equal(await page.title(), "Prefetched Page") }) -const assertPrefetchedOnTouchstart = async ({ page, selector, callback }) => { - let requestMade = false - - page.on("request", (request) => { - callback && callback(request) - requestMade = true - }) - - const selectorXY = await page.$eval(selector, (el) => { - const { x, y } = el.getBoundingClientRect() - return { x, y } - }) - - await page.touchscreen.tap(selectorXY.x, selectorXY.y) - - await sleep(100) - - assertRequestMade(requestMade) -} - const assertPrefetchedOnHover = async ({ page, selector, callback }) => { let requestMade = false