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 Apr 4, 2024
2 parents cdf89ef + 9fb05e3 commit d22bcce
Show file tree
Hide file tree
Showing 21 changed files with 343 additions and 217 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.4",
"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
1 change: 0 additions & 1 deletion src/core/drive/morph_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export class MorphRenderer extends PageRenderer {
this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement)

Idiomorph.morph(currentElement, newElement, {
ignoreActiveValue: true,
morphStyle: morphStyle,
callbacks: {
beforeNodeAdded: this.#shouldAddElement,
Expand Down
2 changes: 1 addition & 1 deletion src/core/drive/visit.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,11 @@ export class Visit {
complete() {
if (this.state == VisitState.started) {
this.recordTimingMetric(TimingMetric.visitEnd)
this.adapter.visitCompleted(this)
this.state = VisitState.completed
this.followRedirect()

if (!this.followedRedirect) {
this.adapter.visitCompleted(this)
this.delegate.visitCompleted(this)
}
}
Expand Down
22 changes: 6 additions & 16 deletions src/core/frames/frame_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,12 @@ export class FrameController {

sourceURLReloaded() {
const { src } = this.element
this.#ignoringChangesToAttribute("complete", () => {
this.element.removeAttribute("complete")
})
this.element.removeAttribute("complete")
this.element.src = null
this.element.src = src
return this.element.loaded
}

completeChanged() {
if (this.#isIgnoringChangesTo("complete")) return

this.#loadSourceURL()
}

loadingStyleChanged() {
if (this.loadingStyle == FrameLoadingStyle.lazy) {
this.appearanceObserver.start()
Expand Down Expand Up @@ -528,13 +520,11 @@ export class FrameController {
}

set complete(value) {
this.#ignoringChangesToAttribute("complete", () => {
if (value) {
this.element.setAttribute("complete", "")
} else {
this.element.removeAttribute("complete")
}
})
if (value) {
this.element.setAttribute("complete", "")
} else {
this.element.removeAttribute("complete")
}
}

get isActive() {
Expand Down
3 changes: 1 addition & 2 deletions src/core/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ export class Session {
refresh(url, requestId) {
const isRecentRequest = requestId && this.recentRequests.has(requestId)
if (!isRecentRequest) {
this.cache.exemptPageFromPreview()
this.visit(url, { action: "replace" })
this.visit(url, { action: "replace", shouldCacheSnapshot: false })
}
}

Expand Down
65 changes: 65 additions & 0 deletions src/core/streams/actions/morph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Idiomorph } from "idiomorph/dist/idiomorph.esm"
import { dispatch } from "../../../util"

export default function morph(streamElement) {
const morphStyle = streamElement.hasAttribute("children-only") ? "innerHTML" : "outerHTML"
streamElement.targetElements.forEach((element) => {
Idiomorph.morph(element, streamElement.templateContent, {
morphStyle: morphStyle,
callbacks: {
beforeNodeAdded,
beforeNodeMorphed,
beforeAttributeUpdated,
beforeNodeRemoved,
afterNodeMorphed
}
})
})
}

function beforeNodeAdded(node) {
return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
}

function beforeNodeRemoved(node) {
return beforeNodeAdded(node)
}

function beforeNodeMorphed(target, newElement) {
if (target instanceof HTMLElement) {
if (!target.hasAttribute("data-turbo-permanent")) {
const event = dispatch("turbo:before-morph-element", {
cancelable: true,
target,
detail: {
newElement
}
})
return !event.defaultPrevented
}
return false
}
}

function beforeAttributeUpdated(attributeName, target, mutationType) {
const event = dispatch("turbo:before-morph-attribute", {
cancelable: true,
target,
detail: {
attributeName,
mutationType
}
})
return !event.defaultPrevented
}

function afterNodeMorphed(target, newElement) {
if (newElement instanceof HTMLElement) {
dispatch("turbo:morph-element", {
target,
detail: {
newElement
}
})
}
}
5 changes: 5 additions & 0 deletions src/core/streams/stream_actions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { session } from "../"
import morph from "./actions/morph"

export const StreamActions = {
after() {
Expand Down Expand Up @@ -36,5 +37,9 @@ export const StreamActions = {

refresh() {
session.refresh(this.baseURI, this.requestId)
},

morph() {
morph(this)
}
}
6 changes: 2 additions & 4 deletions src/elements/frame_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class FrameElement extends HTMLElement {
loaded = Promise.resolve()

static get observedAttributes() {
return ["disabled", "complete", "loading", "src"]
return ["disabled", "loading", "src"]
}

constructor() {
Expand All @@ -48,11 +48,9 @@ export class FrameElement extends HTMLElement {
attributeChangedCallback(name) {
if (name == "loading") {
this.delegate.loadingStyleChanged()
} else if (name == "complete") {
this.delegate.completeChanged()
} else if (name == "src") {
this.delegate.sourceURLChanged()
} else {
} else if (name == "disabled") {
this.delegate.disabledChanged()
}
}
Expand Down
98 changes: 58 additions & 40 deletions src/observers/link_prefetch_observer.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import {
doesNotTargetIFrame,
dispatch,
getLocationForLink,
getMetaContent,
findClosestRecursively
} from "../util"

import { StreamMessage } from "../core/streams/stream_message"
import { FetchMethod, FetchRequest } from "../http/fetch_request"
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 +30,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 +68,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 @@ -85,6 +87,15 @@ export class LinkPrefetchObserver {
}
}

#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 All @@ -109,10 +120,6 @@ export class LinkPrefetchObserver {
if (turboFrameTarget && turboFrameTarget !== "_top") {
request.headers["Turbo-Frame"] = turboFrameTarget
}

if (link.hasAttribute("data-turbo-stream")) {
request.acceptResponseType(StreamMessage.contentType)
}
}

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

if (!href || href === "#" || link.getAttribute("data-turbo") === "false" || link.getAttribute("data-turbo-prefetch") === "false") {
return false
}
if (!href) return false

if (link.origin !== document.location.origin) {
return false
}
if (unfetchableLink(link)) return false
if (linkToTheSamePage(link)) return false
if (linkOptsOut(link)) return false
if (nonSafeLink(link)) return false
if (eventPrevented(link)) return false

if (!["http:", "https:"].includes(link.protocol)) {
return false
}
return true
}
}

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

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

if (targetsIframe(link)) {
return false
}
const linkOptsOut = (link) => {
if (link.getAttribute("data-turbo-prefetch") === "false") return true
if (link.getAttribute("data-turbo") === "false") return true

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

if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") {
return false
}
return false
}

return true
}
const nonSafeLink = (link) => {
const turboMethod = link.getAttribute("data-turbo-method")
if (turboMethod && turboMethod.toLowerCase() !== "get") return true

if (isUJS(link)) return true
if (link.hasAttribute("data-turbo-confirm")) return true
if (link.hasAttribute("data-turbo-stream")) return true

return false
}

const isUJS = (link) => {
return link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-confirm") || link.hasAttribute("data-method")
}

const targetsIframe = (link) => {
return !doesNotTargetIFrame(link)
const eventPrevented = (link) => {
const event = dispatch("turbo:before-prefetch", { target: link, cancelable: true })
return event.defaultPrevented
}
3 changes: 3 additions & 0 deletions src/tests/fixtures/frame_refresh_after_navigation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<turbo-frame id="refresh-after-navigation">
<h2 id="refresh-after-navigation-content">Frame has been navigated</h2>
</turbo-frame>
6 changes: 6 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,12 @@
>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/prefetched.html" id="anchor_with_turbo_stream" data-turbo-stream="true"
>Won't prefetch when hovering me</a>
<a href="/src/tests/fixtures/prefetched.html" id="anchor_with_turbo_confirm" data-turbo-confirm="Are you sure?"
>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
16 changes: 16 additions & 0 deletions src/tests/fixtures/morph_stream_action.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html id="html">
<head>
<meta charset="utf-8">
<title>Morph Stream Action</title>
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
<script src="/src/tests/fixtures/test.js"></script>
<meta name="turbo-refresh-method" content="replace">
</head>

<body>
<div id="message_1">
<div>Morph me</div>
</div>
</body>
</html>
Loading

0 comments on commit d22bcce

Please sign in to comment.