Skip to content

Commit

Permalink
Allow URL based content-type guessing
Browse files Browse the repository at this point in the history
Add `Session.isVisitable` method whitch is
equivalent of no longer accessible `URL.isHTML`.

This allows `Turbo.session.isVisitable` to be overriden by user and makes possible
navigation on URL's other than those with trailing slash or HTML extenion.
  • Loading branch information
f6p committed May 13, 2022
1 parent e2d5305 commit ebbb3f6
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 19 deletions.
5 changes: 3 additions & 2 deletions src/core/drive/navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { Action, isAction } from "../types"
import { FetchMethod } from "../../http/fetch_request"
import { FetchResponse } from "../../http/fetch_response"
import { FormSubmission } from "./form_submission"
import { expandURL, getAnchor, getRequestURL, Locatable, locationIsVisitable } from "../url"
import { expandURL, getAnchor, getRequestURL, Locatable } from "../url"
import { getAttribute } from "../../util"
import { Visit, VisitDelegate, VisitOptions } from "./visit"
import { PageSnapshot } from "./page_snapshot"

export type NavigatorDelegate = VisitDelegate & {
allowsVisitingLocationWithAction(location: URL, action?: Action): boolean
locationIsVisitable(location: URL, rootLocation: URL): boolean
visitProposedToLocation(location: URL, options: Partial<VisitOptions>): void
notifyApplicationAfterVisitingSamePageLocation(oldURL: URL, newURL: URL): void
}
Expand All @@ -24,7 +25,7 @@ export class Navigator {

proposeVisit(location: URL, options: Partial<VisitOptions> = {}) {
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
if (this.delegate.locationIsVisitable(location, this.view.snapshot.rootLocation)) {
this.delegate.visitProposedToLocation(location, options)
} else {
window.location.href = location.toString()
Expand Down
4 changes: 2 additions & 2 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { clearBusyState, getAttribute, parseHTMLDocument, markAsBusy } from "../
import { FormSubmission, FormSubmissionDelegate } from "../drive/form_submission"
import { Snapshot } from "../snapshot"
import { ViewDelegate } from "../view"
import { getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url"
import { getAction, expandURL, urlsAreEqual } from "../url"
import { FormInterceptor, FormInterceptorDelegate } from "./form_interceptor"
import { FrameView } from "./frame_view"
import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor"
Expand Down Expand Up @@ -326,7 +326,7 @@ export class FrameController
private formActionIsVisitable(form: HTMLFormElement, submitter?: HTMLElement) {
const action = getAction(form, submitter)

return locationIsVisitable(expandURL(action), this.rootLocation)
return session.locationIsVisitable(expandURL(action), this.rootLocation)
}

private shouldInterceptNavigation(element: Element, submitter?: HTMLElement) {
Expand Down
9 changes: 6 additions & 3 deletions src/core/frames/frame_redirector.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { FormInterceptor, FormInterceptorDelegate } from "./form_interceptor"
import { FrameElement } from "../../elements/frame_element"
import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor"
import { expandURL, getAction, locationIsVisitable } from "../url"
import { Session } from "../session"
import { expandURL, getAction } from "../url"

export class FrameRedirector implements LinkInterceptorDelegate, FormInterceptorDelegate {
readonly element: Element
readonly session: Session
readonly linkInterceptor: LinkInterceptor
readonly formInterceptor: FormInterceptor

constructor(element: Element) {
constructor(element: Element, session: Session) {
this.element = element
this.session = session
this.linkInterceptor = new LinkInterceptor(this, element)
this.formInterceptor = new FormInterceptor(this, element)
}
Expand Down Expand Up @@ -52,7 +55,7 @@ export class FrameRedirector implements LinkInterceptorDelegate, FormInterceptor
const meta = this.element.ownerDocument.querySelector<HTMLMetaElement>(`meta[name="turbo-root"]`)
const rootLocation = expandURL(meta?.content ?? "/")

return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation)
return this.shouldRedirect(form, submitter) && this.session.locationIsVisitable(action, rootLocation)
}

private shouldRedirect(element: Element, submitter?: HTMLElement) {
Expand Down
16 changes: 12 additions & 4 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FormSubmitObserver, FormSubmitObserverDelegate } from "../observers/for
import { FrameRedirector } from "./frames/frame_redirector"
import { History, HistoryDelegate } from "./drive/history"
import { LinkClickObserver, LinkClickObserverDelegate } from "../observers/link_click_observer"
import { getAction, expandURL, locationIsVisitable, Locatable } from "./url"
import { getAction, getExtension, expandURL, isPrefixedBy, Locatable } from "./url"
import { Navigator, NavigatorDelegate } from "./drive/navigator"
import { PageObserver, PageObserverDelegate } from "../observers/page_observer"
import { ScrollObserver } from "../observers/scroll_observer"
Expand Down Expand Up @@ -42,7 +42,7 @@ export class Session
readonly scrollObserver = new ScrollObserver(this)
readonly streamObserver = new StreamObserver(this)

readonly frameRedirector = new FrameRedirector(document.documentElement)
readonly frameRedirector = new FrameRedirector(document.documentElement, this)

drive = true
enabled = true
Expand Down Expand Up @@ -107,6 +107,14 @@ export class Session
this.view.clearSnapshotCache()
}

isVisitable(url: URL) {
return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/)
}

locationIsVisitable(location: URL, rootLocation: URL) {
return isPrefixedBy(location, rootLocation) && this.isVisitable(location)
}

setProgressBarDelay(delay: number) {
this.progressBarDelay = delay
}
Expand Down Expand Up @@ -149,7 +157,7 @@ export class Session
willFollowLinkToLocation(link: Element, location: URL) {
return (
this.elementDriveEnabled(link) &&
locationIsVisitable(location, this.snapshot.rootLocation) &&
this.locationIsVisitable(location, this.snapshot.rootLocation) &&
this.applicationAllowsFollowingLinkToLocation(link, location)
)
}
Expand Down Expand Up @@ -225,7 +233,7 @@ export class Session
return (
this.elementDriveEnabled(form) &&
(!submitter || this.formElementDriveEnabled(submitter)) &&
locationIsVisitable(expandURL(action), this.snapshot.rootLocation)
this.locationIsVisitable(expandURL(action), this.snapshot.rootLocation)
)
}

Expand Down
8 changes: 0 additions & 8 deletions src/core/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,11 @@ export function getExtension(url: URL) {
return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || ""
}

export function isHTML(url: URL) {
return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/)
}

export function isPrefixedBy(baseURL: URL, url: URL) {
const prefix = getPrefix(url)
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix)
}

export function locationIsVisitable(location: URL, rootLocation: URL) {
return isPrefixedBy(location, rootLocation) && isHTML(location)
}

export function getRequestURL(url: URL) {
const anchor = getAnchor(url)
return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href
Expand Down
16 changes: 16 additions & 0 deletions src/tests/fixtures/visitable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Visitable</title>
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
<script src="/src/tests/fixtures/test.js"></script>
</head>
<body>
<h1>Visitable</h1>

<div>
<a id="link" href="/src/tests/fixtures/visitable.html">link</a>
</div>
</body>
</html>
1 change: 1 addition & 0 deletions src/tests/functional/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from "./rendering_tests"
export * from "./scroll_restoration_tests"
export * from "./stream_tests"
export * from "./visit_tests"
export * from "./visitable_tests"
33 changes: 33 additions & 0 deletions src/tests/functional/visitable_tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case"

export class VisitableTests extends TurboDriveTestCase {
path = "/src/tests/fixtures/visitable.html"

async setup() {
await this.goToLocation(this.path)
}

async "test user-defined visitable URL"() {
await this.remote.execute(() => {
window.Turbo.session.isVisitable = url => true
})

this.clickSelector("#link")
await this.nextBody
this.assert.equal(await this.pathname, this.path)
this.assert.equal(await this.visitAction, "advance")
}

async "test user-defined unvisitable URL"() {
await this.remote.execute(() => {
window.Turbo.session.isVisitable = url => false
})

this.clickSelector("#link")
await this.nextBody
this.assert.equal(await this.pathname, this.path)
this.assert.equal(await this.visitAction, "load")
}
}

VisitableTests.registerSuite()

0 comments on commit ebbb3f6

Please sign in to comment.