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