From c7dab5e502ac0037d74f42b0d9b3c8024c06c253 Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Tue, 10 Oct 2023 11:06:44 +0200 Subject: [PATCH] intermediate update on displays and forms 1/2 --- app/static/js/ResourceLists/ResourceList.js | 54 ++++--- app/static/js/forms/base-form.js | 138 +++++++++++++++++ app/static/js/forms/index.js | 139 ------------------ app/static/js/resource-displays/index.js | 47 ------ .../js/resource-displays/resource-display.js | 46 ++++++ app/templates/_scripts.html.j2 | 2 + 6 files changed, 216 insertions(+), 210 deletions(-) create mode 100644 app/static/js/forms/base-form.js create mode 100644 app/static/js/resource-displays/resource-display.js diff --git a/app/static/js/ResourceLists/ResourceList.js b/app/static/js/ResourceLists/ResourceList.js index 6bc6ac1f..97e65d70 100644 --- a/app/static/js/ResourceLists/ResourceList.js +++ b/app/static/js/ResourceLists/ResourceList.js @@ -1,31 +1,37 @@ -class ResourceList { +var ResourceLists = {}; + +ResourceLists.autoInit = () => { + for (let propertyName in ResourceLists) { + let property = ResourceLists[propertyName]; + // Call autoInit of all properties that are subclasses of `ResourceLists.BaseList`. + // This does not include `ResourceLists.BaseList` itself. + if (property.prototype instanceof ResourceLists.BaseList) { + // 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 listElements = document.querySelectorAll(`.${property.htmlClass}:not(.no-autoinit)`); + // Create an instance of this class for each display element. + for (let listElement of listElements) {new property(listElement);} + } + } +}; + +ResourceLists.defaultOptions = { + page: 5, + pagination: { + innerWindow: 2, + outerWindow: 2 + } +}; + +ResourceLists.BaseList = class BaseList { /* A wrapper class for the list.js list. * This class is not meant to be used directly, instead it should be used as * a base class for concrete resource list implementations. */ - static autoInit() { - CorpusList.autoInit(); - CorpusFileList.autoInit(); - JobList.autoInit(); - JobInputList.autoInit(); - JobResultList.autoInit(); - SpaCyNLPPipelineModelList.autoInit(); - TesseractOCRPipelineModelList.autoInit(); - UserList.autoInit(); - AdminUserList.autoInit(); - CorpusFollowerList.autoInit(); - CorpusTextInfoList.autoInit(); - CorpusTokenList.autoInit(); - } - - static defaultOptions = { - page: 5, - pagination: { - innerWindow: 2, - outerWindow: 2 - } - }; + static htmlClass; constructor(listContainerElement, options = {}) { if ('items' in options) { @@ -36,7 +42,7 @@ class ResourceList { } let _options = Utils.mergeObjectsDeep( {item: this.item, valueNames: this.valueNames}, - ResourceList.defaultOptions, + ResourceLists.defaultOptions, options ); this.listContainerElement = listContainerElement; diff --git a/app/static/js/forms/base-form.js b/app/static/js/forms/base-form.js new file mode 100644 index 00000000..b8560485 --- /dev/null +++ b/app/static/js/forms/base-form.js @@ -0,0 +1,138 @@ +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; + } + } + } +}; diff --git a/app/static/js/forms/index.js b/app/static/js/forms/index.js index 0e7529f6..02909448 100644 --- a/app/static/js/forms/index.js +++ b/app/static/js/forms/index.js @@ -16,142 +16,3 @@ Forms.autoInit = () => { } } }; - -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; - } - } - } -}; diff --git a/app/static/js/resource-displays/index.js b/app/static/js/resource-displays/index.js index 1f795c44..4ec7e997 100644 --- a/app/static/js/resource-displays/index.js +++ b/app/static/js/resource-displays/index.js @@ -16,50 +16,3 @@ ResourceDisplays.autoInit = () => { } } } - -ResourceDisplays.BaseDisplay = class BaseDisplay { - static htmlClass; - - constructor(displayElement) { - this.displayElement = displayElement; - this.userId = this.displayElement.dataset.userId; - this.isInitialized = false; - if (this.userId) { - app.subscribeUser(this.userId) - .then((response) => { - app.socket.on('PATCH', (patch) => { - if (this.isInitialized) {this.onPatch(patch);} - }); - }); - app.getUser(this.userId) - .then((user) => { - this.init(user); - this.isInitialized = true; - }); - } - } - - init(user) {throw 'Not implemented';} - - onPatch(patch) {throw 'Not implemented';} - - setElement(element, value) { - switch (element.tagName) { - case 'INPUT': { - element.value = value; - M.updateTextFields(); - break; - } - default: { - element.innerText = value; - break; - } - } - } - - setElements(elements, value) { - for (let element of elements) { - this.setElement(element, value); - } - } -}; diff --git a/app/static/js/resource-displays/resource-display.js b/app/static/js/resource-displays/resource-display.js new file mode 100644 index 00000000..81fcda5d --- /dev/null +++ b/app/static/js/resource-displays/resource-display.js @@ -0,0 +1,46 @@ +ResourceDisplays.BaseDisplay = class BaseDisplay { + static htmlClass; + + constructor(displayElement) { + this.displayElement = displayElement; + this.userId = this.displayElement.dataset.userId; + this.isInitialized = false; + if (this.userId) { + app.subscribeUser(this.userId) + .then((response) => { + app.socket.on('PATCH', (patch) => { + if (this.isInitialized) {this.onPatch(patch);} + }); + }); + app.getUser(this.userId) + .then((user) => { + this.init(user); + this.isInitialized = true; + }); + } + } + + init(user) {throw 'Not implemented';} + + onPatch(patch) {throw 'Not implemented';} + + setElement(element, value) { + switch (element.tagName) { + case 'INPUT': { + element.value = value; + M.updateTextFields(); + break; + } + default: { + element.innerText = value; + break; + } + } + } + + setElements(elements, value) { + for (let element of elements) { + this.setElement(element, value); + } + } +}; diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index 6213e3df..6d3495c9 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -36,6 +36,7 @@ filters='rjsmin', output='gen/Forms.%(version)s.js', 'js/forms/index.js', + 'js/forms/form.js' 'js/forms/create-contribution-form.js', 'js/forms/create-corpus-file-form.js', 'js/forms/create-job-form.js' @@ -47,6 +48,7 @@ filters='rjsmin', output='gen/resource-displays.%(version)s.js', 'js/resource-displays/index.js', + 'js/resource-displays/base-display.js', 'js/resource-displays/corpus-display.js', 'js/resource-displays/job-display.js' %}