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(
+ `
+
+
+
file_uploadSubmitting...
+
+
+
+
+ `
+ );
+ 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(
- `
-
-
-
file_uploadSubmitting...
-
-
-
-
- `
- );
- 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'
%}