Skip to content

Commit

Permalink
Ensure Turbo does not interfere with IFrames
Browse files Browse the repository at this point in the history
Skip intercepting `<a>` element clicks or `<form>` element submissions
when they target an `<iframe>`. This occurs when an `<a>` declares a
[target][a-target], when a `<form>` declares a [target][form-target], or
a `<form>` element's submitting `<button>` element declares a
[formtarget][formtarget].

[a-target]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target
[form-target]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-target
[formtarget]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formtarget
  • Loading branch information
seanpdoyle committed Nov 13, 2021
1 parent 6b6bdb2 commit 391a557
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 9 deletions.
31 changes: 24 additions & 7 deletions src/observers/form_submit_observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,31 @@ export class FormSubmitObserver {
const form = event.target instanceof HTMLFormElement ? event.target : undefined
const submitter = event.submitter || undefined

if (form) {
const method = submitter?.getAttribute("formmethod") || form.getAttribute("method")

if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
event.preventDefault()
this.delegate.formSubmitted(form, submitter)
}
if (form
&& submissionDoesNotDismissDialog(form, submitter)
&& submissionDoesNotTargetIFrame(form, submitter)
&& this.delegate.willSubmitForm(form, submitter)) {
event.preventDefault()
this.delegate.formSubmitted(form, submitter)
}
}
})
}

function submissionDoesNotDismissDialog(form: HTMLFormElement, submitter?: HTMLElement): boolean {
const method = submitter?.getAttribute("formmethod") || form.getAttribute("method")

return method != "dialog"
}

function submissionDoesNotTargetIFrame(form: HTMLFormElement, submitter?: HTMLElement): boolean {
const target = submitter?.getAttribute("formtarget") || form.target

if (target) {
const iframeExists = !!document.querySelector(`iframe[name="${target}"]`)

return !iframeExists
} else {
return true
}
}
12 changes: 10 additions & 2 deletions src/observers/link_click_observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class LinkClickObserver {
if (this.clickEventIsSignificant(event)) {
const target = (event.composedPath && event.composedPath()[0]) || event.target
const link = this.findLinkFromClickTarget(target)
if (link) {
if (link && doesNotTargetIFrame(link)) {
const location = this.getLocationForLink(link)
if (this.delegate.willFollowLinkToLocation(link, location)) {
event.preventDefault()
Expand All @@ -60,11 +60,19 @@ export class LinkClickObserver {

findLinkFromClickTarget(target: EventTarget | null) {
if (target instanceof Element) {
return target.closest("a[href]:not([target^=_]):not([download])")
return target.closest<HTMLAnchorElement>("a[href]:not([target^=_]):not([download])")
}
}

getLocationForLink(link: Element): URL {
return expandURL(link.getAttribute("href") || "")
}
}

function doesNotTargetIFrame(anchor: HTMLAnchorElement): boolean {
return !targetsIFrame(anchor)
}

function targetsIFrame(anchor: HTMLAnchorElement): boolean {
return !!document.querySelector(`iframe[name="${anchor.target}"]`)
}
11 changes: 11 additions & 0 deletions src/tests/fixtures/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ <h1>Form</h1>
</form>
</dialog>

<hr>
<dialog id="dialog-method-turbo-frame" open>
<form action="/__turbo/redirect" method="dialog">
<input type="hidden" name="path" value="/src/tests/fixtures/frames/frame.html">
Expand All @@ -166,6 +167,15 @@ <h1>Form</h1>
<button formmethod="dialog">Close</button>
</form>
</dialog>

<hr>
<form action="/src/tests/fixtures/one.html" method="get" target="iframe">
<button>Submit iframe target</button>
</form>

<form action="/src/tests/fixtures/one.html" method="get">
<button formtarget="iframe">Submit iframe formtarget</button>
</form>
</div>
<hr>
<div id="targets-frame">
Expand Down Expand Up @@ -282,5 +292,6 @@ <h2>Frame: Form</h2>
<form method="post" action="https://httpbin.org/post" data-turbo-frame="ignored">
<button id="submit-external-target-ignored">POST to https://httpbin.org/post targeting #hello</button>
</form>
<iframe name="iframe"></iframe>
</body>
</html>
3 changes: 3 additions & 0 deletions src/tests/fixtures/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@ <h1>Navigation</h1>
<p><a id="headers-link" href="/__turbo/headers">Headers link</a></p>
<p><custom-link-element id="custom-link-element" link="/src/tests/fixtures/one.html" text="Same-origin unannotated custom element link"></custom-link-element></p>
<p><a id="delayed-link" href="/__turbo/delayed_response">Delayed link</a></p>
<p><a id="targets-iframe" href="/src/tests/fixtures/one.html" target="iframe">Targets iframe</a></p>
</section>

<turbo-frame id="hello" disabled></turbo-frame>

<iframe name="iframe"></iframe>
</body>
</html>
16 changes: 16 additions & 0 deletions src/tests/functional/form_submission_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,22 @@ export class FormSubmissionTests extends TurboDriveTestCase {
this.assert.equal(await this.location, "https://httpbin.org/post")
}

async "test form submission skipped with form[target]"() {
await this.clickSelector("#skipped form[target] button")
await this.nextBeat

this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html")
this.assert.notOk(await this.formSubmitEnded)
}

async "test form submission skipped with submitter button[formtarget]"() {
await this.clickSelector("#skipped [formtarget]")
await this.nextBeat

this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html")
this.assert.notOk(await this.formSubmitEnded)
}

get formSubmitStarted(): Promise<boolean> {
return this.hasSelector("html[data-form-submit-start]")
}
Expand Down
7 changes: 7 additions & 0 deletions src/tests/functional/navigation_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ export class NavigationTests extends TurboDriveTestCase {
this.assert.equal(await this.pathname, "/__turbo/delayed_response")
this.assert.equal(await this.visitAction, "advance")
}

async "test ignores links that target an iframe"() {
await this.clickSelector("#targets-iframe")
await this.nextBeat

this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html")
}
}

NavigationTests.registerSuite()

0 comments on commit 391a557

Please sign in to comment.