diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index ff5909ef3..000000000 --- a/.eslintrc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "prettier" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "rules": { - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }], - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], - "no-unused-vars": "off", - "prettier/prettier": ["error"] - } -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..ba8803d0b --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,42 @@ +module.exports = { + env: { + browser: true, + es2021: true + }, + extends: ["eslint:recommended"], + overrides: [ + { + env: { + node: true + }, + files: [".eslintrc.{js,cjs}"], + parserOptions: { + sourceType: "script" + } + } + ], + parserOptions: { + ecmaVersion: "latest", + sourceType: "module" + }, + rules: { + "comma-dangle": "error", + "curly": ["error", "multi-line"], + "getter-return": "off", + "no-console": "off", + "no-duplicate-imports": ["error"], + "no-multi-spaces": ["error", { "exceptions": { "VariableDeclarator": true }}], + "no-multiple-empty-lines": ["error", { "max": 2 }], + "no-self-assign": ["error", { "props": false }], + "no-trailing-spaces": ["error"], + "no-unused-vars": ["error", { argsIgnorePattern: "_*" }], + "no-useless-escape": "off", + "no-var": ["error"], + "prefer-const": ["error"], + "semi": ["error", "never"] + }, + globals: { + test: true, + setup: true + } +} diff --git a/package.json b/package.json index 8ed7e21a3..f7a9c0bac 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,7 @@ "@playwright/test": "^1.28.0", "@rollup/plugin-node-resolve": "13.1.3", "@rollup/plugin-typescript": "^11.0.0", - "@types/multer": "^1.4.5", "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", "@web/dev-server-esbuild": "^0.3.3", "@web/test-runner": "^0.15.0", "@web/test-runner-playwright": "^0.9.0", @@ -49,29 +47,23 @@ "body-parser": "^1.20.1", "chai": "~4.3.4", "eslint": "^8.13.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.0.0", "express": "^4.18.2", "multer": "^1.4.2", - "prettier": "2.6.2", - "rollup": "^2.35.1", - "ts-node": "^10.9.1", - "tslib": "^2.5.0", - "typescript": "^4.9.5" + "rollup": "^2.35.1" }, "scripts": { "clean": "rm -fr dist", "clean:win": "rmdir /s /q dist", - "build": "tsc --noEmit false --declaration true --emitDeclarationOnly true --outDir dist/types && rollup -c", - "build:win": "tsc --noEmit false --declaration true --emitDeclarationOnly true --outDir dist/types & rollup -c", + "build": "rollup -c", + "build:win": "rollup -c", "watch": "rollup -wc", - "start": "ts-node -O '{\"module\":\"commonjs\"}' src/tests/server.ts", + "start": "node src/tests/server.mjs", "test": "yarn test:unit && yarn test:browser", "test:browser": "playwright test", "test:unit": "NODE_OPTIONS=--inspect web-test-runner", "test:unit:win": "SET NODE_OPTIONS=--inspect & web-test-runner", "release": "yarn build && npm publish", - "lint": "eslint . --ext .ts" + "lint": "eslint . --ext .js" }, "engines": { "node": ">= 14" diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 000000000..a4ee43023 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,29 @@ +import { devices } from "@playwright/test" + +const config = { + projects: [ + { + name: "chrome", + use: { ...devices["Desktop Chrome"] } + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] } + } + ], + retries: 2, + testDir: "./src/tests/", + testMatch: /(functional|integration)\/.*_tests\.js/, + webServer: { + command: "yarn start", + url: "http://localhost:9000/src/tests/fixtures/test.js", + timeout: 120 * 1000, + // eslint-disable-next-line no-undef + reuseExistingServer: !process.env.CI + }, + use: { + baseURL: "http://localhost:9000/" + } +} + +export default config diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index bef521f7c..000000000 --- a/playwright.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { type PlaywrightTestConfig, devices } from "@playwright/test" - -const config: PlaywrightTestConfig = { - projects: [ - { - name: "chrome", - use: { ...devices["Desktop Chrome"] }, - }, - { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, - }, - ], - retries: 2, - testDir: "./src/tests/", - testMatch: /(functional|integration)\/.*_tests\.ts/, - webServer: { - command: "yarn start", - url: "http://localhost:9000/src/tests/fixtures/test.js", - timeout: 120 * 1000, - reuseExistingServer: !process.env.CI, - }, - use: { - baseURL: "http://localhost:9000/", - }, -} - -export default config diff --git a/rollup.config.js b/rollup.config.js index 5073079dd..395c4d497 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,5 +1,4 @@ import resolve from "@rollup/plugin-node-resolve" -import typescript from "@rollup/plugin-typescript" import { version } from "./package.json" const year = new Date().getFullYear() @@ -7,7 +6,7 @@ const banner = `/*!\nTurbo ${version}\nCopyright © ${year} 37signals LLC\n */` export default [ { - input: "src/index.ts", + input: "src/index.js", output: [ { name: "Turbo", @@ -21,10 +20,7 @@ export default [ banner } ], - plugins: [ - resolve(), - typescript() - ], + plugins: [resolve()], watch: { include: "src/**" } diff --git a/src/core/bardo.ts b/src/core/bardo.js similarity index 59% rename from src/core/bardo.ts rename to src/core/bardo.js index bd6b191f0..01355fb03 100644 --- a/src/core/bardo.ts +++ b/src/core/bardo.js @@ -1,26 +1,12 @@ -import { PermanentElementMap } from "./snapshot" - -export interface BardoDelegate { - enteringBardo(currentPermanentElement: Element, newPermanentElement: Element): void - leavingBardo(currentPermanentElement: Element): void -} - export class Bardo { - readonly permanentElementMap: PermanentElementMap - readonly delegate: BardoDelegate - - static async preservingPermanentElements( - delegate: BardoDelegate, - permanentElementMap: PermanentElementMap, - callback: () => void - ) { + static async preservingPermanentElements(delegate, permanentElementMap, callback) { const bardo = new this(delegate, permanentElementMap) bardo.enter() await callback() bardo.leave() } - constructor(delegate: BardoDelegate, permanentElementMap: PermanentElementMap) { + constructor(delegate, permanentElementMap) { this.delegate = delegate this.permanentElementMap = permanentElementMap } @@ -42,31 +28,31 @@ export class Bardo { } } - replaceNewPermanentElementWithPlaceholder(permanentElement: Element) { + replaceNewPermanentElementWithPlaceholder(permanentElement) { const placeholder = createPlaceholderForPermanentElement(permanentElement) permanentElement.replaceWith(placeholder) } - replaceCurrentPermanentElementWithClone(permanentElement: Element) { + replaceCurrentPermanentElementWithClone(permanentElement) { const clone = permanentElement.cloneNode(true) permanentElement.replaceWith(clone) } - replacePlaceholderWithPermanentElement(permanentElement: Element) { + replacePlaceholderWithPermanentElement(permanentElement) { const placeholder = this.getPlaceholderById(permanentElement.id) placeholder?.replaceWith(permanentElement) } - getPlaceholderById(id: string) { + getPlaceholderById(id) { return this.placeholders.find((element) => element.content == id) } - get placeholders(): HTMLMetaElement[] { - return [...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]")] + get placeholders() { + return [...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]")] } } -function createPlaceholderForPermanentElement(permanentElement: Element) { +function createPlaceholderForPermanentElement(permanentElement) { const element = document.createElement("meta") element.setAttribute("name", "turbo-permanent-placeholder") element.setAttribute("content", permanentElement.id) diff --git a/src/core/cache.ts b/src/core/cache.js similarity index 54% rename from src/core/cache.ts rename to src/core/cache.js index 715b7d098..2c163e6f4 100644 --- a/src/core/cache.ts +++ b/src/core/cache.js @@ -1,10 +1,7 @@ -import { Session } from "./session" import { setMetaContent } from "../util" export class Cache { - readonly session: Session - - constructor(session: Session) { + constructor(session) { this.session = session } @@ -13,18 +10,18 @@ export class Cache { } resetCacheControl() { - this.setCacheControl("") + this.#setCacheControl("") } exemptPageFromCache() { - this.setCacheControl("no-cache") + this.#setCacheControl("no-cache") } exemptPageFromPreview() { - this.setCacheControl("no-preview") + this.#setCacheControl("no-preview") } - private setCacheControl(value: string) { + #setCacheControl(value) { setMetaContent("turbo-cache-control", value) } } diff --git a/src/core/drive/error_renderer.ts b/src/core/drive/error_renderer.js similarity index 81% rename from src/core/drive/error_renderer.ts rename to src/core/drive/error_renderer.js index 22deb43e7..0e5f2ce39 100644 --- a/src/core/drive/error_renderer.ts +++ b/src/core/drive/error_renderer.js @@ -1,9 +1,8 @@ -import { PageSnapshot } from "./page_snapshot" -import { Renderer } from "../renderer" import { activateScriptElement } from "../../util" +import { Renderer } from "../renderer" -export class ErrorRenderer extends Renderer { - static renderElement(currentElement: HTMLBodyElement, newElement: HTMLBodyElement) { +export class ErrorRenderer extends Renderer { + static renderElement(currentElement, newElement) { const { documentElement, body } = document documentElement.replaceChild(newElement, body) diff --git a/src/core/drive/form_submission.ts b/src/core/drive/form_submission.js similarity index 67% rename from src/core/drive/form_submission.ts rename to src/core/drive/form_submission.js index efb75bc01..9b72131d4 100644 --- a/src/core/drive/form_submission.ts +++ b/src/core/drive/form_submission.js @@ -1,40 +1,24 @@ import { FetchRequest, FetchMethod, fetchMethodFromString } from "../../http/fetch_request" -import { FetchResponse } from "../../http/fetch_response" import { expandURL } from "../url" import { dispatch, getAttribute, getMetaContent, hasAttribute } from "../../util" import { StreamMessage } from "../streams/stream_message" -export interface FormSubmissionDelegate { - formSubmissionStarted(formSubmission: FormSubmission): void - formSubmissionSucceededWithResponse(formSubmission: FormSubmission, fetchResponse: FetchResponse): void - formSubmissionFailedWithResponse(formSubmission: FormSubmission, fetchResponse: FetchResponse): void - formSubmissionErrored(formSubmission: FormSubmission, error: Error): void - formSubmissionFinished(formSubmission: FormSubmission): void +export const FormSubmissionState = { + initialized: "initialized", + requesting: "requesting", + waiting: "waiting", + receiving: "receiving", + stopping: "stopping", + stopped: "stopped" } -export type FormSubmissionResult = { success: boolean; fetchResponse: FetchResponse } | { success: false; error: Error } - -export enum FormSubmissionState { - initialized, - requesting, - waiting, - receiving, - stopping, - stopped, -} - -enum FormEnctype { - urlEncoded = "application/x-www-form-urlencoded", - multipart = "multipart/form-data", - plain = "text/plain", +export const FormEnctype = { + urlEncoded: "application/x-www-form-urlencoded", + multipart: "multipart/form-data", + plain: "text/plain" } -export type TurboSubmitStartEvent = CustomEvent<{ formSubmission: FormSubmission }> -export type TurboSubmitEndEvent = CustomEvent< - { formSubmission: FormSubmission } & { [K in keyof FormSubmissionResult]?: FormSubmissionResult[K] } -> - -function formEnctypeFromString(encoding: string): FormEnctype { +function formEnctypeFromString(encoding) { switch (encoding.toLowerCase()) { case FormEnctype.multipart: return FormEnctype.multipart @@ -46,31 +30,13 @@ function formEnctypeFromString(encoding: string): FormEnctype { } export class FormSubmission { - readonly delegate: FormSubmissionDelegate - readonly formElement: HTMLFormElement - readonly submitter?: HTMLElement - readonly formData: FormData - readonly location: URL - readonly fetchRequest: FetchRequest - readonly mustRedirect: boolean state = FormSubmissionState.initialized - result?: FormSubmissionResult - originalSubmitText?: string - - static confirmMethod( - message: string, - _element: HTMLFormElement, - _submitter: HTMLElement | undefined - ): Promise { + + static confirmMethod(message, _element, _submitter) { return Promise.resolve(confirm(message)) } - constructor( - delegate: FormSubmissionDelegate, - formElement: HTMLFormElement, - submitter?: HTMLElement, - mustRedirect = false - ) { + constructor(delegate, formElement, submitter, mustRedirect = false) { this.delegate = delegate this.formElement = formElement this.submitter = submitter @@ -83,12 +49,12 @@ export class FormSubmission { this.mustRedirect = mustRedirect } - get method(): FetchMethod { + get method() { const method = this.submitter?.getAttribute("formmethod") || this.formElement.getAttribute("method") || "" return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get } - get action(): string { + get action() { const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null if (this.submitter?.hasAttribute("formaction")) { @@ -106,7 +72,7 @@ export class FormSubmission { } } - get enctype(): FormEnctype { + get enctype() { return formEnctypeFromString(this.submitter?.getAttribute("formenctype") || this.formElement.enctype) } @@ -117,7 +83,7 @@ export class FormSubmission { get stringFormData() { return [...this.formData].reduce((entries, [name, value]) => { return entries.concat(typeof value == "string" ? [[name, value]] : []) - }, [] as [string, string][]) + }, []) } // The submission process @@ -150,7 +116,7 @@ export class FormSubmission { // Fetch request delegate - prepareRequest(request: FetchRequest) { + prepareRequest(request) { if (!request.isSafe) { const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token") if (token) { @@ -163,22 +129,22 @@ export class FormSubmission { } } - requestStarted(_request: FetchRequest) { + requestStarted(_request) { this.state = FormSubmissionState.waiting this.submitter?.setAttribute("disabled", "") this.setSubmitsWith() - dispatch("turbo:submit-start", { + dispatch("turbo:submit-start", { target: this.formElement, - detail: { formSubmission: this }, + detail: { formSubmission: this } }) this.delegate.formSubmissionStarted(this) } - requestPreventedHandlingResponse(request: FetchRequest, response: FetchResponse) { + requestPreventedHandlingResponse(request, response) { this.result = { success: response.succeeded, fetchResponse: response } } - requestSucceededWithResponse(request: FetchRequest, response: FetchResponse) { + requestSucceededWithResponse(request, response) { if (response.clientError || response.serverError) { this.delegate.formSubmissionFailedWithResponse(this, response) } else if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) { @@ -191,23 +157,23 @@ export class FormSubmission { } } - requestFailedWithResponse(request: FetchRequest, response: FetchResponse) { + requestFailedWithResponse(request, response) { this.result = { success: false, fetchResponse: response } this.delegate.formSubmissionFailedWithResponse(this, response) } - requestErrored(request: FetchRequest, error: Error) { + requestErrored(request, error) { this.result = { success: false, error } this.delegate.formSubmissionErrored(this, error) } - requestFinished(_request: FetchRequest) { + requestFinished(_request) { this.state = FormSubmissionState.stopped this.submitter?.removeAttribute("disabled") this.resetSubmitterText() - dispatch("turbo:submit-end", { + dispatch("turbo:submit-end", { target: this.formElement, - detail: { formSubmission: this, ...this.result }, + detail: { formSubmission: this, ...this.result } }) this.delegate.formSubmissionFinished(this) } @@ -221,7 +187,7 @@ export class FormSubmission { this.originalSubmitText = this.submitter.innerHTML this.submitter.innerHTML = this.submitsWith } else if (this.submitter.matches("input")) { - const input = this.submitter as HTMLInputElement + const input = this.submitter this.originalSubmitText = input.value input.value = this.submitsWith } @@ -233,16 +199,16 @@ export class FormSubmission { if (this.submitter.matches("button")) { this.submitter.innerHTML = this.originalSubmitText } else if (this.submitter.matches("input")) { - const input = this.submitter as HTMLInputElement + const input = this.submitter input.value = this.originalSubmitText } } - requestMustRedirect(request: FetchRequest) { + requestMustRedirect(request) { return !request.isSafe && this.mustRedirect } - requestAcceptsTurboStreamResponse(request: FetchRequest) { + requestAcceptsTurboStreamResponse(request) { return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement) } @@ -251,7 +217,7 @@ export class FormSubmission { } } -function buildFormData(formElement: HTMLFormElement, submitter?: HTMLElement): FormData { +function buildFormData(formElement, submitter) { const formData = new FormData(formElement) const name = submitter?.getAttribute("name") const value = submitter?.getAttribute("value") @@ -263,7 +229,7 @@ function buildFormData(formElement: HTMLFormElement, submitter?: HTMLElement): F return formData } -function getCookieValue(cookieName: string | null) { +function getCookieValue(cookieName) { if (cookieName != null) { const cookies = document.cookie ? document.cookie.split("; ") : [] const cookie = cookies.find((cookie) => cookie.startsWith(cookieName)) @@ -274,11 +240,11 @@ function getCookieValue(cookieName: string | null) { } } -function responseSucceededWithoutRedirect(response: FetchResponse) { +function responseSucceededWithoutRedirect(response) { return response.statusCode == 200 && !response.redirected } -function mergeFormDataEntries(url: URL, entries: [string, FormDataEntryValue][]): URL { +function mergeFormDataEntries(url, entries) { const searchParams = new URLSearchParams() for (const [name, value] of entries) { diff --git a/src/core/drive/head_snapshot.ts b/src/core/drive/head_snapshot.js similarity index 59% rename from src/core/drive/head_snapshot.ts rename to src/core/drive/head_snapshot.js index 3892332c1..a015bbc28 100644 --- a/src/core/drive/head_snapshot.ts +++ b/src/core/drive/head_snapshot.js @@ -1,61 +1,51 @@ import { Snapshot } from "../snapshot" -type ElementDetailMap = { [outerHTML: string]: ElementDetails } - -type ElementDetails = { - type?: ElementType - tracked: boolean - elements: Element[] -} - -type ElementType = "script" | "stylesheet" - -export class HeadSnapshot extends Snapshot { - readonly detailsByOuterHTML = this.children +export class HeadSnapshot extends Snapshot { + detailsByOuterHTML = this.children .filter((element) => !elementIsNoscript(element)) .map((element) => elementWithoutNonce(element)) .reduce((result, element) => { const { outerHTML } = element - const details: ElementDetails = + const details = outerHTML in result ? result[outerHTML] : { type: elementType(element), tracked: elementIsTracked(element), - elements: [], + elements: [] } return { ...result, [outerHTML]: { ...details, - elements: [...details.elements, element], - }, + elements: [...details.elements, element] + } } - }, {} as ElementDetailMap) + }, {}) - get trackedElementSignature(): string { + get trackedElementSignature() { return Object.keys(this.detailsByOuterHTML) .filter((outerHTML) => this.detailsByOuterHTML[outerHTML].tracked) .join("") } - getScriptElementsNotInSnapshot(snapshot: HeadSnapshot) { - return this.getElementsMatchingTypeNotInSnapshot("script", snapshot) + getScriptElementsNotInSnapshot(snapshot) { + return this.getElementsMatchingTypeNotInSnapshot("script", snapshot) } - getStylesheetElementsNotInSnapshot(snapshot: HeadSnapshot) { - return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot) + getStylesheetElementsNotInSnapshot(snapshot) { + return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot) } - getElementsMatchingTypeNotInSnapshot(matchedType: ElementType, snapshot: HeadSnapshot): T[] { + getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) { return Object.keys(this.detailsByOuterHTML) .filter((outerHTML) => !(outerHTML in snapshot.detailsByOuterHTML)) .map((outerHTML) => this.detailsByOuterHTML[outerHTML]) .filter(({ type }) => type == matchedType) - .map(({ elements: [element] }) => element) as T[] + .map(({ elements: [element] }) => element) } - get provisionalElements(): Element[] { + get provisionalElements() { return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => { const { type, tracked, elements } = this.detailsByOuterHTML[outerHTML] if (type == null && !tracked) { @@ -65,25 +55,25 @@ export class HeadSnapshot extends Snapshot { } else { return result } - }, [] as Element[]) + }, []) } - getMetaValue(name: string): string | null { + getMetaValue(name) { const element = this.findMetaElementByName(name) return element ? element.getAttribute("content") : null } - findMetaElementByName(name: string) { + findMetaElementByName(name) { return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => { const { - elements: [element], + elements: [element] } = this.detailsByOuterHTML[outerHTML] return elementIsMetaElementWithName(element, name) ? element : result - }, undefined as Element | undefined) + }, undefined | undefined) } } -function elementType(element: Element) { +function elementType(element) { if (elementIsScript(element)) { return "script" } else if (elementIsStylesheet(element)) { @@ -91,31 +81,31 @@ function elementType(element: Element) { } } -function elementIsTracked(element: Element) { +function elementIsTracked(element) { return element.getAttribute("data-turbo-track") == "reload" } -function elementIsScript(element: Element) { +function elementIsScript(element) { const tagName = element.localName return tagName == "script" } -function elementIsNoscript(element: Element) { +function elementIsNoscript(element) { const tagName = element.localName return tagName == "noscript" } -function elementIsStylesheet(element: Element) { +function elementIsStylesheet(element) { const tagName = element.localName return tagName == "style" || (tagName == "link" && element.getAttribute("rel") == "stylesheet") } -function elementIsMetaElementWithName(element: Element, name: string) { +function elementIsMetaElementWithName(element, name) { const tagName = element.localName return tagName == "meta" && element.getAttribute("name") == name } -function elementWithoutNonce(element: Element) { +function elementWithoutNonce(element) { if (element.hasAttribute("nonce")) { element.setAttribute("nonce", "") } diff --git a/src/core/drive/history.ts b/src/core/drive/history.js similarity index 68% rename from src/core/drive/history.ts rename to src/core/drive/history.js index 7143adb1b..215f56eca 100644 --- a/src/core/drive/history.ts +++ b/src/core/drive/history.js @@ -1,28 +1,13 @@ -import { Position } from "../types" import { nextMicrotask, uuid } from "../../util" -export interface HistoryDelegate { - historyPoppedToLocationWithRestorationIdentifier(location: URL, restorationIdentifier: string): void -} - -type HistoryMethod = (this: typeof history, state: any, title: string, url?: string | null | undefined) => void - -export type RestorationData = { scrollPosition?: Position } - -export type RestorationDataMap = { - [restorationIdentifier: string]: RestorationData -} - export class History { - readonly delegate: HistoryDelegate - location!: URL + location restorationIdentifier = uuid() - restorationData: RestorationDataMap = {} + restorationData = {} started = false pageLoaded = false - previousScrollRestoration?: ScrollRestoration - constructor(delegate: HistoryDelegate) { + constructor(delegate) { this.delegate = delegate } @@ -43,15 +28,15 @@ export class History { } } - push(location: URL, restorationIdentifier?: string) { + push(location, restorationIdentifier) { this.update(history.pushState, location, restorationIdentifier) } - replace(location: URL, restorationIdentifier?: string) { + replace(location, restorationIdentifier) { this.update(history.replaceState, location, restorationIdentifier) } - update(method: HistoryMethod, location: URL, restorationIdentifier = uuid()) { + update(method, location, restorationIdentifier = uuid()) { const state = { turbo: { restorationIdentifier } } method.call(history, state, "", location.href) this.location = location @@ -60,16 +45,16 @@ export class History { // Restoration data - getRestorationDataForIdentifier(restorationIdentifier: string): RestorationData { + getRestorationDataForIdentifier(restorationIdentifier) { return this.restorationData[restorationIdentifier] || {} } - updateRestorationData(additionalData: Partial) { + updateRestorationData(additionalData) { const { restorationIdentifier } = this const restorationData = this.restorationData[restorationIdentifier] this.restorationData[restorationIdentifier] = { ...restorationData, - ...additionalData, + ...additionalData } } @@ -91,7 +76,7 @@ export class History { // Event handlers - onPopState = (event: PopStateEvent) => { + onPopState = (event) => { if (this.shouldHandlePopState()) { const { turbo } = event.state || {} if (turbo) { @@ -103,7 +88,7 @@ export class History { } } - onPageLoad = async (_event: Event) => { + onPageLoad = async (_event) => { await nextMicrotask() this.pageLoaded = true } diff --git a/src/core/drive/navigator.ts b/src/core/drive/navigator.js similarity index 67% rename from src/core/drive/navigator.ts rename to src/core/drive/navigator.js index 7579913f1..be81794fd 100644 --- a/src/core/drive/navigator.ts +++ b/src/core/drive/navigator.js @@ -1,27 +1,15 @@ -import { Action } from "../types" import { getVisitAction } from "../../util" -import { FetchResponse } from "../../http/fetch_response" import { FormSubmission } from "./form_submission" -import { expandURL, getAnchor, getRequestURL, Locatable, locationIsVisitable } from "../url" -import { Visit, VisitDelegate, VisitOptions } from "./visit" +import { expandURL, getAnchor, getRequestURL, locationIsVisitable } from "../url" +import { Visit } from "./visit" import { PageSnapshot } from "./page_snapshot" -export type NavigatorDelegate = VisitDelegate & { - allowsVisitingLocationWithAction(location: URL, action?: Action): boolean - visitProposedToLocation(location: URL, options: Partial): void - notifyApplicationAfterVisitingSamePageLocation(oldURL: URL, newURL: URL): void -} - export class Navigator { - readonly delegate: NavigatorDelegate - formSubmission?: FormSubmission - currentVisit?: Visit - - constructor(delegate: NavigatorDelegate) { + constructor(delegate) { this.delegate = delegate } - proposeVisit(location: URL, options: Partial = {}) { + proposeVisit(location, options = {}) { if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) { if (locationIsVisitable(location, this.view.snapshot.rootLocation)) { this.delegate.visitProposedToLocation(location, options) @@ -31,16 +19,16 @@ export class Navigator { } } - startVisit(locatable: Locatable, restorationIdentifier: string, options: Partial = {}) { + startVisit(locatable, restorationIdentifier, options = {}) { this.stop() this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, { referrer: this.location, - ...options, + ...options }) this.currentVisit.start() } - submitForm(form: HTMLFormElement, submitter?: HTMLElement) { + submitForm(form, submitter) { this.stop() this.formSubmission = new FormSubmission(this, form, submitter, true) @@ -73,14 +61,14 @@ export class Navigator { // Form submission delegate - formSubmissionStarted(formSubmission: FormSubmission) { + formSubmissionStarted(formSubmission) { // Not all adapters implement formSubmissionStarted if (typeof this.adapter.formSubmissionStarted === "function") { this.adapter.formSubmissionStarted(formSubmission) } } - async formSubmissionSucceededWithResponse(formSubmission: FormSubmission, fetchResponse: FetchResponse) { + async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) { if (formSubmission == this.formSubmission) { const responseHTML = await fetchResponse.responseHTML if (responseHTML) { @@ -94,14 +82,14 @@ export class Navigator { const visitOptions = { action, shouldCacheSnapshot, - response: { statusCode, responseHTML, redirected }, + response: { statusCode, responseHTML, redirected } } this.proposeVisit(fetchResponse.location, visitOptions) } } } - async formSubmissionFailedWithResponse(formSubmission: FormSubmission, fetchResponse: FetchResponse) { + async formSubmissionFailedWithResponse(formSubmission, fetchResponse) { const responseHTML = await fetchResponse.responseHTML if (responseHTML) { @@ -116,11 +104,11 @@ export class Navigator { } } - formSubmissionErrored(formSubmission: FormSubmission, error: Error) { + formSubmissionErrored(formSubmission, error) { console.error(error) } - formSubmissionFinished(formSubmission: FormSubmission) { + formSubmissionFinished(formSubmission) { // Not all adapters implement formSubmissionFinished if (typeof this.adapter.formSubmissionFinished === "function") { this.adapter.formSubmissionFinished(formSubmission) @@ -129,15 +117,15 @@ export class Navigator { // Visit delegate - visitStarted(visit: Visit) { + visitStarted(visit) { this.delegate.visitStarted(visit) } - visitCompleted(visit: Visit) { + visitCompleted(visit) { this.delegate.visitCompleted(visit) } - locationWithActionIsSamePage(location: URL, action?: Action): boolean { + locationWithActionIsSamePage(location, action) { const anchor = getAnchor(location) const currentAnchor = getAnchor(this.view.lastRenderedLocation) const isRestorationToTop = action === "restore" && typeof anchor === "undefined" @@ -149,7 +137,7 @@ export class Navigator { ) } - visitScrolledToSamePageLocation(oldURL: URL, newURL: URL) { + visitScrolledToSamePageLocation(oldURL, newURL) { this.delegate.visitScrolledToSamePageLocation(oldURL, newURL) } @@ -163,7 +151,7 @@ export class Navigator { return this.history.restorationIdentifier } - getActionForFormSubmission({ submitter, formElement }: FormSubmission): Action { + getActionForFormSubmission({ submitter, formElement }) { return getVisitAction(submitter, formElement) || "advance" } } diff --git a/src/core/drive/page_renderer.ts b/src/core/drive/page_renderer.js similarity index 88% rename from src/core/drive/page_renderer.ts rename to src/core/drive/page_renderer.js index bb401414a..0c1a222e6 100644 --- a/src/core/drive/page_renderer.ts +++ b/src/core/drive/page_renderer.js @@ -1,10 +1,8 @@ import { Renderer } from "../renderer" -import { PageSnapshot } from "./page_snapshot" -import { ReloadReason } from "../native/browser_adapter" import { activateScriptElement, waitForLoad } from "../../util" -export class PageRenderer extends Renderer { - static renderElement(currentElement: HTMLBodyElement, newElement: HTMLBodyElement) { +export class PageRenderer extends Renderer { + static renderElement(currentElement, newElement) { if (document.body && newElement instanceof HTMLBodyElement) { document.body.replaceWith(newElement) } else { @@ -16,16 +14,16 @@ export class PageRenderer extends Renderer { return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical } - get reloadReason(): ReloadReason { + get reloadReason() { if (!this.newSnapshot.isVisitable) { return { - reason: "turbo_visit_control_is_reload", + reason: "turbo_visit_control_is_reload" } } if (!this.trackedElementsAreIdentical) { return { - reason: "tracked_element_mismatch", + reason: "tracked_element_mismatch" } } } @@ -82,7 +80,7 @@ export class PageRenderer extends Renderer { const loadingElements = [] for (const element of this.newHeadStylesheetElements) { - loadingElements.push(waitForLoad(element as HTMLLinkElement)) + loadingElements.push(waitForLoad(element)) document.head.appendChild(element) } @@ -110,7 +108,7 @@ export class PageRenderer extends Renderer { } } - isCurrentElementInElementList(element: Element, elementList: Element[]) { + isCurrentElementInElementList(element, elementList) { for (const [index, newElement] of elementList.entries()) { // if title element... if (element.tagName == "TITLE") { diff --git a/src/core/drive/page_snapshot.ts b/src/core/drive/page_snapshot.js similarity index 80% rename from src/core/drive/page_snapshot.ts rename to src/core/drive/page_snapshot.js index e8b7041a1..575cb25eb 100644 --- a/src/core/drive/page_snapshot.ts +++ b/src/core/drive/page_snapshot.js @@ -3,22 +3,20 @@ import { Snapshot } from "../snapshot" import { expandURL } from "../url" import { HeadSnapshot } from "./head_snapshot" -export class PageSnapshot extends Snapshot { +export class PageSnapshot extends Snapshot { static fromHTMLString(html = "") { return this.fromDocument(parseHTMLDocument(html)) } - static fromElement(element: Element) { + static fromElement(element) { return this.fromDocument(element.ownerDocument) } - static fromDocument({ head, body }: Document) { - return new this(body as HTMLBodyElement, new HeadSnapshot(head)) + static fromDocument({ head, body }) { + return new this(body, new HeadSnapshot(head)) } - readonly headSnapshot: HeadSnapshot - - constructor(element: HTMLBodyElement, headSnapshot: HeadSnapshot) { + constructor(element, headSnapshot) { super(element) this.headSnapshot = headSnapshot } @@ -35,7 +33,7 @@ export class PageSnapshot extends Snapshot { for (const option of source.selectedOptions) clone.options[option.index].selected = true } - for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) { + for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) { clonedPasswordInput.value = "" } @@ -73,7 +71,7 @@ export class PageSnapshot extends Snapshot { // Private - getSetting(name: string) { + getSetting(name) { return this.headSnapshot.getMetaValue(`turbo-${name}`) } } diff --git a/src/core/drive/page_view.ts b/src/core/drive/page_view.js similarity index 62% rename from src/core/drive/page_view.ts rename to src/core/drive/page_view.js index dba0defbb..1b95bb1d1 100644 --- a/src/core/drive/page_view.ts +++ b/src/core/drive/page_view.js @@ -1,29 +1,20 @@ import { nextEventLoopTick } from "../../util" -import { View, ViewDelegate, ViewRenderOptions } from "../view" +import { View } from "../view" import { ErrorRenderer } from "./error_renderer" import { PageRenderer } from "./page_renderer" import { PageSnapshot } from "./page_snapshot" import { SnapshotCache } from "./snapshot_cache" -import { Visit } from "./visit" -export type PageViewRenderOptions = ViewRenderOptions - -export interface PageViewDelegate extends ViewDelegate { - viewWillCacheSnapshot(): void -} - -type PageViewRenderer = PageRenderer | ErrorRenderer - -export class PageView extends View { - readonly snapshotCache = new SnapshotCache(10) +export class PageView extends View { + snapshotCache = new SnapshotCache(10) lastRenderedLocation = new URL(location.href) forceReloaded = false - shouldTransitionTo(newSnapshot: PageSnapshot) { + shouldTransitionTo(newSnapshot) { return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions } - renderPage(snapshot: PageSnapshot, isPreview = false, willRender = true, visit?: Visit) { + renderPage(snapshot, isPreview = false, willRender = true, visit) { const renderer = new PageRenderer(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender) if (!renderer.shouldRender) { @@ -35,7 +26,7 @@ export class PageView extends View(this.selector)) { + preloadOnLoadLinksForView(element) { + for (const link of element.querySelectorAll(this.selector)) { this.preloadURL(link) } } - async preloadURL(link: HTMLAnchorElement) { + async preloadURL(link) { const location = new URL(link.href) if (this.snapshotCache.has(location)) { diff --git a/src/core/drive/progress_bar.ts b/src/core/drive/progress_bar.js similarity index 93% rename from src/core/drive/progress_bar.ts rename to src/core/drive/progress_bar.js index 9a189be92..caff5f5da 100644 --- a/src/core/drive/progress_bar.ts +++ b/src/core/drive/progress_bar.js @@ -21,11 +21,7 @@ export class ProgressBar { ` } - readonly stylesheetElement: HTMLStyleElement - readonly progressElement: HTMLDivElement - hiding = false - trickleInterval?: number value = 0 visible = false @@ -56,7 +52,7 @@ export class ProgressBar { } } - setValue(value: number) { + setValue(value) { this.value = value this.refresh() } @@ -74,7 +70,7 @@ export class ProgressBar { this.refresh() } - fadeProgressElement(callback: () => void) { + fadeProgressElement(callback) { this.progressElement.style.opacity = "0" setTimeout(callback, ProgressBar.animationDuration * 1.5) } diff --git a/src/core/drive/snapshot_cache.ts b/src/core/drive/snapshot_cache.js similarity index 67% rename from src/core/drive/snapshot_cache.ts rename to src/core/drive/snapshot_cache.js index a93a1bde2..6ed37e8fd 100644 --- a/src/core/drive/snapshot_cache.ts +++ b/src/core/drive/snapshot_cache.js @@ -1,20 +1,18 @@ import { toCacheKey } from "../url" -import { PageSnapshot } from "./page_snapshot" export class SnapshotCache { - readonly keys: string[] = [] - readonly size: number - snapshots: { [url: string]: PageSnapshot } = {} + keys = [] + snapshots = {} - constructor(size: number) { + constructor(size) { this.size = size } - has(location: URL) { + has(location) { return toCacheKey(location) in this.snapshots } - get(location: URL): PageSnapshot | undefined { + get(location) { if (this.has(location)) { const snapshot = this.read(location) this.touch(location) @@ -22,7 +20,7 @@ export class SnapshotCache { } } - put(location: URL, snapshot: PageSnapshot) { + put(location, snapshot) { this.write(location, snapshot) this.touch(location) return snapshot @@ -34,15 +32,15 @@ export class SnapshotCache { // Private - read(location: URL) { + read(location) { return this.snapshots[toCacheKey(location)] } - write(location: URL, snapshot: PageSnapshot) { + write(location, snapshot) { this.snapshots[toCacheKey(location)] = snapshot } - touch(location: URL) { + touch(location) { const key = toCacheKey(location) const index = this.keys.indexOf(key) if (index > -1) this.keys.splice(index, 1) diff --git a/src/core/drive/view_transitioner.js b/src/core/drive/view_transitioner.js new file mode 100644 index 000000000..f8663dbac --- /dev/null +++ b/src/core/drive/view_transitioner.js @@ -0,0 +1,21 @@ +export class ViewTransitioner { + #viewTransitionStarted = false + #lastOperation = Promise.resolve() + + renderChange(useViewTransition, render) { + if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) { + this.#viewTransitionStarted = true + this.#lastOperation = this.#lastOperation.then(async () => { + await document.startViewTransition(render).finished + }) + } else { + this.#lastOperation = this.#lastOperation.then(render) + } + + return this.#lastOperation + } + + get viewTransitionsAvailable() { + return document.startViewTransition + } +} diff --git a/src/core/drive/view_transitioner.ts b/src/core/drive/view_transitioner.ts deleted file mode 100644 index b0ec16cce..000000000 --- a/src/core/drive/view_transitioner.ts +++ /dev/null @@ -1,31 +0,0 @@ -declare global { - type ViewTransition = { - finished: Promise - } - - interface Document { - startViewTransition?(callback: () => Promise): ViewTransition - } -} - -export class ViewTransitioner { - private viewTransitionStarted = false - private lastOperation = Promise.resolve() - - renderChange(useViewTransition: boolean, render: () => Promise) { - if (useViewTransition && this.viewTransitionsAvailable && !this.viewTransitionStarted) { - this.viewTransitionStarted = true - this.lastOperation = this.lastOperation.then(async () => { - await document.startViewTransition!(render).finished - }) - } else { - this.lastOperation = this.lastOperation.then(render) - } - - return this.lastOperation - } - - private get viewTransitionsAvailable() { - return document.startViewTransition - } -} diff --git a/src/core/drive/visit.ts b/src/core/drive/visit.js similarity index 74% rename from src/core/drive/visit.ts rename to src/core/drive/visit.js index 1a311b640..7fc494c19 100644 --- a/src/core/drive/visit.ts +++ b/src/core/drive/visit.js @@ -1,116 +1,55 @@ -import { Adapter } from "../native/adapter" -import { FetchMethod, FetchRequest, FetchRequestDelegate } from "../../http/fetch_request" -import { FetchResponse } from "../../http/fetch_response" -import { History } from "./history" +import { FetchMethod, FetchRequest } from "../../http/fetch_request" import { getAnchor } from "../url" -import { Snapshot } from "../snapshot" import { PageSnapshot } from "./page_snapshot" -import { Action } from "../types" import { getHistoryMethodForAction, uuid } from "../../util" -import { PageView } from "./page_view" import { StreamMessage } from "../streams/stream_message" import { ViewTransitioner } from "./view_transitioner" -export interface VisitDelegate { - readonly adapter: Adapter - readonly history: History - readonly view: PageView - - visitStarted(visit: Visit): void - visitCompleted(visit: Visit): void - locationWithActionIsSamePage(location: URL, action: Action): boolean - visitScrolledToSamePageLocation(oldURL: URL, newURL: URL): void -} - -export enum TimingMetric { - visitStart = "visitStart", - requestStart = "requestStart", - requestEnd = "requestEnd", - visitEnd = "visitEnd", -} - -export type TimingMetrics = Partial<{ [metric in TimingMetric]: any }> - -export enum VisitState { - initialized = "initialized", - started = "started", - canceled = "canceled", - failed = "failed", - completed = "completed", -} - -export type VisitOptions = { - action: Action - historyChanged: boolean - referrer?: URL - snapshot?: PageSnapshot - snapshotHTML?: string - response?: VisitResponse - visitCachedSnapshot(snapshot: Snapshot): void - willRender: boolean - updateHistory: boolean - restorationIdentifier?: string - shouldCacheSnapshot: boolean - frame?: string - acceptsStreamResponse: boolean -} - -const defaultOptions: VisitOptions = { +const defaultOptions = { action: "advance", historyChanged: false, visitCachedSnapshot: () => {}, willRender: true, updateHistory: true, shouldCacheSnapshot: true, - acceptsStreamResponse: false, + acceptsStreamResponse: false +} + +export const TimingMetric = { + visitStart: "visitStart", + requestStart: "requestStart", + requestEnd: "requestEnd", + visitEnd: "visitEnd" } -export type VisitResponse = { - statusCode: number - redirected: boolean - responseHTML?: string +export const VisitState = { + initialized: "initialized", + started: "started", + canceled: "canceled", + failed: "failed", + completed: "completed" } -export enum SystemStatusCode { - networkFailure = 0, - timeoutFailure = -1, - contentTypeMismatch = -2, +export const SystemStatusCode = { + networkFailure: 0, + timeoutFailure: -1, + contentTypeMismatch: -2 } -export class Visit implements FetchRequestDelegate { - readonly delegate: VisitDelegate - readonly identifier = uuid() // Required by turbo-ios - readonly restorationIdentifier: string - readonly action: Action - readonly referrer?: URL - readonly timingMetrics: TimingMetrics = {} - readonly visitCachedSnapshot: (snapshot: Snapshot) => void - readonly willRender: boolean - readonly updateHistory: boolean +export class Visit { + identifier = uuid() // Required by turbo-ios + timingMetrics = {} followedRedirect = false - frame?: number historyChanged = false - location: URL - isSamePage: boolean - redirectedToLocation?: URL - request?: FetchRequest - response?: VisitResponse scrolled = false shouldCacheSnapshot = true acceptsStreamResponse = false - snapshotHTML?: string snapshotCached = false state = VisitState.initialized - snapshot?: PageSnapshot viewTransitioner = new ViewTransitioner() - constructor( - delegate: VisitDelegate, - location: URL, - restorationIdentifier: string | undefined, - options: Partial = {} - ) { + constructor(delegate, location, restorationIdentifier, options = {}) { this.delegate = delegate this.location = location this.restorationIdentifier = restorationIdentifier || uuid() @@ -126,10 +65,10 @@ export class Visit implements FetchRequestDelegate { willRender, updateHistory, shouldCacheSnapshot, - acceptsStreamResponse, + acceptsStreamResponse } = { ...defaultOptions, - ...options, + ...options } this.action = action this.historyChanged = historyChanged @@ -324,7 +263,7 @@ export class Visit implements FetchRequestDelegate { action: "replace", response: this.response, shouldCacheSnapshot: false, - willRender: false, + willRender: false }) this.followedRedirect = true } @@ -343,7 +282,7 @@ export class Visit implements FetchRequestDelegate { // Fetch request delegate - prepareRequest(request: FetchRequest) { + prepareRequest(request) { if (this.acceptsStreamResponse) { request.acceptResponseType(StreamMessage.contentType) } @@ -353,15 +292,15 @@ export class Visit implements FetchRequestDelegate { this.startRequest() } - requestPreventedHandlingResponse(_request: FetchRequest, _response: FetchResponse) {} + requestPreventedHandlingResponse(_request, _response) {} - async requestSucceededWithResponse(request: FetchRequest, response: FetchResponse) { + async requestSucceededWithResponse(request, response) { const responseHTML = await response.responseHTML const { redirected, statusCode } = response if (responseHTML == undefined) { this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, - redirected, + redirected }) } else { this.redirectedToLocation = response.redirected ? response.location : undefined @@ -369,23 +308,23 @@ export class Visit implements FetchRequestDelegate { } } - async requestFailedWithResponse(request: FetchRequest, response: FetchResponse) { + async requestFailedWithResponse(request, response) { const responseHTML = await response.responseHTML const { redirected, statusCode } = response if (responseHTML == undefined) { this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, - redirected, + redirected }) } else { this.recordResponse({ statusCode: statusCode, responseHTML, redirected }) } } - requestErrored(_request: FetchRequest, _error: Error) { + requestErrored(_request, _error) { this.recordResponse({ statusCode: SystemStatusCode.networkFailure, - redirected: false, + redirected: false }) } @@ -428,17 +367,17 @@ export class Visit implements FetchRequestDelegate { // Instrumentation - recordTimingMetric(metric: TimingMetric) { + recordTimingMetric(metric) { this.timingMetrics[metric] = new Date().getTime() } - getTimingMetrics(): TimingMetrics { + getTimingMetrics() { return { ...this.timingMetrics } } // Private - getHistoryMethodForAction(action: Action) { + getHistoryMethodForAction(action) { switch (action) { case "replace": return history.replaceState @@ -469,16 +408,16 @@ export class Visit implements FetchRequestDelegate { } } - async render(callback: () => Promise) { + async render(callback) { this.cancelRender() - await new Promise((resolve) => { + await new Promise((resolve) => { this.frame = requestAnimationFrame(() => resolve()) }) await callback() delete this.frame } - async renderPageSnapshot(snapshot: PageSnapshot, isPreview: boolean) { + async renderPageSnapshot(snapshot, isPreview) { await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), async () => { await this.view.renderPage(snapshot, isPreview, this.willRender, this) this.performScroll() @@ -493,6 +432,6 @@ export class Visit implements FetchRequestDelegate { } } -function isSuccessful(statusCode: number) { +function isSuccessful(statusCode) { return statusCode >= 200 && statusCode < 300 } diff --git a/src/core/errors.ts b/src/core/errors.js similarity index 100% rename from src/core/errors.ts rename to src/core/errors.js diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.js similarity index 54% rename from src/core/frames/frame_controller.ts rename to src/core/frames/frame_controller.js index 9282a597c..6fe1dcbe0 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.js @@ -1,12 +1,7 @@ -import { - FrameElement, - FrameElementDelegate, - FrameLoadingStyle, - FrameElementObservedAttribute, -} from "../../elements/frame_element" -import { FetchMethod, FetchRequest, FetchRequestDelegate } from "../../http/fetch_request" +import { FrameElement, FrameLoadingStyle } from "../../elements/frame_element" +import { FetchMethod, FetchRequest } from "../../http/fetch_request" import { FetchResponse } from "../../http/fetch_response" -import { AppearanceObserver, AppearanceObserverDelegate } from "../../observers/appearance_observer" +import { AppearanceObserver } from "../../observers/appearance_observer" import { clearBusyState, dispatch, @@ -15,58 +10,31 @@ import { markAsBusy, uuid, getHistoryMethodForAction, - getVisitAction, + getVisitAction } from "../../util" -import { FormSubmission, FormSubmissionDelegate } from "../drive/form_submission" +import { FormSubmission } from "../drive/form_submission" import { Snapshot } from "../snapshot" -import { ViewDelegate, ViewRenderOptions } from "../view" -import { Locatable, getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url" -import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer" +import { getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url" +import { FormSubmitObserver } from "../../observers/form_submit_observer" import { FrameView } from "./frame_view" -import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor" -import { FormLinkClickObserver, FormLinkClickObserverDelegate } from "../../observers/form_link_click_observer" +import { LinkInterceptor } from "./link_interceptor" +import { FormLinkClickObserver } from "../../observers/form_link_click_observer" import { FrameRenderer } from "./frame_renderer" import { session } from "../index" -import { Action } from "../types" -import { VisitOptions } from "../drive/visit" -import { TurboBeforeFrameRenderEvent } from "../session" import { StreamMessage } from "../streams/stream_message" import { PageSnapshot } from "../drive/page_snapshot" import { TurboFrameMissingError } from "../errors" -type VisitFallback = (location: Response | Locatable, options: Partial) => Promise -export type TurboFrameMissingEvent = CustomEvent<{ response: Response; visit: VisitFallback }> - -export class FrameController - implements - AppearanceObserverDelegate, - FetchRequestDelegate, - FormSubmitObserverDelegate, - FormSubmissionDelegate, - FrameElementDelegate, - FormLinkClickObserverDelegate, - LinkInterceptorDelegate, - ViewDelegate> -{ - readonly element: FrameElement - readonly view: FrameView - readonly appearanceObserver: AppearanceObserver - readonly formLinkClickObserver: FormLinkClickObserver - readonly linkInterceptor: LinkInterceptor - readonly formSubmitObserver: FormSubmitObserver - formSubmission?: FormSubmission - fetchResponseLoaded = (_fetchResponse: FetchResponse) => {} - private currentFetchRequest: FetchRequest | null = null - private resolveVisitPromise = () => {} - private connected = false - private hasBeenLoaded = false - private ignoredAttributes: Set = new Set() - private action: Action | null = null - readonly restorationIdentifier: string - private previousFrameElement?: FrameElement - private currentNavigationElement?: Element - - constructor(element: FrameElement) { +export class FrameController { + fetchResponseLoaded = (_fetchResponse) => {} + #currentFetchRequest = null + #resolveVisitPromise = () => {} + #connected = false + #hasBeenLoaded = false + #ignoredAttributes = new Set() + action = null + + constructor(element) { this.element = element this.view = new FrameView(this, this.element) this.appearanceObserver = new AppearanceObserver(this, this.element) @@ -76,13 +44,15 @@ export class FrameController this.formSubmitObserver = new FormSubmitObserver(this, this.element) } + // Frame delegate + connect() { - if (!this.connected) { - this.connected = true + if (!this.#connected) { + this.#connected = true if (this.loadingStyle == FrameLoadingStyle.lazy) { this.appearanceObserver.start() } else { - this.loadSourceURL() + this.#loadSourceURL() } this.formLinkClickObserver.start() this.linkInterceptor.start() @@ -91,8 +61,8 @@ export class FrameController } disconnect() { - if (this.connected) { - this.connected = false + if (this.#connected) { + this.#connected = false this.appearanceObserver.stop() this.formLinkClickObserver.stop() this.linkInterceptor.stop() @@ -102,25 +72,25 @@ export class FrameController disabledChanged() { if (this.loadingStyle == FrameLoadingStyle.eager) { - this.loadSourceURL() + this.#loadSourceURL() } } sourceURLChanged() { - if (this.isIgnoringChangesTo("src")) return + if (this.#isIgnoringChangesTo("src")) return if (this.element.isConnected) { this.complete = false } - if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) { - this.loadSourceURL() + if (this.loadingStyle == FrameLoadingStyle.eager || this.#hasBeenLoaded) { + this.#loadSourceURL() } } sourceURLReloaded() { const { src } = this.element - this.ignoringChangesToAttribute("complete", () => { + this.#ignoringChangesToAttribute("complete", () => { this.element.removeAttribute("complete") }) this.element.src = null @@ -129,9 +99,9 @@ export class FrameController } completeChanged() { - if (this.isIgnoringChangesTo("complete")) return + if (this.#isIgnoringChangesTo("complete")) return - this.loadSourceURL() + this.#loadSourceURL() } loadingStyleChanged() { @@ -139,20 +109,20 @@ export class FrameController this.appearanceObserver.start() } else { this.appearanceObserver.stop() - this.loadSourceURL() + this.#loadSourceURL() } } - private async loadSourceURL() { + async #loadSourceURL() { if (this.enabled && this.isActive && !this.complete && this.sourceURL) { - this.element.loaded = this.visit(expandURL(this.sourceURL)) + this.element.loaded = this.#visit(expandURL(this.sourceURL)) this.appearanceObserver.stop() await this.element.loaded - this.hasBeenLoaded = true + this.#hasBeenLoaded = true } } - async loadResponse(fetchResponse: FetchResponse) { + async loadResponse(fetchResponse) { if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) { this.sourceURL = fetchResponse.response.url } @@ -164,9 +134,9 @@ export class FrameController const pageSnapshot = PageSnapshot.fromDocument(document) if (pageSnapshot.isVisitable) { - await this.loadFrameResponse(fetchResponse, document) + await this.#loadFrameResponse(fetchResponse, document) } else { - await this.handleUnvisitableFrameResponse(fetchResponse) + await this.#handleUnvisitableFrameResponse(fetchResponse) } } } finally { @@ -176,39 +146,39 @@ export class FrameController // Appearance observer delegate - elementAppearedInViewport(element: FrameElement) { + elementAppearedInViewport(element) { this.proposeVisitIfNavigatedWithAction(element, element) - this.loadSourceURL() + this.#loadSourceURL() } // Form link click observer delegate - willSubmitFormLinkToLocation(link: Element): boolean { - return this.shouldInterceptNavigation(link) + willSubmitFormLinkToLocation(link) { + return this.#shouldInterceptNavigation(link) } - submittedFormLinkToLocation(link: Element, _location: URL, form: HTMLFormElement): void { - const frame = this.findFrameElement(link) + submittedFormLinkToLocation(link, _location, form) { + const frame = this.#findFrameElement(link) if (frame) form.setAttribute("data-turbo-frame", frame.id) } // Link interceptor delegate - shouldInterceptLinkClick(element: Element, _location: string, _event: MouseEvent) { - return this.shouldInterceptNavigation(element) + shouldInterceptLinkClick(element, _location, _event) { + return this.#shouldInterceptNavigation(element) } - linkClickIntercepted(element: Element, location: string) { - this.navigateFrame(element, location) + linkClickIntercepted(element, location) { + this.#navigateFrame(element, location) } // Form submit observer delegate - willSubmitForm(element: HTMLFormElement, submitter?: HTMLElement) { - return element.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(element, submitter) + willSubmitForm(element, submitter) { + return element.closest("turbo-frame") == this.element && this.#shouldInterceptNavigation(element, submitter) } - formSubmitted(element: HTMLFormElement, submitter?: HTMLElement) { + formSubmitted(element, submitter) { if (this.formSubmission) { this.formSubmission.stop() } @@ -221,7 +191,7 @@ export class FrameController // Fetch request delegate - prepareRequest(request: FetchRequest) { + prepareRequest(request) { request.headers["Turbo-Frame"] = this.id if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) { @@ -229,41 +199,41 @@ export class FrameController } } - requestStarted(_request: FetchRequest) { + requestStarted(_request) { markAsBusy(this.element) } - requestPreventedHandlingResponse(_request: FetchRequest, _response: FetchResponse) { - this.resolveVisitPromise() + requestPreventedHandlingResponse(_request, _response) { + this.#resolveVisitPromise() } - async requestSucceededWithResponse(request: FetchRequest, response: FetchResponse) { + async requestSucceededWithResponse(request, response) { await this.loadResponse(response) - this.resolveVisitPromise() + this.#resolveVisitPromise() } - async requestFailedWithResponse(request: FetchRequest, response: FetchResponse) { + async requestFailedWithResponse(request, response) { await this.loadResponse(response) - this.resolveVisitPromise() + this.#resolveVisitPromise() } - requestErrored(request: FetchRequest, error: Error) { + requestErrored(request, error) { console.error(error) - this.resolveVisitPromise() + this.#resolveVisitPromise() } - requestFinished(_request: FetchRequest) { + requestFinished(_request) { clearBusyState(this.element) } // Form submission delegate - formSubmissionStarted({ formElement }: FormSubmission) { - markAsBusy(formElement, this.findFrameElement(formElement)) + formSubmissionStarted({ formElement }) { + markAsBusy(formElement, this.#findFrameElement(formElement)) } - formSubmissionSucceededWithResponse(formSubmission: FormSubmission, response: FetchResponse) { - const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter) + formSubmissionSucceededWithResponse(formSubmission, response) { + const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter) frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter) frame.delegate.loadResponse(response) @@ -273,34 +243,30 @@ export class FrameController } } - formSubmissionFailedWithResponse(formSubmission: FormSubmission, fetchResponse: FetchResponse) { + formSubmissionFailedWithResponse(formSubmission, fetchResponse) { this.element.delegate.loadResponse(fetchResponse) session.clearCache() } - formSubmissionErrored(formSubmission: FormSubmission, error: Error) { + formSubmissionErrored(formSubmission, error) { console.error(error) } - formSubmissionFinished({ formElement }: FormSubmission) { - clearBusyState(formElement, this.findFrameElement(formElement)) + formSubmissionFinished({ formElement }) { + clearBusyState(formElement, this.#findFrameElement(formElement)) } // View delegate - allowsImmediateRender( - { element: newFrame }: Snapshot, - _isPreview: boolean, - options: ViewRenderOptions - ) { - const event = dispatch("turbo:before-frame-render", { + allowsImmediateRender({ element: newFrame }, _isPreview, options) { + const event = dispatch("turbo:before-frame-render", { target: this.element, detail: { newFrame, ...options }, - cancelable: true, + cancelable: true }) const { defaultPrevented, - detail: { render }, + detail: { render } } = event if (this.view.renderer && render) { @@ -310,20 +276,21 @@ export class FrameController return !defaultPrevented } - viewRenderedSnapshot(_snapshot: Snapshot, _isPreview: boolean) {} + viewRenderedSnapshot(_snapshot, _isPreview) {} - preloadOnLoadLinksForView(element: Element) { + preloadOnLoadLinksForView(element) { session.preloadOnLoadLinksForView(element) } viewInvalidated() {} // Frame renderer delegate - willRenderFrame(currentElement: FrameElement, _newElement: FrameElement) { + + willRenderFrame(currentElement, _newElement) { this.previousFrameElement = currentElement.cloneNode(true) } - visitCachedSnapshot = ({ element }: Snapshot) => { + visitCachedSnapshot = ({ element }) => { const frame = element.querySelector("#" + this.element.id) if (frame && this.previousFrameElement) { @@ -335,7 +302,7 @@ export class FrameController // Private - private async loadFrameResponse(fetchResponse: FetchResponse, document: Document) { + async #loadFrameResponse(fetchResponse, document) { const newFrameElement = await this.extractForeignFrameElement(document.body) if (newFrameElement) { @@ -349,56 +316,56 @@ export class FrameController session.frameRendered(fetchResponse, this.element) session.frameLoaded(this.element) this.fetchResponseLoaded(fetchResponse) - } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) { - this.handleFrameMissingFromResponse(fetchResponse) + } else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) { + this.#handleFrameMissingFromResponse(fetchResponse) } } - private async visit(url: URL) { + async #visit(url) { const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams(), this.element) - this.currentFetchRequest?.cancel() - this.currentFetchRequest = request + this.#currentFetchRequest?.cancel() + this.#currentFetchRequest = request - return new Promise((resolve) => { - this.resolveVisitPromise = () => { - this.resolveVisitPromise = () => {} - this.currentFetchRequest = null + return new Promise((resolve) => { + this.#resolveVisitPromise = () => { + this.#resolveVisitPromise = () => {} + this.#currentFetchRequest = null resolve() } request.perform() }) } - private navigateFrame(element: Element, url: string, submitter?: HTMLElement) { - const frame = this.findFrameElement(element, submitter) + #navigateFrame(element, url, submitter) { + const frame = this.#findFrameElement(element, submitter) frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter) - this.withCurrentNavigationElement(element, () => { + this.#withCurrentNavigationElement(element, () => { frame.src = url }) } - proposeVisitIfNavigatedWithAction(frame: FrameElement, element: Element, submitter?: HTMLElement) { + proposeVisitIfNavigatedWithAction(frame, element, submitter) { this.action = getVisitAction(submitter, element, frame) if (this.action) { const pageSnapshot = PageSnapshot.fromElement(frame).clone() const { visitCachedSnapshot } = frame.delegate - frame.delegate.fetchResponseLoaded = (fetchResponse: FetchResponse) => { + frame.delegate.fetchResponseLoaded = (fetchResponse) => { if (frame.src) { const { statusCode, redirected } = fetchResponse const responseHTML = frame.ownerDocument.documentElement.outerHTML const response = { statusCode, redirected, responseHTML } - const options: Partial = { + const options = { response, visitCachedSnapshot, willRender: false, updateHistory: false, restorationIdentifier: this.restorationIdentifier, - snapshot: pageSnapshot, + snapshot: pageSnapshot } if (this.action) options.action = this.action @@ -416,46 +383,46 @@ export class FrameController } } - private async handleUnvisitableFrameResponse(fetchResponse: FetchResponse) { + async #handleUnvisitableFrameResponse(fetchResponse) { console.warn( `The response (${fetchResponse.statusCode}) from is performing a full page visit due to turbo-visit-control.` ) - await this.visitResponse(fetchResponse.response) + await this.#visitResponse(fetchResponse.response) } - private willHandleFrameMissingFromResponse(fetchResponse: FetchResponse): boolean { + #willHandleFrameMissingFromResponse(fetchResponse) { this.element.setAttribute("complete", "") const response = fetchResponse.response - const visit = async (url: Locatable | Response, options: Partial = {}) => { + const visit = async (url, options) => { if (url instanceof Response) { - this.visitResponse(url) + this.#visitResponse(url) } else { session.visit(url, options) } } - const event = dispatch("turbo:frame-missing", { + const event = dispatch("turbo:frame-missing", { target: this.element, detail: { response, visit }, - cancelable: true, + cancelable: true }) return !event.defaultPrevented } - private handleFrameMissingFromResponse(fetchResponse: FetchResponse) { + #handleFrameMissingFromResponse(fetchResponse) { this.view.missing() - this.throwFrameMissingError(fetchResponse) + this.#throwFrameMissingError(fetchResponse) } - private throwFrameMissingError(fetchResponse: FetchResponse) { + #throwFrameMissingError(fetchResponse) { const message = `The response (${fetchResponse.statusCode}) did not contain the expected and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.` throw new TurboFrameMissingError(message) } - private async visitResponse(response: Response): Promise { + async #visitResponse(response) { const wrapped = new FetchResponse(response) const responseHTML = await wrapped.responseHTML const { location, redirected, statusCode } = wrapped @@ -463,12 +430,12 @@ export class FrameController return session.visit(location, { response: { redirected, statusCode, responseHTML } }) } - private findFrameElement(element: Element, submitter?: HTMLElement) { + #findFrameElement(element, submitter) { const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target") return getFrameElementById(id) ?? this.element } - async extractForeignFrameElement(container: ParentNode): Promise { + async extractForeignFrameElement(container) { let element const id = CSS.escape(this.id) @@ -491,16 +458,16 @@ export class FrameController return null } - private formActionIsVisitable(form: HTMLFormElement, submitter?: HTMLElement) { + #formActionIsVisitable(form, submitter) { const action = getAction(form, submitter) return locationIsVisitable(expandURL(action), this.rootLocation) } - private shouldInterceptNavigation(element: Element, submitter?: HTMLElement) { + #shouldInterceptNavigation(element, submitter) { const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target") - if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) { + if (element instanceof HTMLFormElement && !this.#formActionIsVisitable(element, submitter)) { return false } @@ -542,8 +509,8 @@ export class FrameController } } - set sourceURL(sourceURL: string | undefined) { - this.ignoringChangesToAttribute("src", () => { + set sourceURL(sourceURL) { + this.#ignoringChangesToAttribute("src", () => { this.element.src = sourceURL ?? null }) } @@ -553,15 +520,15 @@ export class FrameController } get isLoading() { - return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined + return this.formSubmission !== undefined || this.#resolveVisitPromise() !== undefined } get complete() { return this.element.hasAttribute("complete") } - set complete(value: boolean) { - this.ignoringChangesToAttribute("complete", () => { + set complete(value) { + this.#ignoringChangesToAttribute("complete", () => { if (value) { this.element.setAttribute("complete", "") } else { @@ -571,33 +538,33 @@ export class FrameController } get isActive() { - return this.element.isActive && this.connected + return this.element.isActive && this.#connected } get rootLocation() { - const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`) + const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`) const root = meta?.content ?? "/" return expandURL(root) } - private isIgnoringChangesTo(attributeName: FrameElementObservedAttribute): boolean { - return this.ignoredAttributes.has(attributeName) + #isIgnoringChangesTo(attributeName) { + return this.#ignoredAttributes.has(attributeName) } - private ignoringChangesToAttribute(attributeName: FrameElementObservedAttribute, callback: () => void) { - this.ignoredAttributes.add(attributeName) + #ignoringChangesToAttribute(attributeName, callback) { + this.#ignoredAttributes.add(attributeName) callback() - this.ignoredAttributes.delete(attributeName) + this.#ignoredAttributes.delete(attributeName) } - private withCurrentNavigationElement(element: Element, callback: () => void) { + #withCurrentNavigationElement(element, callback) { this.currentNavigationElement = element callback() delete this.currentNavigationElement } } -function getFrameElementById(id: string | null) { +function getFrameElementById(id) { if (id != null) { const element = document.getElementById(id) if (element instanceof FrameElement) { @@ -606,7 +573,7 @@ function getFrameElementById(id: string | null) { } } -function activateElement(element: Element | null, currentURL?: string | null) { +function activateElement(element, currentURL) { if (element) { const src = element.getAttribute("src") if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) { diff --git a/src/core/frames/frame_redirector.js b/src/core/frames/frame_redirector.js new file mode 100644 index 000000000..10aeee6cc --- /dev/null +++ b/src/core/frames/frame_redirector.js @@ -0,0 +1,85 @@ +import { FormSubmitObserver } from "../../observers/form_submit_observer" +import { FrameElement } from "../../elements/frame_element" +import { LinkInterceptor } from "./link_interceptor" +import { expandURL, getAction, locationIsVisitable } from "../url" + +export class FrameRedirector { + constructor(session, element) { + this.session = session + this.element = element + this.linkInterceptor = new LinkInterceptor(this, element) + this.formSubmitObserver = new FormSubmitObserver(this, element) + } + + start() { + this.linkInterceptor.start() + this.formSubmitObserver.start() + } + + stop() { + this.linkInterceptor.stop() + this.formSubmitObserver.stop() + } + + // Link interceptor delegate + + shouldInterceptLinkClick(element, _location, _event) { + return this.#shouldRedirect(element) + } + + linkClickIntercepted(element, url, event) { + const frame = this.#findFrameElement(element) + if (frame) { + frame.delegate.linkClickIntercepted(element, url, event) + } + } + + // Form submit observer delegate + + willSubmitForm(element, submitter) { + return ( + element.closest("turbo-frame") == null && + this.#shouldSubmit(element, submitter) && + this.#shouldRedirect(element, submitter) + ) + } + + formSubmitted(element, submitter) { + const frame = this.#findFrameElement(element, submitter) + if (frame) { + frame.delegate.formSubmitted(element, submitter) + } + } + + #shouldSubmit(form, submitter) { + const action = getAction(form, submitter) + const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`) + const rootLocation = expandURL(meta?.content ?? "/") + + return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation) + } + + #shouldRedirect(element, submitter) { + 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 + } + } + + #findFrameElement(element, submitter) { + const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame") + if (id && id != "_top") { + const frame = this.element.querySelector(`#${id}:not([disabled])`) + if (frame instanceof FrameElement) { + return frame + } + } + } +} diff --git a/src/core/frames/frame_redirector.ts b/src/core/frames/frame_redirector.ts deleted file mode 100644 index 3175e9fe0..000000000 --- a/src/core/frames/frame_redirector.ts +++ /dev/null @@ -1,86 +0,0 @@ -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 { Session } from "../session" -export class FrameRedirector implements LinkInterceptorDelegate, FormSubmitObserverDelegate { - readonly session: Session - readonly element: Element - readonly linkInterceptor: LinkInterceptor - readonly formSubmitObserver: FormSubmitObserver - - constructor(session: Session, element: Element) { - this.session = session - this.element = element - this.linkInterceptor = new LinkInterceptor(this, element) - this.formSubmitObserver = new FormSubmitObserver(this, element) - } - - start() { - this.linkInterceptor.start() - this.formSubmitObserver.start() - } - - stop() { - this.linkInterceptor.stop() - this.formSubmitObserver.stop() - } - - shouldInterceptLinkClick(element: Element, _location: string, _event: MouseEvent) { - return this.shouldRedirect(element) - } - - linkClickIntercepted(element: Element, url: string, event: MouseEvent) { - const frame = this.findFrameElement(element) - if (frame) { - frame.delegate.linkClickIntercepted(element, url, event) - } - } - - willSubmitForm(element: HTMLFormElement, submitter?: HTMLElement) { - return ( - element.closest("turbo-frame") == null && - this.shouldSubmit(element, submitter) && - this.shouldRedirect(element, submitter) - ) - } - - formSubmitted(element: HTMLFormElement, submitter?: HTMLElement) { - const frame = this.findFrameElement(element, submitter) - if (frame) { - frame.delegate.formSubmitted(element, submitter) - } - } - - private shouldSubmit(form: HTMLFormElement, submitter?: HTMLElement) { - const action = getAction(form, submitter) - const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`) - const rootLocation = expandURL(meta?.content ?? "/") - - return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation) - } - - 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 - } - } - - private findFrameElement(element: Element, submitter?: HTMLElement) { - const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame") - if (id && id != "_top") { - const frame = this.element.querySelector(`#${id}:not([disabled])`) - if (frame instanceof FrameElement) { - return frame - } - } - } -} diff --git a/src/core/frames/frame_renderer.ts b/src/core/frames/frame_renderer.js similarity index 70% rename from src/core/frames/frame_renderer.ts rename to src/core/frames/frame_renderer.js index 4f9a17253..1384465d6 100644 --- a/src/core/frames/frame_renderer.ts +++ b/src/core/frames/frame_renderer.js @@ -1,16 +1,8 @@ -import { FrameElement } from "../../elements/frame_element" import { activateScriptElement, nextAnimationFrame } from "../../util" -import { Render, Renderer } from "../renderer" -import { Snapshot } from "../snapshot" +import { Renderer } from "../renderer" -export interface FrameRendererDelegate { - willRenderFrame(currentElement: FrameElement, newElement: FrameElement): void -} - -export class FrameRenderer extends Renderer { - private readonly delegate: FrameRendererDelegate - - static renderElement(currentElement: FrameElement, newElement: FrameElement) { +export class FrameRenderer extends Renderer { + static renderElement(currentElement, newElement) { const destinationRange = document.createRange() destinationRange.selectNodeContents(currentElement) destinationRange.deleteContents() @@ -23,14 +15,7 @@ export class FrameRenderer extends Renderer { } } - constructor( - delegate: FrameRendererDelegate, - currentSnapshot: Snapshot, - newSnapshot: Snapshot, - renderElement: Render, - isPreview: boolean, - willRender = true - ) { + constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) { super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender) this.delegate = delegate } @@ -82,7 +67,7 @@ export class FrameRenderer extends Renderer { } } -function readScrollLogicalPosition(value: string | null, defaultValue: ScrollLogicalPosition): ScrollLogicalPosition { +function readScrollLogicalPosition(value, defaultValue) { if (value == "end" || value == "start" || value == "center" || value == "nearest") { return value } else { @@ -90,7 +75,7 @@ function readScrollLogicalPosition(value: string | null, defaultValue: ScrollLog } } -function readScrollBehavior(value: string | null, defaultValue: ScrollBehavior): ScrollBehavior { +function readScrollBehavior(value, defaultValue) { if (value == "auto" || value == "smooth") { return value } else { diff --git a/src/core/frames/frame_view.js b/src/core/frames/frame_view.js new file mode 100644 index 000000000..e65f64a77 --- /dev/null +++ b/src/core/frames/frame_view.js @@ -0,0 +1,12 @@ +import { Snapshot } from "../snapshot" +import { View } from "../view" + +export class FrameView extends View { + missing() { + this.element.innerHTML = `Content missing` + } + + get snapshot() { + return new Snapshot(this.element) + } +} diff --git a/src/core/frames/frame_view.ts b/src/core/frames/frame_view.ts deleted file mode 100644 index b19714f35..000000000 --- a/src/core/frames/frame_view.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { FrameElement } from "../../elements" -import { Snapshot } from "../snapshot" -import { View, ViewRenderOptions } from "../view" - -export type FrameViewRenderOptions = ViewRenderOptions - -export class FrameView extends View { - missing() { - this.element.innerHTML = `Content missing` - } - - get snapshot() { - return new Snapshot(this.element) - } -} diff --git a/src/core/frames/link_interceptor.ts b/src/core/frames/link_interceptor.js similarity index 65% rename from src/core/frames/link_interceptor.ts rename to src/core/frames/link_interceptor.js index 8f2e13f40..9b9249544 100644 --- a/src/core/frames/link_interceptor.ts +++ b/src/core/frames/link_interceptor.js @@ -1,16 +1,5 @@ -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) { + constructor(delegate, element) { this.delegate = delegate this.element = element } @@ -27,7 +16,7 @@ export class LinkInterceptor { document.removeEventListener("turbo:before-visit", this.willVisit) } - clickBubbled = (event: Event) => { + clickBubbled = (event) => { if (this.respondsToEventTarget(event.target)) { this.clickEvent = event } else { @@ -35,7 +24,7 @@ export class LinkInterceptor { } } - linkClicked = ((event: TurboClickEvent) => { + linkClicked = (event) => { 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() @@ -44,13 +33,13 @@ export class LinkInterceptor { } } delete this.clickEvent - }) + } - willVisit = ((_event: TurboBeforeVisitEvent) => { + willVisit = (_event) => { delete this.clickEvent - }) + } - respondsToEventTarget(target: EventTarget | null) { + respondsToEventTarget(target) { 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/index.ts b/src/core/index.js similarity index 67% rename from src/core/index.ts rename to src/core/index.js index 7b7507c3f..2a3dd8fe5 100644 --- a/src/core/index.ts +++ b/src/core/index.js @@ -1,10 +1,5 @@ -import { Adapter } from "./native/adapter" -import { FormMode, Session } from "./session" +import { Session } from "./session" import { Cache } from "./cache" -import { Locatable } from "./url" -import { StreamMessage } from "./streams/stream_message" -import { StreamSource } from "./types" -import { VisitOptions } from "./drive/visit" import { PageRenderer } from "./drive/page_renderer" import { PageSnapshot } from "./drive/page_snapshot" import { FrameRenderer } from "./frames/frame_renderer" @@ -14,24 +9,8 @@ const session = new Session() const cache = new Cache(session) const { navigator } = session export { navigator, session, cache, PageRenderer, PageSnapshot, FrameRenderer } -export type { - TurboBeforeCacheEvent, - TurboBeforeRenderEvent, - TurboBeforeVisitEvent, - TurboClickEvent, - TurboBeforeFrameRenderEvent, - TurboFrameLoadEvent, - TurboFrameRenderEvent, - TurboLoadEvent, - TurboRenderEvent, - TurboVisitEvent, -} from "./session" - -export type { TurboSubmitStartEvent, TurboSubmitEndEvent } from "./drive/form_submission" -export type { TurboFrameMissingEvent } from "./frames/frame_controller" export { StreamActions } from "./streams/stream_actions" -export type { TurboStreamAction, TurboStreamActions } from "./streams/stream_actions" /** * Starts the main session. @@ -47,7 +26,7 @@ export function start() { * * @param adapter Adapter to register */ -export function registerAdapter(adapter: Adapter) { +export function registerAdapter(adapter) { session.registerAdapter(adapter) } @@ -65,7 +44,7 @@ export function registerAdapter(adapter: Adapter) { * @param options.snapshotHTML Cached snapshot to render * @param options.response Response of the specified location */ -export function visit(location: Locatable, options?: Partial) { +export function visit(location, options) { session.visit(location, options) } @@ -74,7 +53,7 @@ export function visit(location: Locatable, options?: Partial) { * * @param source Stream source to connect */ -export function connectStreamSource(source: StreamSource) { +export function connectStreamSource(source) { session.connectStreamSource(source) } @@ -83,7 +62,7 @@ export function connectStreamSource(source: StreamSource) { * * @param source Stream source to disconnect */ -export function disconnectStreamSource(source: StreamSource) { +export function disconnectStreamSource(source) { session.disconnectStreamSource(source) } @@ -93,7 +72,7 @@ export function disconnectStreamSource(source: StreamSource) { * * @param message Message to render */ -export function renderStreamMessage(message: StreamMessage | string) { +export function renderStreamMessage(message) { session.renderStreamMessage(message) } @@ -120,16 +99,14 @@ export function clearCache() { * * @param delay Time to delay in milliseconds */ -export function setProgressBarDelay(delay: number) { +export function setProgressBarDelay(delay) { session.setProgressBarDelay(delay) } -export function setConfirmMethod( - confirmMethod: (message: string, element: HTMLFormElement, submitter: HTMLElement | undefined) => Promise -) { +export function setConfirmMethod(confirmMethod) { FormSubmission.confirmMethod = confirmMethod } -export function setFormMode(mode: FormMode) { +export function setFormMode(mode) { session.setFormMode(mode) } diff --git a/src/core/native/adapter.ts b/src/core/native/adapter.ts deleted file mode 100644 index 3ecfd9b22..000000000 --- a/src/core/native/adapter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Visit, VisitOptions } from "../drive/visit" -import { FormSubmission } from "../drive/form_submission" -import { ReloadReason } from "./browser_adapter" - -export interface Adapter { - visitProposedToLocation(location: URL, options?: Partial): void - visitStarted(visit: Visit): void - visitCompleted(visit: Visit): void - visitFailed(visit: Visit): void - visitRequestStarted(visit: Visit): void - visitRequestCompleted(visit: Visit): void - visitRequestFailedWithStatusCode(visit: Visit, statusCode: number): void - visitRequestFinished(visit: Visit): void - visitRendered(visit: Visit): void - formSubmissionStarted?(formSubmission: FormSubmission): void - formSubmissionFinished?(formSubmission: FormSubmission): void - pageInvalidated(reason: ReloadReason): void -} diff --git a/src/core/native/browser_adapter.ts b/src/core/native/browser_adapter.js similarity index 64% rename from src/core/native/browser_adapter.ts rename to src/core/native/browser_adapter.js index ca268e341..7505d7f8f 100644 --- a/src/core/native/browser_adapter.ts +++ b/src/core/native/browser_adapter.js @@ -1,40 +1,26 @@ -import { Adapter } from "./adapter" import { ProgressBar } from "../drive/progress_bar" -import { SystemStatusCode, Visit, VisitOptions } from "../drive/visit" -import { FormSubmission } from "../drive/form_submission" -import { Session } from "../session" +import { SystemStatusCode } from "../drive/visit" import { uuid, dispatch } from "../../util" -export type ReloadReason = StructuredReason | undefined -interface StructuredReason { - reason: string - context?: { [key: string]: any } -} - -export class BrowserAdapter implements Adapter { - readonly session: Session - readonly progressBar = new ProgressBar() +export class BrowserAdapter { + progressBar = new ProgressBar() - visitProgressBarTimeout?: number - formProgressBarTimeout?: number - location?: URL - - constructor(session: Session) { + constructor(session) { this.session = session } - visitProposedToLocation(location: URL, options?: Partial) { + visitProposedToLocation(location, options) { this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options) } - visitStarted(visit: Visit) { + visitStarted(visit) { this.location = visit.location visit.loadCachedSnapshot() visit.issueRequest() visit.goToSamePageAnchor() } - visitRequestStarted(visit: Visit) { + visitRequestStarted(visit) { this.progressBar.setValue(0) if (visit.hasCachedSnapshot() || visit.action != "restore") { this.showVisitProgressBarAfterDelay() @@ -43,11 +29,11 @@ export class BrowserAdapter implements Adapter { } } - visitRequestCompleted(visit: Visit) { + visitRequestCompleted(visit) { visit.loadResponse() } - visitRequestFailedWithStatusCode(visit: Visit, statusCode: number) { + visitRequestFailedWithStatusCode(visit, statusCode) { switch (statusCode) { case SystemStatusCode.networkFailure: case SystemStatusCode.timeoutFailure: @@ -55,35 +41,37 @@ export class BrowserAdapter implements Adapter { return this.reload({ reason: "request_failed", context: { - statusCode, - }, + statusCode + } }) default: return visit.loadResponse() } } - visitRequestFinished(_visit: Visit) { + visitRequestFinished(_visit) { this.progressBar.setValue(1) this.hideVisitProgressBar() } - visitCompleted(_visit: Visit) {} + visitCompleted(_visit) {} - pageInvalidated(reason: ReloadReason) { + pageInvalidated(reason) { this.reload(reason) } - visitFailed(_visit: Visit) {} + visitFailed(_visit) {} + + visitRendered(_visit) {} - visitRendered(_visit: Visit) {} + // Form Submission Delegate - formSubmissionStarted(_formSubmission: FormSubmission) { + formSubmissionStarted(_formSubmission) { this.progressBar.setValue(0) this.showFormProgressBarAfterDelay() } - formSubmissionFinished(_formSubmission: FormSubmission) { + formSubmissionFinished(_formSubmission) { this.progressBar.setValue(1) this.hideFormProgressBar() } @@ -120,7 +108,7 @@ export class BrowserAdapter implements Adapter { this.progressBar.show() } - reload(reason: ReloadReason) { + reload(reason) { dispatch("turbo:reload", { detail: reason }) window.location.href = this.location?.toString() || window.location.href diff --git a/src/core/renderer.js b/src/core/renderer.js new file mode 100644 index 000000000..12d87927e --- /dev/null +++ b/src/core/renderer.js @@ -0,0 +1,86 @@ +import { Bardo } from "./bardo" + +export class Renderer { + #activeElement = null + + constructor(currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) { + this.currentSnapshot = currentSnapshot + this.newSnapshot = newSnapshot + this.isPreview = isPreview + this.willRender = willRender + this.renderElement = renderElement + this.promise = new Promise((resolve, reject) => (this.resolvingFunctions = { resolve, reject })) + } + + get shouldRender() { + return true + } + + get reloadReason() { + return + } + + prepareToRender() { + return + } + + render() { + // Abstract method + } + + finishRendering() { + if (this.resolvingFunctions) { + this.resolvingFunctions.resolve() + delete this.resolvingFunctions + } + } + + async preservingPermanentElements(callback) { + await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback) + } + + focusFirstAutofocusableElement() { + const element = this.connectedSnapshot.firstAutofocusableElement + if (elementIsFocusable(element)) { + element.focus() + } + } + + // Bardo delegate + + enteringBardo(currentPermanentElement) { + if (this.#activeElement) return + + if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) { + this.#activeElement = this.currentSnapshot.activeElement + } + } + + leavingBardo(currentPermanentElement) { + if (currentPermanentElement.contains(this.#activeElement) && this.#activeElement instanceof HTMLElement) { + this.#activeElement.focus() + + this.#activeElement = null + } + } + + get connectedSnapshot() { + return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot + } + + get currentElement() { + return this.currentSnapshot.element + } + + get newElement() { + return this.newSnapshot.element + } + + get permanentElementMap() { + return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot) + } +} + +function elementIsFocusable(element) { + return element && typeof element.focus == "function" +} diff --git a/src/core/renderer.ts b/src/core/renderer.ts deleted file mode 100644 index e77be9d34..000000000 --- a/src/core/renderer.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Bardo, BardoDelegate } from "./bardo" -import { Snapshot } from "./snapshot" -import { ReloadReason } from "./native/browser_adapter" - -type ResolvingFunctions = { - resolve(value: T | PromiseLike): void - reject(reason?: any): void -} - -export type Render = (currentElement: E, newElement: E) => void - -export abstract class Renderer = Snapshot> implements BardoDelegate { - readonly currentSnapshot: S - readonly newSnapshot: S - readonly isPreview: boolean - readonly willRender: boolean - readonly promise: Promise - renderElement: Render - private resolvingFunctions?: ResolvingFunctions - private activeElement: Element | null = null - - constructor(currentSnapshot: S, newSnapshot: S, renderElement: Render, isPreview: boolean, willRender = true) { - this.currentSnapshot = currentSnapshot - this.newSnapshot = newSnapshot - this.isPreview = isPreview - this.willRender = willRender - this.renderElement = renderElement - this.promise = new Promise((resolve, reject) => (this.resolvingFunctions = { resolve, reject })) - } - - get shouldRender() { - return true - } - - get reloadReason(): ReloadReason { - return - } - - prepareToRender() { - return - } - - abstract render(): Promise - - finishRendering() { - if (this.resolvingFunctions) { - this.resolvingFunctions.resolve() - delete this.resolvingFunctions - } - } - - async preservingPermanentElements(callback: () => void) { - await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback) - } - - focusFirstAutofocusableElement() { - const element = this.connectedSnapshot.firstAutofocusableElement - if (elementIsFocusable(element)) { - element.focus() - } - } - - // Bardo delegate - - enteringBardo(currentPermanentElement: Element) { - if (this.activeElement) return - - if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) { - this.activeElement = this.currentSnapshot.activeElement - } - } - - leavingBardo(currentPermanentElement: Element) { - if (currentPermanentElement.contains(this.activeElement) && this.activeElement instanceof HTMLElement) { - this.activeElement.focus() - - this.activeElement = null - } - } - - get connectedSnapshot() { - return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot - } - - get currentElement() { - return this.currentSnapshot.element - } - - get newElement() { - return this.newSnapshot.element - } - - get permanentElementMap() { - return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot) - } -} - -function elementIsFocusable(element: any): element is { focus: () => void } { - return element && typeof element.focus == "function" -} diff --git a/src/core/session.ts b/src/core/session.js similarity index 54% rename from src/core/session.ts rename to src/core/session.js index 31eab98f4..44edc7856 100644 --- a/src/core/session.ts +++ b/src/core/session.js @@ -1,75 +1,44 @@ -import { Adapter } from "./native/adapter" -import { BrowserAdapter, ReloadReason } from "./native/browser_adapter" +import { BrowserAdapter } from "./native/browser_adapter" import { CacheObserver } from "../observers/cache_observer" -import { FormSubmitObserver, FormSubmitObserverDelegate } from "../observers/form_submit_observer" +import { FormSubmitObserver } from "../observers/form_submit_observer" import { FrameRedirector } from "./frames/frame_redirector" -import { History, HistoryDelegate } from "./drive/history" -import { LinkClickObserver, LinkClickObserverDelegate } from "../observers/link_click_observer" -import { FormLinkClickObserver, FormLinkClickObserverDelegate } from "../observers/form_link_click_observer" -import { getAction, expandURL, locationIsVisitable, Locatable } from "./url" -import { Navigator, NavigatorDelegate } from "./drive/navigator" -import { PageObserver, PageObserverDelegate } from "../observers/page_observer" +import { History } from "./drive/history" +import { LinkClickObserver } from "../observers/link_click_observer" +import { FormLinkClickObserver } from "../observers/form_link_click_observer" +import { getAction, expandURL, locationIsVisitable } from "./url" +import { Navigator } from "./drive/navigator" +import { PageObserver } from "../observers/page_observer" import { ScrollObserver } from "../observers/scroll_observer" import { StreamMessage } from "./streams/stream_message" import { StreamMessageRenderer } from "./streams/stream_message_renderer" import { StreamObserver } from "../observers/stream_observer" -import { Action, Position, StreamSource } from "./types" import { clearBusyState, dispatch, findClosestRecursively, getVisitAction, markAsBusy } from "../util" -import { PageView, PageViewDelegate, PageViewRenderOptions } from "./drive/page_view" -import { Visit, VisitOptions } from "./drive/visit" -import { PageSnapshot } from "./drive/page_snapshot" +import { PageView } from "./drive/page_view" import { FrameElement } from "../elements/frame_element" -import { FrameViewRenderOptions } from "./frames/frame_view" -import { FetchResponse } from "../http/fetch_response" -import { Preloader, PreloaderDelegate } from "./drive/preloader" - -export type FormMode = "on" | "off" | "optin" -export type TimingData = unknown -export type TurboBeforeCacheEvent = CustomEvent -export type TurboBeforeRenderEvent = CustomEvent< - { newBody: HTMLBodyElement; isPreview: boolean } & PageViewRenderOptions -> -export type TurboBeforeVisitEvent = CustomEvent<{ url: string }> -export type TurboClickEvent = CustomEvent<{ url: string; originalEvent: MouseEvent }> -export type TurboFrameLoadEvent = CustomEvent -export type TurboBeforeFrameRenderEvent = CustomEvent<{ newFrame: FrameElement } & FrameViewRenderOptions> -export type TurboFrameRenderEvent = CustomEvent<{ fetchResponse: FetchResponse }> -export type TurboLoadEvent = CustomEvent<{ url: string; timing: TimingData }> -export type TurboRenderEvent = CustomEvent<{ isPreview: boolean }> -export type TurboVisitEvent = CustomEvent<{ url: string; action: Action }> - -export class Session - implements - FormSubmitObserverDelegate, - HistoryDelegate, - FormLinkClickObserverDelegate, - LinkClickObserverDelegate, - NavigatorDelegate, - PageObserverDelegate, - PageViewDelegate, - PreloaderDelegate -{ - readonly navigator = new Navigator(this) - readonly history = new History(this) - readonly preloader = new Preloader(this) - readonly view = new PageView(this, document.documentElement as HTMLBodyElement) - adapter: Adapter = new BrowserAdapter(this) - - readonly pageObserver = new PageObserver(this) - readonly cacheObserver = new CacheObserver() - readonly linkClickObserver = new LinkClickObserver(this, window) - readonly formSubmitObserver = new FormSubmitObserver(this, document) - 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 streamMessageRenderer = new StreamMessageRenderer() +import { Preloader } from "./drive/preloader" + +export class Session { + navigator = new Navigator(this) + history = new History(this) + preloader = new Preloader(this) + view = new PageView(this, document.documentElement) + adapter = new BrowserAdapter(this) + + pageObserver = new PageObserver(this) + cacheObserver = new CacheObserver() + linkClickObserver = new LinkClickObserver(this, window) + formSubmitObserver = new FormSubmitObserver(this, document) + scrollObserver = new ScrollObserver(this) + streamObserver = new StreamObserver(this) + formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement) + frameRedirector = new FrameRedirector(this, document.documentElement) + streamMessageRenderer = new StreamMessageRenderer() drive = true enabled = true progressBarDelay = 500 started = false - formMode: FormMode = "on" + formMode = "on" start() { if (!this.started) { @@ -107,11 +76,11 @@ export class Session } } - registerAdapter(adapter: Adapter) { + registerAdapter(adapter) { this.adapter = adapter } - visit(location: Locatable, options: Partial = {}) { + visit(location, options = {}) { const frameElement = options.frame ? document.getElementById(options.frame) : null if (frameElement instanceof FrameElement) { @@ -122,15 +91,15 @@ export class Session } } - connectStreamSource(source: StreamSource) { + connectStreamSource(source) { this.streamObserver.connectStreamSource(source) } - disconnectStreamSource(source: StreamSource) { + disconnectStreamSource(source) { this.streamObserver.disconnectStreamSource(source) } - renderStreamMessage(message: StreamMessage | string) { + renderStreamMessage(message) { this.streamMessageRenderer.render(StreamMessage.wrap(message)) } @@ -138,11 +107,11 @@ export class Session this.view.clearSnapshotCache() } - setProgressBarDelay(delay: number) { + setProgressBarDelay(delay) { this.progressBarDelay = delay } - setFormMode(mode: FormMode) { + setFormMode(mode) { this.formMode = mode } @@ -156,28 +125,28 @@ export class Session // History delegate - historyPoppedToLocationWithRestorationIdentifier(location: URL, restorationIdentifier: string) { + historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) { if (this.enabled) { this.navigator.startVisit(location, restorationIdentifier, { action: "restore", - historyChanged: true, + historyChanged: true }) } else { this.adapter.pageInvalidated({ - reason: "turbo_disabled", + reason: "turbo_disabled" }) } } // Scroll observer delegate - scrollPositionChanged(position: Position) { + scrollPositionChanged(position) { this.history.updateRestorationData({ scrollPosition: position }) } // Form click observer delegate - willSubmitFormLinkToLocation(link: Element, location: URL): boolean { + willSubmitFormLinkToLocation(link, location) { return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) } @@ -185,7 +154,7 @@ export class Session // Link click observer delegate - willFollowLinkToLocation(link: Element, location: URL, event: MouseEvent) { + willFollowLinkToLocation(link, location, event) { return ( this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && @@ -193,7 +162,7 @@ export class Session ) } - followedLinkToLocation(link: Element, location: URL) { + followedLinkToLocation(link, location) { const action = this.getActionForLink(link) const acceptsStreamResponse = link.hasAttribute("data-turbo-stream") @@ -202,16 +171,18 @@ export class Session // Navigator delegate - allowsVisitingLocationWithAction(location: URL, action?: Action) { + allowsVisitingLocationWithAction(location, action) { return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location) } - visitProposedToLocation(location: URL, options: Partial) { + visitProposedToLocation(location, options) { extendURLWithDeprecatedProperties(location) this.adapter.visitProposedToLocation(location, options) } - visitStarted(visit: Visit) { + // Visit delegate + + visitStarted(visit) { if (!visit.acceptsStreamResponse) { markAsBusy(document.documentElement) } @@ -221,22 +192,22 @@ export class Session } } - visitCompleted(visit: Visit) { + visitCompleted(visit) { clearBusyState(document.documentElement) this.notifyApplicationAfterPageLoad(visit.getTimingMetrics()) } - locationWithActionIsSamePage(location: URL, action?: Action): boolean { + locationWithActionIsSamePage(location, action) { return this.navigator.locationWithActionIsSamePage(location, action) } - visitScrolledToSamePageLocation(oldURL: URL, newURL: URL) { + visitScrolledToSamePageLocation(oldURL, newURL) { this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) } // Form submit observer delegate - willSubmitForm(form: HTMLFormElement, submitter?: HTMLElement): boolean { + willSubmitForm(form, submitter) { const action = getAction(form, submitter) return ( @@ -245,7 +216,7 @@ export class Session ) } - formSubmitted(form: HTMLFormElement, submitter?: HTMLElement) { + formSubmitted(form, submitter) { this.navigator.submitForm(form, submitter) } @@ -266,7 +237,7 @@ export class Session // Stream observer delegate - receivedMessageFromStream(message: StreamMessage) { + receivedMessageFromStream(message) { this.renderStreamMessage(message) } @@ -278,11 +249,11 @@ export class Session } } - allowsImmediateRender({ element }: PageSnapshot, isPreview: boolean, options: PageViewRenderOptions) { + allowsImmediateRender({ element }, isPreview, options) { const event = this.notifyApplicationBeforeRender(element, isPreview, options) const { defaultPrevented, - detail: { render }, + detail: { render } } = event if (this.view.renderer && render) { @@ -292,105 +263,105 @@ export class Session return !defaultPrevented } - viewRenderedSnapshot(_snapshot: PageSnapshot, isPreview: boolean) { + viewRenderedSnapshot(_snapshot, isPreview) { this.view.lastRenderedLocation = this.history.location this.notifyApplicationAfterRender(isPreview) } - preloadOnLoadLinksForView(element: Element) { + preloadOnLoadLinksForView(element) { this.preloader.preloadOnLoadLinksForView(element) } - viewInvalidated(reason: ReloadReason) { + viewInvalidated(reason) { this.adapter.pageInvalidated(reason) } // Frame element - frameLoaded(frame: FrameElement) { + frameLoaded(frame) { this.notifyApplicationAfterFrameLoad(frame) } - frameRendered(fetchResponse: FetchResponse, frame: FrameElement) { + frameRendered(fetchResponse, frame) { this.notifyApplicationAfterFrameRender(fetchResponse, frame) } // Application events - applicationAllowsFollowingLinkToLocation(link: Element, location: URL, ev: MouseEvent) { + applicationAllowsFollowingLinkToLocation(link, location, ev) { const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev) return !event.defaultPrevented } - applicationAllowsVisitingLocation(location: URL) { + applicationAllowsVisitingLocation(location) { const event = this.notifyApplicationBeforeVisitingLocation(location) return !event.defaultPrevented } - notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL, event: MouseEvent) { - return dispatch("turbo:click", { + notifyApplicationAfterClickingLinkToLocation(link, location, event) { + return dispatch("turbo:click", { target: link, detail: { url: location.href, originalEvent: event }, - cancelable: true, + cancelable: true }) } - notifyApplicationBeforeVisitingLocation(location: URL) { - return dispatch("turbo:before-visit", { + notifyApplicationBeforeVisitingLocation(location) { + return dispatch("turbo:before-visit", { detail: { url: location.href }, - cancelable: true, + cancelable: true }) } - notifyApplicationAfterVisitingLocation(location: URL, action: Action) { - return dispatch("turbo:visit", { detail: { url: location.href, action } }) + notifyApplicationAfterVisitingLocation(location, action) { + return dispatch("turbo:visit", { detail: { url: location.href, action } }) } notifyApplicationBeforeCachingSnapshot() { - return dispatch("turbo:before-cache") + return dispatch("turbo:before-cache") } - notifyApplicationBeforeRender(newBody: HTMLBodyElement, isPreview: boolean, options: PageViewRenderOptions) { - return dispatch("turbo:before-render", { + notifyApplicationBeforeRender(newBody, isPreview, options) { + return dispatch("turbo:before-render", { detail: { newBody, isPreview, ...options }, - cancelable: true, + cancelable: true }) } - notifyApplicationAfterRender(isPreview: boolean) { - return dispatch("turbo:render", { detail: { isPreview } }) + notifyApplicationAfterRender(isPreview) { + return dispatch("turbo:render", { detail: { isPreview } }) } - notifyApplicationAfterPageLoad(timing: TimingData = {}) { - return dispatch("turbo:load", { - detail: { url: this.location.href, timing }, + notifyApplicationAfterPageLoad(timing = {}) { + return dispatch("turbo:load", { + detail: { url: this.location.href, timing } }) } - notifyApplicationAfterVisitingSamePageLocation(oldURL: URL, newURL: URL) { + notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) { dispatchEvent( new HashChangeEvent("hashchange", { oldURL: oldURL.toString(), - newURL: newURL.toString(), + newURL: newURL.toString() }) ) } - notifyApplicationAfterFrameLoad(frame: FrameElement) { - return dispatch("turbo:frame-load", { target: frame }) + notifyApplicationAfterFrameLoad(frame) { + return dispatch("turbo:frame-load", { target: frame }) } - notifyApplicationAfterFrameRender(fetchResponse: FetchResponse, frame: FrameElement) { - return dispatch("turbo:frame-render", { + notifyApplicationAfterFrameRender(fetchResponse, frame) { + return dispatch("turbo:frame-render", { detail: { fetchResponse }, target: frame, - cancelable: true, + cancelable: true }) } // Helpers - submissionIsNavigatable(form: HTMLFormElement, submitter?: HTMLElement): boolean { + submissionIsNavigatable(form, submitter) { if (this.formMode == "off") { return false } else { @@ -404,7 +375,7 @@ export class Session } } - elementIsNavigatable(element: Element): boolean { + elementIsNavigatable(element) { const container = findClosestRecursively(element, "[data-turbo]") const withinFrame = findClosestRecursively(element, "turbo-frame") @@ -428,7 +399,7 @@ export class Session // Private - getActionForLink(link: Element): Action { + getActionForLink(link) { return getVisitAction(link) || "advance" } @@ -448,7 +419,7 @@ export class Session // older adapters which do not expect URL objects. We should // consider removing this support at some point in the future. -function extendURLWithDeprecatedProperties(url: URL) { +function extendURLWithDeprecatedProperties(url) { Object.defineProperties(url, deprecatedLocationPropertyDescriptors) } @@ -456,6 +427,6 @@ const deprecatedLocationPropertyDescriptors = { absoluteURL: { get() { return this.toString() - }, - }, + } + } } diff --git a/src/core/snapshot.ts b/src/core/snapshot.js similarity index 70% rename from src/core/snapshot.ts rename to src/core/snapshot.js index 7d5431117..2a5c1dbd6 100644 --- a/src/core/snapshot.ts +++ b/src/core/snapshot.js @@ -1,7 +1,5 @@ -export class Snapshot { - readonly element: E - - constructor(element: E) { +export class Snapshot { + constructor(element) { this.element = element } @@ -13,11 +11,11 @@ export class Snapshot { return [...this.element.children] } - hasAnchor(anchor: string | undefined) { + hasAnchor(anchor) { return this.getElementForAnchor(anchor) != null } - getElementForAnchor(anchor: string | undefined) { + getElementForAnchor(anchor) { return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null } @@ -40,12 +38,12 @@ export class Snapshot { return queryPermanentElementsAll(this.element) } - getPermanentElementById(id: string) { + getPermanentElementById(id) { return getPermanentElementById(this.element, id) } - getPermanentElementMapForSnapshot(snapshot: Snapshot) { - const permanentElementMap: PermanentElementMap = {} + getPermanentElementMapForSnapshot(snapshot) { + const permanentElementMap = {} for (const currentPermanentElement of this.permanentElements) { const { id } = currentPermanentElement @@ -59,12 +57,10 @@ export class Snapshot { } } -export function getPermanentElementById(node: ParentNode, id: string) { +export function getPermanentElementById(node, id) { return node.querySelector(`#${id}[data-turbo-permanent]`) } -export function queryPermanentElementsAll(node: ParentNode) { +export function queryPermanentElementsAll(node) { return node.querySelectorAll("[id][data-turbo-permanent]") } - -export type PermanentElementMap = Record diff --git a/src/core/streams/stream_actions.ts b/src/core/streams/stream_actions.js similarity index 76% rename from src/core/streams/stream_actions.ts rename to src/core/streams/stream_actions.js index e4a619f7c..7b06f5b84 100644 --- a/src/core/streams/stream_actions.ts +++ b/src/core/streams/stream_actions.js @@ -1,9 +1,4 @@ -import { StreamElement } from "../../elements/stream_element" - -export type TurboStreamAction = (this: StreamElement) => void -export type TurboStreamActions = { [action: string]: TurboStreamAction } - -export const StreamActions: TurboStreamActions = { +export const StreamActions = { after() { this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e.nextSibling)) }, @@ -35,5 +30,5 @@ export const StreamActions: TurboStreamActions = { targetElement.innerHTML = "" targetElement.append(this.templateContent) }) - }, + } } diff --git a/src/core/streams/stream_message.ts b/src/core/streams/stream_message.js similarity index 59% rename from src/core/streams/stream_message.ts rename to src/core/streams/stream_message.js index e6ca389a4..7a3a65b3c 100644 --- a/src/core/streams/stream_message.ts +++ b/src/core/streams/stream_message.js @@ -1,11 +1,9 @@ -import { StreamElement } from "../../elements/stream_element" import { activateScriptElement, createDocumentFragment } from "../../util" export class StreamMessage { - static readonly contentType = "text/vnd.turbo-stream.html" - readonly fragment: DocumentFragment + static contentType = "text/vnd.turbo-stream.html" - static wrap(message: StreamMessage | string) { + static wrap(message) { if (typeof message == "string") { return new this(createDocumentFragment(message)) } else { @@ -13,13 +11,13 @@ export class StreamMessage { } } - constructor(fragment: DocumentFragment) { + constructor(fragment) { this.fragment = importStreamElements(fragment) } } -function importStreamElements(fragment: DocumentFragment): DocumentFragment { - for (const element of fragment.querySelectorAll("turbo-stream")) { +function importStreamElements(fragment) { + for (const element of fragment.querySelectorAll("turbo-stream")) { const streamElement = document.importNode(element, true) for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll("script")) { diff --git a/src/core/streams/stream_message_renderer.ts b/src/core/streams/stream_message_renderer.js similarity index 51% rename from src/core/streams/stream_message_renderer.ts rename to src/core/streams/stream_message_renderer.js index 549e4de8d..c57a695b0 100644 --- a/src/core/streams/stream_message_renderer.ts +++ b/src/core/streams/stream_message_renderer.js @@ -1,29 +1,29 @@ -import { StreamMessage } from "./stream_message" -import { StreamElement } from "../../elements/stream_element" -import { Bardo, BardoDelegate } from "../bardo" -import { PermanentElementMap, getPermanentElementById, queryPermanentElementsAll } from "../snapshot" +import { Bardo } from "../bardo" +import { getPermanentElementById, queryPermanentElementsAll } from "../snapshot" -export class StreamMessageRenderer implements BardoDelegate { - render({ fragment }: StreamMessage) { +export class StreamMessageRenderer { + render({ fragment }) { Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), () => document.documentElement.appendChild(fragment) ) } - enteringBardo(currentPermanentElement: Element, newPermanentElement: Element) { + // Bardo delegate + + enteringBardo(currentPermanentElement, newPermanentElement) { newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true)) } leavingBardo() {} } -function getPermanentElementMapForFragment(fragment: DocumentFragment): PermanentElementMap { +function getPermanentElementMapForFragment(fragment) { const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement) - const permanentElementMap: PermanentElementMap = {} + const permanentElementMap = {} for (const permanentElementInDocument of permanentElementsInDocument) { const { id } = permanentElementInDocument - for (const streamElement of fragment.querySelectorAll("turbo-stream")) { + for (const streamElement of fragment.querySelectorAll("turbo-stream")) { const elementInStream = getPermanentElementById(streamElement.templateElement.content, id) if (elementInStream) { diff --git a/src/core/types.ts b/src/core/types.ts deleted file mode 100644 index 90d1817ea..000000000 --- a/src/core/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type Action = "advance" | "replace" | "restore" - -export type Position = { x: number; y: number } - -export type StreamSource = { - addEventListener( - type: "message", - listener: (event: MessageEvent) => void, - options?: boolean | AddEventListenerOptions - ): void - removeEventListener( - type: "message", - listener: (event: MessageEvent) => void, - options?: boolean | EventListenerOptions - ): void -} diff --git a/src/core/url.ts b/src/core/url.js similarity index 61% rename from src/core/url.ts rename to src/core/url.js index 0e45d8f2b..dcd50cf26 100644 --- a/src/core/url.ts +++ b/src/core/url.js @@ -1,10 +1,8 @@ -export type Locatable = URL | string - -export function expandURL(locatable: Locatable) { +export function expandURL(locatable) { return new URL(locatable.toString(), document.baseURI) } -export function getAnchor(url: URL) { +export function getAnchor(url) { let anchorMatch if (url.hash) { return url.hash.slice(1) @@ -14,54 +12,54 @@ export function getAnchor(url: URL) { } } -export function getAction(form: HTMLFormElement, submitter?: HTMLElement) { +export function getAction(form, submitter) { const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action return expandURL(action) } -export function getExtension(url: URL) { +export function getExtension(url) { return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "" } -export function isHTML(url: URL) { +export function isHTML(url) { return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/) } -export function isPrefixedBy(baseURL: URL, url: URL) { +export function isPrefixedBy(baseURL, url) { const prefix = getPrefix(url) return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix) } -export function locationIsVisitable(location: URL, rootLocation: URL) { +export function locationIsVisitable(location, rootLocation) { return isPrefixedBy(location, rootLocation) && isHTML(location) } -export function getRequestURL(url: URL) { +export function getRequestURL(url) { const anchor = getAnchor(url) return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href } -export function toCacheKey(url: URL) { +export function toCacheKey(url) { return getRequestURL(url) } -export function urlsAreEqual(left: string, right: string) { +export function urlsAreEqual(left, right) { return expandURL(left).href == expandURL(right).href } -function getPathComponents(url: URL) { +function getPathComponents(url) { return url.pathname.split("/").slice(1) } -function getLastPathComponent(url: URL) { +function getLastPathComponent(url) { return getPathComponents(url).slice(-1)[0] } -function getPrefix(url: URL) { +function getPrefix(url) { return addTrailingSlash(url.origin + url.pathname) } -function addTrailingSlash(value: string) { +function addTrailingSlash(value) { return value.endsWith("/") ? value : value + "/" } diff --git a/src/core/view.ts b/src/core/view.js similarity index 51% rename from src/core/view.ts rename to src/core/view.js index 20109a35d..ca81e8bdb 100644 --- a/src/core/view.ts +++ b/src/core/view.js @@ -1,43 +1,17 @@ -import { ReloadReason } from "./native/browser_adapter" -import { Renderer, Render } from "./renderer" -import { Snapshot } from "./snapshot" -import { Position } from "./types" import { getAnchor } from "./url" -export interface ViewRenderOptions { - resume: (value?: any) => void - render: Render -} - -export interface ViewDelegate> { - allowsImmediateRender(snapshot: S, isPreview: boolean, options: ViewRenderOptions): boolean - preloadOnLoadLinksForView(element: Element): void - viewRenderedSnapshot(snapshot: S, isPreview: boolean): void - viewInvalidated(reason: ReloadReason): void -} +export class View { + #resolveRenderPromise = (_value) => {} + #resolveInterceptionPromise = (_value) => {} -export abstract class View< - E extends Element, - S extends Snapshot = Snapshot, - R extends Renderer = Renderer, - D extends ViewDelegate = ViewDelegate -> { - readonly delegate: D - readonly element: E - renderer?: R - abstract readonly snapshot: S - renderPromise?: Promise - private resolveRenderPromise = (_value: any) => {} - private resolveInterceptionPromise = (_value: any) => {} - - constructor(delegate: D, element: E) { + constructor(delegate, element) { this.delegate = delegate this.element = element } // Scrolling - scrollToAnchor(anchor: string | undefined) { + scrollToAnchor(anchor) { const element = this.snapshot.getElementForAnchor(anchor) if (element) { this.scrollToElement(element) @@ -47,15 +21,15 @@ export abstract class View< } } - scrollToAnchorFromLocation(location: URL) { + scrollToAnchorFromLocation(location) { this.scrollToAnchor(getAnchor(location)) } - scrollToElement(element: Element) { + scrollToElement(element) { element.scrollIntoView() } - focusElement(element: Element) { + focusElement(element) { if (element instanceof HTMLElement) { if (element.hasAttribute("tabindex")) { element.focus() @@ -67,7 +41,7 @@ export abstract class View< } } - scrollToPosition({ x, y }: Position) { + scrollToPosition({ x, y }) { this.scrollRoot.scrollTo(x, y) } @@ -75,22 +49,22 @@ export abstract class View< this.scrollToPosition({ x: 0, y: 0 }) } - get scrollRoot(): { scrollTo(x: number, y: number): void } { + get scrollRoot() { return window } // Rendering - async render(renderer: R) { + async render(renderer) { const { isPreview, shouldRender, newSnapshot: snapshot } = renderer if (shouldRender) { try { - this.renderPromise = new Promise((resolve) => (this.resolveRenderPromise = resolve)) + this.renderPromise = new Promise((resolve) => (this.#resolveRenderPromise = resolve)) this.renderer = renderer await this.prepareToRenderSnapshot(renderer) - const renderInterception = new Promise((resolve) => (this.resolveInterceptionPromise = resolve)) - const options = { resume: this.resolveInterceptionPromise, render: this.renderer.renderElement } + const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve)) + const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement } const immediateRender = this.delegate.allowsImmediateRender(snapshot, isPreview, options) if (!immediateRender) await renderInterception @@ -100,7 +74,7 @@ export abstract class View< this.finishRenderingSnapshot(renderer) } finally { delete this.renderer - this.resolveRenderPromise(undefined) + this.#resolveRenderPromise(undefined) delete this.renderPromise } } else { @@ -108,16 +82,16 @@ export abstract class View< } } - invalidate(reason: ReloadReason) { + invalidate(reason) { this.delegate.viewInvalidated(reason) } - async prepareToRenderSnapshot(renderer: R) { + async prepareToRenderSnapshot(renderer) { this.markAsPreview(renderer.isPreview) await renderer.prepareToRender() } - markAsPreview(isPreview: boolean) { + markAsPreview(isPreview) { if (isPreview) { this.element.setAttribute("data-turbo-preview", "") } else { @@ -125,11 +99,11 @@ export abstract class View< } } - async renderSnapshot(renderer: R) { + async renderSnapshot(renderer) { await renderer.render() } - finishRenderingSnapshot(renderer: R) { + finishRenderingSnapshot(renderer) { renderer.finishRendering() } } diff --git a/src/elements/frame_element.ts b/src/elements/frame_element.js similarity index 67% rename from src/elements/frame_element.ts rename to src/elements/frame_element.js index 8a177a6d7..0e2bee917 100644 --- a/src/elements/frame_element.ts +++ b/src/elements/frame_element.js @@ -1,28 +1,6 @@ -import { FetchResponse } from "../http/fetch_response" -import { Snapshot } from "../core/snapshot" -import { LinkInterceptorDelegate } from "../core/frames/link_interceptor" -import { FormSubmitObserverDelegate } from "../observers/form_submit_observer" - -export enum FrameLoadingStyle { - eager = "eager", - lazy = "lazy", -} - -export type FrameElementObservedAttribute = keyof FrameElement & ("disabled" | "complete" | "loading" | "src") - -export interface FrameElementDelegate extends LinkInterceptorDelegate, FormSubmitObserverDelegate { - connect(): void - disconnect(): void - completeChanged(): void - loadingStyleChanged(): void - sourceURLChanged(): void - sourceURLReloaded(): Promise - disabledChanged(): void - loadResponse(response: FetchResponse): void - proposeVisitIfNavigatedWithAction(frame: FrameElement, element: Element, submitter?: HTMLElement): void - fetchResponseLoaded: (fetchResponse: FetchResponse) => void - visitCachedSnapshot: (snapshot: Snapshot) => void - isLoading: boolean +export const FrameLoadingStyle = { + eager: "eager", + lazy: "lazy" } /** @@ -42,12 +20,11 @@ export interface FrameElementDelegate extends LinkInterceptorDelegate, FormSubmi * */ export class FrameElement extends HTMLElement { - static delegateConstructor: new (element: FrameElement) => FrameElementDelegate + static delegateConstructor = undefined - loaded: Promise = Promise.resolve() - readonly delegate: FrameElementDelegate + loaded = Promise.resolve() - static get observedAttributes(): FrameElementObservedAttribute[] { + static get observedAttributes() { return ["disabled", "complete", "loading", "src"] } @@ -64,11 +41,11 @@ export class FrameElement extends HTMLElement { this.delegate.disconnect() } - reload(): Promise { + reload() { return this.delegate.sourceURLReloaded() } - attributeChangedCallback(name: string) { + attributeChangedCallback(name) { if (name == "loading") { this.delegate.loadingStyleChanged() } else if (name == "complete") { @@ -90,7 +67,7 @@ export class FrameElement extends HTMLElement { /** * Sets the URL to lazily load source HTML from */ - set src(value: string | null) { + set src(value) { if (value) { this.setAttribute("src", value) } else { @@ -101,14 +78,14 @@ export class FrameElement extends HTMLElement { /** * Determines if the element is loading */ - get loading(): FrameLoadingStyle { + get loading() { return frameLoadingStyleFromString(this.getAttribute("loading") || "") } /** * Sets the value of if the element is loading */ - set loading(value: FrameLoadingStyle) { + set loading(value) { if (value) { this.setAttribute("loading", value) } else { @@ -130,7 +107,7 @@ export class FrameElement extends HTMLElement { * * If disabled, no requests will be intercepted by the frame. */ - set disabled(value: boolean) { + set disabled(value) { if (value) { this.setAttribute("disabled", "") } else { @@ -152,7 +129,7 @@ export class FrameElement extends HTMLElement { * * If true, the frame will be scrolled into view automatically on update. */ - set autoscroll(value: boolean) { + set autoscroll(value) { if (value) { this.setAttribute("autoscroll", "") } else { @@ -186,7 +163,7 @@ export class FrameElement extends HTMLElement { } } -function frameLoadingStyleFromString(style: string) { +function frameLoadingStyleFromString(style) { switch (style.toLowerCase()) { case "lazy": return FrameLoadingStyle.lazy diff --git a/src/elements/index.ts b/src/elements/index.js similarity index 100% rename from src/elements/index.ts rename to src/elements/index.js diff --git a/src/elements/stream_element.ts b/src/elements/stream_element.js similarity index 84% rename from src/elements/stream_element.ts rename to src/elements/stream_element.js index e0213f97d..38f463fb3 100644 --- a/src/elements/stream_element.ts +++ b/src/elements/stream_element.js @@ -1,10 +1,6 @@ import { StreamActions } from "../core/streams/stream_actions" import { nextAnimationFrame } from "../util" -type Render = (currentElement: StreamElement) => Promise - -export type TurboBeforeStreamRenderEvent = CustomEvent<{ newStream: StreamElement; render: Render }> - //