diff --git a/app/static/js/CorpusAnalysis/CorpusAnalysisStaticVisualization.js b/app/static/js/CorpusAnalysis/CorpusAnalysisStaticVisualization.js index 38c48c3c..4d51b014 100644 --- a/app/static/js/CorpusAnalysis/CorpusAnalysisStaticVisualization.js +++ b/app/static/js/CorpusAnalysis/CorpusAnalysisStaticVisualization.js @@ -104,7 +104,7 @@ class CorpusAnalysisStaticVisualization { renderTextInfoList() { let corpusData = this.data.corpus.o.staticData; let corpusTextInfoListElement = document.querySelector('.corpus-text-info-list'); - let corpusTextInfoList = new CorpusTextInfoList(corpusTextInfoListElement); + let corpusTextInfoList = new ResourceLists.CorpusTextInfoList(corpusTextInfoListElement); let texts = corpusData.s_attrs.text.lexicon; let textData = []; for (let i = 0; i < Object.entries(texts).length; i++) { @@ -213,7 +213,7 @@ class CorpusAnalysisStaticVisualization { async renderTokenList() { let corpusTokenListElement = document.querySelector('.corpus-token-list'); - let corpusTokenList = new CorpusTokenList(corpusTokenListElement); + let corpusTokenList = new ResourceLists.CorpusTokenList(corpusTokenListElement); let filteredData = this.filterData(); let stopwords = this.data.stopwords; if (this.data.stopwords === undefined) { diff --git a/app/static/js/Utils.js b/app/static/js/Utils.js deleted file mode 100644 index 0377bc56..00000000 --- a/app/static/js/Utils.js +++ /dev/null @@ -1,92 +0,0 @@ -class Utils { - static escape(text) { - // https://codereview.stackexchange.com/a/126722 - var table = { - '<': 'lt', - '>': 'gt', - '"': 'quot', - '\'': 'apos', - '&': 'amp', - '\r': '#10', - '\n': '#13' - }; - - return text.toString().replace(/[<>"'\r\n&]/g, (chr) => { - return '&' + table[chr] + ';'; - }); - }; - - static unescape(escapedText) { - var table = { - 'lt': '<', - 'gt': '>', - 'quot': '"', - 'apos': "'", - 'amp': '&', - '#10': '\r', - '#13': '\n' - }; - - return escapedText.replace(/&(#?\w+);/g, (match, entity) => { - if (table.hasOwnProperty(entity)) { - return table[entity]; - } - - return match; - }); -} - - static HTMLToElement(HTMLString) { - let templateElement = document.createElement('template'); - templateElement.innerHTML = HTMLString.trim(); - return templateElement.content.firstChild; - } - - static generateElementId(prefix='', suffix='') { - for (let i = 0; true; i++) { - if (document.querySelector(`#${prefix}${i}${suffix}`) !== null) {continue;} - return `${prefix}${i}${suffix}`; - } - } - - static isObject(object) { - return object !== null && typeof object === 'object' && !Array.isArray(object); - } - - static mergeObjectsDeep(...objects) { - let mergedObject = {}; - if (objects.length === 0) { - return mergedObject; - } - if (!Utils.isObject(objects[0])) {throw 'Cannot merge non-object';} - if (objects.length === 1) { - return Utils.mergeObjectsDeep(mergedObject, objects[0]); - } - if (!Utils.isObject(objects[1])) {throw 'Cannot merge non-object';} - for (let key in objects[0]) { - if (objects[0].hasOwnProperty(key)) { - if (objects[1].hasOwnProperty(key)) { - if (Utils.isObject(objects[0][key]) && Utils.isObject(objects[1][key])) { - mergedObject[key] = Utils.mergeObjectsDeep(objects[0][key], objects[1][key]); - } else { - mergedObject[key] = objects[1][key]; - } - } else { - mergedObject[key] = objects[0][key]; - } - } - } - for (let key in objects[1]) { - if (objects[1].hasOwnProperty(key)) { - if (!objects[0].hasOwnProperty(key)) { - mergedObject[key] = objects[1][key]; - } - } - } - if (objects.length === 2) { - return mergedObject; - } - return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2)); - } - -} diff --git a/app/static/js/App.js b/app/static/js/app/app.js similarity index 99% rename from app/static/js/App.js rename to app/static/js/app/app.js index cfcb3a05..92a5c546 100644 --- a/app/static/js/App.js +++ b/app/static/js/app/app.js @@ -1,4 +1,4 @@ -class App { +App.App = class App { constructor() { this.data = { promises: {getUser: {}, subscribeUser: {}}, @@ -101,4 +101,4 @@ class App { // Apply Patch jsonpatch.applyPatch(this.data, filteredPatch); } -} +}; diff --git a/app/static/js/app/index.js b/app/static/js/app/index.js new file mode 100644 index 00000000..8a7ef152 --- /dev/null +++ b/app/static/js/app/index.js @@ -0,0 +1 @@ +App = {}; diff --git a/app/static/js/cqi/api/client.js b/app/static/js/cqi/api/client.js index 14bf3690..23695987 100644 --- a/app/static/js/cqi/api/client.js +++ b/app/static/js/cqi/api/client.js @@ -524,7 +524,7 @@ cqi.api.APIClient = class APIClient { /** * Dump the values of for match ranges .. - * in . is one of the CQI_CONST_FIELD_* constants. + * in . is one of the cqi.constants.FIELD_* constants. * * @param {string} subcorpus * @param {number} field @@ -561,9 +561,9 @@ cqi.api.APIClient = class APIClient { * * returns (id, frequency) pairs flattened into a list of size 2* * field is one of - * - CQI_CONST_FIELD_MATCH - * - CQI_CONST_FIELD_TARGET - * - CQI_CONST_FIELD_KEYWORD + * - cqi.constants.FIELD_MATCH + * - cqi.constants.FIELD_TARGET + * - cqi.constants.FIELD_KEYWORD * * NB: pairs are sorted by frequency desc. * diff --git a/app/static/js/cqi/constants.js b/app/static/js/cqi/constants.js new file mode 100644 index 00000000..b12fef88 --- /dev/null +++ b/app/static/js/cqi/constants.js @@ -0,0 +1,43 @@ +cqi.constants = {}; + +/** @type {number} */ +cqi.constants.FIELD_KEYWORD = 9; + +/** @type {number} */ +cqi.constants.FIELD_MATCH = 16; + +/** @type {number} */ +cqi.constants.FIELD_MATCHEND = 17; + +/** @type {number} */ +cqi.constants.FIELD_TARGET = 0; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_0 = 0; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_1 = 1; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_2 = 2; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_3 = 3; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_4 = 4; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_5 = 5; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_6 = 6; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_7 = 7; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_8 = 8; + +/** @type {number} */ +cqi.constants.FIELD_TARGET_9 = 9; diff --git a/app/static/js/cqi/index.js b/app/static/js/cqi/index.js index 1558b308..d941a870 100644 --- a/app/static/js/cqi/index.js +++ b/app/static/js/cqi/index.js @@ -1,6 +1 @@ var cqi = {}; - -cqi.CONST_FIELD_KEYWORD = 9; -cqi.CONST_FIELD_MATCH = 16; -cqi.CONST_FIELD_MATCHEND = 17; -cqi.CONST_FIELD_TARGET = 0; diff --git a/app/static/js/cqi/models/subcorpora.js b/app/static/js/cqi/models/subcorpora.js index 86e6cf67..aeba9485 100644 --- a/app/static/js/cqi/models/subcorpora.js +++ b/app/static/js/cqi/models/subcorpora.js @@ -145,17 +145,17 @@ cqi.models.subcorpora.SubcorpusCollection = class SubcorpusCollection extends cq let apiName = `${this.corpus.apiName}:${subcorpusName}`; /** @type {object} */ let fields = {}; - if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_MATCH)) { - fields.match = cqi.CONST_FIELD_MATCH; + if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.constants.FIELD_MATCH)) { + fields.match = cqi.constants.FIELD_MATCH; } - if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_MATCHEND)) { - fields.matchend = cqi.CONST_FIELD_MATCHEND + if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.constants.FIELD_MATCHEND)) { + fields.matchend = cqi.constants.FIELD_MATCHEND } - if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_TARGET)) { - fields.target = cqi.CONST_FIELD_TARGET + if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.constants.FIELD_TARGET)) { + fields.target = cqi.constants.FIELD_TARGET } - if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_KEYWORD)) { - fields.keyword = cqi.CONST_FIELD_KEYWORD + if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.constants.FIELD_KEYWORD)) { + fields.keyword = cqi.constants.FIELD_KEYWORD } return { api_name: apiName, 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/corpus-display.js b/app/static/js/resource-displays/corpus-display.js index 906b17ac..bf6d2589 100644 --- a/app/static/js/resource-displays/corpus-display.js +++ b/app/static/js/resource-displays/corpus-display.js @@ -1,4 +1,4 @@ -ResourceDisplays.CorpusDisplay = class CorpusDisplay extends ResourceDisplays.BaseDisplay { +ResourceDisplays.CorpusDisplay = class CorpusDisplay extends ResourceDisplays.ResourceDisplay { static htmlClass = 'corpus-display'; constructor(displayElement) { diff --git a/app/static/js/resource-displays/index.js b/app/static/js/resource-displays/index.js index 1f795c44..8cc8809e 100644 --- a/app/static/js/resource-displays/index.js +++ b/app/static/js/resource-displays/index.js @@ -3,9 +3,9 @@ var ResourceDisplays = {}; ResourceDisplays.autoInit = () => { for (let propertyName in ResourceDisplays) { let property = ResourceDisplays[propertyName]; - // Call autoInit of all properties that are subclasses of `ResourceDisplays.BaseDisplay`. - // This does not include `ResourceDisplays.BaseDisplay` itself. - if (property.prototype instanceof ResourceDisplays.BaseDisplay) { + // Call autoInit of all properties that are subclasses of `ResourceDisplays.ResourceDisplay`. + // This does not include `ResourceDisplays.ResourceDisplay` itself. + if (property.prototype instanceof ResourceDisplays.ResourceDisplay) { // Check if the static `htmlClass` property is defined. if (property.htmlClass === undefined) {return;} // Gather all HTML elements that have the `this.htmlClass` class @@ -15,51 +15,4 @@ ResourceDisplays.autoInit = () => { for (let displayElement of displayElements) {new property(displayElement);} } } -} - -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/job-display.js b/app/static/js/resource-displays/job-display.js index 4ab370e2..78ebc22c 100644 --- a/app/static/js/resource-displays/job-display.js +++ b/app/static/js/resource-displays/job-display.js @@ -1,4 +1,4 @@ -ResourceDisplays.JobDisplay = class JobDisplay extends ResourceDisplays.BaseDisplay { +ResourceDisplays.JobDisplay = class JobDisplay extends ResourceDisplays.ResourceDisplay { static htmlClass = 'job-display'; constructor(displayElement) { 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..39401d2b --- /dev/null +++ b/app/static/js/resource-displays/resource-display.js @@ -0,0 +1,46 @@ +ResourceDisplays.ResourceDisplay = class ResourceDisplay { + 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/ResourceLists/AdminUserList.js b/app/static/js/resource-lists/admin-user-list.js similarity index 93% rename from app/static/js/ResourceLists/AdminUserList.js rename to app/static/js/resource-lists/admin-user-list.js index 0b8f0c16..40feab27 100644 --- a/app/static/js/ResourceLists/AdminUserList.js +++ b/app/static/js/resource-lists/admin-user-list.js @@ -1,9 +1,5 @@ -class AdminUserList extends ResourceList { - static autoInit() { - for (let adminUserListElement of document.querySelectorAll('.admin-user-list:not(.no-autoinit)')) { - new AdminUserList(adminUserListElement); - } - } +ResourceLists.AdminUserList = class AdminUserList extends ResourceLists.ResourceList { + static htmlClass = 'admin-user-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -108,4 +104,4 @@ class AdminUserList extends ResourceList { } } } -} +}; diff --git a/app/static/js/ResourceLists/CorpusFileList.js b/app/static/js/resource-lists/corpus-file-list.js similarity index 98% rename from app/static/js/ResourceLists/CorpusFileList.js rename to app/static/js/resource-lists/corpus-file-list.js index 9997b061..7e6a5da9 100644 --- a/app/static/js/ResourceLists/CorpusFileList.js +++ b/app/static/js/resource-lists/corpus-file-list.js @@ -1,9 +1,5 @@ -class CorpusFileList extends ResourceList { - static autoInit() { - for (let corpusFileListElement of document.querySelectorAll('.corpus-file-list:not(.no-autoinit)')) { - new CorpusFileList(corpusFileListElement); - } - } +ResourceLists.CorpusFileList = class CorpusFileList extends ResourceLists.ResourceList { + static htmlClass = 'corpus-file-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -369,4 +365,4 @@ class CorpusFileList extends ResourceList { } } } -} +}; diff --git a/app/static/js/ResourceLists/CorpusFollowerList.js b/app/static/js/resource-lists/corpus-follower-list.js similarity index 96% rename from app/static/js/ResourceLists/CorpusFollowerList.js rename to app/static/js/resource-lists/corpus-follower-list.js index ca70a6c7..b8a4c255 100644 --- a/app/static/js/ResourceLists/CorpusFollowerList.js +++ b/app/static/js/resource-lists/corpus-follower-list.js @@ -1,9 +1,5 @@ -class CorpusFollowerList extends ResourceList { - static autoInit() { - for (let corpusFollowerListElement of document.querySelectorAll('.corpus-follower-list:not(.no-autoinit)')) { - new CorpusFollowerList(corpusFollowerListElement); - } - } +ResourceLists.CorpusFollowerList = class CorpusFollowerList extends ResourceLists.ResourceList { + static htmlClass = 'corpus-follower-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -196,4 +192,4 @@ class CorpusFollowerList extends ResourceList { } } } -} +}; diff --git a/app/static/js/ResourceLists/CorpusList.js b/app/static/js/resource-lists/corpus-list.js similarity index 98% rename from app/static/js/ResourceLists/CorpusList.js rename to app/static/js/resource-lists/corpus-list.js index 985ff1d1..f4289d8b 100644 --- a/app/static/js/ResourceLists/CorpusList.js +++ b/app/static/js/resource-lists/corpus-list.js @@ -1,9 +1,5 @@ -class CorpusList extends ResourceList { - static autoInit() { - for (let corpusListElement of document.querySelectorAll('.corpus-list:not(.no-autoinit)')) { - new CorpusList(corpusListElement); - } - } +ResourceLists.CorpusList = class CorpusList extends ResourceLists.ResourceList { + static htmlClass = 'corpus-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -370,4 +366,4 @@ class CorpusList extends ResourceList { } } } -} +}; diff --git a/app/static/js/ResourceLists/CorpusTextInfoList.js b/app/static/js/resource-lists/corpus-text-info-list.js similarity index 92% rename from app/static/js/ResourceLists/CorpusTextInfoList.js rename to app/static/js/resource-lists/corpus-text-info-list.js index f1545d70..e24502a0 100644 --- a/app/static/js/ResourceLists/CorpusTextInfoList.js +++ b/app/static/js/resource-lists/corpus-text-info-list.js @@ -1,10 +1,5 @@ -class CorpusTextInfoList extends ResourceList { - - static autoInit() { - for (let corpusTextInfoListElement of document.querySelectorAll('.corpus-text-info-list:not(.no-autoinit)')) { - new CorpusTextInfoList(corpusTextInfoListElement); - } - } +ResourceLists.CorpusTextInfoList = class CorpusTextInfoList extends ResourceLists.ResourceList { + static htmlClass = 'corpus-text-info-list'; static defaultOptions = { page: 5 @@ -12,7 +7,7 @@ class CorpusTextInfoList extends ResourceList { constructor(listContainerElement, options = {}) { let _options = Utils.mergeObjectsDeep( - CorpusTextInfoList.defaultOptions, + ResourceLists.CorpusTextInfoList.defaultOptions, options ); super(listContainerElement, _options); @@ -26,7 +21,7 @@ class CorpusTextInfoList extends ResourceList { get item() { return (values) => { return ` - + () @@ -109,4 +104,4 @@ class CorpusTextInfoList extends ResourceList { clickedSortElement.style.color = '#aa9cc9'; clickedSortElement.innerHTML = clickedSortElement.classList.contains('asc') ? 'arrow_drop_down' : 'arrow_drop_up'; } -} +}; diff --git a/app/static/js/ResourceLists/CorpusTokenList.js b/app/static/js/resource-lists/corpus-token-list.js similarity index 94% rename from app/static/js/ResourceLists/CorpusTokenList.js rename to app/static/js/resource-lists/corpus-token-list.js index cc16692b..eddd82ea 100644 --- a/app/static/js/ResourceLists/CorpusTokenList.js +++ b/app/static/js/resource-lists/corpus-token-list.js @@ -1,9 +1,5 @@ -class CorpusTokenList extends ResourceList { - static autoInit() { - for (let corpusTokenListElement of document.querySelectorAll('.corpus-token-list:not(.no-autoinit)')) { - new CorpusTokenList(corpusTokenListElement); - } - } +ResourceLists.CorpusTokenList = class CorpusTokenList extends ResourceLists.ResourceList { + static htmlClass = 'corpus-token-list'; static defaultOptions = { page: 7 @@ -11,7 +7,7 @@ class CorpusTokenList extends ResourceList { constructor(listContainerElement, options = {}) { let _options = Utils.mergeObjectsDeep( - CorpusTokenList.defaultOptions, + ResourceLists.CorpusTokenList.defaultOptions, options ); super(listContainerElement, _options); @@ -138,4 +134,4 @@ class CorpusTokenList extends ResourceList { } } -} +}; diff --git a/app/static/js/ResourceLists/DetailledPublicCorpusList.js b/app/static/js/resource-lists/detailed-public-corpus-list.js similarity index 93% rename from app/static/js/ResourceLists/DetailledPublicCorpusList.js rename to app/static/js/resource-lists/detailed-public-corpus-list.js index 5bfc59ff..e855e07b 100644 --- a/app/static/js/ResourceLists/DetailledPublicCorpusList.js +++ b/app/static/js/resource-lists/detailed-public-corpus-list.js @@ -1,4 +1,6 @@ -class DetailledPublicCorpusList extends CorpusList { +ResourceLists.DetailedPublicCorpusList = class DetailedPublicCorpusList extends ResourceLists.ResourceList { + static htmlClass = 'detailed-public-corpus-list'; + get item() { return (values) => { return ` @@ -68,4 +70,4 @@ class DetailledPublicCorpusList extends CorpusList { 'current-user-is-following': Object.values(corpus.corpus_follower_associations).some(association => association.follower.id === currentUserId) }; } -} +}; diff --git a/app/static/js/resource-lists/index.js b/app/static/js/resource-lists/index.js new file mode 100644 index 00000000..513da46b --- /dev/null +++ b/app/static/js/resource-lists/index.js @@ -0,0 +1,18 @@ +var ResourceLists = {}; + +ResourceLists.autoInit = () => { + for (let propertyName in ResourceLists) { + let property = ResourceLists[propertyName]; + // Call autoInit of all properties that are subclasses of `ResourceLists.ResourceList`. + // This does not include `ResourceLists.ResourceList` itself. + if (property.prototype instanceof ResourceLists.ResourceList) { + // 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);} + } + } +}; diff --git a/app/static/js/ResourceLists/JobInputList.js b/app/static/js/resource-lists/job-input-list.js similarity index 92% rename from app/static/js/ResourceLists/JobInputList.js rename to app/static/js/resource-lists/job-input-list.js index 97a8dd14..609702b5 100644 --- a/app/static/js/ResourceLists/JobInputList.js +++ b/app/static/js/resource-lists/job-input-list.js @@ -1,9 +1,5 @@ -class JobInputList extends ResourceList { - static autoInit() { - for (let jobInputListElement of document.querySelectorAll('.job-input-list:not(.no-autoinit)')) { - new JobInputList(jobInputListElement); - } - } +ResourceLists.JobInputList = class JobInputList extends ResourceLists.ResourceList { + static htmlClass = 'job-input-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -90,4 +86,4 @@ class JobInputList extends ResourceList { } } } -} +}; diff --git a/app/static/js/ResourceLists/JobList.js b/app/static/js/resource-lists/job-list.js similarity index 98% rename from app/static/js/ResourceLists/JobList.js rename to app/static/js/resource-lists/job-list.js index 1cb3ea60..c751e705 100644 --- a/app/static/js/ResourceLists/JobList.js +++ b/app/static/js/resource-lists/job-list.js @@ -1,9 +1,5 @@ -class JobList extends ResourceList { - static autoInit() { - for (let jobListElement of document.querySelectorAll('.job-list:not(.no-autoinit)')) { - new JobList(jobListElement); - } - } +ResourceLists.JobList = class JobList extends ResourceLists.ResourceList { + static htmlClass = 'job-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -323,4 +319,4 @@ class JobList extends ResourceList { } } } -} +}; diff --git a/app/static/js/ResourceLists/JobResultList.js b/app/static/js/resource-lists/job-result-list.js similarity index 93% rename from app/static/js/ResourceLists/JobResultList.js rename to app/static/js/resource-lists/job-result-list.js index b0cbc088..e71759a5 100644 --- a/app/static/js/ResourceLists/JobResultList.js +++ b/app/static/js/resource-lists/job-result-list.js @@ -1,9 +1,5 @@ -class JobResultList extends ResourceList { - static autoInit() { - for (let jobResultListElement of document.querySelectorAll('.job-result-list:not(.no-autoinit)')) { - new JobResultList(jobResultListElement); - } - } +ResourceLists.JobResultList = class JobResultList extends ResourceLists.ResourceList { + static htmlClass = 'job-result-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -115,4 +111,4 @@ class JobResultList extends ResourceList { } } } -} +}; diff --git a/app/static/js/ResourceLists/PublicCorpusList.js b/app/static/js/resource-lists/public-corpus-list.js similarity index 93% rename from app/static/js/ResourceLists/PublicCorpusList.js rename to app/static/js/resource-lists/public-corpus-list.js index 1ef98273..659dfd09 100644 --- a/app/static/js/ResourceLists/PublicCorpusList.js +++ b/app/static/js/resource-lists/public-corpus-list.js @@ -1,4 +1,6 @@ -class PublicCorpusList extends CorpusList { +ResourceLists.PublicCorpusList = class PublicCorpusList extends ResourceLists.ResourceList { + static htmlClass = 'public-corpus-list'; + get item() { return (values) => { return ` @@ -52,4 +54,4 @@ class PublicCorpusList extends CorpusList {
    `.trim(); } -} +}; diff --git a/app/static/js/ResourceLists/ResourceList.js b/app/static/js/resource-lists/resource-list.js similarity index 78% rename from app/static/js/ResourceLists/ResourceList.js rename to app/static/js/resource-lists/resource-list.js index 6bc6ac1f..ba71f03f 100644 --- a/app/static/js/ResourceLists/ResourceList.js +++ b/app/static/js/resource-lists/resource-list.js @@ -1,23 +1,10 @@ -class ResourceList { +ResourceLists.ResourceList = class ResourceList { /* 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 htmlClass; static defaultOptions = { page: 5, @@ -36,7 +23,7 @@ class ResourceList { } let _options = Utils.mergeObjectsDeep( {item: this.item, valueNames: this.valueNames}, - ResourceList.defaultOptions, + ResourceLists.ResourceList.defaultOptions, options ); this.listContainerElement = listContainerElement; diff --git a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js b/app/static/js/resource-lists/spacy-nlp-pipeline-model-list.js similarity index 96% rename from app/static/js/ResourceLists/SpacyNLPPipelineModelList.js rename to app/static/js/resource-lists/spacy-nlp-pipeline-model-list.js index 46d3739d..195fa60c 100644 --- a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js +++ b/app/static/js/resource-lists/spacy-nlp-pipeline-model-list.js @@ -1,9 +1,5 @@ -class SpaCyNLPPipelineModelList extends ResourceList { - static autoInit() { - for (let spaCyNLPPipelineModelListElement of document.querySelectorAll('.spacy-nlp-pipeline-model-list:not(.no-autoinit)')) { - new SpaCyNLPPipelineModelList(spaCyNLPPipelineModelListElement); - } - } +ResourceLists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelList extends ResourceLists.ResourceList { + static htmlClass = 'spacy-nlp-pipeline-model-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -220,4 +216,4 @@ class SpaCyNLPPipelineModelList extends ResourceList { } } } -} +}; diff --git a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js b/app/static/js/resource-lists/tesseract-ocr-pipeline-model-list.js similarity index 96% rename from app/static/js/ResourceLists/TesseractOCRPipelineModelList.js rename to app/static/js/resource-lists/tesseract-ocr-pipeline-model-list.js index 765f44a6..975bfe5c 100644 --- a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js +++ b/app/static/js/resource-lists/tesseract-ocr-pipeline-model-list.js @@ -1,9 +1,5 @@ -class TesseractOCRPipelineModelList extends ResourceList { - static autoInit() { - for (let tesseractOCRPipelineModelListElement of document.querySelectorAll('.tesseract-ocr-pipeline-model-list:not(.no-autoinit)')) { - new TesseractOCRPipelineModelList(tesseractOCRPipelineModelListElement); - } - } +ResourceLists.TesseractOCRPipelineModelList = class TesseractOCRPipelineModelList extends ResourceLists.ResourceList { + static htmlClass = 'tesseract-ocr-pipeline-model-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -229,4 +225,4 @@ class TesseractOCRPipelineModelList extends ResourceList { } } } -} +}; diff --git a/app/static/js/ResourceLists/UserList.js b/app/static/js/resource-lists/user-list.js similarity index 93% rename from app/static/js/ResourceLists/UserList.js rename to app/static/js/resource-lists/user-list.js index 2ba4dc19..6ef14e2f 100644 --- a/app/static/js/ResourceLists/UserList.js +++ b/app/static/js/resource-lists/user-list.js @@ -1,9 +1,5 @@ -class UserList extends ResourceList { - static autoInit() { - for (let userListElement of document.querySelectorAll('.user-list:not(.no-autoinit)')) { - new UserList(userListElement); - } - } +ResourceLists.UserList = class UserList extends ResourceLists.ResourceList { + static htmlClass = 'user-list'; constructor(listContainerElement, options = {}) { super(listContainerElement, options); @@ -101,4 +97,4 @@ class UserList extends ResourceList { } } } -} +}; diff --git a/app/static/js/utils/index.js b/app/static/js/utils/index.js new file mode 100644 index 00000000..39d693f5 --- /dev/null +++ b/app/static/js/utils/index.js @@ -0,0 +1 @@ +Utils = {}; diff --git a/app/static/js/utils/utils.js b/app/static/js/utils/utils.js new file mode 100644 index 00000000..17dcde05 --- /dev/null +++ b/app/static/js/utils/utils.js @@ -0,0 +1,89 @@ +Utils.escape = (text) => { + // https://codereview.stackexchange.com/a/126722 + var table = { + '<': 'lt', + '>': 'gt', + '"': 'quot', + '\'': 'apos', + '&': 'amp', + '\r': '#10', + '\n': '#13' + }; + + return text.toString().replace(/[<>"'\r\n&]/g, (chr) => { + return '&' + table[chr] + ';'; + }); +}; + +Utils.unescape = (escapedText) => { + var table = { + 'lt': '<', + 'gt': '>', + 'quot': '"', + 'apos': "'", + 'amp': '&', + '#10': '\r', + '#13': '\n' + }; + + return escapedText.replace(/&(#?\w+);/g, (match, entity) => { + if (table.hasOwnProperty(entity)) { + return table[entity]; + } + + return match; + }); +}; + +Utils.HTMLToElement = (HTMLString) => { + let templateElement = document.createElement('template'); + templateElement.innerHTML = HTMLString.trim(); + return templateElement.content.firstChild; +}; + +Utils.generateElementId = (prefix='', suffix='') => { + for (let i = 0; true; i++) { + if (document.querySelector(`#${prefix}${i}${suffix}`) !== null) {continue;} + return `${prefix}${i}${suffix}`; + } +}; + +Utils.isObject = (object) => { + return object !== null && typeof object === 'object' && !Array.isArray(object); +}; + +Utils.mergeObjectsDeep = (...objects) => { + let mergedObject = {}; + if (objects.length === 0) { + return mergedObject; + } + if (!Utils.isObject(objects[0])) {throw 'Cannot merge non-object';} + if (objects.length === 1) { + return Utils.mergeObjectsDeep(mergedObject, objects[0]); + } + if (!Utils.isObject(objects[1])) {throw 'Cannot merge non-object';} + for (let key in objects[0]) { + if (objects[0].hasOwnProperty(key)) { + if (objects[1].hasOwnProperty(key)) { + if (Utils.isObject(objects[0][key]) && Utils.isObject(objects[1][key])) { + mergedObject[key] = Utils.mergeObjectsDeep(objects[0][key], objects[1][key]); + } else { + mergedObject[key] = objects[1][key]; + } + } else { + mergedObject[key] = objects[0][key]; + } + } + } + for (let key in objects[1]) { + if (objects[1].hasOwnProperty(key)) { + if (!objects[0].hasOwnProperty(key)) { + mergedObject[key] = objects[1][key]; + } + } + } + if (objects.length === 2) { + return mergedObject; + } + return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2)); +}; diff --git a/app/static/js/XMLtoObject.js b/app/static/js/utils/xml-to-object.js similarity index 100% rename from app/static/js/XMLtoObject.js rename to app/static/js/utils/xml-to-object.js diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index 6213e3df..1fd32f7e 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -7,9 +7,17 @@ {%- assets filters='rjsmin', output='gen/app.%(version)s.js', - 'js/App.js', - 'js/Utils.js', - 'js/XMLtoObject.js' + 'js/app/index.js', + 'js/app/app.js' +%} + +{%- endassets %} + +{%- assets + filters='rjsmin', + output='gen/utils.%(version)s.js', + 'js/utils/index.js', + 'js/utils/utils.js' %} {%- endassets %} @@ -18,6 +26,7 @@ filters='rjsmin', output='gen/cqi.%(version)s.js', 'js/cqi/index.js', + 'js/cqi/constants.js', 'js/cqi/errors.js', 'js/cqi/status.js', 'js/cqi/api/index.js', @@ -36,6 +45,7 @@ filters='rjsmin', output='gen/Forms.%(version)s.js', 'js/forms/index.js', + 'js/forms/base-form.js', 'js/forms/create-contribution-form.js', 'js/forms/create-corpus-file-form.js', 'js/forms/create-job-form.js' @@ -47,6 +57,7 @@ filters='rjsmin', output='gen/resource-displays.%(version)s.js', 'js/resource-displays/index.js', + 'js/resource-displays/resource-display.js', 'js/resource-displays/corpus-display.js', 'js/resource-displays/job-display.js' %} @@ -55,22 +66,23 @@ {%- assets filters='rjsmin', - output='gen/ResourceLists.%(version)s.js', - 'js/ResourceLists/ResourceList.js', - 'js/ResourceLists/CorpusFileList.js', - 'js/ResourceLists/CorpusList.js', - 'js/ResourceLists/PublicCorpusList.js', - 'js/ResourceLists/JobList.js', - 'js/ResourceLists/JobInputList.js', - 'js/ResourceLists/JobResultList.js', - 'js/ResourceLists/SpacyNLPPipelineModelList.js', - 'js/ResourceLists/TesseractOCRPipelineModelList.js', - 'js/ResourceLists/UserList.js', - 'js/ResourceLists/AdminUserList.js', - 'js/ResourceLists/CorpusFollowerList.js', - 'js/ResourceLists/CorpusTextInfoList.js', - 'js/ResourceLists/DetailledPublicCorpusList.js', - 'js/ResourceLists/CorpusTokenList.js' + output='gen/resource-lists.%(version)s.js', + 'js/resource-lists/index.js', + 'js/resource-lists/resource-list.js', + 'js/resource-lists/admin-user-list.js', + 'js/resource-lists/corpus-file-list.js', + 'js/resource-lists/corpus-follower-list.js', + 'js/resource-lists/corpus-list.js', + 'js/resource-lists/corpus-text-info-list.js', + 'js/resource-lists/corpus-token-list.js', + 'js/resource-lists/detailed-public-corpus-list.js', + 'js/resource-lists/job-input-list.js', + 'js/resource-lists/job-list.js', + 'js/resource-lists/job-result-list.js', + 'js/resource-lists/public-corpus-list.js', + 'js/resource-lists/spacy-nlp-pipeline-model-list.js', + 'js/resource-lists/tesseract-ocr-pipeline-model-list.js', + 'js/resource-lists/user-list.js' %} {%- endassets %} @@ -106,7 +118,7 @@