diff --git a/css/admin.css b/css/admin.css index 6b0d610..22c188d 100644 --- a/css/admin.css +++ b/css/admin.css @@ -3,14 +3,34 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -#jsloader-section label, -#jsloader-snippet, -#jsloader-url { +#jsloader-form{ display: block; - max-width: 550px; + max-width: 650px; + padding-top: var(--app-navigation-padding); width: 100%; + + h2 { + display: block; + line-height: var(--default-clickable-area); + padding-inline-start: calc(2* var(--app-navigation-padding) + var(--default-clickable-area) - 30px); + } + + .hint { + color: var(--color-text-maxcontrast); + margin-bottom: calc(2 * var(--default-grid-baseline)); + } + + input:invalid { + border-color: var(--color-element-error); + } + + > * { + width: 100%; + } } #jsloader-snippet { - height: 200px; + min-width: 100%; + min-height: 200px; + margin-bottom: calc(2 * var(--default-grid-baseline)); } diff --git a/js/admin.js b/js/admin.js deleted file mode 100644 index cb89c77..0000000 --- a/js/admin.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -$(document).ready(function() { - var urlRegex = /https?:\/\/([^\/]+)/, - $urlInput = $('#jsloader-url'), - $textarea = $('#jsloader-snippet'), - $section = $('#jsloader-section'), - $saveButton = $section.find('button'), - cachebuster = parseInt($section.data('cachebuster')); - - $('#jsloader-save').on('click', function(event) { - event.preventDefault(); - - var selector = '#jsloader-message', - snippetValue = $textarea.val(), - urlValue = $urlInput.val(); - - $textarea.prop('disabled', true); - $urlInput.prop('disabled', true); - OC.msg.startSaving(selector); - - OCP.AppConfig.setValue('jsloader', 'snippet', snippetValue, { - success: function(){ - $textarea.prop('disabled', false); - - OCP.AppConfig.setValue('jsloader', 'url', urlValue, { - success: function(){ - $urlInput.prop('disabled', false); - OC.msg.finishedSuccess(selector, t('settings', 'Saved')); - }, - error: function(){ - $urlInput.prop('disabled', false); - OC.msg.finishedError(selector, t('jsloader', 'Error while saving')); - } - }); - }, - error: function(){ - $textarea.prop('disabled', false); - $urlInput.prop('disabled', false); - OC.msg.finishedError(selector, t('jsloader', 'Error while saving')); - } - }); - - cachebuster += 1; - OCP.AppConfig.setValue('jsloader', 'cachebuster', cachebuster); - }); - - /** - * try to detect an URL from the snippet input - */ - $textarea.on('change', function() { - var value = $textarea.val(), - result = urlRegex.exec(value); - - if ($urlInput.val() === '' && result !== null) { - $urlInput.val(result[1]); - } - - $saveButton.prop('disabled', false); - }); - - $urlInput.on('change', function() { - $saveButton.prop('disabled', false); - }) -}); diff --git a/js/admin.mjs b/js/admin.mjs new file mode 100644 index 0000000..8c69643 --- /dev/null +++ b/js/admin.mjs @@ -0,0 +1,13 @@ +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { formElement, textareaElement, urlInputElement } from './elements.mjs' +import { onSave, onSnippetChange, onUrlChange } from './handlers.mjs' + +// As this is a module script the content is executed after the DOM is ready +// setup event handlers on the input elements +formElement.addEventListener('submit', onSave) +textareaElement.addEventListener('input', onSnippetChange) +urlInputElement.addEventListener('input', onUrlChange) diff --git a/js/elements.mjs b/js/elements.mjs new file mode 100644 index 0000000..ccf1d5d --- /dev/null +++ b/js/elements.mjs @@ -0,0 +1,43 @@ +/*! + * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + */ + +/** + * @type {HTMLFormElement} + */ +export const formElement = getElementById('jsloader-form') + +/** + * @type {HTMLInputElement} + */ +export const urlInputElement = getElementById('jsloader-url') + +/** + * @type {HTMLTextAreaElement} + */ +export const textareaElement = getElementById('jsloader-snippet') + +/** + * @type {HTMLButtonElement} + */ +export const saveButtonElement = getElementById('jsloader-save') + +/** + * @type {HTMLElement} + */ +export const iconElement = getElementById('jsloader-message') + +/** + * Get an element by its id and throw an error if it is not found + * + * @param {string} id - The id of the element to find + * @return {HTMLElement} The found element + */ +function getElementById(id) { + const el = document.getElementById(id) + if (el === null) { + throw new Error(`Element with id ${id} not found`) + } + return el +} \ No newline at end of file diff --git a/js/handlers.mjs b/js/handlers.mjs new file mode 100644 index 0000000..9ac5934 --- /dev/null +++ b/js/handlers.mjs @@ -0,0 +1,113 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { formElement, iconElement, saveButtonElement, textareaElement, urlInputElement } from "./elements.mjs" + +const URL_REGEX = /https?:\/\/([^\/]+)/ + +/** + * Handle the change event on the snippet textarea. + * If the URL input is empty, try to detect an URL from the snippet and fill it in. + * Otherwise just enable the save button. + * + * @return {void} + */ +export function onSnippetChange() { + saveButtonElement.disabled = false + + if (urlInputElement.value === '' || !urlInputElement.validity.valid) { + const match = URL_REGEX.exec(textareaElement.value) + if (match !== null) { + urlInputElement.value = match[1] + } + } +} + +/** + * Handle the change event on the URL input. + * + * If the URL is valid, enable the save button. + * Otherwise show an error message and keep the save button disabled. + */ +export function onUrlChange() { + urlInputElement.setCustomValidity('') + + if (urlInputElement.value !== '' && urlInputElement.checkValidity?.() !== false) { + try { + // some browsers do not properly validate the URL, so re-do it manually + new URL(urlInputElement.value) + } catch { + urlInputElement.setCustomValidity('Invalid URL') + } + } + saveButtonElement.disabled = false + urlInputElement.reportValidity() +} + +/** + * Handle the click event on the save button + * + * @param {SubmitEvent} event - the submit event + * @return {Promise} + */ +export async function onSave(event) { + event.preventDefault() + const urlValue = urlInputElement.value + const snippetValue = textareaElement.value + + if (urlValue !== '' && !urlInputElement.checkValidity()) { + urlInputElement.reportValidity() + return + } + + try { + textareaElement.disabled = true + urlInputElement.disabled = true + + await new Promise((success, error) => OCP.AppConfig.setValue('jsloader', 'snippet', snippetValue, { success, error })) + textareaElement.disabled = false + + await new Promise((success, error) => OCP.AppConfig.setValue('jsloader', 'url', urlValue, { success, error })) + urlInputElement.disabled = false + showSuccess() + + const cacheBuster = String(Number.parseInt(formElement.dataset.cachebuster) + 1) + await new Promise((success, error) => OCP.AppConfig.setValue('jsloader', 'cachebuster', cacheBuster, { success, error })) + formElement.dataset.cachebuster = cacheBuster + } catch (error) { + console.error('[jsloader] Failed to save the configuration', error) + showError() + } finally { + textareaElement.disabled = false + urlInputElement.disabled = false + } +} + +/** + * Shows a loading icon in the message element + */ +function showLoading() { + iconElement.classList.remove('icon-success') + iconElement.classList.remove('icon-error') + iconElement.classList.add('icon-loading') +} + +/** + * Shows a success icon in the message element + */ +function showSuccess() { + iconElement.classList.remove('icon-loading') + iconElement.classList.remove('icon-error') + iconElement.classList.add('icon-success') +} + +/** + * Shows an error icon in the message element + */ +function showError() { + iconElement.classList.remove('icon-loading') + iconElement.classList.remove('icon-success') + iconElement.classList.add('icon-error') +} \ No newline at end of file diff --git a/templates/settings-admin.php b/templates/settings-admin.php index 1d2ed49..1db4779 100644 --- a/templates/settings-admin.php +++ b/templates/settings-admin.php @@ -13,14 +13,17 @@ Util::addStyle(Application::APP_ID, 'admin'); ?> -
+

t('JavaScript loader')); ?>

-

+

+ - - - + + +
+ t('This is needed to work with the CSP policy that is in place. It is tried to automatically detect this based on the snippet above if empty.')); ?> +
+ -
+