diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index d94c5c005..5f6004ad0 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -23,7 +23,7 @@ import { ViewDelegate, ViewRenderOptions } from "../view" import { Locatable, getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url" import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer" import { FrameView } from "./frame_view" -import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer" +import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor" import { FormLinkClickObserver, FormLinkClickObserverDelegate } from "../../observers/form_link_click_observer" import { FrameRenderer } from "./frame_renderer" import { TurboClickEvent, session } from "../index" @@ -43,14 +43,14 @@ export class FrameController FormSubmissionDelegate, FrameElementDelegate, FormLinkClickObserverDelegate, - LinkClickObserverDelegate, + LinkInterceptorDelegate, ViewDelegate> { readonly element: FrameElement readonly view: FrameView readonly appearanceObserver: AppearanceObserver readonly formLinkClickObserver: FormLinkClickObserver - readonly linkClickObserver: LinkClickObserver + readonly linkInterceptor: LinkInterceptor readonly formSubmitObserver: FormSubmitObserver formSubmission?: FormSubmission fetchResponseLoaded = (_fetchResponse: FetchResponse) => {} @@ -70,7 +70,7 @@ export class FrameController this.view = new FrameView(this, this.element) this.appearanceObserver = new AppearanceObserver(this, this.element) this.formLinkClickObserver = new FormLinkClickObserver(this, this.element) - this.linkClickObserver = new LinkClickObserver(this, this.element) + this.linkInterceptor = new LinkInterceptor(this, this.element) this.restorationIdentifier = uuid() this.formSubmitObserver = new FormSubmitObserver(this, this.element) } @@ -84,7 +84,7 @@ export class FrameController this.loadSourceURL() } this.formLinkClickObserver.start() - this.linkClickObserver.start() + this.linkInterceptor.start() this.formSubmitObserver.start() } } @@ -94,7 +94,7 @@ export class FrameController this.connected = false this.appearanceObserver.stop() this.formLinkClickObserver.stop() - this.linkClickObserver.stop() + this.linkInterceptor.stop() this.formSubmitObserver.stop() } } @@ -204,7 +204,7 @@ export class FrameController // Form link click observer delegate willSubmitFormLinkToLocation(link: Element): boolean { - return link.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(link) + return this.shouldInterceptNavigation(link) } submittedFormLinkToLocation(link: Element, _location: URL, form: HTMLFormElement): void { @@ -212,14 +212,14 @@ export class FrameController if (frame) form.setAttribute("data-turbo-frame", frame.id) } - // Link click observer delegate + // Link interceptor delegate - willFollowLinkToLocation(element: Element, location: URL, event: MouseEvent) { - return this.shouldInterceptNavigation(element) && this.frameAllowsVisitingLocation(element, location, event) + shouldInterceptLinkClick(element: Element, url: string, originalEvent: MouseEvent) { + return this.shouldInterceptNavigation(element) && this.frameAllowsVisitingLocation(element, url, originalEvent) } - followedLinkToLocation(element: Element, location: URL) { - this.navigateFrame(element, location.href) + linkClickIntercepted(element: Element, url: string) { + this.navigateFrame(element, url) } // Form submit observer delegate @@ -555,7 +555,7 @@ export class FrameController return expandURL(root) } - private frameAllowsVisitingLocation(target: Element, { href: url }: URL, originalEvent: MouseEvent): boolean { + private frameAllowsVisitingLocation(target: Element, url: string, originalEvent: MouseEvent): boolean { const event = dispatch("turbo:click", { target, detail: { url, originalEvent }, diff --git a/src/core/frames/frame_redirector.ts b/src/core/frames/frame_redirector.ts index 495aa59f4..f1e271aca 100644 --- a/src/core/frames/frame_redirector.ts +++ b/src/core/frames/frame_redirector.ts @@ -1,41 +1,39 @@ import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer" import { FrameElement } from "../../elements/frame_element" +import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor" import { expandURL, getAction, locationIsVisitable } from "../url" -import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer" -import { Session, TurboClickEvent } from "../session" +import { TurboClickEvent } from "../session" import { dispatch } from "../../util" -export class FrameRedirector implements LinkClickObserverDelegate, FormSubmitObserverDelegate { - readonly session: Session +export class FrameRedirector implements LinkInterceptorDelegate, FormSubmitObserverDelegate { readonly element: Element - readonly linkClickObserver: LinkClickObserver + readonly linkInterceptor: LinkInterceptor readonly formSubmitObserver: FormSubmitObserver - constructor(session: Session, element: Element) { - this.session = session + constructor(element: Element) { this.element = element - this.linkClickObserver = new LinkClickObserver(this, element) + this.linkInterceptor = new LinkInterceptor(this, element) this.formSubmitObserver = new FormSubmitObserver(this, element) } start() { - this.linkClickObserver.start() + this.linkInterceptor.start() this.formSubmitObserver.start() } stop() { - this.linkClickObserver.stop() + this.linkInterceptor.stop() this.formSubmitObserver.stop() } - willFollowLinkToLocation(element: Element, location: URL, event: MouseEvent) { - return this.shouldRedirect(element) && this.frameAllowsVisitingLocation(element, location, event) + shouldInterceptLinkClick(element: Element, url: string, originalEvent: MouseEvent) { + return this.shouldRedirect(element) && this.frameAllowsVisitingLocation(element, url, originalEvent) } - followedLinkToLocation(element: Element, url: URL) { + linkClickIntercepted(element: Element, url: string, originalEvent: MouseEvent) { const frame = this.findFrameElement(element) if (frame) { - frame.delegate.followedLinkToLocation(element, url) + frame.delegate.linkClickIntercepted(element, url, originalEvent) } } @@ -54,7 +52,7 @@ export class FrameRedirector implements LinkClickObserverDelegate, FormSubmitObs } } - private frameAllowsVisitingLocation(target: Element, { href: url }: URL, originalEvent: MouseEvent): boolean { + private frameAllowsVisitingLocation(target: Element, url: string, originalEvent: MouseEvent): boolean { const event = dispatch("turbo:click", { target, detail: { url, originalEvent }, @@ -73,17 +71,8 @@ export class FrameRedirector implements LinkClickObserverDelegate, FormSubmitObs } private shouldRedirect(element: Element, submitter?: HTMLElement) { - const isNavigatable = - element instanceof HTMLFormElement - ? this.session.submissionIsNavigatable(element, submitter) - : this.session.elementIsNavigatable(element) - - if (isNavigatable) { - const frame = this.findFrameElement(element, submitter) - return frame ? frame != element.closest("turbo-frame") : false - } else { - return false - } + const frame = this.findFrameElement(element, submitter) + return frame ? frame != element.closest("turbo-frame") : false } private findFrameElement(element: Element, submitter?: HTMLElement) { diff --git a/src/core/frames/link_interceptor.ts b/src/core/frames/link_interceptor.ts new file mode 100644 index 000000000..8f2e13f40 --- /dev/null +++ b/src/core/frames/link_interceptor.ts @@ -0,0 +1,57 @@ +import { TurboClickEvent, TurboBeforeVisitEvent } from "../session" + +export interface LinkInterceptorDelegate { + shouldInterceptLinkClick(element: Element, url: string, originalEvent: MouseEvent): boolean + linkClickIntercepted(element: Element, url: string, originalEvent: MouseEvent): void +} + +export class LinkInterceptor { + readonly delegate: LinkInterceptorDelegate + readonly element: Element + private clickEvent?: Event + + constructor(delegate: LinkInterceptorDelegate, element: Element) { + this.delegate = delegate + this.element = element + } + + start() { + this.element.addEventListener("click", this.clickBubbled) + document.addEventListener("turbo:click", this.linkClicked) + document.addEventListener("turbo:before-visit", this.willVisit) + } + + stop() { + this.element.removeEventListener("click", this.clickBubbled) + document.removeEventListener("turbo:click", this.linkClicked) + document.removeEventListener("turbo:before-visit", this.willVisit) + } + + clickBubbled = (event: Event) => { + if (this.respondsToEventTarget(event.target)) { + this.clickEvent = event + } else { + delete this.clickEvent + } + } + + linkClicked = ((event: TurboClickEvent) => { + if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) { + if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) { + this.clickEvent.preventDefault() + event.preventDefault() + this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent) + } + } + delete this.clickEvent + }) + + willVisit = ((_event: TurboBeforeVisitEvent) => { + delete this.clickEvent + }) + + respondsToEventTarget(target: EventTarget | null) { + const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null + return element && element.closest("turbo-frame, html") == this.element + } +} diff --git a/src/core/session.ts b/src/core/session.ts index 2d1cb3685..69facef38 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -60,7 +60,7 @@ export class Session readonly scrollObserver = new ScrollObserver(this) readonly streamObserver = new StreamObserver(this) readonly formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement) - readonly frameRedirector = new FrameRedirector(this, document.documentElement) + readonly frameRedirector = new FrameRedirector(document.documentElement) readonly streamMessageRenderer = new StreamMessageRenderer() drive = true diff --git a/src/elements/frame_element.ts b/src/elements/frame_element.ts index 82a4b113a..74367682f 100644 --- a/src/elements/frame_element.ts +++ b/src/elements/frame_element.ts @@ -1,6 +1,6 @@ import { FetchResponse } from "../http/fetch_response" import { Snapshot } from "../core/snapshot" -import { LinkClickObserverDelegate } from "../observers/link_click_observer" +import { LinkInterceptorDelegate } from "../core/frames/link_interceptor" import { FormSubmitObserverDelegate } from "../observers/form_submit_observer" export enum FrameLoadingStyle { @@ -10,7 +10,7 @@ export enum FrameLoadingStyle { export type FrameElementObservedAttribute = keyof FrameElement & ("disabled" | "complete" | "loading" | "src") -export interface FrameElementDelegate extends LinkClickObserverDelegate, FormSubmitObserverDelegate { +export interface FrameElementDelegate extends LinkInterceptorDelegate, FormSubmitObserverDelegate { connect(): void disconnect(): void completeChanged(): void diff --git a/src/observers/form_link_click_observer.ts b/src/observers/form_link_click_observer.ts index 4674b51ab..6f177b8c8 100644 --- a/src/observers/form_link_click_observer.ts +++ b/src/observers/form_link_click_observer.ts @@ -6,20 +6,20 @@ export type FormLinkClickObserverDelegate = { } export class FormLinkClickObserver implements LinkClickObserverDelegate { - readonly linkClickObserver: LinkClickObserver + readonly linkInterceptor: LinkClickObserver readonly delegate: FormLinkClickObserverDelegate constructor(delegate: FormLinkClickObserverDelegate, element: HTMLElement) { this.delegate = delegate - this.linkClickObserver = new LinkClickObserver(this, element) + this.linkInterceptor = new LinkClickObserver(this, element) } start() { - this.linkClickObserver.start() + this.linkInterceptor.start() } stop() { - this.linkClickObserver.stop() + this.linkInterceptor.stop() } willFollowLinkToLocation(link: Element, location: URL, originalEvent: MouseEvent): boolean {