var Forms = {}; Forms.autoInit = () => { for (let propertyName in Forms) { let property = Forms[propertyName]; // Call autoInit of all properties that are subclasses of Forms.BaseForm. // This does not include Forms.BaseForm itself. if (property.prototype instanceof Forms.BaseForm) { // Check if the static htmlClass property is defined. if (property.htmlClass === undefined) {return;} // Gather all HTML elements that have the `this.htmlClass` class // and do not have the no-autoinit class. let formElements = document.querySelectorAll(`.${property.htmlClass}:not(.no-autoinit)`); // Create an instance of this class for each form element. for (let formElement of formElements) {new property(formElement);} } } }; Forms.BaseForm = class BaseForm { static htmlClass; constructor(formElement) { this.formElement = formElement; this.eventListeners = { 'requestLoad': [] }; this.afterRequestListeners = []; for (let selectElement of this.formElement.querySelectorAll('select')) { selectElement.removeAttribute('required'); } this.formElement.addEventListener('submit', (event) => { event.preventDefault(); this.submit(event); }); } addEventListener(eventType, listener) { if (eventType in this.eventListeners) { this.eventListeners[eventType].push(listener); } else { throw `Unknown event type ${eventType}`; } } submit(event) { let request = new XMLHttpRequest(); let modalElement = Utils.HTMLToElement( ` ` ); document.querySelector('#modals').appendChild(modalElement); let modal = M.Modal.init( modalElement, { dismissible: false, onCloseEnd: () => { modal.destroy(); modalElement.remove(); } } ); modal.open(); // Remove all previous helper text elements that indicate errors let errorHelperTextElements = this.formElement .querySelectorAll('.helper-text[data-helper-text-type="error"]'); for (let errorHelperTextElement of errorHelperTextElements) { errorHelperTextElement.remove(); } // Check if select elements are filled out properly for (let selectElement of this.formElement.querySelectorAll('select')) { if (selectElement.value === '') { let inputFieldElement = selectElement.closest('.input-field'); let errorHelperTextElement = Utils.HTMLToElement( 'Please select an option.' ); inputFieldElement.appendChild(errorHelperTextElement); inputFieldElement.querySelector('.select-dropdown').classList.add('invalid'); modal.close(); return; } } // Setup abort handling let cancelElement = modalElement.querySelector('.action-button[data-action="cancel"]'); cancelElement.addEventListener('click', (event) => {request.abort();}); // Setup load handling (after the request completed) request.addEventListener('load', (event) => { for (let listener of this.eventListeners['requestLoad']) { listener(event); } if (request.status === 400) { let responseJson = JSON.parse(request.responseText); for (let [inputName, inputErrors] of Object.entries(responseJson.errors)) { let inputFieldElement = this.formElement .querySelector(`input[name$="${inputName}"], select[name$="${inputName}"]`) .closest('.input-field'); for (let inputError of inputErrors) { let errorHelperTextElement = Utils.HTMLToElement( `${inputError}` ); inputFieldElement.appendChild(errorHelperTextElement); } } } if (request.status === 500) { app.flash('Internal Server Error', 'error'); } modal.close(); }); // Setup progress handling let progressBarElement = modalElement.querySelector('.progress > .determinate'); request.upload.addEventListener('progress', (event) => { let progress = Math.floor(100 * event.loaded / event.total); progressBarElement.style.width = `${progress}%`; }); request.open(this.formElement.method, this.formElement.action); request.setRequestHeader('Accept', 'application/json'); let formData = new FormData(this.formElement); switch (this.formElement.enctype) { case 'application/x-www-form-urlencoded': { let urlSearchParams = new URLSearchParams(formData); request.send(urlSearchParams); break; } case 'multipart/form-data': { request.send(formData); break; } case 'text/plain': { throw 'enctype "text/plain" is not supported'; break; } default: { break; } } } };