Skip to content

Commit

Permalink
Merge branch 'main' into davidramos-customize-delay-for-instant-click…
Browse files Browse the repository at this point in the history
…-behavior
  • Loading branch information
davidalejandroaguilar committed Feb 7, 2024
2 parents 28fa3fb + 52c8533 commit 448cdd7
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 36 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
40 changes: 30 additions & 10 deletions src/observers/link_prefetch_observer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
dispatch,
doesNotTargetIFrame,
getLocationForLink,
getMetaContent,
Expand All @@ -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
Expand All @@ -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])")
Expand All @@ -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,
Expand All @@ -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())
Expand Down Expand Up @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions src/tests/fixtures/hover_to_prefetch.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
>Won't prefetch when hovering me</a>
<a href="/src/tests/fixtures/prefetched.html" id="anchor_with_turbo_false" data-turbo="false"
>Won't prefetch when hovering me</a>
<a href="/src/tests/fixtures/prefetched.html" id="anchor_with_remote_true" data-remote="true"
>Won't prefetch when hovering me</a>
<a href="/src/tests/fixtures/hover_to_prefetch.html" id="anchor_for_same_location"
>Won't prefetch when hovering me</a>
<a href="/src/tests/fixtures/prefetched.html?foo=bar" id="anchor_for_same_location_with_query"
Expand Down
41 changes: 16 additions & 25 deletions src/tests/functional/link_prefetch_observer_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ test("it doesn't prefetch the page when link has data-turbo=false", async ({ pag
await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_turbo_false" })
})

test("allows to cancel prefetch requests with custom logic", async ({ page }) => {
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" })
Expand Down Expand Up @@ -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" })
Expand All @@ -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

Expand Down

0 comments on commit 448cdd7

Please sign in to comment.