/** * Returns an object that maps IDs to the corresponding element. * * @param ids - IDs of the element (empty to retrieve all elements) */ function getElements(...ids) { ids = ids.length ? ids : [...document.querySelectorAll("[id]")].map(e => e.id); return Object.fromEntries(ids.map(id => [id, document.getElementById(id)])); } /** * Acquires all data from the given form and POSTs it as JSON to the target URL. * In case of failure this function will throw an error. * In case of success a parsed JSON body of the response will be returned, * unless a redirect was expected, * in which case a redirect will happen or an error will be thrown if there is no location field. * * @param target - Target URL to POST to. Defaults to the current URL. * @param expectRedirect - If a redirect is expected. Defaults to `false`. * @param transform - A function that gets as input a JSON representation of the form. The output will be POSTed. Defaults to identity function. * @param formId - The ID of the form. Defaults to "mainForm". */ async function postJsonForm(target = '', expectRedirect = false, transform = (json) => json, formId = 'mainForm') { const form = document.getElementById(formId); const formData = new FormData(form); const json = transform(Object.fromEntries(formData)); const res = await postJson(target, json); if (res.status >= 400) { const error = await res.json(); throw new Error(error.message); } else if (res.status === 200 || res.status === 201) { const body = await res.json(); if (body.location) { location.href = body.location; } else { if (expectRedirect) { throw new Error('Expected a location field in the response.'); } return body; } } } /** * Adds a listener to the given form to prevent the default interaction and instead call the provided callback. * In case of an error, it will be caught and the message will be shown in the error block. * * @param callback - Callback to call. * @param formId - ID of the form. Defaults to "mainForm". * @param errorId - ID of the error block. Defaults to "error". */ function addPostListener(callback, formId = 'mainForm', errorId = 'error') { const form = document.getElementById(formId); form.addEventListener('submit', async(event) => { event.preventDefault(); try { await callback(); } catch (error) { setError(error.message, errorId); } }); } /** * Shows or hides the given element. * @param id - ID of the element. * @param visible - If it should be visible. */ function setVisibility(id, visible) { const element = document.getElementById(id); element.classList[visible ? 'remove' : 'add']('hidden'); // Disable children of hidden elements, // such that the browser does not expect input for them for (const child of getDescendants(element)) { if ('disabled' in child) child.disabled = !visible; } } /** * Obtains all children, grandchildren, etc. of the given element. * @param element - Element to get all descendants from. */ function getDescendants(element) { return [...element.querySelectorAll("*")]; } /** * Updates the inner text and href field of an element. * @param id - ID of the element. * @param text - Text to put in the field(s). If this is undefined, instead the element will be hidden. * @param options - Indicates which fields should be updated. * Keys should be `innerText` and/or `href`, values should be booleans. */ function updateElement(id, text, options) { const element = document.getElementById(id); setVisibility(id, Boolean(text)); if (options.innerText) { element.innerText = text; } if (options.href) { element.href = text; } } /** * Fetches JSON from the url and converts it to an object. * @param url - URL to fetch JSON from. * @param redirectUrl - URL to redirect to in case the response code is >= 400. No redirect happens if undefined. */ async function fetchJson(url, redirectUrl) { const res = await fetch(url, { headers: { accept: 'application/json' } }); if (redirectUrl && res.status >= 400) { location.href = redirectUrl; return; } return res.json(); } /** * Returns the controls object that can be found accessing the given URL. */ async function fetchControls(url) { return (await fetchJson(url)).controls; } /** * POSTs JSON to the given URL and returns the response. */ async function postJson(url, json) { return fetch(url, { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(json), }); } /** * Sets the contents of the error block to the given error message. * Default ID of the error block is `error`. */ function setError(message, errorId = 'error') { updateElement(errorId, message, { innerText: true }); } /** * Causes the page to redirect to a specific page when a button is clicked. * @param element - The id of the button. * @param url - The URL to redirect to. */ function setRedirectClick(element, url) { document.getElementById(element).addEventListener('click', () => location.href = url); } /** * Validates a password form to see if the confirmation password matches the password. * * @param passwordId - The id of the password field. * @param formId - ID of the form. Defaults to "mainForm". * @param confirmPasswordId - ID of the password confirmation field. Defaults to "confirmPassword". */ function validatePasswordConfirmation(passwordId, formId = 'mainForm', confirmPasswordId = 'confirmPassword') { const formData = new FormData(document.getElementById(formId)); if (formData.get(passwordId) !== formData.get(confirmPasswordId)) { throw new Error('Password confirmation does not match the password!'); } }