diff --git a/composer.json b/composer.json index c4943e4..3db8a0d 100644 --- a/composer.json +++ b/composer.json @@ -22,8 +22,7 @@ "require": { "php": ">=7.4", "yiisoft/yii2": "^2.0.0", - "simialbi/yii2-simialbi-base": ">=0.10.2 <1.0 | ^1.0.0", - "npm-asset/hotwired--turbo": "^7.0.0-beta.4" + "simialbi/yii2-simialbi-base": ">=0.10.2 <1.0 | ^1.0.0" }, "require-dev": { "yiisoft/yii2-coding-standards": "~2.0", diff --git a/src/Frame.php b/src/Frame.php index 008e9a6..69a7c6a 100644 --- a/src/Frame.php +++ b/src/Frame.php @@ -6,6 +6,7 @@ namespace simialbi\yii2\turbo; +use Yii; use yii\base\InvalidConfigException; use yii\base\Widget; use yii\helpers\Html; @@ -76,7 +77,19 @@ public function init(): void $this->options['target'] = $this->target; } - echo Html::beginTag('turbo-frame', $this->options); + if ($this->getRequestTurboFrame() === $this->options['id']) { + if (isset($this->options['src'])) { + unset($this->options['src']); + } + $view = $this->getView(); + $view->clear(); + $view->beginPage(); + echo Html::beginTag('turbo-frame', $this->options); + $view->head(); + $view->beginBody(); + } else { + echo Html::beginTag('turbo-frame', $this->options); + } } /** @@ -84,8 +97,28 @@ public function init(): void */ public function run() { - TurboAsset::register($this->view); + if ($this->getRequestTurboFrame() === $this->options['id']) { + $view = $this->getView(); + $view->endBody(); + $view->endPage(true); + echo Html::endTag('turbo-frame'); + Yii::$app->end(); + return; + } + + TurboAsset::register($this->view); echo Html::endTag('turbo-frame'); } + + /** + * Check if turbo is needed + * @return string|null + */ + protected function getRequestTurboFrame(): ?string + { + $headers = Yii::$app->getRequest()->getHeaders(); + + return $headers->get('Turbo-Frame'); + } } diff --git a/src/TurboAsset.php b/src/TurboAsset.php index d405ed7..4117c4f 100644 --- a/src/TurboAsset.php +++ b/src/TurboAsset.php @@ -7,6 +7,7 @@ namespace simialbi\yii2\turbo; use simialbi\yii2\web\AssetBundle; +use yii\web\View; /** * The turbo asset bundle @@ -16,13 +17,15 @@ class TurboAsset extends AssetBundle /** * @inheritdoc */ - public $sourcePath = '@npm/hotwired--turbo/dist'; + public $js = [ + 'js/turbo.es2017-umd.js' + ]; /** * @inheritdoc */ - public $js = [ - 'turbo.es2017-umd.js' + public $jsOptions = [ + 'position' => View::POS_HEAD ]; /** @@ -30,8 +33,7 @@ class TurboAsset extends AssetBundle */ public $publishOptions = [ 'only' => [ - 'turbo.es2017-umd.js', - 'turbo.es2017-umd.js.map' + 'turbo.es2017-umd.js' ] ]; } diff --git a/src/assets/js/turbo.es2017-umd.js b/src/assets/js/turbo.es2017-umd.js new file mode 100644 index 0000000..9626141 --- /dev/null +++ b/src/assets/js/turbo.es2017-umd.js @@ -0,0 +1,4364 @@ +/* +Turbo 7.0.1 +Copyright © 2021 Basecamp, LLC + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Turbo = {})); +})(this, (function (exports) { + 'use strict'; + + (function () { + if (window.Reflect === undefined || window.customElements === undefined || window.customElements.polyfillWrapFlushCallback) { + return; + } + const BuiltInHTMLElement = HTMLElement; + const wrapperForTheName = { + 'HTMLElement': function HTMLElement() { + return Reflect.construct(BuiltInHTMLElement, [], this.constructor); + } + }; + window.HTMLElement = wrapperForTheName['HTMLElement']; + HTMLElement.prototype = BuiltInHTMLElement.prototype; + HTMLElement.prototype.constructor = HTMLElement; + Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement); + })(); + + 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 === null || candidate === void 0 ? void 0 : 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; + if ('SubmitEvent' in window && /Apple Computer/.test(navigator.vendor)) { + prototype = window.SubmitEvent.prototype; + } else if ('SubmitEvent' in window) { + return; + } else { + prototype = window.Event.prototype; + } + addEventListener('click', clickCaptured, true); + Object.defineProperty(prototype, 'submitter', { + get() + { + if (this.type == 'submit' && this.target instanceof HTMLFormElement) { + return submittersByForm.get(this.target); + } + } + }); + })(); + + var FrameLoadingStyle; + (function (FrameLoadingStyle) { + FrameLoadingStyle['eager'] = 'eager'; + FrameLoadingStyle['lazy'] = 'lazy'; + })(FrameLoadingStyle || (FrameLoadingStyle = {})); + + class FrameElement extends HTMLElement + { + constructor() + { + super(); + this.loaded = Promise.resolve(); + this.delegate = new FrameElement.delegateConstructor(this); + } + + static get observedAttributes() + { + return ['disabled', 'loading', 'src']; + } + + get src() + { + return this.getAttribute('src'); + } + + set src(value) + { + if (value) { + this.setAttribute('src', value); + } else { + this.removeAttribute('src'); + } + } + + get loading() + { + return frameLoadingStyleFromString(this.getAttribute('loading') || ''); + } + + set loading(value) + { + if (value) { + this.setAttribute('loading', value); + } else { + this.removeAttribute('loading'); + } + } + + get disabled() + { + return this.hasAttribute('disabled'); + } + + set disabled(value) + { + if (value) { + this.setAttribute('disabled', ''); + } else { + this.removeAttribute('disabled'); + } + } + + get autoscroll() + { + return this.hasAttribute('autoscroll'); + } + + set autoscroll(value) + { + if (value) { + this.setAttribute('autoscroll', ''); + } else { + this.removeAttribute('autoscroll'); + } + } + + get complete() + { + return !this.delegate.isLoading; + } + + get isActive() + { + return this.ownerDocument === document && !this.isPreview; + } + + get isPreview() + { + var _a, _b; + return (_b = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.documentElement) === null || _b === void 0 ? void 0 : _b.hasAttribute('data-turbo-preview'); + } + + connectedCallback() + { + this.delegate.connect(); + } + + disconnectedCallback() + { + this.delegate.disconnect(); + } + + reload() + { + const {src} = this; + this.src = null; + this.src = src; + } + + attributeChangedCallback(name) + { + if (name == 'loading') { + this.delegate.loadingStyleChanged(); + } else if (name == 'src') { + this.delegate.sourceURLChanged(); + } else { + this.delegate.disabledChanged(); + } + } + } + + 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); + } else if (anchorMatch = url.href.match(/#(.*)$/)) { + return anchorMatch[1]; + } + } + + function getExtension(url) + { + return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || ''; + } + + function isHTML(url) + { + return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/); + } + + function isPrefixedBy(baseURL, url) + { + const prefix = getPrefix(url); + return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix); + } + + 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 dispatch(eventName, { + target, + cancelable, + detail + } = {}) + { + const event = new CustomEvent(eventName, { + cancelable, + bubbles: true, + detail + }); + if (target && target.isConnected) { + target.dispatchEvent(event); + } else { + document.documentElement.dispatchEvent(event); + } + return event; + } + + 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.apply(null, {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(''); + } + + var FetchMethod; + (function (FetchMethod) { + FetchMethod[FetchMethod['get'] = 0] = 'get'; + FetchMethod[FetchMethod['post'] = 1] = 'post'; + FetchMethod[FetchMethod['put'] = 2] = 'put'; + FetchMethod[FetchMethod['patch'] = 3] = 'patch'; + FetchMethod[FetchMethod['delete'] = 4] = 'delete'; + })(FetchMethod || (FetchMethod = {})); + + 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; + } + } + + class FetchRequest + { + constructor(delegate, method, location, body = new URLSearchParams, target = null) + { + this.abortController = new AbortController; + this.resolveRequestPromise = (value) => { + }; + this.delegate = delegate; + this.method = method; + this.headers = this.defaultHeaders; + if (this.isIdempotent) { + this.url = mergeFormDataEntries(location, [...body.entries()]); + } else { + this.body = body; + this.url = location; + } + this.target = target; + } + + get location() + { + return this.url; + } + + get params() + { + return this.url.searchParams; + } + + get entries() + { + return this.body ? Array.from(this.body.entries()) : []; + } + + get fetchOptions() + { + var _a; + return { + method: FetchMethod[this.method].toUpperCase(), + credentials: 'same-origin', + headers: this.headers, + redirect: 'follow', + body: this.body, + signal: this.abortSignal, + referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href + }; + } + + get defaultHeaders() + { + return { + 'Accept': 'text/html, application/xhtml+xml' + }; + } + + get isIdempotent() + { + return this.method == FetchMethod.get; + } + + get abortSignal() + { + return this.abortController.signal; + } + + cancel() + { + this.abortController.abort(); + } + + async perform() + { + var _a, _b; + const {fetchOptions} = this; + (_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this); + await this.allowRequestToBeIntercepted(fetchOptions); + try { + this.delegate.requestStarted(this); + const response = await fetch(this.url.href, fetchOptions); + return await this.receive(response); + } catch (error) { + if (error.name !== 'AbortError') { + 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; + } + + 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.href, + resume: this.resolveRequestPromise + }, + target: this.target + }); + if (event.defaultPrevented) { + await requestInterception; + } + } + } + + function mergeFormDataEntries(url, entries) + { + const currentSearchParams = new URLSearchParams(url.search); + for (const [name, value] of entries) { + if (value instanceof File) { + continue; + } + if (currentSearchParams.has(name)) { + currentSearchParams.delete(name); + url.searchParams.set(name, value); + } else { + url.searchParams.append(name, value); + } + } + return url; + } + + class AppearanceObserver + { + constructor(delegate, element) + { + this.started = false; + this.intersect = entries => { + const lastEntry = entries.slice(-1)[0]; + if (lastEntry === null || lastEntry === void 0 ? void 0 : lastEntry.isIntersecting) { + this.delegate.elementAppearedInViewport(this.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); + } + } + } + + class StreamMessage + { + constructor(html) + { + this.templateElement = document.createElement('template'); + this.templateElement.innerHTML = html; + } + + get fragment() + { + const fragment = document.createDocumentFragment(); + for (const element of this.foreignElements) { + fragment.appendChild(document.importNode(element, true)); + } + return fragment; + } + + get foreignElements() + { + return this.templateChildren.reduce((streamElements, child) => { + if (child.tagName.toLowerCase() == 'turbo-stream') { + return [...streamElements, child]; + } else { + return streamElements; + } + }, []); + } + + get templateChildren() + { + return Array.from(this.templateElement.content.children); + } + + static wrap(message) + { + if (typeof message == 'string') { + return new this(message); + } else { + return message; + } + } + } + + StreamMessage.contentType = 'text/vnd.turbo-stream.html'; + + var FormSubmissionState; + (function (FormSubmissionState) { + FormSubmissionState[FormSubmissionState['initialized'] = 0] = 'initialized'; + FormSubmissionState[FormSubmissionState['requesting'] = 1] = 'requesting'; + FormSubmissionState[FormSubmissionState['waiting'] = 2] = 'waiting'; + FormSubmissionState[FormSubmissionState['receiving'] = 3] = 'receiving'; + FormSubmissionState[FormSubmissionState['stopping'] = 4] = 'stopping'; + FormSubmissionState[FormSubmissionState['stopped'] = 5] = 'stopped'; + })(FormSubmissionState || (FormSubmissionState = {})); + var FormEnctype; + (function (FormEnctype) { + FormEnctype['urlEncoded'] = 'application/x-www-form-urlencoded'; + FormEnctype['multipart'] = 'multipart/form-data'; + FormEnctype['plain'] = 'text/plain'; + })(FormEnctype || (FormEnctype = {})); + + function formEnctypeFromString(encoding) + { + switch (encoding.toLowerCase()) { + case FormEnctype.multipart: + return FormEnctype.multipart; + case FormEnctype.plain: + return FormEnctype.plain; + default: + return FormEnctype.urlEncoded; + } + } + + class FormSubmission + { + constructor(delegate, formElement, submitter, mustRedirect = false) + { + this.state = FormSubmissionState.initialized; + this.delegate = delegate; + this.formElement = formElement; + this.submitter = submitter; + this.formData = buildFormData(formElement, submitter); + this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement); + this.mustRedirect = mustRedirect; + } + + get method() + { + var _a; + const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute('formmethod')) || this.formElement.getAttribute('method') || ''; + return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get; + } + + get action() + { + var _a; + const formElementAction = typeof this.formElement.action === 'string' ? this.formElement.action : null; + return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute('formaction')) || this.formElement.getAttribute('action') || formElementAction || ''; + } + + get location() + { + return expandURL(this.action); + } + + get body() + { + if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) { + return new URLSearchParams(this.stringFormData); + } else { + return this.formData; + } + } + + get enctype() + { + var _a; + return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute('formenctype')) || this.formElement.enctype); + } + + get isIdempotent() + { + return this.fetchRequest.isIdempotent; + } + + get stringFormData() + { + return [...this.formData].reduce((entries, [name, value]) => { + return entries.concat(typeof value == 'string' ? [[name, value]] : []); + }, []); + } + + async start() + { + const { + initialized, + requesting + } = FormSubmissionState; + 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; + } + } + + prepareHeadersForRequest(headers, request) + { + if (!request.isIdempotent) { + const token = getCookieValue(getMetaContent('csrf-param')) || getMetaContent('csrf-token'); + if (token) { + headers['X-CSRF-Token'] = token; + } + headers['Accept'] = [StreamMessage.contentType, headers['Accept']].join(', '); + } + } + + requestStarted(request) + { + this.state = FormSubmissionState.waiting; + dispatch('turbo:submit-start', { + target: this.formElement, + detail: {formSubmission: this} + }); + this.delegate.formSubmissionStarted(this); + } + + requestPreventedHandlingResponse(request, response) + { + this.result = { + success: response.succeeded, + fetchResponse: response + }; + } + + requestSucceededWithResponse(request, response) + { + if (response.clientError || response.serverError) { + this.delegate.formSubmissionFailedWithResponse(this, response); + } else 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; + dispatch('turbo:submit-end', { + target: this.formElement, + detail: Object.assign({formSubmission: this}, this.result) + }); + this.delegate.formSubmissionFinished(this); + } + + requestMustRedirect(request) + { + return !request.isIdempotent && this.mustRedirect; + } + } + + function buildFormData(formElement, submitter) + { + const formData = new FormData(formElement); + const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('name'); + const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('value'); + if (name && value != null && formData.get(name) != value) { + 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 getMetaContent(name) + { + const element = document.querySelector(`meta[name="${name}"]`); + return element && element.content; + } + + function responseSucceededWithoutRedirect(response) + { + return response.statusCode == 200 && !response.redirected; + } + + class Snapshot + { + constructor(element) + { + this.element = element; + } + + get children() + { + return [...this.element.children]; + } + + get isConnected() + { + return this.element.isConnected; + } + + get firstAutofocusableElement() + { + return this.element.querySelector('[autofocus]'); + } + + get permanentElements() + { + return [...this.element.querySelectorAll('[id][data-turbo-permanent]')]; + } + + hasAnchor(anchor) + { + return this.getElementForAnchor(anchor) != null; + } + + getElementForAnchor(anchor) + { + return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null; + } + + getPermanentElementById(id) + { + return this.element.querySelector(`#${id}[data-turbo-permanent]`); + } + + 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; + } + } + + class FormInterceptor + { + constructor(delegate, element) + { + this.submitBubbled = ((event) => { + const form = event.target; + if (form instanceof HTMLFormElement && form.closest('turbo-frame, html') == this.element) { + const submitter = event.submitter || undefined; + if (this.delegate.shouldInterceptFormSubmission(form, submitter)) { + event.preventDefault(); + event.stopImmediatePropagation(); + this.delegate.formSubmissionIntercepted(form, submitter); + } + } + }); + this.delegate = delegate; + this.element = element; + } + + start() + { + this.element.addEventListener('submit', this.submitBubbled); + } + + stop() + { + this.element.removeEventListener('submit', this.submitBubbled); + } + } + + class View + { + constructor(delegate, element) + { + this.resolveRenderPromise = (value) => { + }; + this.resolveInterceptionPromise = (value) => { + }; + this.delegate = delegate; + this.element = element; + } + + get scrollRoot() + { + return window; + } + + 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 + }); + } + + async render(renderer) + { + const { + isPreview, + shouldRender, + newSnapshot: snapshot + } = renderer; + if (shouldRender) { + try { + this.renderPromise = new Promise(resolve => this.resolveRenderPromise = resolve); + this.renderer = renderer; + this.prepareToRenderSnapshot(renderer); + const renderInterception = new Promise(resolve => this.resolveInterceptionPromise = resolve); + const immediateRender = this.delegate.allowsImmediateRender(snapshot, this.resolveInterceptionPromise); + if (!immediateRender) { + await renderInterception; + } + await this.renderSnapshot(renderer); + this.delegate.viewRenderedSnapshot(snapshot, isPreview); + this.finishRenderingSnapshot(renderer); + } finally { + delete this.renderer; + this.resolveRenderPromise(undefined); + delete this.renderPromise; + } + } else { + this.invalidate(); + } + } + + invalidate() + { + this.delegate.viewInvalidated(); + } + + prepareToRenderSnapshot(renderer) + { + this.markAsPreview(renderer.isPreview); + renderer.prepareToRender(); + } + + markAsPreview(isPreview) + { + if (isPreview) { + this.element.setAttribute('data-turbo-preview', ''); + } else { + this.element.removeAttribute('data-turbo-preview'); + } + } + + async renderSnapshot(renderer) + { + await renderer.render(); + } + + finishRenderingSnapshot(renderer) + { + renderer.finishRendering(); + } + } + + class FrameView extends View + { + get snapshot() + { + return new Snapshot(this.element); + } + + invalidate() + { + this.element.innerHTML = ''; + } + } + + class LinkInterceptor + { + constructor(delegate, element) + { + this.clickBubbled = (event) => { + if (this.respondsToEventTarget(event.target)) { + this.clickEvent = event; + } else { + delete this.clickEvent; + } + }; + this.linkClicked = ((event) => { + if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) { + if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) { + this.clickEvent.preventDefault(); + event.preventDefault(); + this.delegate.linkClickIntercepted(event.target, event.detail.url); + } + } + delete this.clickEvent; + }); + this.willVisit = () => { + delete this.clickEvent; + }; + 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); + } + + respondsToEventTarget(target) + { + const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null; + return element && element.closest('turbo-frame, html') == this.element; + } + } + + class Bardo + { + constructor(permanentElementMap) + { + this.permanentElementMap = permanentElementMap; + } + + get placeholders() + { + return [...document.querySelectorAll('meta[name=turbo-permanent-placeholder][content]')]; + } + + static preservingPermanentElements(permanentElementMap, callback) + { + const bardo = new this(permanentElementMap); + bardo.enter(); + callback(); + bardo.leave(); + } + + enter() + { + for (const id in this.permanentElementMap) { + const [, newPermanentElement] = this.permanentElementMap[id]; + this.replaceNewPermanentElementWithPlaceholder(newPermanentElement); + } + } + + leave() + { + for (const id in this.permanentElementMap) { + const [currentPermanentElement] = this.permanentElementMap[id]; + this.replaceCurrentPermanentElementWithClone(currentPermanentElement); + this.replacePlaceholderWithPermanentElement(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 === null || placeholder === void 0 ? void 0 : placeholder.replaceWith(permanentElement); + } + + getPlaceholderById(id) + { + return this.placeholders.find(element => element.content == id); + } + } + + function createPlaceholderForPermanentElement(permanentElement) + { + const element = document.createElement('meta'); + element.setAttribute('name', 'turbo-permanent-placeholder'); + element.setAttribute('content', permanentElement.id); + return element; + } + + class Renderer + { + constructor(currentSnapshot, newSnapshot, isPreview) + { + this.currentSnapshot = currentSnapshot; + this.newSnapshot = newSnapshot; + this.isPreview = isPreview; + this.promise = new Promise((resolve, reject) => this.resolvingFunctions = { + resolve, + reject + }); + } + + get shouldRender() + { + return true; + } + + 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 cspNonce() + { + var _a; + return (_a = document.head.querySelector('meta[name="csp-nonce"]')) === null || _a === void 0 ? void 0 : _a.getAttribute('content'); + } + + prepareToRender() + { + return; + } + + finishRendering() + { + if (this.resolvingFunctions) { + this.resolvingFunctions.resolve(); + delete this.resolvingFunctions; + } + } + + createScriptElement(element) + { + if (element.getAttribute('data-turbo-eval') == 'false') { + return element; + } else { + const createdScriptElement = document.createElement('script'); + if (this.cspNonce) { + createdScriptElement.nonce = this.cspNonce; + } + createdScriptElement.textContent = element.textContent; + copyElementAttributes(createdScriptElement, element); + createdScriptElement.async = false; + createdScriptElement.defer = false; + return createdScriptElement; + } + } + + preservingPermanentElements(callback) + { + Bardo.preservingPermanentElements(this.permanentElementMap, callback); + } + + focusFirstAutofocusableElement() + { + const element = this.connectedSnapshot.firstAutofocusableElement; + if (elementIsFocusable(element)) { + element.focus(); + } + } + } + + function copyElementAttributes(destinationElement, sourceElement) + { + for (const { + name, + value + } of [...sourceElement.attributes]) { + destinationElement.setAttribute(name, value); + } + } + + function elementIsFocusable(element) + { + return element && typeof element.focus == 'function'; + } + + class FrameRenderer extends Renderer + { + get shouldRender() + { + return true; + } + + get newScriptElements() + { + return this.currentElement.querySelectorAll('script'); + } + + async render() + { + await nextAnimationFrame(); + this.preservingPermanentElements(() => { + this.loadFrameElement(); + }); + this.scrollFrameIntoView(); + await nextAnimationFrame(); + this.focusFirstAutofocusableElement(); + await nextAnimationFrame(); + this.activateScriptElements(); + } + + loadFrameElement() + { + var _a; + const destinationRange = document.createRange(); + destinationRange.selectNodeContents(this.currentElement); + destinationRange.deleteContents(); + const frameElement = this.newElement; + const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange(); + if (sourceRange) { + sourceRange.selectNodeContents(frameElement); + this.currentElement.appendChild(sourceRange.extractContents()); + } + } + + scrollFrameIntoView() + { + if (this.currentElement.autoscroll || this.newElement.autoscroll) { + const element = this.currentElement.firstElementChild; + const block = readScrollLogicalPosition(this.currentElement.getAttribute('data-autoscroll-block'), 'end'); + if (element) { + element.scrollIntoView({block}); + return true; + } + } + return false; + } + + async activateScriptElements() + { + const newScriptElements = this.newScriptElements; + this.currentElement.querySelectorAll('script[src]').forEach(function (e) { + e.remove(); + }); + // const scripts = document.querySelectorAll('script[src]'); + if (!window.externalScripts) { + window.externalScripts = []; + } + // var externalScripts = []; + var internalScripts = []; + // console.log(newScriptElements); + // for await (let i = 0; i < newScriptElements.length; i++) { + for (const inertScriptElement of newScriptElements) { + // const inertScriptElement = newScriptElements[i]; + // let render = true; + // if (inertScriptElement.src !== '') { + // for (const existingScripts of scripts) { + // if (existingScripts.src === inertScriptElement.src) { + // render = false; + // break; + // } + // } + // } + // if (render) { + if (inertScriptElement.src) { + window.externalScripts.push(this.include(inertScriptElement.src)); + } else { + internalScripts.push(inertScriptElement); + } + // } + } + + // console.log(window.externalScripts); + await Promise.all(window.externalScripts); + for (const inertScriptElement of internalScripts) { + // console.log(inertScriptElement); + const activatedScriptElement = this.createScriptElement(inertScriptElement); + inertScriptElement.replaceWith(activatedScriptElement); + } + } + + include(url) + { + const that = this; + return new Promise(function(resolve, reject) { + // console.log(url); + const scripts = document.querySelectorAll('script[src]'); + let render = true; + for (const existingScripts of scripts) { + if (existingScripts.src === url) { + render = false; + break; + } + } + let script; + if (!render) { + let parts = url.split('/'); + script = document.querySelector('script[src$="' + parts[parts.length - 1] + '"]'); + } else { + script = document.createElement('script'); + script.async = false; + script.type = 'text/javascript'; + script.src = url; + } + + // console.log('test'); + script.onload = async function () { + // console.log(url + ' fulfilled'); + await that.wait(50); + resolve({script}); + }; + + + window.setTimeout(function () { + // console.log(url + ' fulfilled delayed'); + resolve({script}); + }, 1000); + + if (render) { + document.body.appendChild(script); + } else { + // console.log(url + ' fulfilled'); + resolve({script}); + } + }); + } + + wait(time) + { + return new Promise(function(resolve, fail){ + window.setTimeout(function() { + resolve(); + }, time) + }) + } + } + + function readScrollLogicalPosition(value, defaultValue) + { + if (value == 'end' || value == 'start' || value == 'center' || value == 'nearest') { + return value; + } else { + return defaultValue; + } + } + + class ProgressBar + { + constructor() + { + this.hiding = false; + this.value = 0; + this.visible = false; + this.trickle = () => { + this.setValue(this.value + Math.random() / 100); + }; + this.stylesheetElement = this.createStylesheetElement(); + this.progressElement = this.createProgressElement(); + this.installStylesheetElement(); + this.setValue(0); + } + + static get defaultCSS() + { + return unindent` + .turbo-progress-bar { + position: fixed; + display: block; + top: 0; + left: 0; + height: 3px; + background: #0076ff; + z-index: 9999; + transition: + width ${ProgressBar.animationDuration}ms ease-out, + opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in; + transform: translate3d(0, 0, 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(); + } + + 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; + } + + refresh() + { + requestAnimationFrame(() => { + this.progressElement.style.width = `${10 + (this.value * 90)}%`; + }); + } + + createStylesheetElement() + { + const element = document.createElement('style'); + element.type = 'text/css'; + element.textContent = ProgressBar.defaultCSS; + return element; + } + + createProgressElement() + { + const element = document.createElement('div'); + element.className = 'turbo-progress-bar'; + return element; + } + } + + ProgressBar.animationDuration = 300; + + class HeadSnapshot extends Snapshot + { + constructor() + { + super(...arguments); + this.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 Object.assign(Object.assign({}, result), {[outerHTML]: Object.assign(Object.assign({}, details), {elements: [...details.elements, element]})}); + }, {}); + } + + get trackedElementSignature() + { + return Object.keys(this.detailsByOuterHTML) + .filter(outerHTML => this.detailsByOuterHTML[outerHTML].tracked) + .join(''); + } + + 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; + } + }, []); + } + + 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); + } + + 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); + } + } + + 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.tagName.toLowerCase(); + return tagName == 'script'; + } + + function elementIsNoscript(element) + { + const tagName = element.tagName.toLowerCase(); + return tagName == 'noscript'; + } + + function elementIsStylesheet(element) + { + const tagName = element.tagName.toLowerCase(); + return tagName == 'style' || (tagName == 'link' && element.getAttribute('rel') == 'stylesheet'); + } + + function elementIsMetaElementWithName(element, name) + { + const tagName = element.tagName.toLowerCase(); + return tagName == 'meta' && element.getAttribute('name') == name; + } + + function elementWithoutNonce(element) + { + if (element.hasAttribute('nonce')) { + element.setAttribute('nonce', ''); + } + return element; + } + + class PageSnapshot extends Snapshot + { + constructor(element, headSnapshot) + { + super(element); + this.headSnapshot = headSnapshot; + } + + get headElement() + { + return this.headSnapshot.element; + } + + get rootLocation() + { + var _a; + const root = (_a = this.getSetting('root')) !== null && _a !== void 0 ? _a : '/'; + 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'; + } + + static fromHTMLString(html = '') + { + return this.fromDocument(parseHTMLDocument(html)); + } + + static fromElement(element) + { + return this.fromDocument(element.ownerDocument); + } + + static fromDocument({ + head, + body + }) + { + return new this(body, new HeadSnapshot(head)); + } + + clone() + { + return new PageSnapshot(this.element.cloneNode(true), this.headSnapshot); + } + + getSetting(name) + { + return this.headSnapshot.getMetaValue(`turbo-${name}`); + } + } + + var TimingMetric; + (function (TimingMetric) { + TimingMetric['visitStart'] = 'visitStart'; + TimingMetric['requestStart'] = 'requestStart'; + TimingMetric['requestEnd'] = 'requestEnd'; + TimingMetric['visitEnd'] = 'visitEnd'; + })(TimingMetric || (TimingMetric = {})); + var VisitState; + (function (VisitState) { + VisitState['initialized'] = 'initialized'; + VisitState['started'] = 'started'; + VisitState['canceled'] = 'canceled'; + VisitState['failed'] = 'failed'; + VisitState['completed'] = 'completed'; + })(VisitState || (VisitState = {})); + const defaultOptions = { + action: 'advance', + historyChanged: false + }; + var SystemStatusCode; + (function (SystemStatusCode) { + SystemStatusCode[SystemStatusCode['networkFailure'] = 0] = 'networkFailure'; + SystemStatusCode[SystemStatusCode['timeoutFailure'] = -1] = 'timeoutFailure'; + SystemStatusCode[SystemStatusCode['contentTypeMismatch'] = -2] = 'contentTypeMismatch'; + })(SystemStatusCode || (SystemStatusCode = {})); + + class Visit + { + constructor(delegate, location, restorationIdentifier, options = {}) + { + this.identifier = uuid(); + this.timingMetrics = {}; + this.followedRedirect = false; + this.historyChanged = false; + this.scrolled = false; + this.snapshotCached = false; + this.state = VisitState.initialized; + this.delegate = delegate; + this.location = location; + this.restorationIdentifier = restorationIdentifier || uuid(); + const { + action, + historyChanged, + referrer, + snapshotHTML, + response + } = Object.assign(Object.assign({}, defaultOptions), options); + this.action = action; + this.historyChanged = historyChanged; + this.referrer = referrer; + this.snapshotHTML = snapshotHTML; + this.response = response; + this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.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.state = VisitState.completed; + this.adapter.visitCompleted(this); + this.delegate.visitCompleted(this); + this.followRedirect(); + } + } + + fail() + { + if (this.state == VisitState.started) { + this.state = VisitState.failed; + this.adapter.visitFailed(this); + } + } + + changeHistory() + { + var _a; + if (!this.historyChanged) { + const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? 'replace' : this.action; + const method = this.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 () => { + this.cacheSnapshot(); + if (this.view.renderPromise) { + await this.view.renderPromise; + } + if (isSuccessful(statusCode) && responseHTML != null) { + await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML)); + this.adapter.visitRendered(this); + this.complete(); + } else { + await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML)); + 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.adapter.visitRendered(this); + } else { + if (this.view.renderPromise) { + await this.view.renderPromise; + } + await this.view.renderPage(snapshot, isPreview); + this.adapter.visitRendered(this); + if (!isPreview) { + this.complete(); + } + } + }); + } + } + + followRedirect() + { + if (this.redirectedToLocation && !this.followedRedirect) { + this.adapter.visitProposedToLocation(this.redirectedToLocation, { + action: 'replace', + response: this.response + }); + this.followedRedirect = true; + } + } + + goToSamePageAnchor() + { + if (this.isSamePage) { + this.render(async () => { + this.cacheSnapshot(); + this.adapter.visitRendered(this); + }); + } + } + + requestStarted() + { + this.startRequest(); + } + + requestPreventedHandlingResponse(request, response) + { + } + + async requestSucceededWithResponse(request, response) + { + const responseHTML = await response.responseHTML; + if (responseHTML == undefined) { + this.recordResponse({statusCode: SystemStatusCode.contentTypeMismatch}); + } else { + this.redirectedToLocation = response.redirected ? response.location : undefined; + this.recordResponse({ + statusCode: response.statusCode, + responseHTML + }); + } + } + + async requestFailedWithResponse(request, response) + { + const responseHTML = await response.responseHTML; + if (responseHTML == undefined) { + this.recordResponse({statusCode: SystemStatusCode.contentTypeMismatch}); + } else { + this.recordResponse({ + statusCode: response.statusCode, + responseHTML + }); + } + } + + requestErrored(request, error) + { + this.recordResponse({statusCode: SystemStatusCode.networkFailure}); + } + + requestFinished() + { + this.finishRequest(); + } + + performScroll() + { + if (!this.scrolled) { + 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; + } + } + + recordTimingMetric(metric) + { + this.timingMetrics[metric] = new Date().getTime(); + } + + getTimingMetrics() + { + return Object.assign({}, this.timingMetrics); + } + + 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 true; + } + } + + cacheSnapshot() + { + if (!this.snapshotCached) { + this.view.cacheSnapshot(); + this.snapshotCached = true; + } + } + + async render(callback) + { + this.cancelRender(); + await new Promise(resolve => { + this.frame = requestAnimationFrame(() => resolve()); + }); + await callback(); + delete this.frame; + this.performScroll(); + } + + cancelRender() + { + if (this.frame) { + cancelAnimationFrame(this.frame); + delete this.frame; + } + } + } + + function isSuccessful(statusCode) + { + return statusCode >= 200 && statusCode < 300; + } + + class BrowserAdapter + { + constructor(session) + { + this.progressBar = new ProgressBar; + this.showProgressBar = () => { + this.progressBar.show(); + }; + this.session = session; + } + + get navigator() + { + return this.session.navigator; + } + + visitProposedToLocation(location, options) + { + this.navigator.startVisit(location, uuid(), options); + } + + visitStarted(visit) + { + visit.issueRequest(); + visit.changeHistory(); + visit.goToSamePageAnchor(); + visit.loadCachedSnapshot(); + } + + 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(); + default: + return visit.loadResponse(); + } + } + + visitRequestFinished(visit) + { + this.progressBar.setValue(1); + this.hideVisitProgressBar(); + } + + visitCompleted(visit) + { + } + + pageInvalidated() + { + this.reload(); + } + + visitFailed(visit) + { + } + + visitRendered(visit) + { + } + + formSubmissionStarted(formSubmission) + { + this.progressBar.setValue(0); + this.showFormProgressBarAfterDelay(); + } + + formSubmissionFinished(formSubmission) + { + this.progressBar.setValue(1); + this.hideFormProgressBar(); + } + + 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; + } + } + + reload() + { + window.location.reload(); + } + } + + class CacheObserver + { + constructor() + { + this.started = false; + } + + start() + { + if (!this.started) { + this.started = true; + addEventListener('turbo:before-cache', this.removeStaleElements, false); + } + } + + stop() + { + if (this.started) { + this.started = false; + removeEventListener('turbo:before-cache', this.removeStaleElements, false); + } + } + + removeStaleElements() + { + const staleElements = [...document.querySelectorAll('[data-turbo-cache="false"]')]; + for (const element of staleElements) { + element.remove(); + } + } + } + + class FormSubmitObserver + { + constructor(delegate) + { + this.started = false; + this.submitCaptured = () => { + removeEventListener('submit', this.submitBubbled, false); + addEventListener('submit', this.submitBubbled, false); + }; + this.submitBubbled = ((event) => { + if (!event.defaultPrevented) { + const form = event.target instanceof HTMLFormElement ? event.target : undefined; + const submitter = event.submitter || undefined; + if (form) { + const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formmethod')) || form.method; + if (method != 'dialog' && this.delegate.willSubmitForm(form, submitter)) { + event.preventDefault(); + this.delegate.formSubmitted(form, submitter); + } + } + } + }); + this.delegate = delegate; + } + + start() + { + if (!this.started) { + addEventListener('submit', this.submitCaptured, true); + this.started = true; + } + } + + stop() + { + if (this.started) { + removeEventListener('submit', this.submitCaptured, true); + this.started = false; + } + } + } + + class FrameRedirector + { + constructor(element) + { + this.element = element; + this.linkInterceptor = new LinkInterceptor(this, element); + this.formInterceptor = new FormInterceptor(this, element); + } + + start() + { + this.linkInterceptor.start(); + this.formInterceptor.start(); + } + + stop() + { + this.linkInterceptor.stop(); + this.formInterceptor.stop(); + } + + shouldInterceptLinkClick(element, url) + { + return this.shouldRedirect(element); + } + + linkClickIntercepted(element, url) + { + const frame = this.findFrameElement(element); + if (frame) { + frame.setAttribute('reloadable', ''); + frame.src = url; + } + } + + shouldInterceptFormSubmission(element, submitter) + { + return this.shouldRedirect(element, submitter); + } + + formSubmissionIntercepted(element, submitter) + { + const frame = this.findFrameElement(element, submitter); + if (frame) { + frame.removeAttribute('reloadable'); + frame.delegate.formSubmissionIntercepted(element, submitter); + } + } + + shouldRedirect(element, submitter) + { + const frame = this.findFrameElement(element, submitter); + return frame ? frame != element.closest('turbo-frame') : false; + } + + findFrameElement(element, submitter) + { + const id = (submitter === null || submitter === void 0 ? void 0 : 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 + { + constructor(delegate) + { + this.restorationIdentifier = uuid(); + this.restorationData = {}; + this.started = false; + this.pageLoaded = false; + this.onPopState = (event) => { + if (this.shouldHandlePopState()) { + const {turbo} = event.state || {}; + if (turbo) { + this.location = new URL(window.location.href); + const {restorationIdentifier} = turbo; + this.restorationIdentifier = restorationIdentifier; + this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier); + } + } + }; + this.onPageLoad = async (event) => { + await nextMicrotask(); + this.pageLoaded = true; + }; + this.delegate = delegate; + } + + start() + { + if (!this.started) { + addEventListener('popstate', this.onPopState, false); + addEventListener('load', this.onPageLoad, false); + 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()) + { + const state = {turbo: {restorationIdentifier}}; + method.call(history, state, '', location.href); + this.location = location; + this.restorationIdentifier = restorationIdentifier; + } + + getRestorationDataForIdentifier(restorationIdentifier) + { + return this.restorationData[restorationIdentifier] || {}; + } + + updateRestorationData(additionalData) + { + const {restorationIdentifier} = this; + const restorationData = this.restorationData[restorationIdentifier]; + this.restorationData[restorationIdentifier] = Object.assign(Object.assign({}, restorationData), additionalData); + } + + assumeControlOfScrollRestoration() + { + var _a; + if (!this.previousScrollRestoration) { + this.previousScrollRestoration = (_a = history.scrollRestoration) !== null && _a !== void 0 ? _a : 'auto'; + history.scrollRestoration = 'manual'; + } + } + + relinquishControlOfScrollRestoration() + { + if (this.previousScrollRestoration) { + history.scrollRestoration = this.previousScrollRestoration; + delete this.previousScrollRestoration; + } + } + + shouldHandlePopState() + { + return this.pageIsLoaded(); + } + + pageIsLoaded() + { + return this.pageLoaded || document.readyState == 'complete'; + } + } + + class LinkClickObserver + { + constructor(delegate) + { + this.started = false; + this.clickCaptured = () => { + removeEventListener('click', this.clickBubbled, false); + addEventListener('click', this.clickBubbled, false); + }; + this.clickBubbled = (event) => { + if (this.clickEventIsSignificant(event)) { + const target = (event.composedPath && event.composedPath()[0]) || event.target; + const link = this.findLinkFromClickTarget(target); + if (link) { + const location = this.getLocationForLink(link); + if (this.delegate.willFollowLinkToLocation(link, location)) { + event.preventDefault(); + this.delegate.followedLinkToLocation(link, location); + } + } + } + }; + this.delegate = delegate; + } + + start() + { + if (!this.started) { + addEventListener('click', this.clickCaptured, true); + this.started = true; + } + } + + stop() + { + if (this.started) { + removeEventListener('click', this.clickCaptured, true); + this.started = false; + } + } + + clickEventIsSignificant(event) + { + return !((event.target && event.target.isContentEditable) || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey); + } + + findLinkFromClickTarget(target) + { + if (target instanceof Element) { + return target.closest('a[href]:not([target^=_]):not([download])'); + } + } + + getLocationForLink(link) + { + return expandURL(link.getAttribute('href') || ''); + } + } + + function isAction(action) + { + return action == 'advance' || action == 'replace' || action == 'restore'; + } + + class Navigator + { + constructor(delegate) + { + this.delegate = delegate; + } + + get adapter() + { + return this.delegate.adapter; + } + + get view() + { + return this.delegate.view; + } + + get history() + { + return this.delegate.history; + } + + get location() + { + return this.history.location; + } + + get restorationIdentifier() + { + return this.history.restorationIdentifier; + } + + 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, Object.assign({referrer: this.location}, options)); + this.currentVisit.start(); + } + + submitForm(form, submitter) + { + this.stop(); + this.formSubmission = new FormSubmission(this, form, submitter, true); + if (this.formSubmission.isIdempotent) { + this.proposeVisit(this.formSubmission.fetchRequest.url, {action: this.getActionForFormSubmission(this.formSubmission)}); + } else { + this.formSubmission.start(); + } + } + + stop() + { + if (this.formSubmission) { + this.formSubmission.stop(); + delete this.formSubmission; + } + if (this.currentVisit) { + this.currentVisit.cancel(); + delete this.currentVisit; + } + } + + formSubmissionStarted(formSubmission) + { + 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) { + if (formSubmission.method != FetchMethod.get) { + this.view.clearSnapshotCache(); + } + const {statusCode} = fetchResponse; + const visitOptions = { + response: { + statusCode, + responseHTML + } + }; + 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); + } else { + await this.view.renderPage(snapshot); + } + this.view.scrollToTop(); + this.view.clearSnapshotCache(); + } + } + + formSubmissionErrored(formSubmission, error) + { + console.error(error); + } + + formSubmissionFinished(formSubmission) + { + if (typeof this.adapter.formSubmissionFinished === 'function') { + this.adapter.formSubmissionFinished(formSubmission); + } + } + + 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); + } + + getActionForFormSubmission(formSubmission) + { + const { + formElement, + submitter + } = formSubmission; + const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('data-turbo-action')) || formElement.getAttribute('data-turbo-action'); + return isAction(action) ? action : 'advance'; + } + } + + var PageStage; + (function (PageStage) { + PageStage[PageStage['initial'] = 0] = 'initial'; + PageStage[PageStage['loading'] = 1] = 'loading'; + PageStage[PageStage['interactive'] = 2] = 'interactive'; + PageStage[PageStage['complete'] = 3] = 'complete'; + })(PageStage || (PageStage = {})); + + class PageObserver + { + constructor(delegate) + { + this.stage = PageStage.initial; + this.started = false; + this.interpretReadyState = () => { + const {readyState} = this; + if (readyState == 'interactive') { + this.pageIsInteractive(); + } else if (readyState == 'complete') { + this.pageIsComplete(); + } + }; + this.pageWillUnload = () => { + this.delegate.pageWillUnload(); + }; + this.delegate = delegate; + } + + get readyState() + { + return document.readyState; + } + + 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; + } + } + + 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(); + } + } + } + + class ScrollObserver + { + constructor(delegate) + { + this.started = false; + this.onScroll = () => { + this.updatePosition({ + x: window.pageXOffset, + y: window.pageYOffset + }); + }; + 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; + } + } + + updatePosition(position) + { + this.delegate.scrollPositionChanged(position); + } + } + + class StreamObserver + { + constructor(delegate) + { + this.sources = new Set; + this.started = false; + this.inspectFetchResponse = ((event) => { + const response = fetchResponseFromEvent(event); + if (response && fetchResponseIsStream(response)) { + event.preventDefault(); + this.receiveMessageResponse(response); + } + }); + this.receiveMessageEvent = (event) => { + if (this.started && typeof event.data == 'string') { + this.receiveMessageHTML(event.data); + } + }; + 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); + } + + async receiveMessageResponse(response) + { + const html = await response.responseHTML; + if (html) { + this.receiveMessageHTML(html); + } + } + + receiveMessageHTML(html) + { + this.delegate.receivedMessageFromStream(new StreamMessage(html)); + } + } + + function fetchResponseFromEvent(event) + { + var _a; + const fetchResponse = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchResponse; + if (fetchResponse instanceof FetchResponse) { + return fetchResponse; + } + } + + function fetchResponseIsStream(response) + { + var _a; + const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : ''; + return contentType.startsWith(StreamMessage.contentType); + } + + class ErrorRenderer extends Renderer + { + get newHead() + { + return this.newSnapshot.headSnapshot.element; + } + + get scriptElements() + { + return [...document.documentElement.querySelectorAll('script')]; + } + + async render() + { + this.replaceHeadAndBody(); + this.activateScriptElements(); + } + + replaceHeadAndBody() + { + const { + documentElement, + head, + body + } = document; + documentElement.replaceChild(this.newHead, head); + documentElement.replaceChild(this.newElement, body); + } + + activateScriptElements() + { + for (const replaceableElement of this.scriptElements) { + const parentNode = replaceableElement.parentNode; + if (parentNode) { + const element = this.createScriptElement(replaceableElement); + parentNode.replaceChild(element, replaceableElement); + } + } + } + } + + class PageRenderer extends Renderer + { + get shouldRender() + { + return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical; + } + + get currentHeadSnapshot() + { + return this.currentSnapshot.headSnapshot; + } + + get newHeadSnapshot() + { + return this.newSnapshot.headSnapshot; + } + + get newElement() + { + return this.newSnapshot.element; + } + + get trackedElementsAreIdentical() + { + return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature; + } + + get newHeadStylesheetElements() + { + return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot); + } + + get newHeadScriptElements() + { + return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot); + } + + get currentHeadProvisionalElements() + { + return this.currentHeadSnapshot.provisionalElements; + } + + get newHeadProvisionalElements() + { + return this.newHeadSnapshot.provisionalElements; + } + + get newBodyScriptElements() + { + return this.newElement.querySelectorAll('script'); + } + + prepareToRender() + { + this.mergeHead(); + } + + async render() + { + this.replaceBody(); + } + + finishRendering() + { + super.finishRendering(); + if (!this.isPreview) { + this.focusFirstAutofocusableElement(); + } + } + + mergeHead() + { + this.copyNewHeadStylesheetElements(); + this.copyNewHeadScriptElements(); + this.removeCurrentHeadProvisionalElements(); + this.copyNewHeadProvisionalElements(); + } + + replaceBody() + { + this.preservingPermanentElements(() => { + this.activateNewBody(); + this.assignNewBody(); + }); + } + + copyNewHeadStylesheetElements() + { + for (const element of this.newHeadStylesheetElements) { + document.head.appendChild(element); + } + } + + copyNewHeadScriptElements() + { + for (const element of this.newHeadScriptElements) { + document.head.appendChild(this.createScriptElement(element)); + } + } + + removeCurrentHeadProvisionalElements() + { + for (const element of this.currentHeadProvisionalElements) { + document.head.removeChild(element); + } + } + + copyNewHeadProvisionalElements() + { + for (const element of this.newHeadProvisionalElements) { + document.head.appendChild(element); + } + } + + activateNewBody() + { + document.adoptNode(this.newElement); + this.activateNewBodyScriptElements(); + } + + activateNewBodyScriptElements() + { + for (const inertScriptElement of this.newBodyScriptElements) { + const activatedScriptElement = this.createScriptElement(inertScriptElement); + inertScriptElement.replaceWith(activatedScriptElement); + } + } + + assignNewBody() + { + if (document.body && this.newElement instanceof HTMLBodyElement) { + document.body.replaceWith(this.newElement); + } else { + document.documentElement.appendChild(this.newElement); + } + } + } + + class SnapshotCache + { + constructor(size) + { + this.keys = []; + this.snapshots = {}; + this.size = size; + } + + has(location) + { + return toCacheKey(location) in this.snapshots; + } + + get(location) + { + if (this.has(location)) { + const snapshot = this.read(location); + this.touch(location); + return snapshot; + } + } + + put(location, snapshot) + { + this.write(location, snapshot); + this.touch(location); + return snapshot; + } + + clear() + { + this.snapshots = {}; + } + + read(location) + { + return this.snapshots[toCacheKey(location)]; + } + + write(location, snapshot) + { + this.snapshots[toCacheKey(location)] = snapshot; + } + + touch(location) + { + const key = toCacheKey(location); + const index = this.keys.indexOf(key); + if (index > -1) { + this.keys.splice(index, 1); + } + this.keys.unshift(key); + this.trim(); + } + + trim() + { + for (const key of this.keys.splice(this.size)) { + delete this.snapshots[key]; + } + } + } + + class PageView extends View + { + constructor() + { + super(...arguments); + this.snapshotCache = new SnapshotCache(10); + this.lastRenderedLocation = new URL(location.href); + } + + get snapshot() + { + return PageSnapshot.fromElement(this.element); + } + + get shouldCacheSnapshot() + { + return this.snapshot.isCacheable; + } + + renderPage(snapshot, isPreview = false) + { + const renderer = new PageRenderer(this.snapshot, snapshot, isPreview); + return this.render(renderer); + } + + renderError(snapshot) + { + const renderer = new ErrorRenderer(this.snapshot, snapshot, false); + return this.render(renderer); + } + + clearSnapshotCache() + { + this.snapshotCache.clear(); + } + + async cacheSnapshot() + { + if (this.shouldCacheSnapshot) { + this.delegate.viewWillCacheSnapshot(); + const { + snapshot, + lastRenderedLocation: location + } = this; + await nextEventLoopTick(); + this.snapshotCache.put(location, snapshot.clone()); + } + } + + getCachedSnapshotForLocation(location) + { + return this.snapshotCache.get(location); + } + } + + class Session + { + constructor() + { + this.navigator = new Navigator(this); + this.history = new History(this); + this.view = new PageView(this, document.documentElement); + this.adapter = new BrowserAdapter(this); + this.pageObserver = new PageObserver(this); + this.cacheObserver = new CacheObserver(); + this.linkClickObserver = new LinkClickObserver(this); + this.formSubmitObserver = new FormSubmitObserver(this); + this.scrollObserver = new ScrollObserver(this); + this.streamObserver = new StreamObserver(this); + this.frameRedirector = new FrameRedirector(document.documentElement); + this.drive = true; + this.enabled = true; + this.progressBarDelay = 500; + this.started = false; + } + + get location() + { + return this.history.location; + } + + get restorationIdentifier() + { + return this.history.restorationIdentifier; + } + + get snapshot() + { + return this.view.snapshot; + } + + start() + { + if (!this.started) { + this.pageObserver.start(); + this.cacheObserver.start(); + this.linkClickObserver.start(); + this.formSubmitObserver.start(); + this.scrollObserver.start(); + this.streamObserver.start(); + this.frameRedirector.start(); + this.history.start(); + this.started = true; + this.enabled = true; + } + } + + disable() + { + this.enabled = false; + } + + stop() + { + if (this.started) { + this.pageObserver.stop(); + this.cacheObserver.stop(); + this.linkClickObserver.stop(); + this.formSubmitObserver.stop(); + this.scrollObserver.stop(); + this.streamObserver.stop(); + this.frameRedirector.stop(); + this.history.stop(); + this.started = false; + } + } + + registerAdapter(adapter) + { + this.adapter = adapter; + } + + visit(location, options = {}) + { + this.navigator.proposeVisit(expandURL(location), options); + } + + connectStreamSource(source) + { + this.streamObserver.connectStreamSource(source); + } + + disconnectStreamSource(source) + { + this.streamObserver.disconnectStreamSource(source); + } + + renderStreamMessage(message) + { + document.documentElement.appendChild(StreamMessage.wrap(message).fragment); + } + + clearCache() + { + this.view.clearSnapshotCache(); + } + + setProgressBarDelay(delay) + { + this.progressBarDelay = delay; + } + + historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) + { + if (this.enabled) { + this.navigator.startVisit(location, restorationIdentifier, { + action: 'restore', + historyChanged: true + }); + } else { + this.adapter.pageInvalidated(); + } + } + + scrollPositionChanged(position) + { + this.history.updateRestorationData({scrollPosition: position}); + } + + willFollowLinkToLocation(link, location) + { + return this.elementDriveEnabled(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location); + } + + followedLinkToLocation(link, location) + { + const action = this.getActionForLink(link); + this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, {action}); + } + + convertLinkWithMethodClickToFormSubmission(link) + { + var _a; + const linkMethod = link.getAttribute('data-turbo-method'); + if (linkMethod) { + const form = document.createElement('form'); + form.method = linkMethod; + form.action = link.getAttribute('href') || 'undefined'; + form.hidden = true; + (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link); + return dispatch('submit', { + cancelable: true, + target: form + }); + } else { + return false; + } + } + + allowsVisitingLocationWithAction(location, action) + { + return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location); + } + + visitProposedToLocation(location, options) + { + extendURLWithDeprecatedProperties(location); + this.adapter.visitProposedToLocation(location, options); + } + + visitStarted(visit) + { + extendURLWithDeprecatedProperties(visit.location); + if (!visit.silent) { + this.notifyApplicationAfterVisitingLocation(visit.location, visit.action); + } + } + + visitCompleted(visit) + { + this.notifyApplicationAfterPageLoad(visit.getTimingMetrics()); + } + + locationWithActionIsSamePage(location, action) + { + return this.navigator.locationWithActionIsSamePage(location, action); + } + + visitScrolledToSamePageLocation(oldURL, newURL) + { + this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL); + } + + willSubmitForm(form, submitter) + { + return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter)); + } + + formSubmitted(form, submitter) + { + this.navigator.submitForm(form, submitter); + } + + pageBecameInteractive() + { + this.view.lastRenderedLocation = this.location; + this.notifyApplicationAfterPageLoad(); + } + + pageLoaded() + { + this.history.assumeControlOfScrollRestoration(); + } + + pageWillUnload() + { + this.history.relinquishControlOfScrollRestoration(); + } + + receivedMessageFromStream(message) + { + this.renderStreamMessage(message); + } + + viewWillCacheSnapshot() + { + var _a; + if (!((_a = this.navigator.currentVisit) === null || _a === void 0 ? void 0 : _a.silent)) { + this.notifyApplicationBeforeCachingSnapshot(); + } + } + + allowsImmediateRender({element}, resume) + { + const event = this.notifyApplicationBeforeRender(element, resume); + return !event.defaultPrevented; + } + + viewRenderedSnapshot(snapshot, isPreview) + { + this.view.lastRenderedLocation = this.history.location; + this.notifyApplicationAfterRender(); + } + + viewInvalidated() + { + this.adapter.pageInvalidated(); + } + + frameLoaded(frame) + { + this.notifyApplicationAfterFrameLoad(frame); + } + + frameRendered(fetchResponse, frame) + { + this.notifyApplicationAfterFrameRender(fetchResponse, frame); + } + + applicationAllowsFollowingLinkToLocation(link, location) + { + const event = this.notifyApplicationAfterClickingLinkToLocation(link, location); + return !event.defaultPrevented; + } + + applicationAllowsVisitingLocation(location) + { + const event = this.notifyApplicationBeforeVisitingLocation(location); + return !event.defaultPrevented; + } + + notifyApplicationAfterClickingLinkToLocation(link, location) + { + return dispatch('turbo:click', { + target: link, + detail: {url: location.href}, + cancelable: true + }); + } + + notifyApplicationBeforeVisitingLocation(location) + { + return dispatch('turbo:before-visit', { + detail: {url: location.href}, + cancelable: true + }); + } + + notifyApplicationAfterVisitingLocation(location, action) + { + return dispatch('turbo:visit', { + detail: { + url: location.href, + action + } + }); + } + + notifyApplicationBeforeCachingSnapshot() + { + return dispatch('turbo:before-cache'); + } + + notifyApplicationBeforeRender(newBody, resume) + { + return dispatch('turbo:before-render', { + detail: { + newBody, + resume + }, + cancelable: true + }); + } + + notifyApplicationAfterRender() + { + return dispatch('turbo:render'); + } + + notifyApplicationAfterPageLoad(timing = {}) + { + return dispatch('turbo:load', { + detail: { + url: this.location.href, + timing + } + }); + } + + notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) + { + dispatchEvent(new HashChangeEvent('hashchange', { + oldURL: oldURL.toString(), + newURL: newURL.toString() + })); + } + + notifyApplicationAfterFrameLoad(frame) + { + return dispatch('turbo:frame-load', {target: frame}); + } + + notifyApplicationAfterFrameRender(fetchResponse, frame) + { + return dispatch('turbo:frame-render', { + detail: {fetchResponse}, + target: frame, + cancelable: true + }); + } + + elementDriveEnabled(element) + { + const container = element === null || element === void 0 ? void 0 : element.closest('[data-turbo]'); + if (this.drive) { + if (container) { + return container.getAttribute('data-turbo') != 'false'; + } else { + return true; + } + } else { + if (container) { + return container.getAttribute('data-turbo') == 'true'; + } else { + return false; + } + } + } + + getActionForLink(link) + { + const action = link.getAttribute('data-turbo-action'); + return isAction(action) ? action : 'advance'; + } + + locationIsVisitable(location) + { + return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location); + } + } + + function extendURLWithDeprecatedProperties(url) + { + Object.defineProperties(url, deprecatedLocationPropertyDescriptors); + } + + const deprecatedLocationPropertyDescriptors = { + absoluteURL: { + get() + { + return this.toString(); + } + } + }; + + const session = new Session; + const {navigator: navigator$1} = session; + + function start() + { + session.start(); + } + + function registerAdapter(adapter) + { + session.registerAdapter(adapter); + } + + function visit(location, options) + { + session.visit(location, options); + } + + function connectStreamSource(source) + { + session.connectStreamSource(source); + } + + function disconnectStreamSource(source) + { + session.disconnectStreamSource(source); + } + + function renderStreamMessage(message) + { + session.renderStreamMessage(message); + } + + function clearCache() + { + session.clearCache(); + } + + function setProgressBarDelay(delay) + { + session.setProgressBarDelay(delay); + } + + var Turbo = /*#__PURE__*/Object.freeze({ + __proto__: null, + navigator: navigator$1, + session: session, + PageRenderer: PageRenderer, + PageSnapshot: PageSnapshot, + start: start, + registerAdapter: registerAdapter, + visit: visit, + connectStreamSource: connectStreamSource, + disconnectStreamSource: disconnectStreamSource, + renderStreamMessage: renderStreamMessage, + clearCache: clearCache, + setProgressBarDelay: setProgressBarDelay + }); + + class FrameController + { + constructor(element) + { + this.resolveVisitPromise = () => { + }; + this.connected = false; + this.hasBeenLoaded = false; + this.settingSourceURL = false; + this.element = element; + this.view = new FrameView(this, this.element); + this.appearanceObserver = new AppearanceObserver(this, this.element); + this.linkInterceptor = new LinkInterceptor(this, this.element); + this.formInterceptor = new FormInterceptor(this, this.element); + } + + get id() + { + return this.element.id; + } + + get enabled() + { + return !this.element.disabled; + } + + get sourceURL() + { + if (this.element.src) { + return this.element.src; + } + } + + set sourceURL(sourceURL) + { + this.settingSourceURL = true; + this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null; + this.currentURL = this.element.src; + this.settingSourceURL = false; + } + + get reloadable() + { + const frame = this.findFrameElement(this.element); + return frame.hasAttribute('reloadable'); + } + + set reloadable(value) + { + const frame = this.findFrameElement(this.element); + if (value) { + frame.setAttribute('reloadable', ''); + } else { + frame.removeAttribute('reloadable'); + } + } + + get loadingStyle() + { + return this.element.loading; + } + + get isLoading() + { + return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined; + } + + get isActive() + { + return this.element.isActive && this.connected; + } + + connect() + { + if (!this.connected) { + this.connected = true; + this.reloadable = false; + if (this.loadingStyle == FrameLoadingStyle.lazy) { + this.appearanceObserver.start(); + } + this.linkInterceptor.start(); + this.formInterceptor.start(); + this.sourceURLChanged(); + } + } + + disconnect() + { + if (this.connected) { + this.connected = false; + this.appearanceObserver.stop(); + this.linkInterceptor.stop(); + this.formInterceptor.stop(); + } + } + + disabledChanged() + { + if (this.loadingStyle == FrameLoadingStyle.eager) { + this.loadSourceURL(); + } + } + + sourceURLChanged() + { + if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) { + this.loadSourceURL(); + } + } + + loadingStyleChanged() + { + if (this.loadingStyle == FrameLoadingStyle.lazy) { + this.appearanceObserver.start(); + } else { + this.appearanceObserver.stop(); + this.loadSourceURL(); + } + } + + async loadSourceURL() + { + if (!this.settingSourceURL && this.enabled && this.isActive && (this.reloadable || this.sourceURL != this.currentURL)) { + const previousURL = this.currentURL; + this.currentURL = this.sourceURL; + if (this.sourceURL) { + try { + this.element.loaded = this.visit(this.sourceURL); + this.appearanceObserver.stop(); + await this.element.loaded; + this.hasBeenLoaded = true; + session.frameLoaded(this.element); + } catch (error) { + this.currentURL = previousURL; + throw error; + } + } + } + } + + async loadResponse(fetchResponse) + { + if (fetchResponse.redirected) { + this.sourceURL = fetchResponse.response.url; + } + try { + const html = await fetchResponse.responseHTML; + if (html) { + const {body} = parseHTMLDocument(html); + const snapshot = new Snapshot(await this.extractForeignFrameElement(body)); + const renderer = new FrameRenderer(this.view.snapshot, snapshot, false); + if (this.view.renderPromise) { + await this.view.renderPromise; + } + await this.view.render(renderer); + session.frameRendered(fetchResponse, this.element); + } + } catch (error) { + console.error(error); + this.view.invalidate(); + } + } + + elementAppearedInViewport(element) + { + this.loadSourceURL(); + } + + shouldInterceptLinkClick(element, url) + { + if (element.hasAttribute('data-turbo-method')) { + return false; + } else { + return this.shouldInterceptNavigation(element); + } + } + + linkClickIntercepted(element, url) + { + this.reloadable = true; + this.navigateFrame(element, url); + } + + shouldInterceptFormSubmission(element, submitter) + { + return this.shouldInterceptNavigation(element, submitter); + } + + formSubmissionIntercepted(element, submitter) + { + if (this.formSubmission) { + this.formSubmission.stop(); + } + this.reloadable = false; + this.formSubmission = new FormSubmission(this, element, submitter); + if (this.formSubmission.fetchRequest.isIdempotent) { + this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter); + } else { + const {fetchRequest} = this.formSubmission; + this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest); + this.formSubmission.start(); + } + } + + prepareHeadersForRequest(headers, request) + { + headers['Turbo-Frame'] = this.id; + } + + requestStarted(request) + { + this.element.setAttribute('busy', ''); + } + + requestPreventedHandlingResponse(request, response) + { + this.resolveVisitPromise(); + } + + async requestSucceededWithResponse(request, response) + { + await this.loadResponse(response); + this.resolveVisitPromise(); + } + + requestFailedWithResponse(request, response) + { + console.error(response); + this.resolveVisitPromise(); + } + + requestErrored(request, error) + { + console.error(error); + this.resolveVisitPromise(); + } + + requestFinished(request) + { + this.element.removeAttribute('busy'); + } + + formSubmissionStarted(formSubmission) + { + const frame = this.findFrameElement(formSubmission.formElement); + frame.setAttribute('busy', ''); + } + + formSubmissionSucceededWithResponse(formSubmission, response) + { + const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter); + frame.delegate.loadResponse(response); + } + + formSubmissionFailedWithResponse(formSubmission, fetchResponse) + { + this.element.delegate.loadResponse(fetchResponse); + } + + formSubmissionErrored(formSubmission, error) + { + console.error(error); + } + + formSubmissionFinished(formSubmission) + { + const frame = this.findFrameElement(formSubmission.formElement); + frame.removeAttribute('busy'); + } + + allowsImmediateRender(snapshot, resume) + { + return true; + } + + viewRenderedSnapshot(snapshot, isPreview) + { + } + + viewInvalidated() + { + } + + async visit(url) + { + const request = new FetchRequest(this, FetchMethod.get, expandURL(url), undefined, this.element); + return new Promise(resolve => { + this.resolveVisitPromise = () => { + this.resolveVisitPromise = () => { + }; + resolve(); + }; + request.perform(); + }); + } + + navigateFrame(element, url, submitter) + { + const frame = this.findFrameElement(element, submitter); + frame.setAttribute('reloadable', ''); + frame.src = url; + } + + findFrameElement(element, submitter) + { + var _a; + const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('data-turbo-frame')) || element.getAttribute('data-turbo-frame') || this.element.getAttribute('target'); + return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element; + } + + async extractForeignFrameElement(container) + { + let element; + const id = CSS.escape(this.id); + try { + if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) { + return element; + } + if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) { + await element.loaded; + return await this.extractForeignFrameElement(element); + } + console.error(`Response has no matching element`); + } catch (error) { + console.error(error); + } + return new FrameElement(); + } + + shouldInterceptNavigation(element, submitter) + { + const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('data-turbo-frame')) || element.getAttribute('data-turbo-frame') || this.element.getAttribute('target'); + if (!this.enabled || id == '_top') { + return false; + } + if (id) { + const frameElement = getFrameElementById(id); + if (frameElement) { + return !frameElement.disabled; + } + } + if (!session.elementDriveEnabled(element)) { + return false; + } + if (submitter && !session.elementDriveEnabled(submitter)) { + return false; + } + return true; + } + } + + function getFrameElementById(id) + { + if (id != null) { + const element = document.getElementById(id); + if (element instanceof FrameElement) { + return element; + } + } + } + + function activateElement(element, currentURL) + { + if (element) { + const src = element.getAttribute('src'); + if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) { + throw new Error(`Matching element has a source URL which references itself`); + } + if (element.ownerDocument !== document) { + element = document.importNode(element, true); + } + if (element instanceof FrameElement) { + element.connectedCallback(); + return element; + } + } + } + + const StreamActions = { + after() + { + this.targetElements.forEach(e => { + var _a; + return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling); + }); + }, + append() + { + this.removeDuplicateTargetChildren(); + this.targetElements.forEach(e => e.append(this.templateContent)); + }, + before() + { + this.targetElements.forEach(e => { + var _a; + return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e); + }); + }, + prepend() + { + this.removeDuplicateTargetChildren(); + this.targetElements.forEach(e => e.prepend(this.templateContent)); + }, + remove() + { + this.targetElements.forEach(e => e.remove()); + }, + replace() + { + this.targetElements.forEach(e => e.replaceWith(this.templateContent)); + }, + update() + { + this.targetElements.forEach(e => { + e.innerHTML = ''; + e.append(this.templateContent); + }); + } + }; + + class StreamElement extends HTMLElement + { + get duplicateChildren() + { + var _a; + const existingChildren = this.targetElements.flatMap(e => [...e.children]).filter(c => !!c.id); + const newChildrenIds = [...(_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children].filter(c => !!c.id).map(c => c.id); + return existingChildren.filter(c => newChildrenIds.includes(c.id)); + } + + get performAction() + { + if (this.action) { + const actionFunction = StreamActions[this.action]; + if (actionFunction) { + return actionFunction; + } + this.raise('unknown action'); + } + this.raise('action attribute is missing'); + } + + get targetElements() + { + if (this.target) { + return this.targetElementsById; + } else if (this.targets) { + return this.targetElementsByQuery; + } else { + this.raise('target or targets attribute is missing'); + } + } + + get templateContent() + { + return this.templateElement.content.cloneNode(true); + } + + get templateElement() + { + if (this.firstElementChild instanceof HTMLTemplateElement) { + return this.firstElementChild; + } + this.raise('first child element must be a