Skip to content

Commit d22bcce

Browse files
Merge branch 'main' into davidramos-customize-delay-for-instant-click-behavior
2 parents cdf89ef + 9fb05e3 commit d22bcce

21 files changed

+343
-217
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hotwired/turbo",
3-
"version": "8.0.0-rc.2",
3+
"version": "8.0.4",
44
"description": "The speed of a single-page web application without having to write any JavaScript",
55
"module": "dist/turbo.es2017-esm.js",
66
"main": "dist/turbo.es2017-umd.js",

src/core/drive/morph_renderer.js

-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export class MorphRenderer extends PageRenderer {
2929
this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement)
3030

3131
Idiomorph.morph(currentElement, newElement, {
32-
ignoreActiveValue: true,
3332
morphStyle: morphStyle,
3433
callbacks: {
3534
beforeNodeAdded: this.#shouldAddElement,

src/core/drive/visit.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ export class Visit {
136136
complete() {
137137
if (this.state == VisitState.started) {
138138
this.recordTimingMetric(TimingMetric.visitEnd)
139+
this.adapter.visitCompleted(this)
139140
this.state = VisitState.completed
140141
this.followRedirect()
141142

142143
if (!this.followedRedirect) {
143-
this.adapter.visitCompleted(this)
144144
this.delegate.visitCompleted(this)
145145
}
146146
}

src/core/frames/frame_controller.js

+6-16
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,12 @@ export class FrameController {
9090

9191
sourceURLReloaded() {
9292
const { src } = this.element
93-
this.#ignoringChangesToAttribute("complete", () => {
94-
this.element.removeAttribute("complete")
95-
})
93+
this.element.removeAttribute("complete")
9694
this.element.src = null
9795
this.element.src = src
9896
return this.element.loaded
9997
}
10098

101-
completeChanged() {
102-
if (this.#isIgnoringChangesTo("complete")) return
103-
104-
this.#loadSourceURL()
105-
}
106-
10799
loadingStyleChanged() {
108100
if (this.loadingStyle == FrameLoadingStyle.lazy) {
109101
this.appearanceObserver.start()
@@ -528,13 +520,11 @@ export class FrameController {
528520
}
529521

530522
set complete(value) {
531-
this.#ignoringChangesToAttribute("complete", () => {
532-
if (value) {
533-
this.element.setAttribute("complete", "")
534-
} else {
535-
this.element.removeAttribute("complete")
536-
}
537-
})
523+
if (value) {
524+
this.element.setAttribute("complete", "")
525+
} else {
526+
this.element.removeAttribute("complete")
527+
}
538528
}
539529

540530
get isActive() {

src/core/session.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,7 @@ export class Session {
110110
refresh(url, requestId) {
111111
const isRecentRequest = requestId && this.recentRequests.has(requestId)
112112
if (!isRecentRequest) {
113-
this.cache.exemptPageFromPreview()
114-
this.visit(url, { action: "replace" })
113+
this.visit(url, { action: "replace", shouldCacheSnapshot: false })
115114
}
116115
}
117116

src/core/streams/actions/morph.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Idiomorph } from "idiomorph/dist/idiomorph.esm"
2+
import { dispatch } from "../../../util"
3+
4+
export default function morph(streamElement) {
5+
const morphStyle = streamElement.hasAttribute("children-only") ? "innerHTML" : "outerHTML"
6+
streamElement.targetElements.forEach((element) => {
7+
Idiomorph.morph(element, streamElement.templateContent, {
8+
morphStyle: morphStyle,
9+
callbacks: {
10+
beforeNodeAdded,
11+
beforeNodeMorphed,
12+
beforeAttributeUpdated,
13+
beforeNodeRemoved,
14+
afterNodeMorphed
15+
}
16+
})
17+
})
18+
}
19+
20+
function beforeNodeAdded(node) {
21+
return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
22+
}
23+
24+
function beforeNodeRemoved(node) {
25+
return beforeNodeAdded(node)
26+
}
27+
28+
function beforeNodeMorphed(target, newElement) {
29+
if (target instanceof HTMLElement) {
30+
if (!target.hasAttribute("data-turbo-permanent")) {
31+
const event = dispatch("turbo:before-morph-element", {
32+
cancelable: true,
33+
target,
34+
detail: {
35+
newElement
36+
}
37+
})
38+
return !event.defaultPrevented
39+
}
40+
return false
41+
}
42+
}
43+
44+
function beforeAttributeUpdated(attributeName, target, mutationType) {
45+
const event = dispatch("turbo:before-morph-attribute", {
46+
cancelable: true,
47+
target,
48+
detail: {
49+
attributeName,
50+
mutationType
51+
}
52+
})
53+
return !event.defaultPrevented
54+
}
55+
56+
function afterNodeMorphed(target, newElement) {
57+
if (newElement instanceof HTMLElement) {
58+
dispatch("turbo:morph-element", {
59+
target,
60+
detail: {
61+
newElement
62+
}
63+
})
64+
}
65+
}

src/core/streams/stream_actions.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { session } from "../"
2+
import morph from "./actions/morph"
23

34
export const StreamActions = {
45
after() {
@@ -36,5 +37,9 @@ export const StreamActions = {
3637

3738
refresh() {
3839
session.refresh(this.baseURI, this.requestId)
40+
},
41+
42+
morph() {
43+
morph(this)
3944
}
4045
}

src/elements/frame_element.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class FrameElement extends HTMLElement {
2525
loaded = Promise.resolve()
2626

2727
static get observedAttributes() {
28-
return ["disabled", "complete", "loading", "src"]
28+
return ["disabled", "loading", "src"]
2929
}
3030

3131
constructor() {
@@ -48,11 +48,9 @@ export class FrameElement extends HTMLElement {
4848
attributeChangedCallback(name) {
4949
if (name == "loading") {
5050
this.delegate.loadingStyleChanged()
51-
} else if (name == "complete") {
52-
this.delegate.completeChanged()
5351
} else if (name == "src") {
5452
this.delegate.sourceURLChanged()
55-
} else {
53+
} else if (name == "disabled") {
5654
this.delegate.disabledChanged()
5755
}
5856
}

src/observers/link_prefetch_observer.js

+58-40
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import {
2-
doesNotTargetIFrame,
2+
dispatch,
33
getLocationForLink,
44
getMetaContent,
55
findClosestRecursively
66
} from "../util"
77

8-
import { StreamMessage } from "../core/streams/stream_message"
98
import { FetchMethod, FetchRequest } from "../http/fetch_request"
109
import { prefetchCache, cacheTtl } from "../core/drive/prefetch_cache"
1110

1211
export class LinkPrefetchObserver {
1312
started = false
14-
hoverTriggerEvent = "mouseenter"
15-
touchTriggerEvent = "touchstart"
13+
#prefetchedLink = null
1614

1715
constructor(delegate, eventTarget) {
1816
this.delegate = delegate
@@ -32,33 +30,35 @@ export class LinkPrefetchObserver {
3230
stop() {
3331
if (!this.started) return
3432

35-
this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
33+
this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, {
3634
capture: true,
3735
passive: true
3836
})
39-
this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
37+
this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, {
4038
capture: true,
4139
passive: true
4240
})
41+
4342
this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true)
4443
this.started = false
4544
}
4645

4746
#enable = () => {
48-
this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
47+
this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, {
4948
capture: true,
5049
passive: true
5150
})
52-
this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
51+
this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, {
5352
capture: true,
5453
passive: true
5554
})
55+
5656
this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true)
5757
this.started = true
5858
}
5959

6060
#tryToPrefetchRequest = (event) => {
61-
if (getMetaContent("turbo-prefetch") !== "true") return
61+
if (getMetaContent("turbo-prefetch") === "false") return
6262

6363
const target = event.target
6464
const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])")
@@ -68,6 +68,8 @@ export class LinkPrefetchObserver {
6868
const location = getLocationForLink(link)
6969

7070
if (this.delegate.canPrefetchRequestToLocation(link, location)) {
71+
this.#prefetchedLink = link
72+
7173
const fetchRequest = new FetchRequest(
7274
this,
7375
FetchMethod.get,
@@ -85,6 +87,15 @@ export class LinkPrefetchObserver {
8587
}
8688
}
8789

90+
#cancelRequestIfObsolete = (event) => {
91+
if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest()
92+
}
93+
94+
#cancelPrefetchRequest = () => {
95+
prefetchCache.clear()
96+
this.#prefetchedLink = null
97+
}
98+
8899
#tryToUsePrefetchedRequest = (event) => {
89100
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
90101
const cached = prefetchCache.get(event.detail.url.toString())
@@ -109,10 +120,6 @@ export class LinkPrefetchObserver {
109120
if (turboFrameTarget && turboFrameTarget !== "_top") {
110121
request.headers["Turbo-Frame"] = turboFrameTarget
111122
}
112-
113-
if (link.hasAttribute("data-turbo-stream")) {
114-
request.acceptResponseType(StreamMessage.contentType)
115-
}
116123
}
117124

118125
// Fetch request interface
@@ -136,41 +143,52 @@ export class LinkPrefetchObserver {
136143
#isPrefetchable(link) {
137144
const href = link.getAttribute("href")
138145

139-
if (!href || href === "#" || link.getAttribute("data-turbo") === "false" || link.getAttribute("data-turbo-prefetch") === "false") {
140-
return false
141-
}
146+
if (!href) return false
142147

143-
if (link.origin !== document.location.origin) {
144-
return false
145-
}
148+
if (unfetchableLink(link)) return false
149+
if (linkToTheSamePage(link)) return false
150+
if (linkOptsOut(link)) return false
151+
if (nonSafeLink(link)) return false
152+
if (eventPrevented(link)) return false
146153

147-
if (!["http:", "https:"].includes(link.protocol)) {
148-
return false
149-
}
154+
return true
155+
}
156+
}
150157

151-
if (link.pathname + link.search === document.location.pathname + document.location.search) {
152-
return false
153-
}
158+
const unfetchableLink = (link) => {
159+
return link.origin !== document.location.origin || !["http:", "https:"].includes(link.protocol) || link.hasAttribute("target")
160+
}
154161

155-
const turboMethod = link.getAttribute("data-turbo-method")
156-
if (turboMethod && turboMethod !== "get") {
157-
return false
158-
}
162+
const linkToTheSamePage = (link) => {
163+
return (link.pathname + link.search === document.location.pathname + document.location.search) || link.href.startsWith("#")
164+
}
159165

160-
if (targetsIframe(link)) {
161-
return false
162-
}
166+
const linkOptsOut = (link) => {
167+
if (link.getAttribute("data-turbo-prefetch") === "false") return true
168+
if (link.getAttribute("data-turbo") === "false") return true
163169

164-
const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]")
170+
const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]")
171+
if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") return true
165172

166-
if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") {
167-
return false
168-
}
173+
return false
174+
}
169175

170-
return true
171-
}
176+
const nonSafeLink = (link) => {
177+
const turboMethod = link.getAttribute("data-turbo-method")
178+
if (turboMethod && turboMethod.toLowerCase() !== "get") return true
179+
180+
if (isUJS(link)) return true
181+
if (link.hasAttribute("data-turbo-confirm")) return true
182+
if (link.hasAttribute("data-turbo-stream")) return true
183+
184+
return false
185+
}
186+
187+
const isUJS = (link) => {
188+
return link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-confirm") || link.hasAttribute("data-method")
172189
}
173190

174-
const targetsIframe = (link) => {
175-
return !doesNotTargetIFrame(link)
191+
const eventPrevented = (link) => {
192+
const event = dispatch("turbo:before-prefetch", { target: link, cancelable: true })
193+
return event.defaultPrevented
176194
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<turbo-frame id="refresh-after-navigation">
2+
<h2 id="refresh-after-navigation-content">Frame has been navigated</h2>
3+
</turbo-frame>

src/tests/fixtures/hover_to_prefetch.html

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
>Won't prefetch when hovering me</a>
3131
<a href="/src/tests/fixtures/prefetched.html" id="anchor_with_turbo_false" data-turbo="false"
3232
>Won't prefetch when hovering me</a>
33+
<a href="/src/tests/fixtures/prefetched.html" id="anchor_with_remote_true" data-remote="true"
34+
>Won't prefetch when hovering me</a>
35+
<a href="/src/tests/fixtures/prefetched.html" id="anchor_with_turbo_stream" data-turbo-stream="true"
36+
>Won't prefetch when hovering me</a>
37+
<a href="/src/tests/fixtures/prefetched.html" id="anchor_with_turbo_confirm" data-turbo-confirm="Are you sure?"
38+
>Won't prefetch when hovering me</a>
3339
<a href="/src/tests/fixtures/hover_to_prefetch.html" id="anchor_for_same_location"
3440
>Won't prefetch when hovering me</a>
3541
<a href="/src/tests/fixtures/prefetched.html?foo=bar" id="anchor_for_same_location_with_query"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html id="html">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Morph Stream Action</title>
6+
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
7+
<script src="/src/tests/fixtures/test.js"></script>
8+
<meta name="turbo-refresh-method" content="replace">
9+
</head>
10+
11+
<body>
12+
<div id="message_1">
13+
<div>Morph me</div>
14+
</div>
15+
</body>
16+
</html>

0 commit comments

Comments
 (0)