diff --git a/plinth/templates/base.html b/plinth/templates/base.html index 174f33bc6..72ad95c74 100644 --- a/plinth/templates/base.html +++ b/plinth/templates/base.html @@ -62,6 +62,9 @@ + + + {% block app_head %}{% endblock %} {% block page_head %}{% endblock %} diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css index edc66372e..be8db5614 100644 --- a/static/themes/default/css/main.css +++ b/static/themes/default/css/main.css @@ -522,6 +522,26 @@ a.menu_link_active { transform: translateY(-50%) translateX(-100%) } +/* + * Form button with loading progress. + */ +.running-status-button-before { + display: inline-block; + border: 4px solid #f3f3f3; /* Light grey */ + border-top: 4px solid #3498db; /* Blue */ + border-radius: 50%; + width: 16px; + height: 16px; + animation: spin 1s linear infinite; + margin-left: 10px; + margin-bottom: -4px; + margin-right: -26px; +} + +input[type='submit'].running-status-button { + padding-left: 32px; +} + /* * Button toolbar */ diff --git a/static/themes/default/js/main.js b/static/themes/default/js/main.js new file mode 100644 index 000000000..5fea98824 --- /dev/null +++ b/static/themes/default/js/main.js @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/* + This file is part of FreedomBox. + + @licstart The following is the entire license notice for the + JavaScript code in this page. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + + @licend The above is the entire license notice + for the JavaScript code in this page. +*/ + +/* + * Disable submit button on click. + */ +function onSubmitAddProgress(event) { + // Using activeElement is not reliable. If the user presses Enter on a text + // field, activeElement with be that text field. However, we do safety + // checks and fallback to not disabling/animating the submit button, which + // is okay. + button = document.activeElement; + if (!button.classList.contains('btn') || + button.classList.contains('btn-link') || + button.classList.contains('no-running-status') || + button.hasAttribute('disabled')) { + return; + } + + // Don't disable the submit button immediately as that will prevent the + // button from being sent in the HTTP request. Instead schedule disabling + // for the next event loop run which will happen after current event is + // processed. + window.setTimeout(() => { + const beforeElement = document.createElement('div'); + beforeElement.classList.add('running-status-button-before'); + button.parentNode.insertBefore(beforeElement, button); + + button.classList.add('running-status-button'); + button.setAttribute('disabled', 'disabled'); + }, 0); +} + +document.addEventListener('DOMContentLoaded', function(event) { + const submitButtons = document.querySelectorAll("input[type='submit']"); + for (const button of submitButtons) { + if (button.form) { + // Don't listen for 'click' event on buttons as they are triggered + // even when the form is invalid. + button.form.addEventListener('submit', onSubmitAddProgress); + } + } +}); + +// When using back/forward browser's bfcache is used and pages won't receive +// 'load' events. Instead a 'pageshow' event is available. When a user does +// back/forward we want them to be able to submit the forms again. So clear all +// the button disabling. +window.addEventListener('pageshow', function(event) { + const selector = "input[type='submit'].running-status-button"; + const submitButtons = document.querySelectorAll(selector); + for (const button of submitButtons) { + button.classList.remove('running-status-button'); + button.removeAttribute('disabled'); + } + + const beforeSelector = ".running-status-button-before"; + const beforeElements = document.querySelectorAll(beforeSelector); + for (const element of beforeElements) { + element.remove(); + } +});