From 6343df000ba9990d6baa9e08015bfc187a4c092c Mon Sep 17 00:00:00 2001
From: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com>
Date: Fri, 14 Jun 2024 07:38:56 +0200
Subject: [PATCH] Deleted Turbo.
---
static/turbo.es2017-esm.js | 6630 ------------------------------------
1 file changed, 6630 deletions(-)
delete mode 100644 static/turbo.es2017-esm.js
diff --git a/static/turbo.es2017-esm.js b/static/turbo.es2017-esm.js
deleted file mode 100644
index c5f15fa..0000000
--- a/static/turbo.es2017-esm.js
+++ /dev/null
@@ -1,6630 +0,0 @@
-/*!
-Turbo 8.0.4
-Copyright © 2024 37signals LLC
- */
-/**
- * The MIT License (MIT)
- *
- * Copyright (c) 2019 Javan Makhmali
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-;(function (prototype) {
- if (typeof prototype.requestSubmit == 'function') return
-
- prototype.requestSubmit = function (submitter) {
- if (submitter) {
- validateSubmitter(submitter, this)
- submitter.click()
- } else {
- submitter = document.createElement('input')
- submitter.type = 'submit'
- submitter.hidden = true
- this.appendChild(submitter)
- submitter.click()
- this.removeChild(submitter)
- }
- }
-
- function validateSubmitter(submitter, form) {
- submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'")
- submitter.type == 'submit' || raise(TypeError, 'The specified element is not a submit button')
- submitter.form == form ||
- raise(DOMException, 'The specified element is not owned by this form element', 'NotFoundError')
- }
-
- function raise(errorConstructor, message, name) {
- throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + '.', name)
- }
-})(HTMLFormElement.prototype)
-
-const submittersByForm = new WeakMap()
-
-function findSubmitterFromClickTarget(target) {
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null
- const candidate = element ? element.closest('input, button') : null
- return candidate?.type == 'submit' ? candidate : null
-}
-
-function clickCaptured(event) {
- const submitter = findSubmitterFromClickTarget(event.target)
-
- if (submitter && submitter.form) {
- submittersByForm.set(submitter.form, submitter)
- }
-}
-
-;(function () {
- if ('submitter' in Event.prototype) return
-
- let prototype = window.Event.prototype
- // Certain versions of Safari 15 have a bug where they won't
- // populate the submitter. This hurts TurboDrive's enable/disable detection.
- // See https://bugs.webkit.org/show_bug.cgi?id=229660
- if ('SubmitEvent' in window) {
- const prototypeOfSubmitEvent = window.SubmitEvent.prototype
-
- if (/Apple Computer/.test(navigator.vendor) && !('submitter' in prototypeOfSubmitEvent)) {
- prototype = prototypeOfSubmitEvent
- } else {
- return // polyfill not needed
- }
- }
-
- addEventListener('click', clickCaptured, true)
-
- Object.defineProperty(prototype, 'submitter', {
- get() {
- if (this.type == 'submit' && this.target instanceof HTMLFormElement) {
- return submittersByForm.get(this.target)
- }
- }
- })
-})()
-
-const FrameLoadingStyle = {
- eager: 'eager',
- lazy: 'lazy'
-}
-
-/**
- * Contains a fragment of HTML which is updated based on navigation within
- * it (e.g. via links or form submissions).
- *
- * @customElement turbo-frame
- * @example
- *
- *
- * Show all expanded messages in this frame.
- *
- *
- *
- *
- */
-class FrameElement extends HTMLElement {
- static delegateConstructor = undefined
-
- loaded = Promise.resolve()
-
- static get observedAttributes() {
- return ['disabled', 'loading', 'src']
- }
-
- constructor() {
- super()
- this.delegate = new FrameElement.delegateConstructor(this)
- }
-
- connectedCallback() {
- this.delegate.connect()
- }
-
- disconnectedCallback() {
- this.delegate.disconnect()
- }
-
- reload() {
- return this.delegate.sourceURLReloaded()
- }
-
- attributeChangedCallback(name) {
- if (name == 'loading') {
- this.delegate.loadingStyleChanged()
- } else if (name == 'src') {
- this.delegate.sourceURLChanged()
- } else if (name == 'disabled') {
- this.delegate.disabledChanged()
- }
- }
-
- /**
- * Gets the URL to lazily load source HTML from
- */
- get src() {
- return this.getAttribute('src')
- }
-
- /**
- * Sets the URL to lazily load source HTML from
- */
- set src(value) {
- if (value) {
- this.setAttribute('src', value)
- } else {
- this.removeAttribute('src')
- }
- }
-
- /**
- * Gets the refresh mode for the frame.
- */
- get refresh() {
- return this.getAttribute('refresh')
- }
-
- /**
- * Sets the refresh mode for the frame.
- */
- set refresh(value) {
- if (value) {
- this.setAttribute('refresh', value)
- } else {
- this.removeAttribute('refresh')
- }
- }
-
- /**
- * Determines if the element is loading
- */
- get loading() {
- return frameLoadingStyleFromString(this.getAttribute('loading') || '')
- }
-
- /**
- * Sets the value of if the element is loading
- */
- set loading(value) {
- if (value) {
- this.setAttribute('loading', value)
- } else {
- this.removeAttribute('loading')
- }
- }
-
- /**
- * Gets the disabled state of the frame.
- *
- * If disabled, no requests will be intercepted by the frame.
- */
- get disabled() {
- return this.hasAttribute('disabled')
- }
-
- /**
- * Sets the disabled state of the frame.
- *
- * If disabled, no requests will be intercepted by the frame.
- */
- set disabled(value) {
- if (value) {
- this.setAttribute('disabled', '')
- } else {
- this.removeAttribute('disabled')
- }
- }
-
- /**
- * Gets the autoscroll state of the frame.
- *
- * If true, the frame will be scrolled into view automatically on update.
- */
- get autoscroll() {
- return this.hasAttribute('autoscroll')
- }
-
- /**
- * Sets the autoscroll state of the frame.
- *
- * If true, the frame will be scrolled into view automatically on update.
- */
- set autoscroll(value) {
- if (value) {
- this.setAttribute('autoscroll', '')
- } else {
- this.removeAttribute('autoscroll')
- }
- }
-
- /**
- * Determines if the element has finished loading
- */
- get complete() {
- return !this.delegate.isLoading
- }
-
- /**
- * Gets the active state of the frame.
- *
- * If inactive, source changes will not be observed.
- */
- get isActive() {
- return this.ownerDocument === document && !this.isPreview
- }
-
- /**
- * Sets the active state of the frame.
- *
- * If inactive, source changes will not be observed.
- */
- get isPreview() {
- return this.ownerDocument?.documentElement?.hasAttribute('data-turbo-preview')
- }
-}
-
-function frameLoadingStyleFromString(style) {
- switch (style.toLowerCase()) {
- case 'lazy':
- return FrameLoadingStyle.lazy
- default:
- return FrameLoadingStyle.eager
- }
-}
-
-function expandURL(locatable) {
- return new URL(locatable.toString(), document.baseURI)
-}
-
-function getAnchor(url) {
- let anchorMatch
- if (url.hash) {
- return url.hash.slice(1)
- // eslint-disable-next-line no-cond-assign
- } else if ((anchorMatch = url.href.match(/#(.*)$/))) {
- return anchorMatch[1]
- }
-}
-
-function getAction$1(form, submitter) {
- const action = submitter?.getAttribute('formaction') || form.getAttribute('action') || form.action
-
- return expandURL(action)
-}
-
-function getExtension(url) {
- return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || ''
-}
-
-function isHTML(url) {
- return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/)
-}
-
-function isPrefixedBy(baseURL, url) {
- const prefix = getPrefix(url)
- return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix)
-}
-
-function locationIsVisitable(location, rootLocation) {
- return isPrefixedBy(location, rootLocation) && isHTML(location)
-}
-
-function getRequestURL(url) {
- const anchor = getAnchor(url)
- return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href
-}
-
-function toCacheKey(url) {
- return getRequestURL(url)
-}
-
-function urlsAreEqual(left, right) {
- return expandURL(left).href == expandURL(right).href
-}
-
-function getPathComponents(url) {
- return url.pathname.split('/').slice(1)
-}
-
-function getLastPathComponent(url) {
- return getPathComponents(url).slice(-1)[0]
-}
-
-function getPrefix(url) {
- return addTrailingSlash(url.origin + url.pathname)
-}
-
-function addTrailingSlash(value) {
- return value.endsWith('/') ? value : value + '/'
-}
-
-class FetchResponse {
- constructor(response) {
- this.response = response
- }
-
- get succeeded() {
- return this.response.ok
- }
-
- get failed() {
- return !this.succeeded
- }
-
- get clientError() {
- return this.statusCode >= 400 && this.statusCode <= 499
- }
-
- get serverError() {
- return this.statusCode >= 500 && this.statusCode <= 599
- }
-
- get redirected() {
- return this.response.redirected
- }
-
- get location() {
- return expandURL(this.response.url)
- }
-
- get isHTML() {
- return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/)
- }
-
- get statusCode() {
- return this.response.status
- }
-
- get contentType() {
- return this.header('Content-Type')
- }
-
- get responseText() {
- return this.response.clone().text()
- }
-
- get responseHTML() {
- if (this.isHTML) {
- return this.response.clone().text()
- } else {
- return Promise.resolve(undefined)
- }
- }
-
- header(name) {
- return this.response.headers.get(name)
- }
-}
-
-function activateScriptElement(element) {
- if (element.getAttribute('data-turbo-eval') == 'false') {
- return element
- } else {
- const createdScriptElement = document.createElement('script')
- const cspNonce = getMetaContent('csp-nonce')
- if (cspNonce) {
- createdScriptElement.nonce = cspNonce
- }
- createdScriptElement.textContent = element.textContent
- createdScriptElement.async = false
- copyElementAttributes(createdScriptElement, element)
- return createdScriptElement
- }
-}
-
-function copyElementAttributes(destinationElement, sourceElement) {
- for (const { name, value } of sourceElement.attributes) {
- destinationElement.setAttribute(name, value)
- }
-}
-
-function createDocumentFragment(html) {
- const template = document.createElement('template')
- template.innerHTML = html
- return template.content
-}
-
-function dispatch(eventName, { target, cancelable, detail } = {}) {
- const event = new CustomEvent(eventName, {
- cancelable,
- bubbles: true,
- composed: true,
- detail
- })
-
- if (target && target.isConnected) {
- target.dispatchEvent(event)
- } else {
- document.documentElement.dispatchEvent(event)
- }
-
- return event
-}
-
-function nextRepaint() {
- if (document.visibilityState === 'hidden') {
- return nextEventLoopTick()
- } else {
- return nextAnimationFrame()
- }
-}
-
-function nextAnimationFrame() {
- return new Promise(resolve => requestAnimationFrame(() => resolve()))
-}
-
-function nextEventLoopTick() {
- return new Promise(resolve => setTimeout(() => resolve(), 0))
-}
-
-function nextMicrotask() {
- return Promise.resolve()
-}
-
-function parseHTMLDocument(html = '') {
- return new DOMParser().parseFromString(html, 'text/html')
-}
-
-function unindent(strings, ...values) {
- const lines = interpolate(strings, values).replace(/^\n/, '').split('\n')
- const match = lines[0].match(/^\s+/)
- const indent = match ? match[0].length : 0
- return lines.map(line => line.slice(indent)).join('\n')
-}
-
-function interpolate(strings, values) {
- return strings.reduce((result, string, i) => {
- const value = values[i] == undefined ? '' : values[i]
- return result + string + value
- }, '')
-}
-
-function uuid() {
- return Array.from({ length: 36 })
- .map((_, i) => {
- if (i == 8 || i == 13 || i == 18 || i == 23) {
- return '-'
- } else if (i == 14) {
- return '4'
- } else if (i == 19) {
- return (Math.floor(Math.random() * 4) + 8).toString(16)
- } else {
- return Math.floor(Math.random() * 15).toString(16)
- }
- })
- .join('')
-}
-
-function getAttribute(attributeName, ...elements) {
- for (const value of elements.map(element => element?.getAttribute(attributeName))) {
- if (typeof value == 'string') return value
- }
-
- return null
-}
-
-function hasAttribute(attributeName, ...elements) {
- return elements.some(element => element && element.hasAttribute(attributeName))
-}
-
-function markAsBusy(...elements) {
- for (const element of elements) {
- if (element.localName == 'turbo-frame') {
- element.setAttribute('busy', '')
- }
- element.setAttribute('aria-busy', 'true')
- }
-}
-
-function clearBusyState(...elements) {
- for (const element of elements) {
- if (element.localName == 'turbo-frame') {
- element.removeAttribute('busy')
- }
-
- element.removeAttribute('aria-busy')
- }
-}
-
-function waitForLoad(element, timeoutInMilliseconds = 2000) {
- return new Promise(resolve => {
- const onComplete = () => {
- element.removeEventListener('error', onComplete)
- element.removeEventListener('load', onComplete)
- resolve()
- }
-
- element.addEventListener('load', onComplete, { once: true })
- element.addEventListener('error', onComplete, { once: true })
- setTimeout(resolve, timeoutInMilliseconds)
- })
-}
-
-function getHistoryMethodForAction(action) {
- switch (action) {
- case 'replace':
- return history.replaceState
- case 'advance':
- case 'restore':
- return history.pushState
- }
-}
-
-function isAction(action) {
- return action == 'advance' || action == 'replace' || action == 'restore'
-}
-
-function getVisitAction(...elements) {
- const action = getAttribute('data-turbo-action', ...elements)
-
- return isAction(action) ? action : null
-}
-
-function getMetaElement(name) {
- return document.querySelector(`meta[name="${name}"]`)
-}
-
-function getMetaContent(name) {
- const element = getMetaElement(name)
- return element && element.content
-}
-
-function setMetaContent(name, content) {
- let element = getMetaElement(name)
-
- if (!element) {
- element = document.createElement('meta')
- element.setAttribute('name', name)
-
- document.head.appendChild(element)
- }
-
- element.setAttribute('content', content)
-
- return element
-}
-
-function findClosestRecursively(element, selector) {
- if (element instanceof Element) {
- return (
- element.closest(selector) ||
- findClosestRecursively(element.assignedSlot || element.getRootNode()?.host, selector)
- )
- }
-}
-
-function elementIsFocusable(element) {
- const inertDisabledOrHidden = '[inert], :disabled, [hidden], details:not([open]), dialog:not([open])'
-
- return !!element && element.closest(inertDisabledOrHidden) == null && typeof element.focus == 'function'
-}
-
-function queryAutofocusableElement(elementOrDocumentFragment) {
- return Array.from(elementOrDocumentFragment.querySelectorAll('[autofocus]')).find(elementIsFocusable)
-}
-
-async function around(callback, reader) {
- const before = reader()
-
- callback()
-
- await nextAnimationFrame()
-
- const after = reader()
-
- return [before, after]
-}
-
-function doesNotTargetIFrame(anchor) {
- if (anchor.hasAttribute('target')) {
- for (const element of document.getElementsByName(anchor.target)) {
- if (element instanceof HTMLIFrameElement) return false
- }
- }
-
- return true
-}
-
-function findLinkFromClickTarget(target) {
- return findClosestRecursively(target, 'a[href]:not([target^=_]):not([download])')
-}
-
-function getLocationForLink(link) {
- return expandURL(link.getAttribute('href') || '')
-}
-
-function debounce(fn, delay) {
- let timeoutId = null
-
- return (...args) => {
- const callback = () => fn.apply(this, args)
- clearTimeout(timeoutId)
- timeoutId = setTimeout(callback, delay)
- }
-}
-
-class LimitedSet extends Set {
- constructor(maxSize) {
- super()
- this.maxSize = maxSize
- }
-
- add(value) {
- if (this.size >= this.maxSize) {
- const iterator = this.values()
- const oldestValue = iterator.next().value
- this.delete(oldestValue)
- }
- super.add(value)
- }
-}
-
-const recentRequests = new LimitedSet(20)
-
-const nativeFetch = window.fetch
-
-function fetchWithTurboHeaders(url, options = {}) {
- const modifiedHeaders = new Headers(options.headers || {})
- const requestUID = uuid()
- recentRequests.add(requestUID)
- modifiedHeaders.append('X-Turbo-Request-Id', requestUID)
-
- return nativeFetch(url, {
- ...options,
- headers: modifiedHeaders
- })
-}
-
-function fetchMethodFromString(method) {
- switch (method.toLowerCase()) {
- case 'get':
- return FetchMethod.get
- case 'post':
- return FetchMethod.post
- case 'put':
- return FetchMethod.put
- case 'patch':
- return FetchMethod.patch
- case 'delete':
- return FetchMethod.delete
- }
-}
-
-const FetchMethod = {
- get: 'get',
- post: 'post',
- put: 'put',
- patch: 'patch',
- delete: 'delete'
-}
-
-function fetchEnctypeFromString(encoding) {
- switch (encoding.toLowerCase()) {
- case FetchEnctype.multipart:
- return FetchEnctype.multipart
- case FetchEnctype.plain:
- return FetchEnctype.plain
- default:
- return FetchEnctype.urlEncoded
- }
-}
-
-const FetchEnctype = {
- urlEncoded: 'application/x-www-form-urlencoded',
- multipart: 'multipart/form-data',
- plain: 'text/plain'
-}
-
-class FetchRequest {
- abortController = new AbortController()
- #resolveRequestPromise = _value => {}
-
- constructor(
- delegate,
- method,
- location,
- requestBody = new URLSearchParams(),
- target = null,
- enctype = FetchEnctype.urlEncoded
- ) {
- const [url, body] = buildResourceAndBody(expandURL(location), method, requestBody, enctype)
-
- this.delegate = delegate
- this.url = url
- this.target = target
- this.fetchOptions = {
- credentials: 'same-origin',
- redirect: 'follow',
- method: method,
- headers: { ...this.defaultHeaders },
- body: body,
- signal: this.abortSignal,
- referrer: this.delegate.referrer?.href
- }
- this.enctype = enctype
- }
-
- get method() {
- return this.fetchOptions.method
- }
-
- set method(value) {
- const fetchBody = this.isSafe ? this.url.searchParams : this.fetchOptions.body || new FormData()
- const fetchMethod = fetchMethodFromString(value) || FetchMethod.get
-
- this.url.search = ''
-
- const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype)
-
- this.url = url
- this.fetchOptions.body = body
- this.fetchOptions.method = fetchMethod
- }
-
- get headers() {
- return this.fetchOptions.headers
- }
-
- set headers(value) {
- this.fetchOptions.headers = value
- }
-
- get body() {
- if (this.isSafe) {
- return this.url.searchParams
- } else {
- return this.fetchOptions.body
- }
- }
-
- set body(value) {
- this.fetchOptions.body = value
- }
-
- get location() {
- return this.url
- }
-
- get params() {
- return this.url.searchParams
- }
-
- get entries() {
- return this.body ? Array.from(this.body.entries()) : []
- }
-
- cancel() {
- this.abortController.abort()
- }
-
- async perform() {
- const { fetchOptions } = this
- this.delegate.prepareRequest(this)
- const event = await this.#allowRequestToBeIntercepted(fetchOptions)
- try {
- this.delegate.requestStarted(this)
-
- if (event.detail.fetchRequest) {
- this.response = event.detail.fetchRequest.response
- } else {
- this.response = fetchWithTurboHeaders(this.url.href, fetchOptions)
- }
-
- const response = await this.response
- return await this.receive(response)
- } catch (error) {
- if (error.name !== 'AbortError') {
- if (this.#willDelegateErrorHandling(error)) {
- this.delegate.requestErrored(this, error)
- }
- throw error
- }
- } finally {
- this.delegate.requestFinished(this)
- }
- }
-
- async receive(response) {
- const fetchResponse = new FetchResponse(response)
- const event = dispatch('turbo:before-fetch-response', {
- cancelable: true,
- detail: { fetchResponse },
- target: this.target
- })
- if (event.defaultPrevented) {
- this.delegate.requestPreventedHandlingResponse(this, fetchResponse)
- } else if (fetchResponse.succeeded) {
- this.delegate.requestSucceededWithResponse(this, fetchResponse)
- } else {
- this.delegate.requestFailedWithResponse(this, fetchResponse)
- }
- return fetchResponse
- }
-
- get defaultHeaders() {
- return {
- Accept: 'text/html, application/xhtml+xml'
- }
- }
-
- get isSafe() {
- return isSafe(this.method)
- }
-
- get abortSignal() {
- return this.abortController.signal
- }
-
- acceptResponseType(mimeType) {
- this.headers['Accept'] = [mimeType, this.headers['Accept']].join(', ')
- }
-
- async #allowRequestToBeIntercepted(fetchOptions) {
- const requestInterception = new Promise(resolve => (this.#resolveRequestPromise = resolve))
- const event = dispatch('turbo:before-fetch-request', {
- cancelable: true,
- detail: {
- fetchOptions,
- url: this.url,
- resume: this.#resolveRequestPromise
- },
- target: this.target
- })
- this.url = event.detail.url
- if (event.defaultPrevented) await requestInterception
-
- return event
- }
-
- #willDelegateErrorHandling(error) {
- const event = dispatch('turbo:fetch-request-error', {
- target: this.target,
- cancelable: true,
- detail: { request: this, error: error }
- })
-
- return !event.defaultPrevented
- }
-}
-
-function isSafe(fetchMethod) {
- return fetchMethodFromString(fetchMethod) == FetchMethod.get
-}
-
-function buildResourceAndBody(resource, method, requestBody, enctype) {
- const searchParams =
- Array.from(requestBody).length > 0
- ? new URLSearchParams(entriesExcludingFiles(requestBody))
- : resource.searchParams
-
- if (isSafe(method)) {
- return [mergeIntoURLSearchParams(resource, searchParams), null]
- } else if (enctype == FetchEnctype.urlEncoded) {
- return [resource, searchParams]
- } else {
- return [resource, requestBody]
- }
-}
-
-function entriesExcludingFiles(requestBody) {
- const entries = []
-
- for (const [name, value] of requestBody) {
- if (value instanceof File) continue
- else entries.push([name, value])
- }
-
- return entries
-}
-
-function mergeIntoURLSearchParams(url, requestBody) {
- const searchParams = new URLSearchParams(entriesExcludingFiles(requestBody))
-
- url.search = searchParams.toString()
-
- return url
-}
-
-class AppearanceObserver {
- started = false
-
- constructor(delegate, element) {
- this.delegate = delegate
- this.element = element
- this.intersectionObserver = new IntersectionObserver(this.intersect)
- }
-
- start() {
- if (!this.started) {
- this.started = true
- this.intersectionObserver.observe(this.element)
- }
- }
-
- stop() {
- if (this.started) {
- this.started = false
- this.intersectionObserver.unobserve(this.element)
- }
- }
-
- intersect = entries => {
- const lastEntry = entries.slice(-1)[0]
- if (lastEntry?.isIntersecting) {
- this.delegate.elementAppearedInViewport(this.element)
- }
- }
-}
-
-class StreamMessage {
- static contentType = 'text/vnd.turbo-stream.html'
-
- static wrap(message) {
- if (typeof message == 'string') {
- return new this(createDocumentFragment(message))
- } else {
- return message
- }
- }
-
- constructor(fragment) {
- this.fragment = importStreamElements(fragment)
- }
-}
-
-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')) {
- inertScriptElement.replaceWith(activateScriptElement(inertScriptElement))
- }
-
- element.replaceWith(streamElement)
- }
-
- return fragment
-}
-
-const PREFETCH_DELAY = 100
-
-class PrefetchCache {
- #prefetchTimeout = null
- #prefetched = null
-
- get(url) {
- if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
- return this.#prefetched.request
- }
- }
-
- setLater(url, request, ttl) {
- this.clear()
-
- this.#prefetchTimeout = setTimeout(() => {
- request.perform()
- this.set(url, request, ttl)
- this.#prefetchTimeout = null
- }, PREFETCH_DELAY)
- }
-
- set(url, request, ttl) {
- this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) }
- }
-
- clear() {
- if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout)
- this.#prefetched = null
- }
-}
-
-const cacheTtl = 10 * 1000
-const prefetchCache = new PrefetchCache()
-
-const FormSubmissionState = {
- initialized: 'initialized',
- requesting: 'requesting',
- waiting: 'waiting',
- receiving: 'receiving',
- stopping: 'stopping',
- stopped: 'stopped'
-}
-
-class FormSubmission {
- state = FormSubmissionState.initialized
-
- static confirmMethod(message, _element, _submitter) {
- return Promise.resolve(confirm(message))
- }
-
- constructor(delegate, formElement, submitter, mustRedirect = false) {
- const method = getMethod(formElement, submitter)
- const action = getAction(getFormAction(formElement, submitter), method)
- const body = buildFormData(formElement, submitter)
- const enctype = getEnctype(formElement, submitter)
-
- this.delegate = delegate
- this.formElement = formElement
- this.submitter = submitter
- this.fetchRequest = new FetchRequest(this, method, action, body, formElement, enctype)
- this.mustRedirect = mustRedirect
- }
-
- get method() {
- return this.fetchRequest.method
- }
-
- set method(value) {
- this.fetchRequest.method = value
- }
-
- get action() {
- return this.fetchRequest.url.toString()
- }
-
- set action(value) {
- this.fetchRequest.url = expandURL(value)
- }
-
- get body() {
- return this.fetchRequest.body
- }
-
- get enctype() {
- return this.fetchRequest.enctype
- }
-
- get isSafe() {
- return this.fetchRequest.isSafe
- }
-
- get location() {
- return this.fetchRequest.url
- }
-
- // The submission process
-
- async start() {
- const { initialized, requesting } = FormSubmissionState
- const confirmationMessage = getAttribute('data-turbo-confirm', this.submitter, this.formElement)
-
- if (typeof confirmationMessage === 'string') {
- const answer = await FormSubmission.confirmMethod(confirmationMessage, this.formElement, this.submitter)
- if (!answer) {
- return
- }
- }
-
- if (this.state == initialized) {
- this.state = requesting
- return this.fetchRequest.perform()
- }
- }
-
- stop() {
- const { stopping, stopped } = FormSubmissionState
- if (this.state != stopping && this.state != stopped) {
- this.state = stopping
- this.fetchRequest.cancel()
- return true
- }
- }
-
- // Fetch request delegate
-
- prepareRequest(request) {
- if (!request.isSafe) {
- const token = getCookieValue(getMetaContent('csrf-param')) || getMetaContent('csrf-token')
- if (token) {
- request.headers['X-CSRF-Token'] = token
- }
- }
-
- if (this.requestAcceptsTurboStreamResponse(request)) {
- request.acceptResponseType(StreamMessage.contentType)
- }
- }
-
- requestStarted(_request) {
- this.state = FormSubmissionState.waiting
- this.submitter?.setAttribute('disabled', '')
- this.setSubmitsWith()
- markAsBusy(this.formElement)
- dispatch('turbo:submit-start', {
- target: this.formElement,
- detail: { formSubmission: this }
- })
- this.delegate.formSubmissionStarted(this)
- }
-
- requestPreventedHandlingResponse(request, response) {
- prefetchCache.clear()
-
- this.result = { success: response.succeeded, fetchResponse: response }
- }
-
- requestSucceededWithResponse(request, response) {
- if (response.clientError || response.serverError) {
- this.delegate.formSubmissionFailedWithResponse(this, response)
- return
- }
-
- prefetchCache.clear()
-
- if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
- const error = new Error('Form responses must redirect to another location')
- this.delegate.formSubmissionErrored(this, error)
- } else {
- this.state = FormSubmissionState.receiving
- this.result = { success: true, fetchResponse: response }
- this.delegate.formSubmissionSucceededWithResponse(this, response)
- }
- }
-
- requestFailedWithResponse(request, response) {
- this.result = { success: false, fetchResponse: response }
- this.delegate.formSubmissionFailedWithResponse(this, response)
- }
-
- requestErrored(request, error) {
- this.result = { success: false, error }
- this.delegate.formSubmissionErrored(this, error)
- }
-
- requestFinished(_request) {
- this.state = FormSubmissionState.stopped
- this.submitter?.removeAttribute('disabled')
- this.resetSubmitterText()
- clearBusyState(this.formElement)
- dispatch('turbo:submit-end', {
- target: this.formElement,
- detail: { formSubmission: this, ...this.result }
- })
- this.delegate.formSubmissionFinished(this)
- }
-
- // Private
-
- setSubmitsWith() {
- if (!this.submitter || !this.submitsWith) return
-
- if (this.submitter.matches('button')) {
- this.originalSubmitText = this.submitter.innerHTML
- this.submitter.innerHTML = this.submitsWith
- } else if (this.submitter.matches('input')) {
- const input = this.submitter
- this.originalSubmitText = input.value
- input.value = this.submitsWith
- }
- }
-
- resetSubmitterText() {
- if (!this.submitter || !this.originalSubmitText) return
-
- if (this.submitter.matches('button')) {
- this.submitter.innerHTML = this.originalSubmitText
- } else if (this.submitter.matches('input')) {
- const input = this.submitter
- input.value = this.originalSubmitText
- }
- }
-
- requestMustRedirect(request) {
- return !request.isSafe && this.mustRedirect
- }
-
- requestAcceptsTurboStreamResponse(request) {
- return !request.isSafe || hasAttribute('data-turbo-stream', this.submitter, this.formElement)
- }
-
- get submitsWith() {
- return this.submitter?.getAttribute('data-turbo-submits-with')
- }
-}
-
-function buildFormData(formElement, submitter) {
- const formData = new FormData(formElement)
- const name = submitter?.getAttribute('name')
- const value = submitter?.getAttribute('value')
-
- if (name) {
- formData.append(name, value || '')
- }
-
- return formData
-}
-
-function getCookieValue(cookieName) {
- if (cookieName != null) {
- const cookies = document.cookie ? document.cookie.split('; ') : []
- const cookie = cookies.find(cookie => cookie.startsWith(cookieName))
- if (cookie) {
- const value = cookie.split('=').slice(1).join('=')
- return value ? decodeURIComponent(value) : undefined
- }
- }
-}
-
-function responseSucceededWithoutRedirect(response) {
- return response.statusCode == 200 && !response.redirected
-}
-
-function getFormAction(formElement, submitter) {
- const formElementAction = typeof formElement.action === 'string' ? formElement.action : null
-
- if (submitter?.hasAttribute('formaction')) {
- return submitter.getAttribute('formaction') || ''
- } else {
- return formElement.getAttribute('action') || formElementAction || ''
- }
-}
-
-function getAction(formAction, fetchMethod) {
- const action = expandURL(formAction)
-
- if (isSafe(fetchMethod)) {
- action.search = ''
- }
-
- return action
-}
-
-function getMethod(formElement, submitter) {
- const method = submitter?.getAttribute('formmethod') || formElement.getAttribute('method') || ''
- return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get
-}
-
-function getEnctype(formElement, submitter) {
- return fetchEnctypeFromString(submitter?.getAttribute('formenctype') || formElement.enctype)
-}
-
-class Snapshot {
- constructor(element) {
- this.element = element
- }
-
- get activeElement() {
- return this.element.ownerDocument.activeElement
- }
-
- get children() {
- return [...this.element.children]
- }
-
- hasAnchor(anchor) {
- return this.getElementForAnchor(anchor) != null
- }
-
- getElementForAnchor(anchor) {
- return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null
- }
-
- get isConnected() {
- return this.element.isConnected
- }
-
- get firstAutofocusableElement() {
- return queryAutofocusableElement(this.element)
- }
-
- get permanentElements() {
- return queryPermanentElementsAll(this.element)
- }
-
- getPermanentElementById(id) {
- return getPermanentElementById(this.element, id)
- }
-
- getPermanentElementMapForSnapshot(snapshot) {
- const permanentElementMap = {}
-
- for (const currentPermanentElement of this.permanentElements) {
- const { id } = currentPermanentElement
- const newPermanentElement = snapshot.getPermanentElementById(id)
- if (newPermanentElement) {
- permanentElementMap[id] = [currentPermanentElement, newPermanentElement]
- }
- }
-
- return permanentElementMap
- }
-}
-
-function getPermanentElementById(node, id) {
- return node.querySelector(`#${id}[data-turbo-permanent]`)
-}
-
-function queryPermanentElementsAll(node) {
- return node.querySelectorAll('[id][data-turbo-permanent]')
-}
-
-class FormSubmitObserver {
- started = false
-
- constructor(delegate, eventTarget) {
- this.delegate = delegate
- this.eventTarget = eventTarget
- }
-
- start() {
- if (!this.started) {
- this.eventTarget.addEventListener('submit', this.submitCaptured, true)
- this.started = true
- }
- }
-
- stop() {
- if (this.started) {
- this.eventTarget.removeEventListener('submit', this.submitCaptured, true)
- this.started = false
- }
- }
-
- submitCaptured = () => {
- this.eventTarget.removeEventListener('submit', this.submitBubbled, false)
- this.eventTarget.addEventListener('submit', this.submitBubbled, false)
- }
-
- submitBubbled = event => {
- if (!event.defaultPrevented) {
- const form = event.target instanceof HTMLFormElement ? event.target : undefined
- const submitter = event.submitter || undefined
-
- if (
- form &&
- submissionDoesNotDismissDialog(form, submitter) &&
- submissionDoesNotTargetIFrame(form, submitter) &&
- this.delegate.willSubmitForm(form, submitter)
- ) {
- event.preventDefault()
- event.stopImmediatePropagation()
- this.delegate.formSubmitted(form, submitter)
- }
- }
- }
-}
-
-function submissionDoesNotDismissDialog(form, submitter) {
- const method = submitter?.getAttribute('formmethod') || form.getAttribute('method')
-
- return method != 'dialog'
-}
-
-function submissionDoesNotTargetIFrame(form, submitter) {
- if (submitter?.hasAttribute('formtarget') || form.hasAttribute('target')) {
- const target = submitter?.getAttribute('formtarget') || form.target
-
- for (const element of document.getElementsByName(target)) {
- if (element instanceof HTMLIFrameElement) return false
- }
-
- return true
- } else {
- return true
- }
-}
-
-class View {
- #resolveRenderPromise = _value => {}
- #resolveInterceptionPromise = _value => {}
-
- constructor(delegate, element) {
- this.delegate = delegate
- this.element = element
- }
-
- // Scrolling
-
- scrollToAnchor(anchor) {
- const element = this.snapshot.getElementForAnchor(anchor)
- if (element) {
- this.scrollToElement(element)
- this.focusElement(element)
- } else {
- this.scrollToPosition({ x: 0, y: 0 })
- }
- }
-
- scrollToAnchorFromLocation(location) {
- this.scrollToAnchor(getAnchor(location))
- }
-
- scrollToElement(element) {
- element.scrollIntoView()
- }
-
- focusElement(element) {
- if (element instanceof HTMLElement) {
- if (element.hasAttribute('tabindex')) {
- element.focus()
- } else {
- element.setAttribute('tabindex', '-1')
- element.focus()
- element.removeAttribute('tabindex')
- }
- }
- }
-
- scrollToPosition({ x, y }) {
- this.scrollRoot.scrollTo(x, y)
- }
-
- scrollToTop() {
- this.scrollToPosition({ x: 0, y: 0 })
- }
-
- get scrollRoot() {
- return window
- }
-
- // Rendering
-
- async render(renderer) {
- const { isPreview, shouldRender, willRender, newSnapshot: snapshot } = renderer
-
- // A workaround to ignore tracked element mismatch reloads when performing
- // a promoted Visit from a frame navigation
- const shouldInvalidate = willRender
-
- if (shouldRender) {
- try {
- 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,
- renderMethod: this.renderer.renderMethod
- }
- const immediateRender = this.delegate.allowsImmediateRender(snapshot, options)
- if (!immediateRender) await renderInterception
-
- await this.renderSnapshot(renderer)
- this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod)
- this.delegate.preloadOnLoadLinksForView(this.element)
- this.finishRenderingSnapshot(renderer)
- } finally {
- delete this.renderer
- this.#resolveRenderPromise(undefined)
- delete this.renderPromise
- }
- } else if (shouldInvalidate) {
- this.invalidate(renderer.reloadReason)
- }
- }
-
- invalidate(reason) {
- this.delegate.viewInvalidated(reason)
- }
-
- async prepareToRenderSnapshot(renderer) {
- this.markAsPreview(renderer.isPreview)
- await renderer.prepareToRender()
- }
-
- markAsPreview(isPreview) {
- if (isPreview) {
- this.element.setAttribute('data-turbo-preview', '')
- } else {
- this.element.removeAttribute('data-turbo-preview')
- }
- }
-
- markVisitDirection(direction) {
- this.element.setAttribute('data-turbo-visit-direction', direction)
- }
-
- unmarkVisitDirection() {
- this.element.removeAttribute('data-turbo-visit-direction')
- }
-
- async renderSnapshot(renderer) {
- await renderer.render()
- }
-
- finishRenderingSnapshot(renderer) {
- renderer.finishRendering()
- }
-}
-
-class FrameView extends View {
- missing() {
- this.element.innerHTML = `Content missing`
- }
-
- get snapshot() {
- return new Snapshot(this.element)
- }
-}
-
-class LinkInterceptor {
- constructor(delegate, element) {
- this.delegate = delegate
- this.element = element
- }
-
- start() {
- this.element.addEventListener('click', this.clickBubbled)
- document.addEventListener('turbo:click', this.linkClicked)
- document.addEventListener('turbo:before-visit', this.willVisit)
- }
-
- stop() {
- this.element.removeEventListener('click', this.clickBubbled)
- document.removeEventListener('turbo:click', this.linkClicked)
- document.removeEventListener('turbo:before-visit', this.willVisit)
- }
-
- clickBubbled = event => {
- if (this.respondsToEventTarget(event.target)) {
- this.clickEvent = event
- } else {
- delete this.clickEvent
- }
- }
-
- 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()
- event.preventDefault()
- this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent)
- }
- }
- delete this.clickEvent
- }
-
- willVisit = _event => {
- delete this.clickEvent
- }
-
- respondsToEventTarget(target) {
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null
- return element && element.closest('turbo-frame, html') == this.element
- }
-}
-
-class LinkClickObserver {
- started = false
-
- constructor(delegate, eventTarget) {
- this.delegate = delegate
- this.eventTarget = eventTarget
- }
-
- start() {
- if (!this.started) {
- this.eventTarget.addEventListener('click', this.clickCaptured, true)
- this.started = true
- }
- }
-
- stop() {
- if (this.started) {
- this.eventTarget.removeEventListener('click', this.clickCaptured, true)
- this.started = false
- }
- }
-
- clickCaptured = () => {
- this.eventTarget.removeEventListener('click', this.clickBubbled, false)
- this.eventTarget.addEventListener('click', this.clickBubbled, false)
- }
-
- clickBubbled = event => {
- if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
- const target = (event.composedPath && event.composedPath()[0]) || event.target
- const link = findLinkFromClickTarget(target)
- if (link && doesNotTargetIFrame(link)) {
- const location = getLocationForLink(link)
- if (this.delegate.willFollowLinkToLocation(link, location, event)) {
- event.preventDefault()
- this.delegate.followedLinkToLocation(link, location)
- }
- }
- }
- }
-
- clickEventIsSignificant(event) {
- return !(
- (event.target && event.target.isContentEditable) ||
- event.defaultPrevented ||
- event.which > 1 ||
- event.altKey ||
- event.ctrlKey ||
- event.metaKey ||
- event.shiftKey
- )
- }
-}
-
-class FormLinkClickObserver {
- constructor(delegate, element) {
- this.delegate = delegate
- this.linkInterceptor = new LinkClickObserver(this, element)
- }
-
- start() {
- this.linkInterceptor.start()
- }
-
- stop() {
- this.linkInterceptor.stop()
- }
-
- // Link hover observer delegate
-
- canPrefetchRequestToLocation(link, location) {
- return false
- }
-
- prefetchAndCacheRequestToLocation(link, location) {
- return
- }
-
- // Link click observer delegate
-
- willFollowLinkToLocation(link, location, originalEvent) {
- return (
- this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) &&
- (link.hasAttribute('data-turbo-method') || link.hasAttribute('data-turbo-stream'))
- )
- }
-
- followedLinkToLocation(link, location) {
- const form = document.createElement('form')
-
- const type = 'hidden'
- for (const [name, value] of location.searchParams) {
- form.append(Object.assign(document.createElement('input'), { type, name, value }))
- }
-
- const action = Object.assign(location, { search: '' })
- form.setAttribute('data-turbo', 'true')
- form.setAttribute('action', action.href)
- form.setAttribute('hidden', '')
-
- const method = link.getAttribute('data-turbo-method')
- if (method) form.setAttribute('method', method)
-
- const turboFrame = link.getAttribute('data-turbo-frame')
- if (turboFrame) form.setAttribute('data-turbo-frame', turboFrame)
-
- const turboAction = getVisitAction(link)
- if (turboAction) form.setAttribute('data-turbo-action', turboAction)
-
- const turboConfirm = link.getAttribute('data-turbo-confirm')
- if (turboConfirm) form.setAttribute('data-turbo-confirm', turboConfirm)
-
- const turboStream = link.hasAttribute('data-turbo-stream')
- if (turboStream) form.setAttribute('data-turbo-stream', '')
-
- this.delegate.submittedFormLinkToLocation(link, location, form)
-
- document.body.appendChild(form)
- form.addEventListener('turbo:submit-end', () => form.remove(), { once: true })
- requestAnimationFrame(() => form.requestSubmit())
- }
-}
-
-class Bardo {
- static async preservingPermanentElements(delegate, permanentElementMap, callback) {
- const bardo = new this(delegate, permanentElementMap)
- bardo.enter()
- await callback()
- bardo.leave()
- }
-
- constructor(delegate, permanentElementMap) {
- this.delegate = delegate
- this.permanentElementMap = permanentElementMap
- }
-
- enter() {
- for (const id in this.permanentElementMap) {
- const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id]
- this.delegate.enteringBardo(currentPermanentElement, newPermanentElement)
- this.replaceNewPermanentElementWithPlaceholder(newPermanentElement)
- }
- }
-
- leave() {
- for (const id in this.permanentElementMap) {
- const [currentPermanentElement] = this.permanentElementMap[id]
- this.replaceCurrentPermanentElementWithClone(currentPermanentElement)
- this.replacePlaceholderWithPermanentElement(currentPermanentElement)
- this.delegate.leavingBardo(currentPermanentElement)
- }
- }
-
- replaceNewPermanentElementWithPlaceholder(permanentElement) {
- const placeholder = createPlaceholderForPermanentElement(permanentElement)
- permanentElement.replaceWith(placeholder)
- }
-
- replaceCurrentPermanentElementWithClone(permanentElement) {
- const clone = permanentElement.cloneNode(true)
- permanentElement.replaceWith(clone)
- }
-
- replacePlaceholderWithPermanentElement(permanentElement) {
- const placeholder = this.getPlaceholderById(permanentElement.id)
- placeholder?.replaceWith(permanentElement)
- }
-
- getPlaceholderById(id) {
- return this.placeholders.find(element => element.content == id)
- }
-
- get placeholders() {
- return [...document.querySelectorAll('meta[name=turbo-permanent-placeholder][content]')]
- }
-}
-
-function createPlaceholderForPermanentElement(permanentElement) {
- const element = document.createElement('meta')
- element.setAttribute('name', 'turbo-permanent-placeholder')
- element.setAttribute('content', permanentElement.id)
- return element
-}
-
-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 (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)
- }
-
- get renderMethod() {
- return 'replace'
- }
-}
-
-class FrameRenderer extends Renderer {
- static renderElement(currentElement, newElement) {
- const destinationRange = document.createRange()
- destinationRange.selectNodeContents(currentElement)
- destinationRange.deleteContents()
-
- const frameElement = newElement
- const sourceRange = frameElement.ownerDocument?.createRange()
- if (sourceRange) {
- sourceRange.selectNodeContents(frameElement)
- currentElement.appendChild(sourceRange.extractContents())
- }
- }
-
- constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
- super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender)
- this.delegate = delegate
- }
-
- get shouldRender() {
- return true
- }
-
- async render() {
- await nextRepaint()
- this.preservingPermanentElements(() => {
- this.loadFrameElement()
- })
- this.scrollFrameIntoView()
- await nextRepaint()
- this.focusFirstAutofocusableElement()
- await nextRepaint()
- this.activateScriptElements()
- }
-
- loadFrameElement() {
- this.delegate.willRenderFrame(this.currentElement, this.newElement)
- this.renderElement(this.currentElement, this.newElement)
- }
-
- scrollFrameIntoView() {
- if (this.currentElement.autoscroll || this.newElement.autoscroll) {
- const element = this.currentElement.firstElementChild
- const block = readScrollLogicalPosition(this.currentElement.getAttribute('data-autoscroll-block'), 'end')
- const behavior = readScrollBehavior(this.currentElement.getAttribute('data-autoscroll-behavior'), 'auto')
-
- if (element) {
- element.scrollIntoView({ block, behavior })
- return true
- }
- }
- return false
- }
-
- activateScriptElements() {
- for (const inertScriptElement of this.newScriptElements) {
- const activatedScriptElement = activateScriptElement(inertScriptElement)
- inertScriptElement.replaceWith(activatedScriptElement)
- }
- }
-
- get newScriptElements() {
- return this.currentElement.querySelectorAll('script')
- }
-}
-
-function readScrollLogicalPosition(value, defaultValue) {
- if (value == 'end' || value == 'start' || value == 'center' || value == 'nearest') {
- return value
- } else {
- return defaultValue
- }
-}
-
-function readScrollBehavior(value, defaultValue) {
- if (value == 'auto' || value == 'smooth') {
- return value
- } else {
- return defaultValue
- }
-}
-
-class ProgressBar {
- static animationDuration = 300 /*ms*/
-
- static get defaultCSS() {
- return unindent`
- .turbo-progress-bar {
- position: fixed;
- display: block;
- top: 0;
- left: 0;
- height: 3px;
- background: #0076ff;
- z-index: 2147483647;
- transition:
- width ${ProgressBar.animationDuration}ms ease-out,
- opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
- transform: translate3d(0, 0, 0);
- }
- `
- }
-
- hiding = false
- value = 0
- visible = false
-
- constructor() {
- this.stylesheetElement = this.createStylesheetElement()
- this.progressElement = this.createProgressElement()
- this.installStylesheetElement()
- this.setValue(0)
- }
-
- show() {
- if (!this.visible) {
- this.visible = true
- this.installProgressElement()
- this.startTrickling()
- }
- }
-
- hide() {
- if (this.visible && !this.hiding) {
- this.hiding = true
- this.fadeProgressElement(() => {
- this.uninstallProgressElement()
- this.stopTrickling()
- this.visible = false
- this.hiding = false
- })
- }
- }
-
- setValue(value) {
- this.value = value
- this.refresh()
- }
-
- // Private
-
- installStylesheetElement() {
- document.head.insertBefore(this.stylesheetElement, document.head.firstChild)
- }
-
- installProgressElement() {
- this.progressElement.style.width = '0'
- this.progressElement.style.opacity = '1'
- document.documentElement.insertBefore(this.progressElement, document.body)
- this.refresh()
- }
-
- fadeProgressElement(callback) {
- this.progressElement.style.opacity = '0'
- setTimeout(callback, ProgressBar.animationDuration * 1.5)
- }
-
- uninstallProgressElement() {
- if (this.progressElement.parentNode) {
- document.documentElement.removeChild(this.progressElement)
- }
- }
-
- startTrickling() {
- if (!this.trickleInterval) {
- this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration)
- }
- }
-
- stopTrickling() {
- window.clearInterval(this.trickleInterval)
- delete this.trickleInterval
- }
-
- trickle = () => {
- this.setValue(this.value + Math.random() / 100)
- }
-
- refresh() {
- requestAnimationFrame(() => {
- this.progressElement.style.width = `${10 + this.value * 90}%`
- })
- }
-
- createStylesheetElement() {
- const element = document.createElement('style')
- element.type = 'text/css'
- element.textContent = ProgressBar.defaultCSS
- if (this.cspNonce) {
- element.nonce = this.cspNonce
- }
- return element
- }
-
- createProgressElement() {
- const element = document.createElement('div')
- element.className = 'turbo-progress-bar'
- return element
- }
-
- get cspNonce() {
- return getMetaContent('csp-nonce')
- }
-}
-
-class HeadSnapshot extends Snapshot {
- detailsByOuterHTML = this.children
- .filter(element => !elementIsNoscript(element))
- .map(element => elementWithoutNonce(element))
- .reduce((result, element) => {
- const { outerHTML } = element
- const details =
- outerHTML in result
- ? result[outerHTML]
- : {
- type: elementType(element),
- tracked: elementIsTracked(element),
- elements: []
- }
- return {
- ...result,
- [outerHTML]: {
- ...details,
- elements: [...details.elements, element]
- }
- }
- }, {})
-
- get trackedElementSignature() {
- return Object.keys(this.detailsByOuterHTML)
- .filter(outerHTML => this.detailsByOuterHTML[outerHTML].tracked)
- .join('')
- }
-
- getScriptElementsNotInSnapshot(snapshot) {
- return this.getElementsMatchingTypeNotInSnapshot('script', snapshot)
- }
-
- getStylesheetElementsNotInSnapshot(snapshot) {
- return this.getElementsMatchingTypeNotInSnapshot('stylesheet', snapshot)
- }
-
- 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)
- }
-
- get provisionalElements() {
- return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
- const { type, tracked, elements } = this.detailsByOuterHTML[outerHTML]
- if (type == null && !tracked) {
- return [...result, ...elements]
- } else if (elements.length > 1) {
- return [...result, ...elements.slice(1)]
- } else {
- return result
- }
- }, [])
- }
-
- getMetaValue(name) {
- const element = this.findMetaElementByName(name)
- return element ? element.getAttribute('content') : null
- }
-
- findMetaElementByName(name) {
- return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
- const {
- elements: [element]
- } = this.detailsByOuterHTML[outerHTML]
- return elementIsMetaElementWithName(element, name) ? element : result
- }, undefined | undefined)
- }
-}
-
-function elementType(element) {
- if (elementIsScript(element)) {
- return 'script'
- } else if (elementIsStylesheet(element)) {
- return 'stylesheet'
- }
-}
-
-function elementIsTracked(element) {
- return element.getAttribute('data-turbo-track') == 'reload'
-}
-
-function elementIsScript(element) {
- const tagName = element.localName
- return tagName == 'script'
-}
-
-function elementIsNoscript(element) {
- const tagName = element.localName
- return tagName == 'noscript'
-}
-
-function elementIsStylesheet(element) {
- const tagName = element.localName
- return tagName == 'style' || (tagName == 'link' && element.getAttribute('rel') == 'stylesheet')
-}
-
-function elementIsMetaElementWithName(element, name) {
- const tagName = element.localName
- return tagName == 'meta' && element.getAttribute('name') == name
-}
-
-function elementWithoutNonce(element) {
- if (element.hasAttribute('nonce')) {
- element.setAttribute('nonce', '')
- }
-
- return element
-}
-
-class PageSnapshot extends Snapshot {
- static fromHTMLString(html = '') {
- return this.fromDocument(parseHTMLDocument(html))
- }
-
- static fromElement(element) {
- return this.fromDocument(element.ownerDocument)
- }
-
- static fromDocument({ documentElement, body, head }) {
- return new this(documentElement, body, new HeadSnapshot(head))
- }
-
- constructor(documentElement, body, headSnapshot) {
- super(body)
- this.documentElement = documentElement
- this.headSnapshot = headSnapshot
- }
-
- clone() {
- const clonedElement = this.element.cloneNode(true)
-
- const selectElements = this.element.querySelectorAll('select')
- const clonedSelectElements = clonedElement.querySelectorAll('select')
-
- for (const [index, source] of selectElements.entries()) {
- const clone = clonedSelectElements[index]
- for (const option of clone.selectedOptions) option.selected = false
- for (const option of source.selectedOptions) clone.options[option.index].selected = true
- }
-
- for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
- clonedPasswordInput.value = ''
- }
-
- return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot)
- }
-
- get lang() {
- return this.documentElement.getAttribute('lang')
- }
-
- get headElement() {
- return this.headSnapshot.element
- }
-
- get rootLocation() {
- const root = this.getSetting('root') ?? '/'
- return expandURL(root)
- }
-
- get cacheControlValue() {
- return this.getSetting('cache-control')
- }
-
- get isPreviewable() {
- return this.cacheControlValue != 'no-preview'
- }
-
- get isCacheable() {
- return this.cacheControlValue != 'no-cache'
- }
-
- get isVisitable() {
- return this.getSetting('visit-control') != 'reload'
- }
-
- get prefersViewTransitions() {
- return this.headSnapshot.getMetaValue('view-transition') === 'same-origin'
- }
-
- get shouldMorphPage() {
- return this.getSetting('refresh-method') === 'morph'
- }
-
- get shouldPreserveScrollPosition() {
- return this.getSetting('refresh-scroll') === 'preserve'
- }
-
- // Private
-
- getSetting(name) {
- return this.headSnapshot.getMetaValue(`turbo-${name}`)
- }
-}
-
-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
- }
-}
-
-const defaultOptions = {
- action: 'advance',
- historyChanged: false,
- visitCachedSnapshot: () => {},
- willRender: true,
- updateHistory: true,
- shouldCacheSnapshot: true,
- acceptsStreamResponse: false
-}
-
-const TimingMetric = {
- visitStart: 'visitStart',
- requestStart: 'requestStart',
- requestEnd: 'requestEnd',
- visitEnd: 'visitEnd'
-}
-
-const VisitState = {
- initialized: 'initialized',
- started: 'started',
- canceled: 'canceled',
- failed: 'failed',
- completed: 'completed'
-}
-
-const SystemStatusCode = {
- networkFailure: 0,
- timeoutFailure: -1,
- contentTypeMismatch: -2
-}
-
-const Direction = {
- advance: 'forward',
- restore: 'back',
- replace: 'none'
-}
-
-class Visit {
- identifier = uuid() // Required by turbo-ios
- timingMetrics = {}
-
- followedRedirect = false
- historyChanged = false
- scrolled = false
- shouldCacheSnapshot = true
- acceptsStreamResponse = false
- snapshotCached = false
- state = VisitState.initialized
- viewTransitioner = new ViewTransitioner()
-
- constructor(delegate, location, restorationIdentifier, options = {}) {
- this.delegate = delegate
- this.location = location
- this.restorationIdentifier = restorationIdentifier || uuid()
-
- const {
- action,
- historyChanged,
- referrer,
- snapshot,
- snapshotHTML,
- response,
- visitCachedSnapshot,
- willRender,
- updateHistory,
- shouldCacheSnapshot,
- acceptsStreamResponse,
- direction
- } = {
- ...defaultOptions,
- ...options
- }
- this.action = action
- this.historyChanged = historyChanged
- this.referrer = referrer
- this.snapshot = snapshot
- this.snapshotHTML = snapshotHTML
- this.response = response
- this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action)
- this.isPageRefresh = this.view.isPageRefresh(this)
- this.visitCachedSnapshot = visitCachedSnapshot
- this.willRender = willRender
- this.updateHistory = updateHistory
- this.scrolled = !willRender
- this.shouldCacheSnapshot = shouldCacheSnapshot
- this.acceptsStreamResponse = acceptsStreamResponse
- this.direction = direction || Direction[action]
- }
-
- get adapter() {
- return this.delegate.adapter
- }
-
- get view() {
- return this.delegate.view
- }
-
- get history() {
- return this.delegate.history
- }
-
- get restorationData() {
- return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)
- }
-
- get silent() {
- return this.isSamePage
- }
-
- start() {
- if (this.state == VisitState.initialized) {
- this.recordTimingMetric(TimingMetric.visitStart)
- this.state = VisitState.started
- this.adapter.visitStarted(this)
- this.delegate.visitStarted(this)
- }
- }
-
- cancel() {
- if (this.state == VisitState.started) {
- if (this.request) {
- this.request.cancel()
- }
- this.cancelRender()
- this.state = VisitState.canceled
- }
- }
-
- complete() {
- if (this.state == VisitState.started) {
- this.recordTimingMetric(TimingMetric.visitEnd)
- this.adapter.visitCompleted(this)
- this.state = VisitState.completed
- this.followRedirect()
-
- if (!this.followedRedirect) {
- this.delegate.visitCompleted(this)
- }
- }
- }
-
- fail() {
- if (this.state == VisitState.started) {
- this.state = VisitState.failed
- this.adapter.visitFailed(this)
- this.delegate.visitCompleted(this)
- }
- }
-
- changeHistory() {
- if (!this.historyChanged && this.updateHistory) {
- const actionForHistory = this.location.href === this.referrer?.href ? 'replace' : this.action
- const method = getHistoryMethodForAction(actionForHistory)
- this.history.update(method, this.location, this.restorationIdentifier)
- this.historyChanged = true
- }
- }
-
- issueRequest() {
- if (this.hasPreloadedResponse()) {
- this.simulateRequest()
- } else if (this.shouldIssueRequest() && !this.request) {
- this.request = new FetchRequest(this, FetchMethod.get, this.location)
- this.request.perform()
- }
- }
-
- simulateRequest() {
- if (this.response) {
- this.startRequest()
- this.recordResponse()
- this.finishRequest()
- }
- }
-
- startRequest() {
- this.recordTimingMetric(TimingMetric.requestStart)
- this.adapter.visitRequestStarted(this)
- }
-
- recordResponse(response = this.response) {
- this.response = response
- if (response) {
- const { statusCode } = response
- if (isSuccessful(statusCode)) {
- this.adapter.visitRequestCompleted(this)
- } else {
- this.adapter.visitRequestFailedWithStatusCode(this, statusCode)
- }
- }
- }
-
- finishRequest() {
- this.recordTimingMetric(TimingMetric.requestEnd)
- this.adapter.visitRequestFinished(this)
- }
-
- loadResponse() {
- if (this.response) {
- const { statusCode, responseHTML } = this.response
- this.render(async () => {
- if (this.shouldCacheSnapshot) this.cacheSnapshot()
- if (this.view.renderPromise) await this.view.renderPromise
-
- if (isSuccessful(statusCode) && responseHTML != null) {
- const snapshot = PageSnapshot.fromHTMLString(responseHTML)
- await this.renderPageSnapshot(snapshot, false)
-
- this.adapter.visitRendered(this)
- this.complete()
- } else {
- await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this)
- this.adapter.visitRendered(this)
- this.fail()
- }
- })
- }
- }
-
- getCachedSnapshot() {
- const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot()
-
- if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
- if (this.action == 'restore' || snapshot.isPreviewable) {
- return snapshot
- }
- }
- }
-
- getPreloadedSnapshot() {
- if (this.snapshotHTML) {
- return PageSnapshot.fromHTMLString(this.snapshotHTML)
- }
- }
-
- hasCachedSnapshot() {
- return this.getCachedSnapshot() != null
- }
-
- loadCachedSnapshot() {
- const snapshot = this.getCachedSnapshot()
- if (snapshot) {
- const isPreview = this.shouldIssueRequest()
- this.render(async () => {
- this.cacheSnapshot()
- if (this.isSamePage || this.isPageRefresh) {
- this.adapter.visitRendered(this)
- } else {
- if (this.view.renderPromise) await this.view.renderPromise
-
- await this.renderPageSnapshot(snapshot, isPreview)
-
- this.adapter.visitRendered(this)
- if (!isPreview) {
- this.complete()
- }
- }
- })
- }
- }
-
- followRedirect() {
- if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) {
- this.adapter.visitProposedToLocation(this.redirectedToLocation, {
- action: 'replace',
- response: this.response,
- shouldCacheSnapshot: false,
- willRender: false
- })
- this.followedRedirect = true
- }
- }
-
- goToSamePageAnchor() {
- if (this.isSamePage) {
- this.render(async () => {
- this.cacheSnapshot()
- this.performScroll()
- this.changeHistory()
- this.adapter.visitRendered(this)
- })
- }
- }
-
- // Fetch request delegate
-
- prepareRequest(request) {
- if (this.acceptsStreamResponse) {
- request.acceptResponseType(StreamMessage.contentType)
- }
- }
-
- requestStarted() {
- this.startRequest()
- }
-
- requestPreventedHandlingResponse(_request, _response) {}
-
- async requestSucceededWithResponse(request, response) {
- const responseHTML = await response.responseHTML
- const { redirected, statusCode } = response
- if (responseHTML == undefined) {
- this.recordResponse({
- statusCode: SystemStatusCode.contentTypeMismatch,
- redirected
- })
- } else {
- this.redirectedToLocation = response.redirected ? response.location : undefined
- this.recordResponse({ statusCode: statusCode, responseHTML, redirected })
- }
- }
-
- async requestFailedWithResponse(request, response) {
- const responseHTML = await response.responseHTML
- const { redirected, statusCode } = response
- if (responseHTML == undefined) {
- this.recordResponse({
- statusCode: SystemStatusCode.contentTypeMismatch,
- redirected
- })
- } else {
- this.recordResponse({ statusCode: statusCode, responseHTML, redirected })
- }
- }
-
- requestErrored(_request, _error) {
- this.recordResponse({
- statusCode: SystemStatusCode.networkFailure,
- redirected: false
- })
- }
-
- requestFinished() {
- this.finishRequest()
- }
-
- // Scrolling
-
- performScroll() {
- if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
- if (this.action == 'restore') {
- this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop()
- } else {
- this.scrollToAnchor() || this.view.scrollToTop()
- }
- if (this.isSamePage) {
- this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location)
- }
-
- this.scrolled = true
- }
- }
-
- scrollToRestoredPosition() {
- const { scrollPosition } = this.restorationData
- if (scrollPosition) {
- this.view.scrollToPosition(scrollPosition)
- return true
- }
- }
-
- scrollToAnchor() {
- const anchor = getAnchor(this.location)
- if (anchor != null) {
- this.view.scrollToAnchor(anchor)
- return true
- }
- }
-
- // Instrumentation
-
- recordTimingMetric(metric) {
- this.timingMetrics[metric] = new Date().getTime()
- }
-
- getTimingMetrics() {
- return { ...this.timingMetrics }
- }
-
- // Private
-
- getHistoryMethodForAction(action) {
- switch (action) {
- case 'replace':
- return history.replaceState
- case 'advance':
- case 'restore':
- return history.pushState
- }
- }
-
- hasPreloadedResponse() {
- return typeof this.response == 'object'
- }
-
- shouldIssueRequest() {
- if (this.isSamePage) {
- return false
- } else if (this.action == 'restore') {
- return !this.hasCachedSnapshot()
- } else {
- return this.willRender
- }
- }
-
- cacheSnapshot() {
- if (!this.snapshotCached) {
- this.view.cacheSnapshot(this.snapshot).then(snapshot => snapshot && this.visitCachedSnapshot(snapshot))
- this.snapshotCached = true
- }
- }
-
- async render(callback) {
- this.cancelRender()
- this.frame = await nextRepaint()
- await callback()
- delete this.frame
- }
-
- async renderPageSnapshot(snapshot, isPreview) {
- await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), async () => {
- await this.view.renderPage(snapshot, isPreview, this.willRender, this)
- this.performScroll()
- })
- }
-
- cancelRender() {
- if (this.frame) {
- cancelAnimationFrame(this.frame)
- delete this.frame
- }
- }
-}
-
-function isSuccessful(statusCode) {
- return statusCode >= 200 && statusCode < 300
-}
-
-class BrowserAdapter {
- progressBar = new ProgressBar()
-
- constructor(session) {
- this.session = session
- }
-
- visitProposedToLocation(location, options) {
- if (locationIsVisitable(location, this.navigator.rootLocation)) {
- this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options)
- } else {
- window.location.href = location.toString()
- }
- }
-
- visitStarted(visit) {
- this.location = visit.location
- visit.loadCachedSnapshot()
- visit.issueRequest()
- visit.goToSamePageAnchor()
- }
-
- visitRequestStarted(visit) {
- this.progressBar.setValue(0)
- if (visit.hasCachedSnapshot() || visit.action != 'restore') {
- this.showVisitProgressBarAfterDelay()
- } else {
- this.showProgressBar()
- }
- }
-
- visitRequestCompleted(visit) {
- visit.loadResponse()
- }
-
- visitRequestFailedWithStatusCode(visit, statusCode) {
- switch (statusCode) {
- case SystemStatusCode.networkFailure:
- case SystemStatusCode.timeoutFailure:
- case SystemStatusCode.contentTypeMismatch:
- return this.reload({
- reason: 'request_failed',
- context: {
- statusCode
- }
- })
- default:
- return visit.loadResponse()
- }
- }
-
- visitRequestFinished(_visit) {}
-
- visitCompleted(_visit) {
- this.progressBar.setValue(1)
- this.hideVisitProgressBar()
- }
-
- pageInvalidated(reason) {
- this.reload(reason)
- }
-
- visitFailed(_visit) {
- this.progressBar.setValue(1)
- this.hideVisitProgressBar()
- }
-
- visitRendered(_visit) {}
-
- // Form Submission Delegate
-
- formSubmissionStarted(_formSubmission) {
- this.progressBar.setValue(0)
- this.showFormProgressBarAfterDelay()
- }
-
- formSubmissionFinished(_formSubmission) {
- this.progressBar.setValue(1)
- this.hideFormProgressBar()
- }
-
- // Private
-
- showVisitProgressBarAfterDelay() {
- this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay)
- }
-
- hideVisitProgressBar() {
- this.progressBar.hide()
- if (this.visitProgressBarTimeout != null) {
- window.clearTimeout(this.visitProgressBarTimeout)
- delete this.visitProgressBarTimeout
- }
- }
-
- showFormProgressBarAfterDelay() {
- if (this.formProgressBarTimeout == null) {
- this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay)
- }
- }
-
- hideFormProgressBar() {
- this.progressBar.hide()
- if (this.formProgressBarTimeout != null) {
- window.clearTimeout(this.formProgressBarTimeout)
- delete this.formProgressBarTimeout
- }
- }
-
- showProgressBar = () => {
- this.progressBar.show()
- }
-
- reload(reason) {
- dispatch('turbo:reload', { detail: reason })
-
- window.location.href = this.location?.toString() || window.location.href
- }
-
- get navigator() {
- return this.session.navigator
- }
-}
-
-class CacheObserver {
- selector = '[data-turbo-temporary]'
- deprecatedSelector = '[data-turbo-cache=false]'
-
- started = false
-
- start() {
- if (!this.started) {
- this.started = true
- addEventListener('turbo:before-cache', this.removeTemporaryElements, false)
- }
- }
-
- stop() {
- if (this.started) {
- this.started = false
- removeEventListener('turbo:before-cache', this.removeTemporaryElements, false)
- }
- }
-
- removeTemporaryElements = _event => {
- for (const element of this.temporaryElements) {
- element.remove()
- }
- }
-
- get temporaryElements() {
- return [...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation]
- }
-
- get temporaryElementsWithDeprecation() {
- const elements = document.querySelectorAll(this.deprecatedSelector)
-
- if (elements.length) {
- console.warn(
- `The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`
- )
- }
-
- return [...elements]
- }
-}
-
-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$1(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
- }
- }
- }
-}
-
-class History {
- location
- restorationIdentifier = uuid()
- restorationData = {}
- started = false
- pageLoaded = false
- currentIndex = 0
-
- constructor(delegate) {
- this.delegate = delegate
- }
-
- start() {
- if (!this.started) {
- addEventListener('popstate', this.onPopState, false)
- addEventListener('load', this.onPageLoad, false)
- this.currentIndex = history.state?.turbo?.restorationIndex || 0
- this.started = true
- this.replace(new URL(window.location.href))
- }
- }
-
- stop() {
- if (this.started) {
- removeEventListener('popstate', this.onPopState, false)
- removeEventListener('load', this.onPageLoad, false)
- this.started = false
- }
- }
-
- push(location, restorationIdentifier) {
- this.update(history.pushState, location, restorationIdentifier)
- }
-
- replace(location, restorationIdentifier) {
- this.update(history.replaceState, location, restorationIdentifier)
- }
-
- update(method, location, restorationIdentifier = uuid()) {
- if (method === history.pushState) ++this.currentIndex
-
- const state = { turbo: { restorationIdentifier, restorationIndex: this.currentIndex } }
- method.call(history, state, '', location.href)
- this.location = location
- this.restorationIdentifier = restorationIdentifier
- }
-
- // Restoration data
-
- getRestorationDataForIdentifier(restorationIdentifier) {
- return this.restorationData[restorationIdentifier] || {}
- }
-
- updateRestorationData(additionalData) {
- const { restorationIdentifier } = this
- const restorationData = this.restorationData[restorationIdentifier]
- this.restorationData[restorationIdentifier] = {
- ...restorationData,
- ...additionalData
- }
- }
-
- // Scroll restoration
-
- assumeControlOfScrollRestoration() {
- if (!this.previousScrollRestoration) {
- this.previousScrollRestoration = history.scrollRestoration ?? 'auto'
- history.scrollRestoration = 'manual'
- }
- }
-
- relinquishControlOfScrollRestoration() {
- if (this.previousScrollRestoration) {
- history.scrollRestoration = this.previousScrollRestoration
- delete this.previousScrollRestoration
- }
- }
-
- // Event handlers
-
- onPopState = event => {
- if (this.shouldHandlePopState()) {
- const { turbo } = event.state || {}
- if (turbo) {
- this.location = new URL(window.location.href)
- const { restorationIdentifier, restorationIndex } = turbo
- this.restorationIdentifier = restorationIdentifier
- const direction = restorationIndex > this.currentIndex ? 'forward' : 'back'
- this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(
- this.location,
- restorationIdentifier,
- direction
- )
- this.currentIndex = restorationIndex
- }
- }
- }
-
- onPageLoad = async _event => {
- await nextMicrotask()
- this.pageLoaded = true
- }
-
- // Private
-
- shouldHandlePopState() {
- // Safari dispatches a popstate event after window's load event, ignore it
- return this.pageIsLoaded()
- }
-
- pageIsLoaded() {
- return this.pageLoaded || document.readyState == 'complete'
- }
-}
-
-class LinkPrefetchObserver {
- started = false
- #prefetchedLink = null
-
- constructor(delegate, eventTarget) {
- this.delegate = delegate
- this.eventTarget = eventTarget
- }
-
- start() {
- if (this.started) return
-
- if (this.eventTarget.readyState === 'loading') {
- this.eventTarget.addEventListener('DOMContentLoaded', this.#enable, { once: true })
- } else {
- this.#enable()
- }
- }
-
- stop() {
- if (!this.started) return
-
- this.eventTarget.removeEventListener('mouseenter', this.#tryToPrefetchRequest, {
- capture: true,
- passive: true
- })
- this.eventTarget.removeEventListener('mouseleave', this.#cancelRequestIfObsolete, {
- capture: true,
- passive: true
- })
-
- this.eventTarget.removeEventListener('turbo:before-fetch-request', this.#tryToUsePrefetchedRequest, true)
- this.started = false
- }
-
- #enable = () => {
- this.eventTarget.addEventListener('mouseenter', this.#tryToPrefetchRequest, {
- capture: true,
- passive: true
- })
- this.eventTarget.addEventListener('mouseleave', this.#cancelRequestIfObsolete, {
- capture: true,
- passive: true
- })
-
- this.eventTarget.addEventListener('turbo:before-fetch-request', this.#tryToUsePrefetchedRequest, true)
- this.started = true
- }
-
- #tryToPrefetchRequest = event => {
- if (getMetaContent('turbo-prefetch') === 'false') return
-
- const target = event.target
- const isLink = target.matches && target.matches('a[href]:not([target^=_]):not([download])')
-
- if (isLink && this.#isPrefetchable(target)) {
- const link = target
- const location = getLocationForLink(link)
-
- if (this.delegate.canPrefetchRequestToLocation(link, location)) {
- this.#prefetchedLink = link
-
- const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams(), target)
-
- prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl)
- }
- }
- }
-
- #cancelRequestIfObsolete = event => {
- if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest()
- }
-
- #cancelPrefetchRequest = () => {
- prefetchCache.clear()
- this.#prefetchedLink = null
- }
-
- #tryToUsePrefetchedRequest = event => {
- if (event.target.tagName !== 'FORM' && event.detail.fetchOptions.method === 'get') {
- const cached = prefetchCache.get(event.detail.url.toString())
-
- if (cached) {
- // User clicked link, use cache response
- event.detail.fetchRequest = cached
- }
-
- prefetchCache.clear()
- }
- }
-
- prepareRequest(request) {
- const link = request.target
-
- request.headers['X-Sec-Purpose'] = 'prefetch'
-
- const turboFrame = link.closest('turbo-frame')
- const turboFrameTarget =
- link.getAttribute('data-turbo-frame') || turboFrame?.getAttribute('target') || turboFrame?.id
-
- if (turboFrameTarget && turboFrameTarget !== '_top') {
- request.headers['Turbo-Frame'] = turboFrameTarget
- }
- }
-
- // Fetch request interface
-
- requestSucceededWithResponse() {}
-
- requestStarted(fetchRequest) {}
-
- requestErrored(fetchRequest) {}
-
- requestFinished(fetchRequest) {}
-
- requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
-
- requestFailedWithResponse(fetchRequest, fetchResponse) {}
-
- get #cacheTtl() {
- return Number(getMetaContent('turbo-prefetch-cache-time')) || cacheTtl
- }
-
- #isPrefetchable(link) {
- const href = link.getAttribute('href')
-
- if (!href) return false
-
- if (unfetchableLink(link)) return false
- if (linkToTheSamePage(link)) return false
- if (linkOptsOut(link)) return false
- if (nonSafeLink(link)) return false
- if (eventPrevented(link)) return false
-
- return true
- }
-}
-
-const unfetchableLink = link => {
- return (
- link.origin !== document.location.origin ||
- !['http:', 'https:'].includes(link.protocol) ||
- link.hasAttribute('target')
- )
-}
-
-const linkToTheSamePage = link => {
- return (
- link.pathname + link.search === document.location.pathname + document.location.search ||
- link.href.startsWith('#')
- )
-}
-
-const linkOptsOut = link => {
- if (link.getAttribute('data-turbo-prefetch') === 'false') return true
- if (link.getAttribute('data-turbo') === 'false') return true
-
- const turboPrefetchParent = findClosestRecursively(link, '[data-turbo-prefetch]')
- if (turboPrefetchParent && turboPrefetchParent.getAttribute('data-turbo-prefetch') === 'false') return true
-
- return false
-}
-
-const nonSafeLink = link => {
- const turboMethod = link.getAttribute('data-turbo-method')
- if (turboMethod && turboMethod.toLowerCase() !== 'get') return true
-
- if (isUJS(link)) return true
- if (link.hasAttribute('data-turbo-confirm')) return true
- if (link.hasAttribute('data-turbo-stream')) return true
-
- return false
-}
-
-const isUJS = link => {
- return (
- link.hasAttribute('data-remote') ||
- link.hasAttribute('data-behavior') ||
- link.hasAttribute('data-confirm') ||
- link.hasAttribute('data-method')
- )
-}
-
-const eventPrevented = link => {
- const event = dispatch('turbo:before-prefetch', { target: link, cancelable: true })
- return event.defaultPrevented
-}
-
-class Navigator {
- constructor(delegate) {
- this.delegate = delegate
- }
-
- proposeVisit(location, options = {}) {
- if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
- this.delegate.visitProposedToLocation(location, options)
- }
- }
-
- startVisit(locatable, restorationIdentifier, options = {}) {
- this.stop()
- this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, {
- referrer: this.location,
- ...options
- })
- this.currentVisit.start()
- }
-
- submitForm(form, submitter) {
- this.stop()
- this.formSubmission = new FormSubmission(this, form, submitter, true)
-
- this.formSubmission.start()
- }
-
- stop() {
- if (this.formSubmission) {
- this.formSubmission.stop()
- delete this.formSubmission
- }
-
- if (this.currentVisit) {
- this.currentVisit.cancel()
- delete this.currentVisit
- }
- }
-
- get adapter() {
- return this.delegate.adapter
- }
-
- get view() {
- return this.delegate.view
- }
-
- get rootLocation() {
- return this.view.snapshot.rootLocation
- }
-
- get history() {
- return this.delegate.history
- }
-
- // Form submission delegate
-
- formSubmissionStarted(formSubmission) {
- // Not all adapters implement formSubmissionStarted
- if (typeof this.adapter.formSubmissionStarted === 'function') {
- this.adapter.formSubmissionStarted(formSubmission)
- }
- }
-
- async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
- if (formSubmission == this.formSubmission) {
- const responseHTML = await fetchResponse.responseHTML
- if (responseHTML) {
- const shouldCacheSnapshot = formSubmission.isSafe
- if (!shouldCacheSnapshot) {
- this.view.clearSnapshotCache()
- }
-
- const { statusCode, redirected } = fetchResponse
- const action = this.#getActionForFormSubmission(formSubmission, fetchResponse)
- const visitOptions = {
- action,
- shouldCacheSnapshot,
- response: { statusCode, responseHTML, redirected }
- }
- this.proposeVisit(fetchResponse.location, visitOptions)
- }
- }
- }
-
- async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
- const responseHTML = await fetchResponse.responseHTML
-
- if (responseHTML) {
- const snapshot = PageSnapshot.fromHTMLString(responseHTML)
- if (fetchResponse.serverError) {
- await this.view.renderError(snapshot, this.currentVisit)
- } else {
- await this.view.renderPage(snapshot, false, true, this.currentVisit)
- }
- if (!snapshot.shouldPreserveScrollPosition) {
- this.view.scrollToTop()
- }
- this.view.clearSnapshotCache()
- }
- }
-
- formSubmissionErrored(formSubmission, error) {
- console.error(error)
- }
-
- formSubmissionFinished(formSubmission) {
- // Not all adapters implement formSubmissionFinished
- if (typeof this.adapter.formSubmissionFinished === 'function') {
- this.adapter.formSubmissionFinished(formSubmission)
- }
- }
-
- // Visit delegate
-
- visitStarted(visit) {
- this.delegate.visitStarted(visit)
- }
-
- visitCompleted(visit) {
- this.delegate.visitCompleted(visit)
- }
-
- locationWithActionIsSamePage(location, action) {
- const anchor = getAnchor(location)
- const currentAnchor = getAnchor(this.view.lastRenderedLocation)
- const isRestorationToTop = action === 'restore' && typeof anchor === 'undefined'
-
- return (
- action !== 'replace' &&
- getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) &&
- (isRestorationToTop || (anchor != null && anchor !== currentAnchor))
- )
- }
-
- visitScrolledToSamePageLocation(oldURL, newURL) {
- this.delegate.visitScrolledToSamePageLocation(oldURL, newURL)
- }
-
- // Visits
-
- get location() {
- return this.history.location
- }
-
- get restorationIdentifier() {
- return this.history.restorationIdentifier
- }
-
- #getActionForFormSubmission(formSubmission, fetchResponse) {
- const { submitter, formElement } = formSubmission
- return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse)
- }
-
- #getDefaultAction(fetchResponse) {
- const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href
- return sameLocationRedirect ? 'replace' : 'advance'
- }
-}
-
-const PageStage = {
- initial: 0,
- loading: 1,
- interactive: 2,
- complete: 3
-}
-
-class PageObserver {
- stage = PageStage.initial
- started = false
-
- constructor(delegate) {
- this.delegate = delegate
- }
-
- start() {
- if (!this.started) {
- if (this.stage == PageStage.initial) {
- this.stage = PageStage.loading
- }
- document.addEventListener('readystatechange', this.interpretReadyState, false)
- addEventListener('pagehide', this.pageWillUnload, false)
- this.started = true
- }
- }
-
- stop() {
- if (this.started) {
- document.removeEventListener('readystatechange', this.interpretReadyState, false)
- removeEventListener('pagehide', this.pageWillUnload, false)
- this.started = false
- }
- }
-
- interpretReadyState = () => {
- const { readyState } = this
- if (readyState == 'interactive') {
- this.pageIsInteractive()
- } else if (readyState == 'complete') {
- this.pageIsComplete()
- }
- }
-
- pageIsInteractive() {
- if (this.stage == PageStage.loading) {
- this.stage = PageStage.interactive
- this.delegate.pageBecameInteractive()
- }
- }
-
- pageIsComplete() {
- this.pageIsInteractive()
- if (this.stage == PageStage.interactive) {
- this.stage = PageStage.complete
- this.delegate.pageLoaded()
- }
- }
-
- pageWillUnload = () => {
- this.delegate.pageWillUnload()
- }
-
- get readyState() {
- return document.readyState
- }
-}
-
-class ScrollObserver {
- started = false
-
- constructor(delegate) {
- this.delegate = delegate
- }
-
- start() {
- if (!this.started) {
- addEventListener('scroll', this.onScroll, false)
- this.onScroll()
- this.started = true
- }
- }
-
- stop() {
- if (this.started) {
- removeEventListener('scroll', this.onScroll, false)
- this.started = false
- }
- }
-
- onScroll = () => {
- this.updatePosition({ x: window.pageXOffset, y: window.pageYOffset })
- }
-
- // Private
-
- updatePosition(position) {
- this.delegate.scrollPositionChanged(position)
- }
-}
-
-class StreamMessageRenderer {
- render({ fragment }) {
- Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), () => {
- withAutofocusFromFragment(fragment, () => {
- withPreservedFocus(() => {
- document.documentElement.appendChild(fragment)
- })
- })
- })
- }
-
- // Bardo delegate
-
- enteringBardo(currentPermanentElement, newPermanentElement) {
- newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true))
- }
-
- leavingBardo() {}
-}
-
-function getPermanentElementMapForFragment(fragment) {
- const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement)
- const permanentElementMap = {}
- for (const permanentElementInDocument of permanentElementsInDocument) {
- const { id } = permanentElementInDocument
-
- for (const streamElement of fragment.querySelectorAll('turbo-stream')) {
- const elementInStream = getPermanentElementById(streamElement.templateElement.content, id)
-
- if (elementInStream) {
- permanentElementMap[id] = [permanentElementInDocument, elementInStream]
- }
- }
- }
-
- return permanentElementMap
-}
-
-async function withAutofocusFromFragment(fragment, callback) {
- const generatedID = `turbo-stream-autofocus-${uuid()}`
- const turboStreams = fragment.querySelectorAll('turbo-stream')
- const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams)
- let willAutofocusId = null
-
- if (elementWithAutofocus) {
- if (elementWithAutofocus.id) {
- willAutofocusId = elementWithAutofocus.id
- } else {
- willAutofocusId = generatedID
- }
-
- elementWithAutofocus.id = willAutofocusId
- }
-
- callback()
- await nextRepaint()
-
- const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body
-
- if (hasNoActiveElement && willAutofocusId) {
- const elementToAutofocus = document.getElementById(willAutofocusId)
-
- if (elementIsFocusable(elementToAutofocus)) {
- elementToAutofocus.focus()
- }
- if (elementToAutofocus && elementToAutofocus.id == generatedID) {
- elementToAutofocus.removeAttribute('id')
- }
- }
-}
-
-async function withPreservedFocus(callback) {
- const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, () => document.activeElement)
-
- const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id
-
- if (restoreFocusTo) {
- const elementToFocus = document.getElementById(restoreFocusTo)
-
- if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
- elementToFocus.focus()
- }
- }
-}
-
-function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
- for (const streamElement of nodeListOfStreamElements) {
- const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content)
-
- if (elementWithAutofocus) return elementWithAutofocus
- }
-
- return null
-}
-
-class StreamObserver {
- sources = new Set()
- #started = false
-
- constructor(delegate) {
- this.delegate = delegate
- }
-
- start() {
- if (!this.#started) {
- this.#started = true
- addEventListener('turbo:before-fetch-response', this.inspectFetchResponse, false)
- }
- }
-
- stop() {
- if (this.#started) {
- this.#started = false
- removeEventListener('turbo:before-fetch-response', this.inspectFetchResponse, false)
- }
- }
-
- connectStreamSource(source) {
- if (!this.streamSourceIsConnected(source)) {
- this.sources.add(source)
- source.addEventListener('message', this.receiveMessageEvent, false)
- }
- }
-
- disconnectStreamSource(source) {
- if (this.streamSourceIsConnected(source)) {
- this.sources.delete(source)
- source.removeEventListener('message', this.receiveMessageEvent, false)
- }
- }
-
- streamSourceIsConnected(source) {
- return this.sources.has(source)
- }
-
- inspectFetchResponse = event => {
- const response = fetchResponseFromEvent(event)
- if (response && fetchResponseIsStream(response)) {
- event.preventDefault()
- this.receiveMessageResponse(response)
- }
- }
-
- receiveMessageEvent = event => {
- if (this.#started && typeof event.data == 'string') {
- this.receiveMessageHTML(event.data)
- }
- }
-
- async receiveMessageResponse(response) {
- const html = await response.responseHTML
- if (html) {
- this.receiveMessageHTML(html)
- }
- }
-
- receiveMessageHTML(html) {
- this.delegate.receivedMessageFromStream(StreamMessage.wrap(html))
- }
-}
-
-function fetchResponseFromEvent(event) {
- const fetchResponse = event.detail?.fetchResponse
- if (fetchResponse instanceof FetchResponse) {
- return fetchResponse
- }
-}
-
-function fetchResponseIsStream(response) {
- const contentType = response.contentType ?? ''
- return contentType.startsWith(StreamMessage.contentType)
-}
-
-class ErrorRenderer extends Renderer {
- static renderElement(currentElement, newElement) {
- const { documentElement, body } = document
-
- documentElement.replaceChild(newElement, body)
- }
-
- async render() {
- this.replaceHeadAndBody()
- this.activateScriptElements()
- }
-
- replaceHeadAndBody() {
- const { documentElement, head } = document
- documentElement.replaceChild(this.newHead, head)
- this.renderElement(this.currentElement, this.newElement)
- }
-
- activateScriptElements() {
- for (const replaceableElement of this.scriptElements) {
- const parentNode = replaceableElement.parentNode
- if (parentNode) {
- const element = activateScriptElement(replaceableElement)
- parentNode.replaceChild(element, replaceableElement)
- }
- }
- }
-
- get newHead() {
- return this.newSnapshot.headSnapshot.element
- }
-
- get scriptElements() {
- return document.documentElement.querySelectorAll('script')
- }
-}
-
-// base IIFE to define idiomorph
-var Idiomorph = (function () {
- //=============================================================================
- // AND NOW IT BEGINS...
- //=============================================================================
- let EMPTY_SET = new Set()
-
- // default configuration values, updatable by users now
- let defaults = {
- morphStyle: 'outerHTML',
- callbacks: {
- beforeNodeAdded: noOp,
- afterNodeAdded: noOp,
- beforeNodeMorphed: noOp,
- afterNodeMorphed: noOp,
- beforeNodeRemoved: noOp,
- afterNodeRemoved: noOp,
- beforeAttributeUpdated: noOp
- },
- head: {
- style: 'merge',
- shouldPreserve: function (elt) {
- return elt.getAttribute('im-preserve') === 'true'
- },
- shouldReAppend: function (elt) {
- return elt.getAttribute('im-re-append') === 'true'
- },
- shouldRemove: noOp,
- afterHeadMorphed: noOp
- }
- }
-
- //=============================================================================
- // Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
- //=============================================================================
- function morph(oldNode, newContent, config = {}) {
- if (oldNode instanceof Document) {
- oldNode = oldNode.documentElement
- }
-
- if (typeof newContent === 'string') {
- newContent = parseContent(newContent)
- }
-
- let normalizedContent = normalizeContent(newContent)
-
- let ctx = createMorphContext(oldNode, normalizedContent, config)
-
- return morphNormalizedContent(oldNode, normalizedContent, ctx)
- }
-
- function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
- if (ctx.head.block) {
- let oldHead = oldNode.querySelector('head')
- let newHead = normalizedNewContent.querySelector('head')
- if (oldHead && newHead) {
- let promises = handleHeadElement(newHead, oldHead, ctx)
- // when head promises resolve, call morph again, ignoring the head tag
- Promise.all(promises).then(function () {
- morphNormalizedContent(
- oldNode,
- normalizedNewContent,
- Object.assign(ctx, {
- head: {
- block: false,
- ignore: true
- }
- })
- )
- })
- return
- }
- }
-
- if (ctx.morphStyle === 'innerHTML') {
- // innerHTML, so we are only updating the children
- morphChildren(normalizedNewContent, oldNode, ctx)
- return oldNode.children
- } else if (ctx.morphStyle === 'outerHTML' || ctx.morphStyle == null) {
- // otherwise find the best element match in the new content, morph that, and merge its siblings
- // into either side of the best match
- let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx)
-
- // stash the siblings that will need to be inserted on either side of the best match
- let previousSibling = bestMatch?.previousSibling
- let nextSibling = bestMatch?.nextSibling
-
- // morph it
- let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx)
-
- if (bestMatch) {
- // if there was a best match, merge the siblings in too and return the
- // whole bunch
- return insertSiblings(previousSibling, morphedNode, nextSibling)
- } else {
- // otherwise nothing was added to the DOM
- return []
- }
- } else {
- throw 'Do not understand how to morph style ' + ctx.morphStyle
- }
- }
-
- /**
- * @param possibleActiveElement
- * @param ctx
- * @returns {boolean}
- */
- function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
- return (
- ctx.ignoreActiveValue &&
- possibleActiveElement === document.activeElement &&
- possibleActiveElement !== document.body
- )
- }
-
- /**
- * @param oldNode root node to merge content into
- * @param newContent new content to merge
- * @param ctx the merge context
- * @returns {Element} the element that ended up in the DOM
- */
- function morphOldNodeTo(oldNode, newContent, ctx) {
- if (ctx.ignoreActive && oldNode === document.activeElement);
- else if (newContent == null) {
- if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode
-
- oldNode.remove()
- ctx.callbacks.afterNodeRemoved(oldNode)
- return null
- } else if (!isSoftMatch(oldNode, newContent)) {
- if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode
- if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode
-
- oldNode.parentElement.replaceChild(newContent, oldNode)
- ctx.callbacks.afterNodeAdded(newContent)
- ctx.callbacks.afterNodeRemoved(oldNode)
- return newContent
- } else {
- if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode
-
- if (oldNode instanceof HTMLHeadElement && ctx.head.ignore);
- else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== 'morph') {
- handleHeadElement(newContent, oldNode, ctx)
- } else {
- syncNodeFrom(newContent, oldNode, ctx)
- if (!ignoreValueOfActiveElement(oldNode, ctx)) {
- morphChildren(newContent, oldNode, ctx)
- }
- }
- ctx.callbacks.afterNodeMorphed(oldNode, newContent)
- return oldNode
- }
- }
-
- /**
- * This is the core algorithm for matching up children. The idea is to use id sets to try to match up
- * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
- * by using id sets, we are able to better match up with content deeper in the DOM.
- *
- * Basic algorithm is, for each node in the new content:
- *
- * - if we have reached the end of the old parent, append the new content
- * - if the new content has an id set match with the current insertion point, morph
- * - search for an id set match
- * - if id set match found, morph
- * - otherwise search for a "soft" match
- * - if a soft match is found, morph
- * - otherwise, prepend the new node before the current insertion point
- *
- * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
- * with the current node. See findIdSetMatch() and findSoftMatch() for details.
- *
- * @param {Element} newParent the parent element of the new content
- * @param {Element } oldParent the old content that we are merging the new content into
- * @param ctx the merge context
- */
- function morphChildren(newParent, oldParent, ctx) {
- let nextNewChild = newParent.firstChild
- let insertionPoint = oldParent.firstChild
- let newChild
-
- // run through all the new content
- while (nextNewChild) {
- newChild = nextNewChild
- nextNewChild = newChild.nextSibling
-
- // if we are at the end of the exiting parent's children, just append
- if (insertionPoint == null) {
- if (ctx.callbacks.beforeNodeAdded(newChild) === false) return
-
- oldParent.appendChild(newChild)
- ctx.callbacks.afterNodeAdded(newChild)
- removeIdsFromConsideration(ctx, newChild)
- continue
- }
-
- // if the current node has an id set match then morph
- if (isIdSetMatch(newChild, insertionPoint, ctx)) {
- morphOldNodeTo(insertionPoint, newChild, ctx)
- insertionPoint = insertionPoint.nextSibling
- removeIdsFromConsideration(ctx, newChild)
- continue
- }
-
- // otherwise search forward in the existing old children for an id set match
- let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx)
-
- // if we found a potential match, remove the nodes until that point and morph
- if (idSetMatch) {
- insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx)
- morphOldNodeTo(idSetMatch, newChild, ctx)
- removeIdsFromConsideration(ctx, newChild)
- continue
- }
-
- // no id set match found, so scan forward for a soft match for the current node
- let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx)
-
- // if we found a soft match for the current node, morph
- if (softMatch) {
- insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx)
- morphOldNodeTo(softMatch, newChild, ctx)
- removeIdsFromConsideration(ctx, newChild)
- continue
- }
-
- // abandon all hope of morphing, just insert the new child before the insertion point
- // and move on
- if (ctx.callbacks.beforeNodeAdded(newChild) === false) return
-
- oldParent.insertBefore(newChild, insertionPoint)
- ctx.callbacks.afterNodeAdded(newChild)
- removeIdsFromConsideration(ctx, newChild)
- }
-
- // remove any remaining old nodes that didn't match up with new content
- while (insertionPoint !== null) {
- let tempNode = insertionPoint
- insertionPoint = insertionPoint.nextSibling
- removeNode(tempNode, ctx)
- }
- }
-
- //=============================================================================
- // Attribute Syncing Code
- //=============================================================================
-
- /**
- * @param attr {String} the attribute to be mutated
- * @param to {Element} the element that is going to be updated
- * @param updateType {("update"|"remove")}
- * @param ctx the merge context
- * @returns {boolean} true if the attribute should be ignored, false otherwise
- */
- function ignoreAttribute(attr, to, updateType, ctx) {
- if (attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement) {
- return true
- }
- return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false
- }
-
- /**
- * syncs a given node with another node, copying over all attributes and
- * inner element state from the 'from' node to the 'to' node
- *
- * @param {Element} from the element to copy attributes & state from
- * @param {Element} to the element to copy attributes & state to
- * @param ctx the merge context
- */
- function syncNodeFrom(from, to, ctx) {
- let type = from.nodeType
-
- // if is an element type, sync the attributes from the
- // new node into the new node
- if (type === 1 /* element type */) {
- const fromAttributes = from.attributes
- const toAttributes = to.attributes
- for (const fromAttribute of fromAttributes) {
- if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
- continue
- }
- if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
- to.setAttribute(fromAttribute.name, fromAttribute.value)
- }
- }
- // iterate backwards to avoid skipping over items when a delete occurs
- for (let i = toAttributes.length - 1; 0 <= i; i--) {
- const toAttribute = toAttributes[i]
- if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
- continue
- }
- if (!from.hasAttribute(toAttribute.name)) {
- to.removeAttribute(toAttribute.name)
- }
- }
- }
-
- // sync text nodes
- if (type === 8 /* comment */ || type === 3 /* text */) {
- if (to.nodeValue !== from.nodeValue) {
- to.nodeValue = from.nodeValue
- }
- }
-
- if (!ignoreValueOfActiveElement(to, ctx)) {
- // sync input values
- syncInputValue(from, to, ctx)
- }
- }
-
- /**
- * @param from {Element} element to sync the value from
- * @param to {Element} element to sync the value to
- * @param attributeName {String} the attribute name
- * @param ctx the merge context
- */
- function syncBooleanAttribute(from, to, attributeName, ctx) {
- if (from[attributeName] !== to[attributeName]) {
- let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx)
- if (!ignoreUpdate) {
- to[attributeName] = from[attributeName]
- }
- if (from[attributeName]) {
- if (!ignoreUpdate) {
- to.setAttribute(attributeName, from[attributeName])
- }
- } else {
- if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
- to.removeAttribute(attributeName)
- }
- }
- }
- }
-
- /**
- * NB: many bothans died to bring us information:
- *
- * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
- * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
- *
- * @param from {Element} the element to sync the input value from
- * @param to {Element} the element to sync the input value to
- * @param ctx the merge context
- */
- function syncInputValue(from, to, ctx) {
- if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== 'file') {
- let fromValue = from.value
- let toValue = to.value
-
- // sync boolean attributes
- syncBooleanAttribute(from, to, 'checked', ctx)
- syncBooleanAttribute(from, to, 'disabled', ctx)
-
- if (!from.hasAttribute('value')) {
- if (!ignoreAttribute('value', to, 'remove', ctx)) {
- to.value = ''
- to.removeAttribute('value')
- }
- } else if (fromValue !== toValue) {
- if (!ignoreAttribute('value', to, 'update', ctx)) {
- to.setAttribute('value', fromValue)
- to.value = fromValue
- }
- }
- } else if (from instanceof HTMLOptionElement) {
- syncBooleanAttribute(from, to, 'selected', ctx)
- } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
- let fromValue = from.value
- let toValue = to.value
- if (ignoreAttribute('value', to, 'update', ctx)) {
- return
- }
- if (fromValue !== toValue) {
- to.value = fromValue
- }
- if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
- to.firstChild.nodeValue = fromValue
- }
- }
- }
-
- //=============================================================================
- // the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
- //=============================================================================
- function handleHeadElement(newHeadTag, currentHead, ctx) {
- let added = []
- let removed = []
- let preserved = []
- let nodesToAppend = []
-
- let headMergeStyle = ctx.head.style
-
- // put all new head elements into a Map, by their outerHTML
- let srcToNewHeadNodes = new Map()
- for (const newHeadChild of newHeadTag.children) {
- srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild)
- }
-
- // for each elt in the current head
- for (const currentHeadElt of currentHead.children) {
- // If the current head element is in the map
- let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML)
- let isReAppended = ctx.head.shouldReAppend(currentHeadElt)
- let isPreserved = ctx.head.shouldPreserve(currentHeadElt)
- if (inNewContent || isPreserved) {
- if (isReAppended) {
- // remove the current version and let the new version replace it and re-execute
- removed.push(currentHeadElt)
- } else {
- // this element already exists and should not be re-appended, so remove it from
- // the new content map, preserving it in the DOM
- srcToNewHeadNodes.delete(currentHeadElt.outerHTML)
- preserved.push(currentHeadElt)
- }
- } else {
- if (headMergeStyle === 'append') {
- // we are appending and this existing element is not new content
- // so if and only if it is marked for re-append do we do anything
- if (isReAppended) {
- removed.push(currentHeadElt)
- nodesToAppend.push(currentHeadElt)
- }
- } else {
- // if this is a merge, we remove this content since it is not in the new head
- if (ctx.head.shouldRemove(currentHeadElt) !== false) {
- removed.push(currentHeadElt)
- }
- }
- }
- }
-
- // Push the remaining new head elements in the Map into the
- // nodes to append to the head tag
- nodesToAppend.push(...srcToNewHeadNodes.values())
-
- let promises = []
- for (const newNode of nodesToAppend) {
- let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild
- if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
- if (newElt.href || newElt.src) {
- let resolve = null
- let promise = new Promise(function (_resolve) {
- resolve = _resolve
- })
- newElt.addEventListener('load', function () {
- resolve()
- })
- promises.push(promise)
- }
- currentHead.appendChild(newElt)
- ctx.callbacks.afterNodeAdded(newElt)
- added.push(newElt)
- }
- }
-
- // remove all removed elements, after we have appended the new elements to avoid
- // additional network requests for things like style sheets
- for (const removedElement of removed) {
- if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
- currentHead.removeChild(removedElement)
- ctx.callbacks.afterNodeRemoved(removedElement)
- }
- }
-
- ctx.head.afterHeadMorphed(currentHead, { added: added, kept: preserved, removed: removed })
- return promises
- }
-
- function noOp() {}
-
- /*
- Deep merges the config object and the Idiomoroph.defaults object to
- produce a final configuration object
- */
- function mergeDefaults(config) {
- let finalConfig = {}
- // copy top level stuff into final config
- Object.assign(finalConfig, defaults)
- Object.assign(finalConfig, config)
-
- // copy callbacks into final config (do this to deep merge the callbacks)
- finalConfig.callbacks = {}
- Object.assign(finalConfig.callbacks, defaults.callbacks)
- Object.assign(finalConfig.callbacks, config.callbacks)
-
- // copy head config into final config (do this to deep merge the head)
- finalConfig.head = {}
- Object.assign(finalConfig.head, defaults.head)
- Object.assign(finalConfig.head, config.head)
- return finalConfig
- }
-
- function createMorphContext(oldNode, newContent, config) {
- config = mergeDefaults(config)
- return {
- target: oldNode,
- newContent: newContent,
- config: config,
- morphStyle: config.morphStyle,
- ignoreActive: config.ignoreActive,
- ignoreActiveValue: config.ignoreActiveValue,
- idMap: createIdMap(oldNode, newContent),
- deadIds: new Set(),
- callbacks: config.callbacks,
- head: config.head
- }
- }
-
- function isIdSetMatch(node1, node2, ctx) {
- if (node1 == null || node2 == null) {
- return false
- }
- if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
- if (node1.id !== '' && node1.id === node2.id) {
- return true
- } else {
- return getIdIntersectionCount(ctx, node1, node2) > 0
- }
- }
- return false
- }
-
- function isSoftMatch(node1, node2) {
- if (node1 == null || node2 == null) {
- return false
- }
- return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
- }
-
- function removeNodesBetween(startInclusive, endExclusive, ctx) {
- while (startInclusive !== endExclusive) {
- let tempNode = startInclusive
- startInclusive = startInclusive.nextSibling
- removeNode(tempNode, ctx)
- }
- removeIdsFromConsideration(ctx, endExclusive)
- return endExclusive.nextSibling
- }
-
- //=============================================================================
- // Scans forward from the insertionPoint in the old parent looking for a potential id match
- // for the newChild. We stop if we find a potential id match for the new child OR
- // if the number of potential id matches we are discarding is greater than the
- // potential id matches for the new child
- //=============================================================================
- function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
- // max id matches we are willing to discard in our search
- let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent)
-
- let potentialMatch = null
-
- // only search forward if there is a possibility of an id match
- if (newChildPotentialIdCount > 0) {
- let potentialMatch = insertionPoint
- // if there is a possibility of an id match, scan forward
- // keep track of the potential id match count we are discarding (the
- // newChildPotentialIdCount must be greater than this to make it likely
- // worth it)
- let otherMatchCount = 0
- while (potentialMatch != null) {
- // If we have an id match, return the current potential match
- if (isIdSetMatch(newChild, potentialMatch, ctx)) {
- return potentialMatch
- }
-
- // computer the other potential matches of this new content
- otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent)
- if (otherMatchCount > newChildPotentialIdCount) {
- // if we have more potential id matches in _other_ content, we
- // do not have a good candidate for an id match, so return null
- return null
- }
-
- // advanced to the next old content child
- potentialMatch = potentialMatch.nextSibling
- }
- }
- return potentialMatch
- }
-
- //=============================================================================
- // Scans forward from the insertionPoint in the old parent looking for a potential soft match
- // for the newChild. We stop if we find a potential soft match for the new child OR
- // if we find a potential id match in the old parents children OR if we find two
- // potential soft matches for the next two pieces of new content
- //=============================================================================
- function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
- let potentialSoftMatch = insertionPoint
- let nextSibling = newChild.nextSibling
- let siblingSoftMatchCount = 0
-
- while (potentialSoftMatch != null) {
- if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
- // the current potential soft match has a potential id set match with the remaining new
- // content so bail out of looking
- return null
- }
-
- // if we have a soft match with the current node, return it
- if (isSoftMatch(newChild, potentialSoftMatch)) {
- return potentialSoftMatch
- }
-
- if (isSoftMatch(nextSibling, potentialSoftMatch)) {
- // the next new node has a soft match with this node, so
- // increment the count of future soft matches
- siblingSoftMatchCount++
- nextSibling = nextSibling.nextSibling
-
- // If there are two future soft matches, bail to allow the siblings to soft match
- // so that we don't consume future soft matches for the sake of the current node
- if (siblingSoftMatchCount >= 2) {
- return null
- }
- }
-
- // advanced to the next old content child
- potentialSoftMatch = potentialSoftMatch.nextSibling
- }
-
- return potentialSoftMatch
- }
-
- function parseContent(newContent) {
- let parser = new DOMParser()
-
- // remove svgs to avoid false-positive matches on head, etc.
- let contentWithSvgsRemoved = newContent.replace(/