diff --git a/components/progress-bar-delay/progress-bar-delay.component.yml b/components/progress-bar-delay/progress-bar-delay.component.yml new file mode 100644 index 0000000000000000000000000000000000000000..5021c0b9d4c456917df214cf2c3fe03f6cbbf4cb --- /dev/null +++ b/components/progress-bar-delay/progress-bar-delay.component.yml @@ -0,0 +1,12 @@ +$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json +name: RefreshLess progress bar delay +description: Wraps the Refreshless progress bar to delay displaying until configured time has elapsed. +props: + type: object + additionalProperties: false + properties: {} +libraryOverrides: + dependencies: + - core/drupal + js: + progress-bar-delay.js: { attributes: { defer: true }, group: refreshless, preprocess: false } diff --git a/components/progress-bar-delay/progress-bar-delay.js b/components/progress-bar-delay/progress-bar-delay.js new file mode 100644 index 0000000000000000000000000000000000000000..374062143523a1751a9681a3235d556c78b08877 --- /dev/null +++ b/components/progress-bar-delay/progress-bar-delay.js @@ -0,0 +1,215 @@ +/** + * @file + * RefreshLess progress bar delay. + * + * @see https://www.drupal.org/project/refreshless/issues/3488464 + * Issue for adapting this out of Turbo. + * + * @see https://github.com/hotwired/turbo/blob/ea54ae5ad4b6b28cb62ccd62951352641ed08293/src/core/drive/progress_bar.js + * Originally adapted from the Turbo progress bar. + */ +((Drupal) => { + + 'use strict'; + + /** + * CSS custom property base name. + * + * @type {String} + */ + const customPropertyBase = '--refreshless-progress-bar'; + + /** + * Name of the CSS custom property containing the progress bar delay. + * + * The value will be a time in ms. + * + * @type {String} + */ + const delayCustomProperty = `${customPropertyBase}-delay`; + + /** + * The <html> element. + * + * @type {HTMLHtmlElement} + */ + const html = document.documentElement; + + /** + * Represents a progress bar delay wrapper. + */ + class ProgressBarDelay { + + /** + * Time in milliseconds before the progress bar is shown. + * + * @type {Number} + */ + #delay = 0; + + /** + * The visit timeout ID, if any. + * + * @type {Number|null} + */ + #visitTimeout = null; + + /** + * The form submit timeout ID, if any. + * + * @type {Number|null} + */ + #formTimeout = null; + + /** + * The progress bar instance that we're wrapping. + * + * @type {ProgressBar} + */ + #progressBar; + + constructor(delay, progressBar) { + + this.#delay = delay; + + this.#progressBar = progressBar; + + } + + /** + * Install the progress bar in the document and set various properties. + */ + install() { + + html.style.setProperty( + delayCustomProperty, `${this.#delay}ms`, + ); + + } + + /** + * Uninstall the progress bar from the document and remove properties. + */ + uninstall() { + + html.style.removeProperty(delayCustomProperty); + + } + + /** + * Start the timeout to show the progress bar. + * + * @param {Number|null} timeoutId + * An existing timeout ID, if any. + * + * @return {Number} + * A new timeout ID. + */ + showAfterDelay(timeoutId) { + + if (timeoutId !== null) { + window.clearTimeout(timeoutId); + } + + this.install(); + + return window.setTimeout(async () => { + + await this.#progressBar.setValue(0); + + this.#progressBar.show(); + + }, this.#delay); + + } + + /** + * Hide the progress bar and cancel an existing timeout, if any. + * + * @param {Number|null} timeoutId + * + * @return {null} + */ + hideAndCancelDelay(timeoutId) { + + if (timeoutId === null) { + return timeoutId; + } + + window.clearTimeout(timeoutId); + + timeoutId = null; + + this.#progressBar.finish().then(() => this.uninstall()); + + return timeoutId; + + } + + /** + * Show the progress bar after a delay for non-form submission visits. + */ + showVisitAfterDelay() { + + this.#visitTimeout = this.showAfterDelay(this.#visitTimeout); + + } + + /** + * Hide the progress bar for non-form submission visits. + */ + hideVisit() { + + this.#visitTimeout = this.hideAndCancelDelay(this.#visitTimeout); + + } + + /** + * Show the progress bar after a delay for form submissions. + */ + showFormAfterDelay() { + + // Unlike the visit progress bar, we prefer to not replace an existing + // form submission progress bar. + // + // @todo Is this necessary? Don't browsers by default treat both types the + // same when showing progress? + if (this.#formTimeout !== null) { + return; + } + + this.#formTimeout = this.showAfterDelay(this.#formTimeout); + + } + + /** + * Hide the progress bar for form submissions. + */ + hideForm() { + + this.#formTimeout = this.hideAndCancelDelay(this.#formTimeout); + + } + + /** + * Get the progress bar instance we're wrapping. + * + * @return {ProgressBar} + */ + get progressBar() { + return this.#progressBar; + } + + } + + if (!('RefreshLess' in Drupal)) { + Drupal.RefreshLess = {}; + } + + if (!('classes' in Drupal.RefreshLess)) { + Drupal.RefreshLess.classes = {}; + } + + Drupal.RefreshLess.classes.ProgressBarDelay = ProgressBarDelay; + +})(Drupal); diff --git a/components/progress-bar-delay/progress-bar-delay.twig b/components/progress-bar-delay/progress-bar-delay.twig new file mode 100644 index 0000000000000000000000000000000000000000..34cb7d028fc3d01a5b4337405f567968b56ad460 --- /dev/null +++ b/components/progress-bar-delay/progress-bar-delay.twig @@ -0,0 +1 @@ +{# Required or core will throw a fatal error. #} diff --git a/components/progress-bar/progress-bar.component.yml b/components/progress-bar/progress-bar.component.yml new file mode 100644 index 0000000000000000000000000000000000000000..725ffe419305dc757c3dff1f4967ecdbe5b8df6e --- /dev/null +++ b/components/progress-bar/progress-bar.component.yml @@ -0,0 +1,12 @@ +$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json +name: RefreshLess progress bar +description: Displays progress for page loads and form submissions. +props: + type: object + additionalProperties: false + properties: {} +libraryOverrides: + dependencies: + - core/drupal + js: + progress-bar.js: { attributes: { defer: true }, group: refreshless, preprocess: false } diff --git a/components/progress-bar/progress-bar.css b/components/progress-bar/progress-bar.css new file mode 100644 index 0000000000000000000000000000000000000000..785582b9a3f64f18e27910debb04dba3b0c3cdeb --- /dev/null +++ b/components/progress-bar/progress-bar.css @@ -0,0 +1,99 @@ +:root { + + /** + * The progress bar colour. + * + * Defaults to Drupal blue. + * + * @type {Color} + */ + --refreshless-progress-bar-colour: #0678be; + + /** + * The progress bar thickness. + * + * @type {Number} + */ + --refreshless-progress-bar-thickness: 3px; + + /** + * The progress bar z-index. + * + * Themes should override this to a more sensible value. + * + * @type {Number} + */ + --refreshless-progress-bar-z-index: 2147483647; + + /** + * The progress bar minimum/start value. + * + * This should be a percentage. + * + * @type {Number} + */ + --refreshless-progress-bar-start: 10%; + + /** + * Progress bar width transition. + */ + --refreshless-progress-bar-width-transition: + width var(--refreshless-progress-bar-transition-duration) ease-out; + + /** + * Progress bar opacity transition out. + */ + --refreshless-progress-bar-opacity-transition-out: + opacity + calc(var(--refreshless-progress-bar-transition-duration) / 2) + calc(var(--refreshless-progress-bar-transition-duration) / 2) + ease-in; + + /** + * Progress bar opacity transition in. + */ + --refreshless-progress-bar-opacity-transition-in: + opacity + calc(var(--refreshless-progress-bar-transition-duration) / 2) + ease-in; + +} + +.refreshless-document-progress-bar { + + position: fixed; + + display: block; + + top: 0; + left: 0; + + height: var(--refreshless-progress-bar-thickness); + + width: calc( + var(--refreshless-progress-bar-value) * ( + 100% - var(--refreshless-progress-bar-start) + ) + var(--refreshless-progress-bar-start) + ); + + background-color: var(--refreshless-progress-bar-colour); + + z-index: var(--refreshless-progress-bar-z-index); + + transition: + var(--refreshless-progress-bar-width-transition), + var(--refreshless-progress-bar-opacity-transition-out); + + opacity: 0; + +} + +.refreshless-document-progress-bar--active { + + opacity: 1; + + transition: + var(--refreshless-progress-bar-width-transition), + var(--refreshless-progress-bar-opacity-transition-in); + +} diff --git a/components/progress-bar/progress-bar.js b/components/progress-bar/progress-bar.js new file mode 100644 index 0000000000000000000000000000000000000000..064467feb55e6d83b88d94215ac91b7b1f3abca5 --- /dev/null +++ b/components/progress-bar/progress-bar.js @@ -0,0 +1,354 @@ +/** + * @file + * RefreshLess progress bar. + * + * @see https://www.drupal.org/project/refreshless/issues/3488464 + * Issue for adapting this out of Turbo. + * + * @see https://github.com/hotwired/turbo/blob/ea54ae5ad4b6b28cb62ccd62951352641ed08293/src/core/drive/progress_bar.js + * Originally adapted from the Turbo progress bar. + */ +((Drupal) => { + + 'use strict'; + + Drupal.theme.refreshlessProgressBar = ( + elementType = 'div', + elementClass = 'refreshless-document-progress-bar', + ) => { + + const element = document.createElement(elementType); + + element.classList.add(elementClass); + + // This provides the ProgressBar JavaScript class the configured class name + // to build modifier classes from. + element.refreshlessProgressBarClass = elementClass; + + return element; + + } + + /** + * CSS custom property base name. + * + * @type {String} + */ + const customPropertyBase = '--refreshless-progress-bar'; + + /** + * Name of the CSS custom property containing the transition duration. + * + * The value will be a time in ms. + * + * @type {String} + */ + const durationCustomProperty = `${customPropertyBase}-transition-duration`; + + /** + * Name of the CSS custom property containing the current progress bar value. + * + * The value will be float between 0 and 1, inclusive. + * + * @type {String} + */ + const valueCustomProperty = `${customPropertyBase}-value`; + + /** + * The <html> element. + * + * @type {HTMLHtmlElement} + */ + const html = document.documentElement; + + /** + * Represents a progress bar. + */ + class ProgressBar { + + /** + * The progress bar HTML element. + * + * @type {HTMLElement} + */ + #element; + + /** + * Whether the progress bar is currently in the processing of hiding. + * + * @type {Boolean} + */ + #hiding = false; + + /** + * The current value of the progress bar, from 0 to 1, inclusive. + * + * @type {Number} + */ + #value = 0; + + /** + * Whether the progress bar is currently visible. + * + * @type {Boolean} + */ + #visible = false; + + /** + * The trickle interval ID, or null if one is not active. + * + * @type {Number|null} + */ + #trickleInterval = null; + + /** + * The progress bar transition duration, in milliseconds. + * + * @type {Number} + */ + #transitionDuration = 300; + + constructor() { + + this.#element = Drupal.theme('refreshlessProgressBar'); + + this.setValue(0); + + } + + /** + * Show the progress bar if not already visible. + */ + show() { + + if (this.#visible === true) { + return; + } + + this.#visible = true; + this.install(); + this.startTrickling(); + + } + + /** + * Hide the progress bar if visible and not already hiding. + * + * @return {Promise} + */ + hide() { + + if (!(this.#visible === true && this.#hiding === false)) { + return Promise.resolve(); + } + + this.#hiding = true; + + this.stopTrickling(); + + return new Promise(async (resolve, reject) => { + + this.#setInactive(); + + await new Promise((resolve, reject) => { + + window.setTimeout(resolve, this.#transitionDuration * 1.5); + + }); + + this.uninstall(); + this.#visible = false; + this.#hiding = false; + + resolve(); + + }); + + } + + /** + * Set the progress bar as active, causing CSS to transition it in. + */ + #setActive() { + + this.#element.classList.add( + `${this.#element.refreshlessProgressBarClass}--active`, + ); + + } + + /** + * Set the progress bar as inactive, causing CSS to transition it out. + */ + #setInactive() { + + this.#element.classList.remove( + `${this.#element.refreshlessProgressBarClass}--active`, + ); + + } + + /** + * Explicitly set the progress bar to a value. + * + * @param {Number} value + * A number between 0 and 1, inclusive. + * + * @throws If value is NaN, or if the value is less than 0 or greater than + * 1. + */ + setValue(value) { + + if (Number.isNaN(value)) { + throw `Progress bar value must be a number! Got: "${typeof value}"`; + } + + if (value < 0 || value > 1) { + throw `Progress bar value must be an integer or float between 0 and 1! Got: "${value}"`; + } + + this.#value = value; + + return this.refresh(); + + } + + /** + * Install the progress bar in the document and set various properties. + */ + install() { + + html.style.setProperty( + durationCustomProperty, `${this.#transitionDuration}ms`, + ); + + html.style.setProperty(valueCustomProperty, 0); + + this.#setInactive(); + + document.body.insertAdjacentElement('beforebegin', this.#element); + + this.refresh().then(async () => { + + await new Promise(requestAnimationFrame); + + this.#setActive(); + + }); + + } + + /** + * Uninstall the progress bar from the document and remove properties. + */ + uninstall() { + + if (this.#element.parentNode) { + this.#element.remove(); + } + + html.style.removeProperty(durationCustomProperty); + html.style.removeProperty(valueCustomProperty); + + } + + /** + * Start the trickling animation. + */ + startTrickling() { + + if (this.#trickleInterval !== null) { + return; + } + + this.#trickleInterval = window.setInterval( + this.trickle, this.#transitionDuration, + ); + + } + + /** + * Stop the trickling animation. + */ + stopTrickling() { + + window.clearInterval(this.#trickleInterval); + + this.#trickleInterval = null; + + } + + /** + * Trickle animation interval callback. + * + * This generates a random value to give the trickle the irregular movement. + */ + trickle = () => { + this.setValue(Math.min(1, this.#value + Math.random() / 100)); + } + + /** + * Update the progress bar element's value with the current value. + */ + async refresh() { + + // Don't write the custom property if not visible or if currently hiding. + if (!(this.#visible === true && this.#hiding === false)) { + return; + } + + await new Promise(requestAnimationFrame); + + html.style.setProperty(valueCustomProperty, this.#value); + + } + + /** + * Finish/complete the progress bar by setting to 100% and start hiding it. + * + * @return {Promise} + */ + finish() { + return this.setValue(1).then(() => this.hide()); + } + + /** + * Get the current value of the progress bar. + * + * @return {Number} + */ + get value() { + return this.#value; + } + + /** + * Get the progress bar HTML element. + * + * @return {HTMLElement} + */ + get element() { + return this.#element; + } + + /** + * Get the progress bar transition value. + * + * @return {Number} + */ + get transitionDuration() { + return this.#transitionDuration; + } + + } + + if (!('RefreshLess' in Drupal)) { + Drupal.RefreshLess = {}; + } + + if (!('classes' in Drupal.RefreshLess)) { + Drupal.RefreshLess.classes = {}; + } + + Drupal.RefreshLess.classes.ProgressBar = ProgressBar; + +})(Drupal); diff --git a/components/progress-bar/progress-bar.twig b/components/progress-bar/progress-bar.twig new file mode 100644 index 0000000000000000000000000000000000000000..34cb7d028fc3d01a5b4337405f567968b56ad460 --- /dev/null +++ b/components/progress-bar/progress-bar.twig @@ -0,0 +1 @@ +{# Required or core will throw a fatal error. #} diff --git a/modules/refreshless_turbo/css/scroll.css b/modules/refreshless_turbo/css/scroll.css new file mode 100644 index 0000000000000000000000000000000000000000..e1dc6fce1513ac32b8fdfd94c0980e1888f18a39 --- /dev/null +++ b/modules/refreshless_turbo/css/scroll.css @@ -0,0 +1,5 @@ +:root.refreshless-disable-smooth-scroll { + + scroll-behavior: auto !important; + +} diff --git a/modules/refreshless_turbo/css/turbo.css b/modules/refreshless_turbo/css/turbo.css deleted file mode 100644 index d05fddaab915ad0f0cc4cd877eced2936308cc15..0000000000000000000000000000000000000000 --- a/modules/refreshless_turbo/css/turbo.css +++ /dev/null @@ -1,28 +0,0 @@ -/* @see https://turbo.hotwired.dev/handbook/drive#displaying-progress */ - -:root { - - /* Default values shipped with Turbo. */ - --turbo-progress-colour: #0076ff; - --turbo-progress-thickness: 3px; - - /* We recommend overriding these as they can apply to other (future) - RefreshLess implementations as well. */ - --refreshless-progress-bar-colour: var(--turbo-progress-colour); - --refreshless-progress-bar-thickness: var(--turbo-progress-thickness); - -} - -.turbo-progress-bar { - - height: var(--refreshless-progress-bar-thickness); - - background-color: var(--refreshless-progress-bar-colour); - -} - -:root.refreshless-disable-smooth-scroll { - - scroll-behavior: auto !important; - -} diff --git a/modules/refreshless_turbo/js/progress_bar.js b/modules/refreshless_turbo/js/progress_bar.js index 7b35d581cf9cbe29d5362d0f24c27051985a4302..deed8be302fc45f3ceb672aa748b5c17931c6f27 100644 --- a/modules/refreshless_turbo/js/progress_bar.js +++ b/modules/refreshless_turbo/js/progress_bar.js @@ -1,32 +1,105 @@ -(function (Drupal) { +/** + * @file + * Implements our progress bar, replacing Turbo's implementation. + * + * @see https://www.drupal.org/project/refreshless/issues/3488464 + * Issue for adapting this out of Turbo. + */ +((ProgressBar, ProgressBarDelay) => { once( + 'refreshless-turbo-progress-bar', document.documentElement, +).forEach((html) => { 'use strict'; - let turboPopstateProgressBarTimeoutId; + const progressBarDelay = new ProgressBarDelay( + Turbo.session.progressBarDelay, new ProgressBar(), + ); - // Fixes the progress bar delay not being obeyed with back/forward navigation. - // - // @see https://github.com/hotwired/turbo/issues/1058#issuecomment-2161270766 - // - // @todo Remove if/when Turbo fixes this. - window.addEventListener('popstate', (event) => { + html.refreshlessDocumentProgressBar = progressBarDelay; - if (!event.state.turbo) { + // Note that this intentionally listens to the fetch request event rather than + // turbo:click so that the progress bar also kicks in during back/forward + // history navigation. + html.addEventListener('turbo:before-fetch-request', (event) => { + + if ( + // Ignore prefetch requests. + 'X-Sec-Purpose' in event.detail.fetchOptions.headers && + event.detail.fetchOptions.headers['X-Sec-Purpose'] === 'prefetch' || + // We want to specifically ignore forms as those are handled separately + // via the turbo:submit-start and turbo:submit-end event handlers. + event.target.matches('form') === true + ) { return; } - Turbo.session.adapter.progressBar.progressElement.hidden = true; + progressBarDelay.showVisitAfterDelay(); - if (turboPopstateProgressBarTimeoutId) { - clearTimeout(turboPopstateProgressBarTimeoutId); - } + const hideSuccess = () => { + + progressBarDelay.hideVisit(); + + html.removeEventListener( + 'turbo:fetch-request-error', hideFailure, {once: true}, + ) + + }; + + const hideFailure = () => { + + progressBarDelay.hideVisit(); + + html.removeEventListener( + 'turbo:load', hideSuccess, {once: true}, + ) + + }; + + html.addEventListener( + 'turbo:load', hideSuccess, {once: true}, + ); + + html.addEventListener( + 'turbo:fetch-request-error', hideFailure, {once: true}, + ); + + }); + + html.addEventListener('turbo:submit-start', (event) => { + + progressBarDelay.showFormAfterDelay(); + + const hideSuccess = () => { + + progressBarDelay.hideForm(); + + html.removeEventListener( + 'turbo:fetch-request-error', hideFailure, {once: true}, + ) + + }; + + const hideFailure = () => { + + progressBarDelay.hideForm(); + + html.removeEventListener( + 'turbo:submit-end', hideSuccess, {once: true}, + ) - turboPopstateProgressBarTimeoutId = setTimeout(() => { + }; - Turbo.session.adapter.progressBar.progressElement.hidden = false; + html.addEventListener( + 'turbo:submit-end', hideSuccess, {once: true}, + ); - }, Turbo.session.progressBarDelay); + html.addEventListener( + 'turbo:fetch-request-error', hideFailure, {once: true}, + ); }); -})(Drupal); +}); })( + Drupal.RefreshLess.classes.ProgressBar, + Drupal.RefreshLess.classes.ProgressBarDelay, +); diff --git a/modules/refreshless_turbo/patches/@hotwired/turbo/05-issue-1058-no-progress-bar-delay-on-restore.patch b/modules/refreshless_turbo/patches/@hotwired/turbo/05-issue-1058-no-progress-bar-delay-on-restore.patch deleted file mode 100644 index aa59dc7575e8c06c2908a231733aa8e11f9813df..0000000000000000000000000000000000000000 --- a/modules/refreshless_turbo/patches/@hotwired/turbo/05-issue-1058-no-progress-bar-delay-on-restore.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/dist/turbo.es2017-umd.js b/dist/turbo.es2017-umd.js -index 37058d1..cd2cc8c 100644 ---- a/dist/turbo.es2017-umd.js -+++ b/dist/turbo.es2017-umd.js -@@ -3744,11 +3744,7 @@ Copyright © 2024 37signals LLC - - visitRequestStarted(visit) { - this.progressBar.setValue(0); -- if (visit.hasCachedSnapshot() || visit.action != "restore") { -- this.showVisitProgressBarAfterDelay(); -- } else { -- this.showProgressBar(); -- } -+ this.showVisitProgressBarAfterDelay(); - } - - visitRequestCompleted(visit) { diff --git a/modules/refreshless_turbo/patches/@hotwired/turbo/05-remove-progress-bar.patch b/modules/refreshless_turbo/patches/@hotwired/turbo/05-remove-progress-bar.patch new file mode 100644 index 0000000000000000000000000000000000000000..e3ec8a39cbf5ffd0eb4319078cd145421d5b672c --- /dev/null +++ b/modules/refreshless_turbo/patches/@hotwired/turbo/05-remove-progress-bar.patch @@ -0,0 +1,229 @@ +diff --git a/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js b/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js +index 37058d1..2117d16 100644 +--- a/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js ++++ b/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js +@@ -2935,129 +2935,6 @@ Copyright © 2024 37signals LLC + } + } + +- class ProgressBar { +- static animationDuration = 300 /*ms*/ +- +- static get defaultCSS() { +- return unindent` +- .turbo-progress-bar { +- position: fixed; +- display: block; +- top: 0; +- left: 0; +- height: 3px; +- background: #0076ff; +- z-index: 2147483647; +- transition: +- width ${ProgressBar.animationDuration}ms ease-out, +- opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in; +- transform: translate3d(0, 0, 0); +- } +- ` +- } +- +- hiding = false +- value = 0 +- visible = false +- +- constructor() { +- this.stylesheetElement = this.createStylesheetElement(); +- this.progressElement = this.createProgressElement(); +- this.installStylesheetElement(); +- this.setValue(0); +- } +- +- show() { +- if (!this.visible) { +- this.visible = true; +- this.installProgressElement(); +- this.startTrickling(); +- } +- } +- +- hide() { +- if (this.visible && !this.hiding) { +- this.hiding = true; +- this.fadeProgressElement(() => { +- this.uninstallProgressElement(); +- this.stopTrickling(); +- this.visible = false; +- this.hiding = false; +- }); +- } +- } +- +- setValue(value) { +- this.value = value; +- this.refresh(); +- } +- +- // Private +- +- installStylesheetElement() { +- document.head.insertBefore(this.stylesheetElement, document.head.firstChild); +- } +- +- installProgressElement() { +- this.progressElement.style.width = "0"; +- this.progressElement.style.opacity = "1"; +- document.documentElement.insertBefore(this.progressElement, document.body); +- this.refresh(); +- } +- +- fadeProgressElement(callback) { +- this.progressElement.style.opacity = "0"; +- setTimeout(callback, ProgressBar.animationDuration * 1.5); +- } +- +- uninstallProgressElement() { +- if (this.progressElement.parentNode) { +- document.documentElement.removeChild(this.progressElement); +- } +- } +- +- startTrickling() { +- if (!this.trickleInterval) { +- this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration); +- } +- } +- +- stopTrickling() { +- window.clearInterval(this.trickleInterval); +- delete this.trickleInterval; +- } +- +- trickle = () => { +- this.setValue(this.value + Math.random() / 100); +- } +- +- refresh() { +- requestAnimationFrame(() => { +- this.progressElement.style.width = `${10 + this.value * 90}%`; +- }); +- } +- +- createStylesheetElement() { +- const element = document.createElement("style"); +- element.type = "text/css"; +- element.textContent = ProgressBar.defaultCSS; +- if (this.cspNonce) { +- element.nonce = this.cspNonce; +- } +- return element +- } +- +- createProgressElement() { +- const element = document.createElement("div"); +- element.className = "turbo-progress-bar"; +- return element +- } +- +- get cspNonce() { +- return getMetaContent("csp-nonce") +- } +- } +- + class HeadSnapshot extends Snapshot { + detailsByOuterHTML = this.children + .filter((element) => !elementIsNoscript(element)) +@@ -3721,7 +3598,6 @@ Copyright © 2024 37signals LLC + } + + class BrowserAdapter { +- progressBar = new ProgressBar() + + constructor(session) { + this.session = session; +@@ -3742,14 +3618,7 @@ Copyright © 2024 37signals LLC + visit.goToSamePageAnchor(); + } + +- visitRequestStarted(visit) { +- this.progressBar.setValue(0); +- if (visit.hasCachedSnapshot() || visit.action != "restore") { +- this.showVisitProgressBarAfterDelay(); +- } else { +- this.showProgressBar(); +- } +- } ++ visitRequestStarted(visit) {} + + visitRequestCompleted(visit) { + visit.loadResponse(); +@@ -3773,66 +3642,24 @@ Copyright © 2024 37signals LLC + + visitRequestFinished(_visit) {} + +- visitCompleted(_visit) { +- this.progressBar.setValue(1); +- this.hideVisitProgressBar(); +- } ++ visitCompleted(_visit) {} + + pageInvalidated(reason) { + this.reload(reason); + } + +- visitFailed(_visit) { +- this.progressBar.setValue(1); +- this.hideVisitProgressBar(); +- } ++ visitFailed(_visit) {} + + visitRendered(_visit) {} + + // Form Submission Delegate + +- formSubmissionStarted(_formSubmission) { +- this.progressBar.setValue(0); +- this.showFormProgressBarAfterDelay(); +- } ++ formSubmissionStarted(_formSubmission) {} + +- formSubmissionFinished(_formSubmission) { +- this.progressBar.setValue(1); +- this.hideFormProgressBar(); +- } ++ formSubmissionFinished(_formSubmission) {} + + // Private + +- showVisitProgressBarAfterDelay() { +- this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay); +- } +- +- hideVisitProgressBar() { +- this.progressBar.hide(); +- if (this.visitProgressBarTimeout != null) { +- window.clearTimeout(this.visitProgressBarTimeout); +- delete this.visitProgressBarTimeout; +- } +- } +- +- showFormProgressBarAfterDelay() { +- if (this.formProgressBarTimeout == null) { +- this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay); +- } +- } +- +- hideFormProgressBar() { +- this.progressBar.hide(); +- if (this.formProgressBarTimeout != null) { +- window.clearTimeout(this.formProgressBarTimeout); +- delete this.formProgressBarTimeout; +- } +- } +- +- showProgressBar = () => { +- this.progressBar.show(); +- } +- + reload(reason) { + dispatch("turbo:reload", { detail: reason }); + diff --git a/modules/refreshless_turbo/refreshless_turbo.info.yml b/modules/refreshless_turbo/refreshless_turbo.info.yml index 172a3622dc755d35ba2d78ecce94680f932fe474..1e8b8133c884ce8d364bd14bcb2d678cf84f8c85 100644 --- a/modules/refreshless_turbo/refreshless_turbo.info.yml +++ b/modules/refreshless_turbo/refreshless_turbo.info.yml @@ -7,3 +7,4 @@ php: 8.1 dependencies: - hux:hux + - refreshless:refreshless diff --git a/modules/refreshless_turbo/refreshless_turbo.install b/modules/refreshless_turbo/refreshless_turbo.install index 5d0730cf0f2499b44ecd2fd4897bc12732713348..eca9a5240f257ae22ad5fb3456235d7e26bd058d 100644 --- a/modules/refreshless_turbo/refreshless_turbo.install +++ b/modules/refreshless_turbo/refreshless_turbo.install @@ -309,3 +309,12 @@ function refreshless_turbo_update_10217(): void { \Drupal::service('library.discovery')->clearCachedDefinitions(); } + +/** + * Install the base RefreshLess module if not installed for progress bar. + */ +function refreshless_turbo_update_10218(): void { + + \Drupal::service('module_installer')->install(['refreshless']); + +} diff --git a/modules/refreshless_turbo/refreshless_turbo.libraries.yml b/modules/refreshless_turbo/refreshless_turbo.libraries.yml index 174b7b4fd3b85458b852ca86a4d7fd960a9e1caf..d21c35e6a94d795b24d056a6033b77c9e0898424 100644 --- a/modules/refreshless_turbo/refreshless_turbo.libraries.yml +++ b/modules/refreshless_turbo/refreshless_turbo.libraries.yml @@ -1,7 +1,4 @@ refreshless: - css: - theme: - css/turbo.css: {} js: # The explicit aggregation group should not be changed or removed as it's # used to group our JavaScript into a separate aggregate if/when core @@ -18,8 +15,6 @@ refreshless: # RefreshLess issue implementing JavaScript aggregation support. js/drupal_settings.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false } js/behaviours.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false } - # js/progress_bar.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false } - js/scroll.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false } js/stylesheet_sorter.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false } js/refreshless.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false } header: true @@ -28,7 +23,9 @@ refreshless: - core/drupalSettings - core/jquery - core/once + - refreshless_turbo/progress_bar - refreshless_turbo/reload + - refreshless_turbo/scroll # @see \Drupal\refreshless_turbo\Hooks\Library::alterJsCookie() # Conditionally attaches our js-cookie if core's is no longer present. # @@ -52,6 +49,14 @@ reload: js/reload.js: { attributes: { defer: false }, group: refreshless-turbo-reload, header: true, preprocess: false } header: true +progress_bar: + js: + js/progress_bar.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false } + header: true + dependencies: + - refreshless/progress_bar + - refreshless/progress_bar_delay + ajax: js: js/ajax.js: { attributes: { defer: true } } @@ -84,12 +89,20 @@ js-cookie: js: vendor/js-cookie/dist/js.cookie.js: {} +scroll: + css: + theme: + css/scroll.css: {} + js: + js/scroll.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false } + header: true + turbo: # The "-patchN" is to force downloading of a patched Turbo despite the # official release version remaining the same. The "-patchN" number can be # incremented if we apply another patch, or removed if a new Turbo version # becomes available; whichever comes first. - version: 8.0.10-patch1 + version: 8.0.10-patch2 js: # Note that at the time of writing, Turbo does not cope well with being # aggregated (and/or minified?), so the lack of a group here is intentional diff --git a/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js b/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js index cd2cc8cb768b9285b4f5cf38970bc01f493cf0e5..2117d16504090d6013c43cec243239635b866b13 100644 --- a/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js +++ b/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js @@ -2935,129 +2935,6 @@ Copyright © 2024 37signals LLC } } - class ProgressBar { - static animationDuration = 300 /*ms*/ - - static get defaultCSS() { - return unindent` - .turbo-progress-bar { - position: fixed; - display: block; - top: 0; - left: 0; - height: 3px; - background: #0076ff; - z-index: 2147483647; - transition: - width ${ProgressBar.animationDuration}ms ease-out, - opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in; - transform: translate3d(0, 0, 0); - } - ` - } - - hiding = false - value = 0 - visible = false - - constructor() { - this.stylesheetElement = this.createStylesheetElement(); - this.progressElement = this.createProgressElement(); - this.installStylesheetElement(); - this.setValue(0); - } - - show() { - if (!this.visible) { - this.visible = true; - this.installProgressElement(); - this.startTrickling(); - } - } - - hide() { - if (this.visible && !this.hiding) { - this.hiding = true; - this.fadeProgressElement(() => { - this.uninstallProgressElement(); - this.stopTrickling(); - this.visible = false; - this.hiding = false; - }); - } - } - - setValue(value) { - this.value = value; - this.refresh(); - } - - // Private - - installStylesheetElement() { - document.head.insertBefore(this.stylesheetElement, document.head.firstChild); - } - - installProgressElement() { - this.progressElement.style.width = "0"; - this.progressElement.style.opacity = "1"; - document.documentElement.insertBefore(this.progressElement, document.body); - this.refresh(); - } - - fadeProgressElement(callback) { - this.progressElement.style.opacity = "0"; - setTimeout(callback, ProgressBar.animationDuration * 1.5); - } - - uninstallProgressElement() { - if (this.progressElement.parentNode) { - document.documentElement.removeChild(this.progressElement); - } - } - - startTrickling() { - if (!this.trickleInterval) { - this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration); - } - } - - stopTrickling() { - window.clearInterval(this.trickleInterval); - delete this.trickleInterval; - } - - trickle = () => { - this.setValue(this.value + Math.random() / 100); - } - - refresh() { - requestAnimationFrame(() => { - this.progressElement.style.width = `${10 + this.value * 90}%`; - }); - } - - createStylesheetElement() { - const element = document.createElement("style"); - element.type = "text/css"; - element.textContent = ProgressBar.defaultCSS; - if (this.cspNonce) { - element.nonce = this.cspNonce; - } - return element - } - - createProgressElement() { - const element = document.createElement("div"); - element.className = "turbo-progress-bar"; - return element - } - - get cspNonce() { - return getMetaContent("csp-nonce") - } - } - class HeadSnapshot extends Snapshot { detailsByOuterHTML = this.children .filter((element) => !elementIsNoscript(element)) @@ -3721,7 +3598,6 @@ Copyright © 2024 37signals LLC } class BrowserAdapter { - progressBar = new ProgressBar() constructor(session) { this.session = session; @@ -3742,10 +3618,7 @@ Copyright © 2024 37signals LLC visit.goToSamePageAnchor(); } - visitRequestStarted(visit) { - this.progressBar.setValue(0); - this.showVisitProgressBarAfterDelay(); - } + visitRequestStarted(visit) {} visitRequestCompleted(visit) { visit.loadResponse(); @@ -3769,66 +3642,24 @@ Copyright © 2024 37signals LLC visitRequestFinished(_visit) {} - visitCompleted(_visit) { - this.progressBar.setValue(1); - this.hideVisitProgressBar(); - } + visitCompleted(_visit) {} pageInvalidated(reason) { this.reload(reason); } - visitFailed(_visit) { - this.progressBar.setValue(1); - this.hideVisitProgressBar(); - } + visitFailed(_visit) {} visitRendered(_visit) {} // Form Submission Delegate - formSubmissionStarted(_formSubmission) { - this.progressBar.setValue(0); - this.showFormProgressBarAfterDelay(); - } + formSubmissionStarted(_formSubmission) {} - formSubmissionFinished(_formSubmission) { - this.progressBar.setValue(1); - this.hideFormProgressBar(); - } + formSubmissionFinished(_formSubmission) {} // Private - showVisitProgressBarAfterDelay() { - this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay); - } - - hideVisitProgressBar() { - this.progressBar.hide(); - if (this.visitProgressBarTimeout != null) { - window.clearTimeout(this.visitProgressBarTimeout); - delete this.visitProgressBarTimeout; - } - } - - showFormProgressBarAfterDelay() { - if (this.formProgressBarTimeout == null) { - this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay); - } - } - - hideFormProgressBar() { - this.progressBar.hide(); - if (this.formProgressBarTimeout != null) { - window.clearTimeout(this.formProgressBarTimeout); - delete this.formProgressBarTimeout; - } - } - - showProgressBar = () => { - this.progressBar.show(); - } - reload(reason) { dispatch("turbo:reload", { detail: reason }); diff --git a/refreshless.info.yml b/refreshless.info.yml index 51fc081191539dced752588cc44126eeffed0a37..be5ec8e942ad7bb38d619b55ada1b73428d776e5 100644 --- a/refreshless.info.yml +++ b/refreshless.info.yml @@ -4,6 +4,3 @@ description: 'Layers JavaScript-based navigation on top of Drupal''s existing se package: User interface core_version_requirement: ^10.2 || ^11.0 php: 8.1 -# Hidden until we either merge Turbo into this root module or we implement a -# shared framework here that multiple implementations can depend on. -hidden: true diff --git a/refreshless.install b/refreshless.install index c331edf23758d4a0027f31fd0261dfec0af58aed..3d3495e00f2cb751d90dc2c67bd88b7954b5ecaf 100644 --- a/refreshless.install +++ b/refreshless.install @@ -10,3 +10,12 @@ function refreshless_update_10201(): void { \Drupal::service('kernel')->invalidateContainer(); } + +/** + * Rebuild libraries for addition of progress bar components. + */ +function refreshless_update_10202(): void { + + \Drupal::service('library.discovery')->clearCachedDefinitions(); + +} diff --git a/refreshless.libraries.yml b/refreshless.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..2068ee7743d29b7730db60ea2dab85e240b7adf6 --- /dev/null +++ b/refreshless.libraries.yml @@ -0,0 +1,18 @@ +# @todo Switch to the real components when we require drupal/core:^10.3 for SDC. + +progress_bar: + css: + theme: + components/progress-bar/progress-bar.css: {} + js: + components/progress-bar/progress-bar.js: { attributes: { defer: true }, group: refreshless, preprocess: false } + header: true + dependencies: + - core/drupal + +progress_bar_delay: + js: + components/progress-bar-delay/progress-bar-delay.js: { attributes: { defer: true }, group: refreshless, preprocess: false } + header: true + dependencies: + - core/drupal