From 6b1f588f29da4323df5926f11a4806791cd2a6ba Mon Sep 17 00:00:00 2001 From: Stephan Porada Date: Thu, 6 Aug 2020 16:08:03 +0200 Subject: [PATCH 01/15] Add asynch version of Session initialization --- .../modules/nopaque.CorpusAnalysisClient.js | 121 +++++ ...js => nopaque.CorpusAnalysisClient.js.bak} | 0 .../templates/corpora/analyse_corpus.html.j2 | 386 +-------------- .../corpora/analyse_corpus.html.j2.bak | 440 ++++++++++++++++++ 4 files changed, 580 insertions(+), 367 deletions(-) create mode 100644 web/app/static/js/modules/nopaque.CorpusAnalysisClient.js rename web/app/static/js/{nopaque.CorpusAnalysisClient.js => nopaque.CorpusAnalysisClient.js.bak} (100%) create mode 100644 web/app/templates/corpora/analyse_corpus.html.j2.bak diff --git a/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js new file mode 100644 index 00000000..9c172608 --- /dev/null +++ b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js @@ -0,0 +1,121 @@ +class CorpusAnalysisClient { + constructor(corpusId, socket) { + this.corpusId = corpusId; + this.socket = socket; + this.displays = {}; + } + + setDisplay(type, display) { + this.displays[type] = display; + } + + async initSession() { + let request = await this.requestSession(); + let recvieveSession = await this.recvieveSession(); + console.log("corpus_analysis_init: Waiting for response"); // this happens inbetween the two functions above + } + + requestSession() { // should be private with ES2020 + console.log("corpus_analysis_init: Emitting to server"); + if (this.displays.init != undefined) { + this.displays.init.element.M_Modal.open(); + this.displays.init.setVisibilityByStatus("waiting"); + } + return new Promise((resolve, reject) => { + this.socket.emit("corpus_analysis_init", this.corpusId); + resolve(); + }) + } + + recvieveSession() { // should be private with ES2020 + this.socket.on("corpus_analysis_init", (response) => { + console.log("corpus_analysis_init: Recieving response from server"); + if (response.code === 200) { + console.log("corpus_analysis_init: Initialization succeeded"); + if (this.displays.init != undefined) { + this.displays.init.element.M_Modal.close(); + this.displays.init.setVisibilityByStatus("success"); + } + } else { + let errorText = `Error ${response.code} - ${response.msg}`; + if (this.displays.init.errorContainer != undefined) { + this.displays.init.errorContainer.innerHTML = `

` + + `error ${errorText}

`; + } + if (this.displays.init != undefined) { + this.displays.init.setVisibilityByStatus("error"); + } + console.error(`corpus_analysis_init: ${errorText}`); + } + }); + } +} + +class CorpusAnalysisDisplay { + constructor(element) { + this.element = (() => {if (element instanceof HTMLElement) { + return element; + } else { + element = element["$el"][0]; + return element; + } + })(); // with this function modals can also be handeld + this.errorContainer = element.querySelector(".error-container"); + this.showOnError = element.querySelectorAll(".show-on-error"); + this.showOnSuccess = element.querySelectorAll(".show-on-success"); + this.showWhileWaiting = element.querySelectorAll(".show-while-waiting"); + this.hideOnComplete = element.querySelectorAll(".hide-on-complete") + } + + setVisibilityByStatus(status) { + switch (status) { + case "error": + for (let element of this.showOnError) { + element.classList.remove("hide"); + } + for (let element of this.showOnSuccess) { + element.classList.add("hide"); + } + for (let element of this.showWhileWaiting) { + element.classList.add("hide"); + } + break; + case "success": + for (let element of this.showOnError) { + element.classList.add("hide"); + + } + for (let element of this.showOnSuccess) { + element.classList.remove("hide"); + } + for (let element of this.showWhileWaiting) { + element.classList.add("hide"); + } + break; + case "waiting": + for (let element of this.showOnError) { + element.classList.add("hide"); + } + for (let element of this.showOnSuccess) { + element.classList.add("hide"); + } + for (let element of this.showWhileWaiting) { + element.classList.remove("hide"); + } + break; + default: + // Hide all + for (let element of this.showOnError) { + element.classList.add("hide"); + } + for (let element of this.showOnSuccess) { + element.classList.add("hide"); + } + for (let element of this.showWhileWaiting) { + element.classList.add("hide"); + } + } + } +} + +export {CorpusAnalysisClient, CorpusAnalysisDisplay}; \ No newline at end of file diff --git a/web/app/static/js/nopaque.CorpusAnalysisClient.js b/web/app/static/js/nopaque.CorpusAnalysisClient.js.bak similarity index 100% rename from web/app/static/js/nopaque.CorpusAnalysisClient.js rename to web/app/static/js/nopaque.CorpusAnalysisClient.js.bak diff --git a/web/app/templates/corpora/analyse_corpus.html.j2 b/web/app/templates/corpora/analyse_corpus.html.j2 index 1ca61ba3..420ab29f 100644 --- a/web/app/templates/corpora/analyse_corpus.html.j2 +++ b/web/app/templates/corpora/analyse_corpus.html.j2 @@ -65,376 +65,28 @@ {% include 'modals/export_query_results.html.j2' %} {% include 'modals/context_modal.html.j2' %} + + - - - - {% endblock %} diff --git a/web/app/templates/corpora/analyse_corpus.html.j2.bak b/web/app/templates/corpora/analyse_corpus.html.j2.bak new file mode 100644 index 00000000..1ca61ba3 --- /dev/null +++ b/web/app/templates/corpora/analyse_corpus.html.j2.bak @@ -0,0 +1,440 @@ +{% extends "nopaque.html.j2" %} + +{% set headline = ' ' %} + +{% set full_width = True %} +{% set imported = False %} + +{% block page_content %} +
+
+
+ +
+
+
+
+ search + {{ query_form.query() }} + {{ query_form.query.label }} + + + help + + CQP query language tutorial + + +
+
+
+
+ {{ M.render_field(query_form.submit, material_icon='send') }} +
+
+
+
+
+
+ + +
+
+
+
+
+ {% include 'interactions/infos.html.j2' %} + {% include 'interactions/export.html.j2' %} + {% include 'interactions/create.html.j2' %} + {% include 'interactions/display.html.j2' %} + {% include 'interactions/analysis.html.j2' %} + {% include 'interactions/cite.html.j2' %} +
+ {% include 'tables/query_results.html.j2' %} +
+
+
+ + +{% include 'interactions/scroll_to_top.html.j2' %} + + +{% include 'modals/show_metadata.html.j2' %} +{% include 'modals/show_text_details.html.j2' %} +{% include 'modals/analysis_init.html.j2' %} +{% include 'modals/export_query_results.html.j2' %} +{% include 'modals/context_modal.html.j2' %} + + + + + + + +{% endblock %} From 25ea9ba583643a5c39ea235ae73c0ae5be412513 Mon Sep 17 00:00:00 2001 From: Stephan Porada Date: Fri, 7 Aug 2020 17:32:10 +0200 Subject: [PATCH 02/15] Remove asynch rework because it was already event driven --- .../modules/nopaque.CorpusAnalysisClient.js | 166 +++++++++++------- .../js/modules/nopaque.listenerFunctions.js | 60 +++++++ .../templates/corpora/analyse_corpus.html.j2 | 57 +++++- 3 files changed, 218 insertions(+), 65 deletions(-) create mode 100644 web/app/static/js/modules/nopaque.listenerFunctions.js diff --git a/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js index 9c172608..a2659b76 100644 --- a/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js +++ b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js @@ -1,121 +1,169 @@ +/** + * This class is used to create an CorpusAnalysisClient object. + * The client handels the client server communication. + * It requests data (e.g. the analysis session or query results) from the + * the server and recieves them. + */ class CorpusAnalysisClient { - constructor(corpusId, socket) { + constructor(corpusId, socket, logging=false) { this.corpusId = corpusId; - this.socket = socket; this.displays = {}; + this.logging = logging; + this.socket = socket; + this.socketEventListeners = {}; } - setDisplay(type, display) { - this.displays[type] = display; + // Registers one or more SocketEventListeners to the CorpusAnalysisClient. + setSocketEventListeners(socketEventListeners) { + for (let listener of socketEventListeners) { + this.socketEventListeners[listener.type] = listener.listenerFunction; + } } - async initSession() { - let request = await this.requestSession(); - let recvieveSession = await this.recvieveSession(); - console.log("corpus_analysis_init: Waiting for response"); // this happens inbetween the two functions above + loadListeners() { + for (let [type, listener] of Object.entries(this.socketEventListeners)) { + listener(this); + } } - requestSession() { // should be private with ES2020 - console.log("corpus_analysis_init: Emitting to server"); + // Registers a CorpusAnalysisDisplay object to the CorpusAnalysisClient. + setDisplay(type, corpusAnalysisDisplay) { + this.displays[type] = corpusAnalysisDisplay; + } + + // /** + // * Initializes the interactive corpus analysis session via socket.io. + // * This function uses helper functions. + // */ + // initSession() { + // let request = this.requestSession(); + // let recvieveSession = this.recvieveSession(); + // console.info('corpus_analysis_init: Client waiting for response'); // this happens inbetween the two functions above + // } + + /** + * Requests a corpus analysis session via socket.io. + * Opens a loading modal at the start of the request + * Should be a private method if ES2020 is finalized (Maybe?) + */ + requestSession() { + console.info('corpus_analysis_init: Client requesting session via', + 'socket.emit'); if (this.displays.init != undefined) { this.displays.init.element.M_Modal.open(); - this.displays.init.setVisibilityByStatus("waiting"); + this.displays.init.setVisibilityByStatus('waiting'); } - return new Promise((resolve, reject) => { - this.socket.emit("corpus_analysis_init", this.corpusId); - resolve(); - }) + this.socket.emit('corpus_analysis_init', this.corpusId); } - recvieveSession() { // should be private with ES2020 - this.socket.on("corpus_analysis_init", (response) => { - console.log("corpus_analysis_init: Recieving response from server"); - if (response.code === 200) { - console.log("corpus_analysis_init: Initialization succeeded"); - if (this.displays.init != undefined) { - this.displays.init.element.M_Modal.close(); - this.displays.init.setVisibilityByStatus("success"); - } - } else { - let errorText = `Error ${response.code} - ${response.msg}`; - if (this.displays.init.errorContainer != undefined) { - this.displays.init.errorContainer.innerHTML = `

` + - `error ${errorText}

`; - } - if (this.displays.init != undefined) { - this.displays.init.setVisibilityByStatus("error"); - } - console.error(`corpus_analysis_init: ${errorText}`); - } - }); + // /** + // * Sends a query to the server and handles the response to that query. + // * This function uses helper functions. + // */ + // query(queryStr) { + // let requestQueryData = this.requestQueryData(queryStr); + // let recieveQueryProcessStatus = this.recieveQueryProcessStatus(); + // let recieveQueryData = this.recieveQueryData(); + // console.info('corpus_analysis_query: Client waiting for query data'); // this happens inbetween the two functions above + // } + + /** + * Sends the query string to the server. + * Should be a private method if ES2020 is finalized (Maybe?) + */ + requestQueryData(queryStr) { + console.info('corpus_analysis_query: Client requesting query data via', + 'socket.emit for the query', queryStr); + // TODO: Display stuff + this.socket.emit('corpus_analysis_query', queryStr); } } +/** + * This class is used to create an CorpusAnalysisDisplay object. + * Input is one HTMLElement that can then be hidden or shown depending on + * its CSS classes. + */ class CorpusAnalysisDisplay { constructor(element) { + // with this function initalized modals can also be handeld this.element = (() => {if (element instanceof HTMLElement) { return element; } else { - element = element["$el"][0]; + element = element['$el'][0]; return element; } - })(); // with this function modals can also be handeld - this.errorContainer = element.querySelector(".error-container"); - this.showOnError = element.querySelectorAll(".show-on-error"); - this.showOnSuccess = element.querySelectorAll(".show-on-success"); - this.showWhileWaiting = element.querySelectorAll(".show-while-waiting"); - this.hideOnComplete = element.querySelectorAll(".hide-on-complete") + })(); + this.errorContainer = element.querySelector('.error-container'); + this.showOnError = element.querySelectorAll('.show-on-error'); + this.showOnSuccess = element.querySelectorAll('.show-on-success'); + this.showWhileWaiting = element.querySelectorAll('.show-while-waiting'); + this.hideOnComplete = element.querySelectorAll('.hide-on-complete') } + // Changes the visibility of its own setVisibilityByStatus(status) { switch (status) { - case "error": + case 'error': for (let element of this.showOnError) { - element.classList.remove("hide"); + element.classList.remove('hide'); } for (let element of this.showOnSuccess) { - element.classList.add("hide"); + element.classList.add('hide'); } for (let element of this.showWhileWaiting) { - element.classList.add("hide"); + element.classList.add('hide'); } break; - case "success": + case 'success': for (let element of this.showOnError) { - element.classList.add("hide"); + element.classList.add('hide'); } for (let element of this.showOnSuccess) { - element.classList.remove("hide"); + element.classList.remove('hide'); } for (let element of this.showWhileWaiting) { - element.classList.add("hide"); + element.classList.add('hide'); } break; - case "waiting": + case 'waiting': for (let element of this.showOnError) { - element.classList.add("hide"); + element.classList.add('hide'); } for (let element of this.showOnSuccess) { - element.classList.add("hide"); + element.classList.add('hide'); } for (let element of this.showWhileWaiting) { - element.classList.remove("hide"); + element.classList.remove('hide'); } break; default: // Hide all for (let element of this.showOnError) { - element.classList.add("hide"); + element.classList.add('hide'); } for (let element of this.showOnSuccess) { - element.classList.add("hide"); + element.classList.add('hide'); } for (let element of this.showWhileWaiting) { - element.classList.add("hide"); + element.classList.add('hide'); } } } } -export {CorpusAnalysisClient, CorpusAnalysisDisplay}; \ No newline at end of file +/** + * This class is used to create an CorpusAnalysisDisplay object. + * Input is one HTMLElement that can then be hidden or shown depending on + * its CSS classes. + */ +class SocketEventListener { + constructor(type, listenerFunction) { + this.listenerFunction = listenerFunction; + this.type = type; + } +} + +// export both Classes from this module +export {CorpusAnalysisClient, CorpusAnalysisDisplay, SocketEventListener}; \ No newline at end of file diff --git a/web/app/static/js/modules/nopaque.listenerFunctions.js b/web/app/static/js/modules/nopaque.listenerFunctions.js new file mode 100644 index 00000000..ce94ca91 --- /dev/null +++ b/web/app/static/js/modules/nopaque.listenerFunctions.js @@ -0,0 +1,60 @@ +/** + * Recieves a corpus analysis session via socket.io. + * Closes the loading modal that has been opend with requestSession at the + * start of the request. + */ +function recieveSession(client) { + client.socket.on('corpus_analysis_init', (response) => { + console.group('recieve session') + console.info('corpus_analysis_init: Client recieving session/or error', + 'codes via socket.on'); + if (response.code === 200) { + console.info('corpus_analysis_init: Initialization succeeded'); + console.info(response); + console.groupEnd(); + if (client.displays.init != undefined) { + client.displays.init.element.M_Modal.close(); + client.displays.init.setVisibilityByStatus('success'); + } + } else { + let errorText = `Error ${response.code} - ${response.msg}`; + if (client.displays.init.errorContainer != undefined) { + client.displays.init.errorContainer.innerHTML = `

` + + `error${errorText}

`; + } + if (client.displays.init != undefined) { + client.displays.init.setVisibilityByStatus('error'); + } + console.error(`corpus_analysis_init: ${errorText}`); + } + }); +} + +/** + * Recieves the query process status before any actual results are being + * transmitted. So it recieves error codes if a query failed or + * was invalid etc. + */ +function recieveQueryStatus(client) { + client.socket.on('corpus_analysis_query', (response) => { + console.group('corpus_analysis_query: Client recieving query process', + 'status via socket.on'); + console.info(response); + console.groupEnd(); + }); +} + +/** + * Recieves the query data from the request and handles it. + */ +function recieveQueryData(client) { + client.socket.on('corpus_analysis_query_results', (response) => { + console.group('corpus_analysis_query_results: Client recieving query', + 'data via socket.on'); + console.info(response); + console.groupEnd(); + }); +} + +// export listeners from this module +export {recieveSession, recieveQueryStatus, recieveQueryData}; \ No newline at end of file diff --git a/web/app/templates/corpora/analyse_corpus.html.j2 b/web/app/templates/corpora/analyse_corpus.html.j2 index 420ab29f..d96bdbea 100644 --- a/web/app/templates/corpora/analyse_corpus.html.j2 +++ b/web/app/templates/corpora/analyse_corpus.html.j2 @@ -67,11 +67,20 @@ {% endblock %} From f94d21fa96145f88dbf996b9e76c0915d8be2317 Mon Sep 17 00:00:00 2001 From: Stephan Porada Date: Mon, 10 Aug 2020 14:48:45 +0200 Subject: [PATCH 03/15] Add rudimentary rework of corpus analysis --- .../modules/nopaque.CorpusAnalysisClient.js | 25 +----- .../js/modules/nopaque.listenerCallbacks.js | 84 +++++++++++++++++++ .../js/modules/nopaque.listenerFunctions.js | 11 ++- web/app/static/js/nopaque.Results.js | 6 +- web/app/static/js/nopaque.lists.js | 46 ++++++---- .../templates/corpora/analyse_corpus.html.j2 | 42 +++++++--- web/app/templates/nopaque.html.j2 | 41 +-------- 7 files changed, 163 insertions(+), 92 deletions(-) create mode 100644 web/app/static/js/modules/nopaque.listenerCallbacks.js diff --git a/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js index a2659b76..7eeb6216 100644 --- a/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js +++ b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js @@ -20,7 +20,7 @@ class CorpusAnalysisClient { } } - loadListeners() { + loadSocketEventListeners() { for (let [type, listener] of Object.entries(this.socketEventListeners)) { listener(this); } @@ -31,16 +31,6 @@ class CorpusAnalysisClient { this.displays[type] = corpusAnalysisDisplay; } - // /** - // * Initializes the interactive corpus analysis session via socket.io. - // * This function uses helper functions. - // */ - // initSession() { - // let request = this.requestSession(); - // let recvieveSession = this.recvieveSession(); - // console.info('corpus_analysis_init: Client waiting for response'); // this happens inbetween the two functions above - // } - /** * Requests a corpus analysis session via socket.io. * Opens a loading modal at the start of the request @@ -56,17 +46,6 @@ class CorpusAnalysisClient { this.socket.emit('corpus_analysis_init', this.corpusId); } - // /** - // * Sends a query to the server and handles the response to that query. - // * This function uses helper functions. - // */ - // query(queryStr) { - // let requestQueryData = this.requestQueryData(queryStr); - // let recieveQueryProcessStatus = this.recieveQueryProcessStatus(); - // let recieveQueryData = this.recieveQueryData(); - // console.info('corpus_analysis_query: Client waiting for query data'); // this happens inbetween the two functions above - // } - /** * Sends the query string to the server. * Should be a private method if ES2020 is finalized (Maybe?) @@ -165,5 +144,5 @@ class SocketEventListener { } } -// export both Classes from this module +// export Classes from this module export {CorpusAnalysisClient, CorpusAnalysisDisplay, SocketEventListener}; \ No newline at end of file diff --git a/web/app/static/js/modules/nopaque.listenerCallbacks.js b/web/app/static/js/modules/nopaque.listenerCallbacks.js new file mode 100644 index 00000000..b0369c6b --- /dev/null +++ b/web/app/static/js/modules/nopaque.listenerCallbacks.js @@ -0,0 +1,84 @@ +/** + * This callback is called when the SocketEventListener 'recieveQueryData' + * has been triggered. It takes the incoming chunk and renders the results in a + * the results.jsList. It can eiterh hande llive incoming data or imported + * results data. + */ +function queryRenderResults(payload, client, imported=false) { + console.info("Current recieved chunk:", payload.chunk); + if (payload.chunk.cpos_ranges == true) { + client.results.data["cpos_ranges"] = true; + } else { + client.results.data["cpos_ranges"] = false; + } + /** + * resultItem is a list where + */ + let resultItems = []; + // get infos for full match row + for (let [index, match] of payload.chunk.matches.entries()) { + resultItems.push({...match, ...{"index": index + client.results.data.matches.length}}); + } + if (!imported) { + // update progress bar + // queryResultsDeterminateElement.style.width = `${payload.progress}%`; + client.results.jsList.add(resultItems, (items) => { + for (let item of items) { + item.elm = client.results.jsList.createResultRowElement(item, payload.chunk); + } + }); + helperQueryRenderResults(payload, client); + // if (progress === 100) { + // resultsCreateElement.classList.remove("disabled"); + // queryResultsProgressElement.classList.add("hide"); + // queryResultsUserFeedbackElement.classList.add("hide"); + // resultsExportElement.classList.remove("disabled"); + // addToSubResultsElement.removeAttribute("disabled"); + // // inital expert mode check and sub results activation + // client.results.jsList.activateInspect(); + // if (addToSubResultsElement.checked) { + // client.results.jsList.activateAddToSubResults(); + // } + // if (expertModeSwitchElement.checked) { + // client.results.jsList.expertModeOn("query-display"); + // } + // } + } else if (imported) { + client.results.jsList.add(resultItems, (items) => { + for (let item of items) { + item.elm = client.results.jsList.createResultRowElement(item, payload.chunk, + true); + } + helperQueryRenderResults(payload, client); + progress = 100; + client.results.jsList.activateInspect(); + if (expertModeSwitchElement.checked) { + client.results.jsList.expertModeOn("query-display"); + } + }); + } +} + +function helperQueryRenderResults (payload, client) { + // updating table on finished item creation callback via createResultRowElement + client.results.jsList.update(); + client.results.jsList.changeContext(); // sets lr context on first result load + // incorporating new chunk results into full results + client.results.data.matches.push(...payload.chunk.matches); + client.results.data.addData(payload.chunk.cpos_lookup, "cpos_lookup"); + client.results.data.addData(payload.chunk.text_lookup, "text_lookup"); + // complete metaData + // client.results.metaData.add(); + // show user current and total match count + // receivedMatchCountElement.innerText = `${client.results.data.matches.length}`; + // textLookupCountElement.innerText = `${Object.keys(client.results.data.text_lookup).length}`; + let titles = new Array(); + for (let [key, value] of Object.entries(client.results.data.text_lookup)) { + titles.push(`${value.title} (${value.publishing_year})`); + }; + // textTitlesElement.innerText = `${titles.join(", ")}`; + // upate progress status + // progress = payload.progress; // global declaration +} + +export { queryRenderResults } \ No newline at end of file diff --git a/web/app/static/js/modules/nopaque.listenerFunctions.js b/web/app/static/js/modules/nopaque.listenerFunctions.js index ce94ca91..ad63d3ae 100644 --- a/web/app/static/js/modules/nopaque.listenerFunctions.js +++ b/web/app/static/js/modules/nopaque.listenerFunctions.js @@ -1,3 +1,5 @@ +import {queryRenderResults} from './nopaque.listenerCallbacks.js' + /** * Recieves a corpus analysis session via socket.io. * Closes the loading modal that has been opend with requestSession at the @@ -34,11 +36,13 @@ function recieveSession(client) { * Recieves the query process status before any actual results are being * transmitted. So it recieves error codes if a query failed or * was invalid etc. + * Also prepares the result.jsList for the incoming data. */ function recieveQueryStatus(client) { client.socket.on('corpus_analysis_query', (response) => { console.group('corpus_analysis_query: Client recieving query process', 'status via socket.on'); + client.results.clearAll(); console.info(response); console.groupEnd(); }); @@ -51,10 +55,13 @@ function recieveQueryData(client) { client.socket.on('corpus_analysis_query_results', (response) => { console.group('corpus_analysis_query_results: Client recieving query', 'data via socket.on'); - console.info(response); + console.info('Recieved chunk', response); + queryRenderResults(response.payload, client); + console.info('Added chunk data to results.data and rendered it with', + 'results.jsList') console.groupEnd(); }); } // export listeners from this module -export {recieveSession, recieveQueryStatus, recieveQueryData}; \ No newline at end of file +export { recieveSession, recieveQueryStatus, recieveQueryData }; \ No newline at end of file diff --git a/web/app/static/js/nopaque.Results.js b/web/app/static/js/nopaque.Results.js index 2b3c1caa..c05af727 100644 --- a/web/app/static/js/nopaque.Results.js +++ b/web/app/static/js/nopaque.Results.js @@ -22,7 +22,7 @@ class Results { class Data { // Sets empty object structure. Also usefull to delete old results. // matchCount default is 0 - init(matchCount = 0) { + init(matchCount = 0, type = "results") { this["matches"] = []; // list of all c with lc and rc this["cpos_lookup"] = {}; // object contains all this key value pair this["text_lookup"] = {}; // same as above for all text ids @@ -122,4 +122,6 @@ class MetaData { init(json = {}) { Object.assign(this, json); } -} \ No newline at end of file +} + +export {Results, Data, MetaData}; \ No newline at end of file diff --git a/web/app/static/js/nopaque.lists.js b/web/app/static/js/nopaque.lists.js index 2f577e32..033519ab 100644 --- a/web/app/static/js/nopaque.lists.js +++ b/web/app/static/js/nopaque.lists.js @@ -425,15 +425,31 @@ RessourceList.options = { class ResultsList extends List { - constructor(idOrElement, options={}) { - super(idOrElement, options); - this.eventTokens = {}; // all span tokens which are holdeing events if expert - // mode is on. Collected here to delete later on - this.currentExpertTokenElements = {}; // all token elements which have added - // classes like chip and hoverable for expert view. Collected - //here to delete later on - this.addToSubResultsStatus = {}; // holds True/false for check buttons used to add matches tu sub-results. If checked, it is True. If unchecked, it is false. Buttons for this have the class add. Those little round check buttons. - this.addToSubResultsIdsToShow = new Set(); // If check button is pressed its corresponding data_index is saved in this set. The set is shown to the user. + static options = { + page: 10, + pagination: [{ + name: "paginationTop", + paginationClass: "paginationTop", + innerWindow: 8, + outerWindow: 1 + }, { + paginationClass: "paginationBottom", + innerWindow: 8, + outerWindow: 1 + }], + valueNames: ["titles", "lc", "c", "rc", {data: ["index"]}], + item: `` + }; + constructor(idOrElement, options) { + super(idOrElement, options); + this.options = options; + this.eventTokens = {}; // all span tokens which are holdeing events if expert + // mode is on. Collected here to delete later on + this.currentExpertTokenElements = {}; // all token elements which have added + // classes like chip and hoverable for expert view. Collected + //here to delete later on + this.addToSubResultsStatus = {}; // holds True/false for check buttons used to add matches tu sub-results. If checked, it is True. If unchecked, it is false. Buttons for this have the class add. Those little round check buttons. + this.addToSubResultsIdsToShow = new Set(); // If check button is pressed its corresponding data_index is saved in this set. The set is shown to the user. } helperCreateCpos(cpos_ranges, cpos_values) { @@ -479,12 +495,11 @@ class ResultsList extends List { } // get display options from display options form element - static getDisplayOptions(displayOptionsFormElement) { + static getDisplayOptions(htmlId) { // gets display options parameters - let displayOptionsFormData - let displayOptionsData; - displayOptionsFormData = new FormData(displayOptionsFormElement); - displayOptionsData = + let displayOptionsFormElement = document.getElementById(htmlId); + let displayOptionsFormData = new FormData(displayOptionsFormElement); + let displayOptionsData = { "resultsPerPage": displayOptionsFormData.get("display-options-form-results_per_page"), "resultsContex": displayOptionsFormData.get("display-options-form-result_context"), @@ -1182,5 +1197,6 @@ class ResultsList extends List { `); } } +}; -} +export {RessourceList, ResultsList}; diff --git a/web/app/templates/corpora/analyse_corpus.html.j2 b/web/app/templates/corpora/analyse_corpus.html.j2 index d96bdbea..a5845fdb 100644 --- a/web/app/templates/corpora/analyse_corpus.html.j2 +++ b/web/app/templates/corpora/analyse_corpus.html.j2 @@ -75,25 +75,43 @@ CorpusAnalysisDisplay, SocketEventListener} from '../../static/js/modules/nopaque.CorpusAnalysisClient.js'; import {recieveSession, recieveQueryStatus, - recieveQueryData} from '../../static/js/modules/nopaque.listenerFunctions.js' + recieveQueryData} from '../../static/js/modules/nopaque.listenerFunctions.js'; + import {Results, Data, MetaData} from '../../static/js/nopaque.Results.js'; + import {ResultsList} from '../../static/js/nopaque.lists.js'; /** * Second Phase: * Asynchronus and event driven code */ document.addEventListener("DOMContentLoaded", () => { - // init some modals + // Initialize the CorpusAnalysisClient + const client = new CorpusAnalysisClient({{ corpus_id }}, nopaque.socket); + console.info("CorpusAnalysisClient created as client:", client); + // Initialize modals which are shown depending on events or client status const initLoadingElement = document.getElementById("init-display"); const initLoadingModal = M.Modal.init(initLoadingElement, {"dismissible": false}); - // set up display elements + // Set up display elements which hare show depending on the client status const initLoadingDisplay = new CorpusAnalysisDisplay(initLoadingModal); - // set up CorpusAnalysisClient - const client = new CorpusAnalysisClient({{ corpus_id }}, nopaque.socket); - console.info("CorpusAnalysisClient created as client:", client); - // register display elements to client + // Register those display elements to client client.setDisplay("init", initLoadingDisplay); - // register listeners and load them + /** + * Initializing the results object holding all the data of a query. + * Also holds the metadata of one query. + * resultsListOptions is set to determine how many results per apge are + * shown etc. + * Lastly it also contains the object ResultsList which is a list.js + * subclass which handles the visual reprensetnation of the query data. + */ + let displayOptionsData = ResultsList.getDisplayOptions('display-options-form'); + ResultsList.options.page = displayOptionsData["resultsPerPage"]; + let resultsList = new ResultsList("result-list", ResultsList.options); + let resultsMetaData = new MetaData(); + let results = new Results(new Data(), resultsList, resultsMetaData); + // make results part of the client + client.results = results; + console.info('Initialized the Results object.') + // register listeners listening to socket.io events and load them const listenForSession = new SocketEventListener('corpus_analysis_init', recieveSession); const listenForQueryStatus = new SocketEventListener('corpus_analysis_query', @@ -102,10 +120,10 @@ recieveQueryData); client.setSocketEventListeners([listenForSession, listenForQueryStatus, listenForQueryData]); - client.loadListeners(); + client.loadSocketEventListeners(); // Session initialization client.requestSession(); - // send a query and recieve its answer data + // Send a query and recieve its answer data let queryFormElement = document.getElementById("query-form"); queryFormElement.addEventListener("submit", (event) => { try { @@ -129,8 +147,8 @@ // Prevent page from reloading on submit event.preventDefault(); // Get query string and send query to server - // results.data.getQueryStr(queryFormElement); - client.requestQueryData('"this" []* "that" within 10 words;'); + results.data.getQueryStr(queryFormElement); + client.requestQueryData(results.data.query); }); }); diff --git a/web/app/templates/nopaque.html.j2 b/web/app/templates/nopaque.html.j2 index 8a9511ca..2e0f1c90 100644 --- a/web/app/templates/nopaque.html.j2 +++ b/web/app/templates/nopaque.html.j2 @@ -47,7 +47,9 @@ - + From b3e8976c1ca582d9dec3642c53eccf6a77082a75 Mon Sep 17 00:00:00 2001 From: Stephan Porada Date: Mon, 17 Aug 2020 16:15:34 +0200 Subject: [PATCH 04/15] Further rework --- .../modules/nopaque.CorpusAnalysisClient.js | 55 ++- .../js/modules/nopaque.listenerCallbacks.js | 130 +++++--- .../js/modules/nopaque.listenerFunctions.js | 64 +++- web/app/static/js/nopaque.Results.js | 16 +- web/app/static/js/web-components/InfoMenu.js | 83 +++++ .../templates/corpora/analyse_corpus.html.j2 | 164 ++++----- web/app/templates/corpora/corpus.html.j2 | 7 +- web/app/templates/interactions/export.html.j2 | 2 +- web/app/templates/interactions/infos.html.j2 | 20 +- web/app/templates/main/dashboard.html.j2 | 10 +- .../templates/query_results/inspect.html.j2 | 312 ++++++++---------- .../query_results/inspect.html.j2.bak | 233 +++++++++++++ .../services/corpus_analysis.html.j2 | 7 +- 13 files changed, 755 insertions(+), 348 deletions(-) create mode 100644 web/app/static/js/web-components/InfoMenu.js create mode 100644 web/app/templates/query_results/inspect.html.j2.bak diff --git a/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js index 7eeb6216..0fb6fcae 100644 --- a/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js +++ b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js @@ -5,12 +5,38 @@ * the server and recieves them. */ class CorpusAnalysisClient { - constructor(corpusId, socket, logging=false) { + constructor({corpusId = null, + socket = null, + logging = true, + dynamicMode = true} = {}) { this.corpusId = corpusId; this.displays = {}; + this.dynamicMode = dynamicMode; this.logging = logging; + this.requestQueryProgress = 0; + this.results = undefined; // holds the results object later on this.socket = socket; this.socketEventListeners = {}; + + /** + * Set client into imported mode if SOME THIGN IS INDICATING it + * + */ + + // Disable all console logging. Credits to https://gist.github.com/kmonsoor/0244fdb4ad79a4826371e58a1a5fa984 + if (!logging) { + (() => { + let console = (window.console = window.console || {}); + [ + 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', + 'error', 'exception', 'group', 'groupCollapsed', 'groupEnd', + 'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'table', + 'time', 'timeEnd', 'timeStamp', 'trace', 'warn' + ].forEach(method => { + console[method] = () => {}; + }); + })(); + } } // Registers one or more SocketEventListeners to the CorpusAnalysisClient. @@ -31,10 +57,32 @@ class CorpusAnalysisClient { this.displays[type] = corpusAnalysisDisplay; } + /** + * Function that takes one or more query selector + * strings in an array as an input. The function then creates a + * class field in the client object with the query selector + * string as the key. The selector will be converted to a valid JavaScript + * Field name i. e. #html-id-string -> this.htmlIdString + * The value will be the identifed element fetched with the querySelector + * method. MutlipleResults and atattchSomeCallback not yet implemented. + */ + getHTMLElements(arrayOfSelectors, multipleResults=false, atattchSomeCallback=false) { + for (let selector of arrayOfSelectors) { + let element = document.querySelector(selector); + let cleanKey = []; + selector.match(/\w+/g).forEach((word) => { + let tmp = word[0].toUpperCase() + word.slice(1); + cleanKey.push(tmp); + }); + cleanKey[0] = cleanKey[0].toLowerCase(); + cleanKey = cleanKey.join(''); + this[cleanKey] = element; + } + } + /** * Requests a corpus analysis session via socket.io. * Opens a loading modal at the start of the request - * Should be a private method if ES2020 is finalized (Maybe?) */ requestSession() { console.info('corpus_analysis_init: Client requesting session via', @@ -48,12 +96,11 @@ class CorpusAnalysisClient { /** * Sends the query string to the server. - * Should be a private method if ES2020 is finalized (Maybe?) */ requestQueryData(queryStr) { console.info('corpus_analysis_query: Client requesting query data via', 'socket.emit for the query', queryStr); - // TODO: Display stuff + // TODO: Display stuff ? this.socket.emit('corpus_analysis_query', queryStr); } } diff --git a/web/app/static/js/modules/nopaque.listenerCallbacks.js b/web/app/static/js/modules/nopaque.listenerCallbacks.js index b0369c6b..07b37c66 100644 --- a/web/app/static/js/modules/nopaque.listenerCallbacks.js +++ b/web/app/static/js/modules/nopaque.listenerCallbacks.js @@ -1,37 +1,68 @@ +// This callback is called on socket.on "query". +// Does some hiding, showing disabling etc. +/** + * This call back is called when the SocketEventListener 'recieveQueryStatus' + * has been triggered. It just gets the incoming status data of the issued + * and does some preperation work like hiding or showing elements and deleting + * the date from the last query. + */ +function querySetup(payload, client) { + // deletes old data from query issued before this new query + client.results.clearAll(); + // load necessary HTMLElements with selectory syntax and save them as fields + client.getHTMLElements(['#query-progress-bar', '#query-results-user-feedback', + '#recieved-match-count', '#total-match-count', + '#text-lookup-count', '#text-lookup-titles']); + client.requestQueryProgress = 0; + client.recievedMatchCount.textContent = 0; + client.totalMatchCount.textContent = `${payload.match_count}`; + client.queryResultsUserFeedback.classList.toggle('hide'); + client.queryProgressBar.classList.toggle('hide'); + client.queryProgressBar.lastElementChild.style.width = '0%' +} + /** * This callback is called when the SocketEventListener 'recieveQueryData' * has been triggered. It takes the incoming chunk and renders the results in a - * the results.jsList. It can eiterh hande llive incoming data or imported + * the results.jsList. It can either handle live incoming data or imported * results data. */ -function queryRenderResults(payload, client, imported=false) { - console.info("Current recieved chunk:", payload.chunk); - if (payload.chunk.cpos_ranges == true) { - client.results.data["cpos_ranges"] = true; - } else { - client.results.data["cpos_ranges"] = false; +function queryRenderResults(payload, client) { + client.getHTMLElements(['#recieved-match-count', '#match-count']) + const renderResults = (data) => { + /** + * resultItem saves the incoming chunk matches as objects to add those later + * to the client.results.jsList + */ + let resultItems = []; + // get infos for full match row + for (let [index, match] of data.matches.entries()) { + resultItems.push({...match, ...{'index': index + client.results.data.matches.length}}); + } + client.results.jsList.add(resultItems, (items) => { + for (let item of items) { + item.elm = client.results.jsList.createResultRowElement(item, data); + } + }); } - /** - * resultItem is a list where - */ - let resultItems = []; - // get infos for full match row - for (let [index, match] of payload.chunk.matches.entries()) { - resultItems.push({...match, ...{"index": index + client.results.data.matches.length}}); - } - if (!imported) { - // update progress bar - // queryResultsDeterminateElement.style.width = `${payload.progress}%`; - client.results.jsList.add(resultItems, (items) => { - for (let item of items) { - item.elm = client.results.jsList.createResultRowElement(item, payload.chunk); - } - }); + if (client.dynamicMode) { + if (payload.chunk.cpos_ranges == true) { + client.results.data['cpos_ranges'] = true; + } else { + client.results.data['cpos_ranges'] = false; + } + renderResults(payload.chunk); helperQueryRenderResults(payload, client); - // if (progress === 100) { - // resultsCreateElement.classList.remove("disabled"); - // queryResultsProgressElement.classList.add("hide"); - // queryResultsUserFeedbackElement.classList.add("hide"); + console.info('Result progress is:', client.requestQueryProgress); + if (client.requestQueryProgress === 100) { + /** + * activate, hide or show elements if all reults have been recieved + * also load some new elements taht have not ben loaded before + */ + client.getHTMLElements(['#query-results-create']); + client.queryProgressBar.classList.toggle('hide'); + client.queryResultsUserFeedback.classList.toggle('hide'); + client.queryResultsCreate.classList.toggle('disabled'); // resultsExportElement.classList.remove("disabled"); // addToSubResultsElement.removeAttribute("disabled"); // // inital expert mode check and sub results activation @@ -42,20 +73,18 @@ function queryRenderResults(payload, client, imported=false) { // if (expertModeSwitchElement.checked) { // client.results.jsList.expertModeOn("query-display"); // } - // } - } else if (imported) { - client.results.jsList.add(resultItems, (items) => { - for (let item of items) { - item.elm = client.results.jsList.createResultRowElement(item, payload.chunk, - true); - } - helperQueryRenderResults(payload, client); - progress = 100; - client.results.jsList.activateInspect(); - if (expertModeSwitchElement.checked) { - client.results.jsList.expertModeOn("query-display"); - } - }); + } + } else if (!client.dynamicMode) { + client.requestQueryProgress === 100; + client.queryResultsUserFeedback.classList.toggle('hide'); + renderResults(payload); + helperQueryRenderResults({'chunk': payload}, client); + client.queryProgressBar.classList.toggle('hide'); + client.queryResultsUserFeedback.classList.toggle('hide'); + client.results.jsList.activateInspect(); + if (expertModeSwitchElement.checked) { + client.results.jsList.expertModeOn("query-display"); + } } } @@ -65,20 +94,23 @@ function helperQueryRenderResults (payload, client) { client.results.jsList.changeContext(); // sets lr context on first result load // incorporating new chunk results into full results client.results.data.matches.push(...payload.chunk.matches); - client.results.data.addData(payload.chunk.cpos_lookup, "cpos_lookup"); - client.results.data.addData(payload.chunk.text_lookup, "text_lookup"); + client.results.data.addData(payload.chunk.cpos_lookup, 'cpos_lookup'); + client.results.data.addData(payload.chunk.text_lookup, 'text_lookup'); // complete metaData // client.results.metaData.add(); // show user current and total match count - // receivedMatchCountElement.innerText = `${client.results.data.matches.length}`; - // textLookupCountElement.innerText = `${Object.keys(client.results.data.text_lookup).length}`; + client.recievedMatchCount.textContent = `${client.results.data.matches.length}`; + client.textLookupCount.textContent = `${Object.keys(client.results.data.text_lookup).length}`; let titles = new Array(); for (let [key, value] of Object.entries(client.results.data.text_lookup)) { titles.push(`${value.title} (${value.publishing_year})`); }; - // textTitlesElement.innerText = `${titles.join(", ")}`; - // upate progress status - // progress = payload.progress; // global declaration + client.textLookupTitles.textContent = `${titles.join(", ")}`; + // update progress bar and requestQueryProgress + client.queryProgressBar.lastElementChild.style.width = `${payload.progress}%`; + client.requestQueryProgress = payload.progress; } -export { queryRenderResults } \ No newline at end of file +// TODO: Add data to data objekt using its own socket on event? + +export { querySetup, queryRenderResults } \ No newline at end of file diff --git a/web/app/static/js/modules/nopaque.listenerFunctions.js b/web/app/static/js/modules/nopaque.listenerFunctions.js index ad63d3ae..8c2d6044 100644 --- a/web/app/static/js/modules/nopaque.listenerFunctions.js +++ b/web/app/static/js/modules/nopaque.listenerFunctions.js @@ -1,4 +1,4 @@ -import {queryRenderResults} from './nopaque.listenerCallbacks.js' +import { querySetup, queryRenderResults } from './nopaque.listenerCallbacks.js' /** * Recieves a corpus analysis session via socket.io. @@ -7,13 +7,18 @@ import {queryRenderResults} from './nopaque.listenerCallbacks.js' */ function recieveSession(client) { client.socket.on('corpus_analysis_init', (response) => { + /** + * Check if request for session was ok. + * If OK execute callbacks and hide/show displays. + */ console.group('recieve session') - console.info('corpus_analysis_init: Client recieving session/or error', - 'codes via socket.on'); if (response.code === 200) { + console.info('corpus_analysis_init: Client recieving session/or error', + 'codes via socket.on'); + console.info(`corpus_analysis_init: ${response.code} - ${response.msg}`); console.info('corpus_analysis_init: Initialization succeeded'); console.info(response); - console.groupEnd(); + // Handling hide/show of displays if (client.displays.init != undefined) { client.displays.init.element.M_Modal.close(); client.displays.init.setVisibilityByStatus('success'); @@ -29,6 +34,7 @@ function recieveSession(client) { } console.error(`corpus_analysis_init: ${errorText}`); } + console.groupEnd(); }); } @@ -40,10 +46,36 @@ function recieveSession(client) { */ function recieveQueryStatus(client) { client.socket.on('corpus_analysis_query', (response) => { + /** + * Check if issued query was ok. + * If OK execute callbacks and hide/show displays. + */ console.group('corpus_analysis_query: Client recieving query process', - 'status via socket.on'); - client.results.clearAll(); - console.info(response); + 'status via socket.on'); + if (response.code === 200) { + console.info(`corpus_analysis_query: ${response.code} - ${response.msg}`); + console.info(response); + // Handling hide/show of displays + if (client.displays.query != undefined) { + client.displays.query.setVisibilityByStatus("success"); + } + // executing the callbacks + querySetup(response.payload, client); + } else { + let errorText = `Error ${response.payload.code} - ${response.payload.msg}`; + if (response.payload.code == 1281) { + errorText += ' - Invalid Query'; + } + nopaque.flash(errorText, "error"); + if (client.displays.query.errorContainer != undefined) { + client.displays.query.errorContainer.innerHTML = `

`+ + `error ${errorText}

`; + } + if (client.displays.query != undefined) { + client.displays.query.setVisibilityByStatus("error"); + } + console.error(`corpus_analysis_query: ${errorText}`); + } console.groupEnd(); }); } @@ -52,15 +84,21 @@ function recieveQueryStatus(client) { * Recieves the query data from the request and handles it. */ function recieveQueryData(client) { - client.socket.on('corpus_analysis_query_results', (response) => { - console.group('corpus_analysis_query_results: Client recieving query', - 'data via socket.on'); + console.group('corpus_analysis_query_results: Client recieving or loading', + 'query data.'); + if (client.dynamicMode) { + console.info('Client recieving query data via socket.on'); + client.socket.on('corpus_analysis_query_results', (response) => { console.info('Recieved chunk', response); queryRenderResults(response.payload, client); console.info('Added chunk data to results.data and rendered it with', - 'results.jsList') - console.groupEnd(); - }); + 'results.jsList'); + }); + } else { + console.info('Client loading imported query data from database.'); + queryRenderResults(client.results.data, client); + } + console.groupEnd(); } // export listeners from this module diff --git a/web/app/static/js/nopaque.Results.js b/web/app/static/js/nopaque.Results.js index c05af727..2f7778f1 100644 --- a/web/app/static/js/nopaque.Results.js +++ b/web/app/static/js/nopaque.Results.js @@ -23,12 +23,12 @@ class Data { // Sets empty object structure. Also usefull to delete old results. // matchCount default is 0 init(matchCount = 0, type = "results") { - this["matches"] = []; // list of all c with lc and rc - this["cpos_lookup"] = {}; // object contains all this key value pair - this["text_lookup"] = {}; // same as above for all text ids - this["match_count"] = matchCount; - this["corpus_type"] = "results"; - this["query"] = ""; + this.matches = []; // list of all c with lc and rc + this.cpos_lookup = {}; // object contains all this key value pair + this.text_lookup = {}; // same as above for all text ids + this.match_count = matchCount; + this.corpus_type = 'results'; + this.query = ''; } addData(jsonData, key=null) { @@ -45,7 +45,7 @@ class Data { let queryFormData; let queryStr; queryFormData = new FormData(queryFormElement); - queryStr = queryFormData.get("query-form-query"); + queryStr = queryFormData.get('query-form-query'); this["query"] = queryStr; } @@ -72,7 +72,7 @@ class Data { // Function to download data as Blob created from string // should be private but that is not yet a feature of javascript 08.04.2020 download(downloadElement, dataStr, filename, type, filenameSlug) { - console.log("Start Download!"); + console.log('Start Download!'); let file; filename += filenameSlug; file = new Blob([dataStr], {type: type}); diff --git a/web/app/static/js/web-components/InfoMenu.js b/web/app/static/js/web-components/InfoMenu.js new file mode 100644 index 00000000..af6f8599 --- /dev/null +++ b/web/app/static/js/web-components/InfoMenu.js @@ -0,0 +1,83 @@ +/** + * HTML for showing infos about the current query or result. Also gives + * the user the abiltiy to access the meta data for the current query or + * result. + */ +const template = document.createElement('template'); +template.innerHTML = ` + + + + + +
+
Infos
+
+
+
+ +
+
+

+ + 0 + of + (to be determined) + matches loaded. +
+ Matches occured in + (to be determined) + corpus files: +
+ (to be determined) +

+

+ help + The Server is still sending your results. + Functions like "Export Results" and "Match Inspect" will be + available after all matches have been loaded. +

+
+
+
+
+
+
+`; + +class InfoMenu extends HTMLElement { + constructor() { + super(); + + this.appendChild(template.content.cloneNode(true)); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + + } + + // methods that will be used in connectedCallback on eventListeners + showMetadata() { + console.log('Show metadata somehow'); + } + + connectedCallback() { + const showMetadataBtn = this.querySelector('#show-metadata'); + showMetadataBtn.addEventListener('click', () => this.showMetadata()); + } + + disconnectedCallback() { + const showMetadataBtn = this.querySelector('#show-metadata'); + showMetadataBtn.removeEventListener(); + } +} + +window.customElements.define('info-menu', InfoMenu); + +export { InfoMenu }; \ No newline at end of file diff --git a/web/app/templates/corpora/analyse_corpus.html.j2 b/web/app/templates/corpora/analyse_corpus.html.j2 index a5845fdb..b59c5171 100644 --- a/web/app/templates/corpora/analyse_corpus.html.j2 +++ b/web/app/templates/corpora/analyse_corpus.html.j2 @@ -67,89 +67,93 @@ {% endblock %} diff --git a/web/app/templates/corpora/corpus.html.j2 b/web/app/templates/corpora/corpus.html.j2 index 232a420a..53b514da 100644 --- a/web/app/templates/corpora/corpus.html.j2 +++ b/web/app/templates/corpora/corpus.html.j2 @@ -107,8 +107,8 @@ - - {% endblock %} diff --git a/web/app/templates/query_results/inspect.html.j2 b/web/app/templates/query_results/inspect.html.j2 index 85f954da..601e065e 100644 --- a/web/app/templates/query_results/inspect.html.j2 +++ b/web/app/templates/query_results/inspect.html.j2 @@ -54,180 +54,150 @@ {% include 'modals/context_modal.html.j2' %} - - - - {% endblock %} diff --git a/web/app/templates/query_results/inspect.html.j2.bak b/web/app/templates/query_results/inspect.html.j2.bak new file mode 100644 index 00000000..85f954da --- /dev/null +++ b/web/app/templates/query_results/inspect.html.j2.bak @@ -0,0 +1,233 @@ +{% extends "nopaque.html.j2" %} + +{% set headline = ' ' %} + +{% set full_width = True %} +{% set imported = True %} + +{% block page_content %} + +
+
+
+ +
+
+
+
+ search + + +
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ {% include 'interactions/infos.html.j2' %} + {% include 'interactions/display.html.j2' %} + {% include 'interactions/analysis.html.j2' %} + {% include 'interactions/cite.html.j2' %} +
+ {% include 'tables/query_results.html.j2' %} +
+
+
+ + +{% include 'interactions/scroll_to_top.html.j2' %} + + +{% include 'modals/show_metadata.html.j2' %} +{% include 'modals/show_text_details.html.j2' %} +{% include 'modals/context_modal.html.j2' %} + + + + + + +{% endblock %} diff --git a/web/app/templates/services/corpus_analysis.html.j2 b/web/app/templates/services/corpus_analysis.html.j2 index 53251cff..26e9905b 100644 --- a/web/app/templates/services/corpus_analysis.html.j2 +++ b/web/app/templates/services/corpus_analysis.html.j2 @@ -83,8 +83,9 @@ - {% endblock %} From 21ce07f3ef9a2045e5ae5aeaa5d1f2f6c67907e5 Mon Sep 17 00:00:00 2001 From: Stephan Porada Date: Thu, 20 Aug 2020 16:03:37 +0200 Subject: [PATCH 05/15] Push before rework part 2 --- .../modules/nopaque.CorpusAnalysisClient.js | 84 +++++++++--- .../nopaque.InteractionElement.js | 3 + .../js/modules/nopaque.listenerCallbacks.js | 44 ++++--- .../js/modules/nopaque.listenerFunctions.js | 28 ++-- .../static/js/modules/nopaque.scrollToTop.js | 21 +++ web/app/static/js/nopaque.lists.js | 53 ++++---- .../templates/corpora/analyse_corpus.html.j2 | 124 ++++++++++++++---- web/app/templates/interactions/create.html.j2 | 3 +- web/app/templates/interactions/export.html.j2 | 2 - .../templates/query_results/inspect.html.j2 | 75 +++++++---- 10 files changed, 309 insertions(+), 128 deletions(-) rename web/app/static/js/{ => modules}/nopaque.InteractionElement.js (96%) create mode 100644 web/app/static/js/modules/nopaque.scrollToTop.js diff --git a/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js index 0fb6fcae..b64c24d8 100644 --- a/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js +++ b/web/app/static/js/modules/nopaque.CorpusAnalysisClient.js @@ -1,8 +1,10 @@ /** - * This class is used to create an CorpusAnalysisClient object. + * This class is used to create a CorpusAnalysisClient object. * The client handels the client server communication. * It requests data (e.g. the analysis session or query results) from the - * the server and recieves them. + * the server and recieves them, if it dynamicMode is true. + * If dynamicMode is false, the client can also handle data that is already + * loaded and not coming in in chunks. */ class CorpusAnalysisClient { constructor({corpusId = null, @@ -19,11 +21,9 @@ class CorpusAnalysisClient { this.socketEventListeners = {}; /** - * Set client into imported mode if SOME THIGN IS INDICATING it - * + * Disable all console logging. + * Credits to https://gist.github.com/kmonsoor/0244fdb4ad79a4826371e58a1a5fa984 */ - - // Disable all console logging. Credits to https://gist.github.com/kmonsoor/0244fdb4ad79a4826371e58a1a5fa984 if (!logging) { (() => { let console = (window.console = window.console || {}); @@ -32,7 +32,7 @@ class CorpusAnalysisClient { 'error', 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeStamp', 'trace', 'warn' - ].forEach(method => { + ].forEach((method) => { console[method] = () => {}; }); })(); @@ -41,14 +41,18 @@ class CorpusAnalysisClient { // Registers one or more SocketEventListeners to the CorpusAnalysisClient. setSocketEventListeners(socketEventListeners) { - for (let listener of socketEventListeners) { - this.socketEventListeners[listener.type] = listener.listenerFunction; + for (let socketEventListener of socketEventListeners) { + this.socketEventListeners[socketEventListener.type] = socketEventListener; } } + /** + * Loads the SocketEventListeners so they will be triggered on their assigned + * type strings because they double as the socket event event names. + */ loadSocketEventListeners() { for (let [type, listener] of Object.entries(this.socketEventListeners)) { - listener(this); + listener.listenerFunction(type, this); } } @@ -64,12 +68,14 @@ class CorpusAnalysisClient { * string as the key. The selector will be converted to a valid JavaScript * Field name i. e. #html-id-string -> this.htmlIdString * The value will be the identifed element fetched with the querySelector - * method. MutlipleResults and atattchSomeCallback not yet implemented. + * method. */ - getHTMLElements(arrayOfSelectors, multipleResults=false, atattchSomeCallback=false) { + // TODO: multipleResults=false, atattchSomeCallback=false ? + getHTMLElements(arrayOfSelectors) { for (let selector of arrayOfSelectors) { let element = document.querySelector(selector); let cleanKey = []; + selector = selector.replace('_', '-'); selector.match(/\w+/g).forEach((word) => { let tmp = word[0].toUpperCase() + word.slice(1); cleanKey.push(tmp); @@ -82,7 +88,8 @@ class CorpusAnalysisClient { /** * Requests a corpus analysis session via socket.io. - * Opens a loading modal at the start of the request + * Opens a loading modal at the start of the request. + * Will be closed if session has been successfully recieved. */ requestSession() { console.info('corpus_analysis_init: Client requesting session via', @@ -95,7 +102,7 @@ class CorpusAnalysisClient { } /** - * Sends the query string to the server. + * Request query data for the query string that has been sent to the server. */ requestQueryData(queryStr) { console.info('corpus_analysis_query: Client requesting query data via', @@ -180,16 +187,57 @@ class CorpusAnalysisDisplay { } /** - * This class is used to create an CorpusAnalysisDisplay object. - * Input is one HTMLElement that can then be hidden or shown depending on - * its CSS classes. + * This class is used to create an SocketEventListener. + * Input are an identifying type string, the listener function and callbacks + * which will be executed as part of the listener function. The identifying + * type string is also used as the socket event event identifier. */ class SocketEventListener { constructor(type, listenerFunction) { + this.listenerCallbacks = {}; this.listenerFunction = listenerFunction; this.type = type; } + + // Registers callbacks to this SocketEventListener + setCallbacks(listenerCallbacks) { + for (let listenerCallback of listenerCallbacks) { + this.listenerCallbacks[listenerCallback.type] = listenerCallback; + } + } + + /** Shorthand to execute all registered callbacks with same args in insertion + * order. + * NOTE: + * Since ECMAScript 2015, objects do preserve creation order for + * string and Symbol keys. In JavaScript engines that comply with the + * ECMAScript 2015 spec, iterating over an object with only string keys will + * yield the keys in order of insertion. + * So all modern Browsers. + */ + executeCallbacks(args) { + for (let [type, listenerCallback] of Object.entries(this.listenerCallbacks)) { + listenerCallback.callbackFunction(...args); + } + } +} + +/** + * This class is used to create an ListenerCallback which will be registered + * to an SocketEventListener so the Listener cann invoke the ListenerCallback + * callback functions. + */ +class ListenerCallback { + constructor(type, callbackFunction) { + this.callbackFunction = callbackFunction; + this.type = type; + } } // export Classes from this module -export {CorpusAnalysisClient, CorpusAnalysisDisplay, SocketEventListener}; \ No newline at end of file +export { + CorpusAnalysisClient, + CorpusAnalysisDisplay, + SocketEventListener, + ListenerCallback, +}; \ No newline at end of file diff --git a/web/app/static/js/nopaque.InteractionElement.js b/web/app/static/js/modules/nopaque.InteractionElement.js similarity index 96% rename from web/app/static/js/nopaque.InteractionElement.js rename to web/app/static/js/modules/nopaque.InteractionElement.js index a1d9064b..7856cf48 100644 --- a/web/app/static/js/nopaque.InteractionElement.js +++ b/web/app/static/js/modules/nopaque.InteractionElement.js @@ -62,3 +62,6 @@ class InteractionElements { }; } } + +// export Classes +export { InteractionElement, InteractionElements }; \ No newline at end of file diff --git a/web/app/static/js/modules/nopaque.listenerCallbacks.js b/web/app/static/js/modules/nopaque.listenerCallbacks.js index 07b37c66..37f7b61d 100644 --- a/web/app/static/js/modules/nopaque.listenerCallbacks.js +++ b/web/app/static/js/modules/nopaque.listenerCallbacks.js @@ -1,10 +1,8 @@ -// This callback is called on socket.on "query". -// Does some hiding, showing disabling etc. /** - * This call back is called when the SocketEventListener 'recieveQueryStatus' - * has been triggered. It just gets the incoming status data of the issued + * This callback should be registered to the SocketEventListener 'recieveQueryStatus'. + * It just gets the incoming status data of the issued query * and does some preperation work like hiding or showing elements and deleting - * the date from the last query. + * the data from the last query. */ function querySetup(payload, client) { // deletes old data from query issued before this new query @@ -12,23 +10,29 @@ function querySetup(payload, client) { // load necessary HTMLElements with selectory syntax and save them as fields client.getHTMLElements(['#query-progress-bar', '#query-results-user-feedback', '#recieved-match-count', '#total-match-count', - '#text-lookup-count', '#text-lookup-titles']); + '#text-lookup-count', '#text-lookup-titles', + '#query-results-create', '#add-to-sub-results']); client.requestQueryProgress = 0; client.recievedMatchCount.textContent = 0; client.totalMatchCount.textContent = `${payload.match_count}`; client.queryResultsUserFeedback.classList.toggle('hide'); client.queryProgressBar.classList.toggle('hide'); - client.queryProgressBar.lastElementChild.style.width = '0%' + client.queryProgressBar.lastElementChild.style.width = '0%'; + if (client.dynamicMode) { + client.addToSubResults.toggleAttribute('disabled'); + client.queryResultsCreate.classList.toggle('disabled'); + } } /** - * This callback is called when the SocketEventListener 'recieveQueryData' - * has been triggered. It takes the incoming chunk and renders the results in a - * the results.jsList. It can either handle live incoming data or imported - * results data. + * This callback should be registered to the SocketEventListener 'recieveQueryData' + * It takes the incoming chunk and renders the results using the + * results.jsList object. It can either handle live incoming data chunks or + * already loaded/imported results data. */ function queryRenderResults(payload, client) { - client.getHTMLElements(['#recieved-match-count', '#match-count']) + client.getHTMLElements(['#recieved-match-count', '#match-count', + '#display-options-form-expert_mode']); const renderResults = (data) => { /** * resultItem saves the incoming chunk matches as objects to add those later @@ -59,11 +63,10 @@ function queryRenderResults(payload, client) { * activate, hide or show elements if all reults have been recieved * also load some new elements taht have not ben loaded before */ - client.getHTMLElements(['#query-results-create']); client.queryProgressBar.classList.toggle('hide'); client.queryResultsUserFeedback.classList.toggle('hide'); client.queryResultsCreate.classList.toggle('disabled'); - // resultsExportElement.classList.remove("disabled"); + client.addToSubResults.toggleAttribute('disabled'); // addToSubResultsElement.removeAttribute("disabled"); // // inital expert mode check and sub results activation // client.results.jsList.activateInspect(); @@ -75,19 +78,21 @@ function queryRenderResults(payload, client) { // } } } else if (!client.dynamicMode) { - client.requestQueryProgress === 100; - client.queryResultsUserFeedback.classList.toggle('hide'); renderResults(payload); helperQueryRenderResults({'chunk': payload}, client); client.queryProgressBar.classList.toggle('hide'); client.queryResultsUserFeedback.classList.toggle('hide'); client.results.jsList.activateInspect(); - if (expertModeSwitchElement.checked) { + if (client.displayOptionsFormExpertMode.checked) { client.results.jsList.expertModeOn("query-display"); } } } +/** + * Helper function that saves result data into the client.results.data object. + * Also does some showing and hiding of Elements and user feedback text. + */ function helperQueryRenderResults (payload, client) { // updating table on finished item creation callback via createResultRowElement client.results.jsList.update(); @@ -111,6 +116,5 @@ function helperQueryRenderResults (payload, client) { client.requestQueryProgress = payload.progress; } -// TODO: Add data to data objekt using its own socket on event? - -export { querySetup, queryRenderResults } \ No newline at end of file +// export callbacks +export { querySetup, queryRenderResults }; \ No newline at end of file diff --git a/web/app/static/js/modules/nopaque.listenerFunctions.js b/web/app/static/js/modules/nopaque.listenerFunctions.js index 8c2d6044..bbdcd432 100644 --- a/web/app/static/js/modules/nopaque.listenerFunctions.js +++ b/web/app/static/js/modules/nopaque.listenerFunctions.js @@ -5,10 +5,10 @@ import { querySetup, queryRenderResults } from './nopaque.listenerCallbacks.js' * Closes the loading modal that has been opend with requestSession at the * start of the request. */ -function recieveSession(client) { - client.socket.on('corpus_analysis_init', (response) => { +function recieveSession(type, client) { + client.socket.on(type, (response) => { /** - * Check if request for session was ok. + * Check if request for session was OK. * If OK execute callbacks and hide/show displays. */ console.group('recieve session') @@ -44,11 +44,11 @@ function recieveSession(client) { * was invalid etc. * Also prepares the result.jsList for the incoming data. */ -function recieveQueryStatus(client) { - client.socket.on('corpus_analysis_query', (response) => { +function recieveQueryStatus(type, client) { + client.socket.on(type, (response) => { /** - * Check if issued query was ok. - * If OK execute callbacks and hide/show displays. + * Check if issued query was OK. + * If OK execute registered callbacks and hide/show displays. */ console.group('corpus_analysis_query: Client recieving query process', 'status via socket.on'); @@ -59,8 +59,8 @@ function recieveQueryStatus(client) { if (client.displays.query != undefined) { client.displays.query.setVisibilityByStatus("success"); } - // executing the callbacks - querySetup(response.payload, client); + // executing the registered callbacks + client.socketEventListeners[type].executeCallbacks([response.payload, client]); } else { let errorText = `Error ${response.payload.code} - ${response.payload.msg}`; if (response.payload.code == 1281) { @@ -83,20 +83,22 @@ function recieveQueryStatus(client) { /** * Recieves the query data from the request and handles it. */ -function recieveQueryData(client) { +function recieveQueryData(type, client) { console.group('corpus_analysis_query_results: Client recieving or loading', 'query data.'); if (client.dynamicMode) { console.info('Client recieving query data via socket.on'); - client.socket.on('corpus_analysis_query_results', (response) => { + client.socket.on(type, (response) => { console.info('Recieved chunk', response); - queryRenderResults(response.payload, client); + // executing the registered callbacks + client.socketEventListeners[type].executeCallbacks([response.payload, client]); console.info('Added chunk data to results.data and rendered it with', 'results.jsList'); }); } else { console.info('Client loading imported query data from database.'); - queryRenderResults(client.results.data, client); + // executing the registered callbacks + client.socketEventListeners[type].executeCallbacks([client.results.data, client]); } console.groupEnd(); } diff --git a/web/app/static/js/modules/nopaque.scrollToTop.js b/web/app/static/js/modules/nopaque.scrollToTop.js new file mode 100644 index 00000000..82b6e774 --- /dev/null +++ b/web/app/static/js/modules/nopaque.scrollToTop.js @@ -0,0 +1,21 @@ +/** + * Function to show a scrol lto top button if the user has scrolled down + * 250 pixels from teh headline element. + */ +function scrollToTop() { + let headline = document.querySelector(".headline"); + let scrollToTop = document.querySelector("#menu-scroll-to-top-div"); + window.addEventListener("scroll", (event) => { + if (pageYOffset > 250) { + scrollToTop.classList.toggle("hide", false); + } else { + scrollToTop.classList.toggle("hide", true); + } + }); + scrollToTop.onclick = () => { + headline.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"}); + }; +} + +// export function +export { scrollToTop }; \ No newline at end of file diff --git a/web/app/static/js/nopaque.lists.js b/web/app/static/js/nopaque.lists.js index 033519ab..f7b640ed 100644 --- a/web/app/static/js/nopaque.lists.js +++ b/web/app/static/js/nopaque.lists.js @@ -425,6 +425,10 @@ RessourceList.options = { class ResultsList extends List { + /** + * If no options are given when a new instance of this class is created + * the options below are used. + */ static options = { page: 10, pagination: [{ @@ -443,12 +447,19 @@ class ResultsList extends List { constructor(idOrElement, options) { super(idOrElement, options); this.options = options; - this.eventTokens = {}; // all span tokens which are holdeing events if expert - // mode is on. Collected here to delete later on - this.currentExpertTokenElements = {}; // all token elements which have added - // classes like chip and hoverable for expert view. Collected - //here to delete later on - this.addToSubResultsStatus = {}; // holds True/false for check buttons used to add matches tu sub-results. If checked, it is True. If unchecked, it is false. Buttons for this have the class add. Those little round check buttons. + /** + * All span tokens which are holding events if expert + * mode is on. Collected here to delete later on. + */ + this.eventTokens = {}; + /** + * all token elements which have added + * classes like chip and hoverable for expert view. Collected + * here to delete later on + */ + this.currentExpertTokenElements = {}; + // holds True/false for check buttons used to add matches tu sub-results. If checked, it is True. If unchecked, it is false. Buttons for this have the class add. Those little round check buttons. + this.addToSubResultsStatus = {}; this.addToSubResultsIdsToShow = new Set(); // If check button is pressed its corresponding data_index is saved in this set. The set is shown to the user. } @@ -620,7 +631,7 @@ class ResultsList extends List { // ###### Functions to inspect one match, to show more details ###### // activate inspect buttons if progress is 100 activateInspect() { - if (progress === 100) { + if (this.requestQueryProgress === 100) { let inspectBtnElements; inspectBtnElements = document.getElementsByClassName("inspect"); for (let inspectBtn of inspectBtnElements) { @@ -958,45 +969,43 @@ class ResultsList extends List { // ###### Expert view event functions ###### // function to create a tooltip for the current hovered token - tooltipEventCreate(event) { + tooltipEventCreate(event, client) { // console.log("Create Tooltip on mouseover."); - let token; - token = results.data.cpos_lookup[event.target.dataset.cpos]; + let token = client.results.data.cpos_lookup[event.target.dataset.cpos]; if (!token) { token = this.contextData.cpos_lookup[event.target.dataset.cpos]; } - this.addToolTipToTokenElement(event.target, token); + this.addToolTipToTokenElement(event.target, token, client); } // Function to destroy the current Tooltip for the current hovered tooltip // on mouse leave - tooltipEventDestroy(event) { + tooltipEventDestroy() { // console.log("Tooltip destroy on leave."); this.currentTooltipElement.destroy(); } - expertModeOn(htmlId) { - // turn the expert mode on for all tokens in the DOM element identified by its htmlID + // turn the expert mode on for all tokens in the DOM element identified by its htmlID + expertModeOn(htmlId, client) { if (!Array.isArray(this.currentExpertTokenElements[htmlId])) { this.currentExpertTokenElements[htmlId] = []; } let container = document.getElementById(htmlId); let tokens = container.querySelectorAll("span.token"); this.currentExpertTokenElements[htmlId].push(...tokens); - this.tooltipEventCreateBind = this.tooltipEventCreate.bind(this); - this.tooltipEventDestroyBind = this.tooltipEventDestroy.bind(this); this.eventTokens[htmlId] = []; for (let tokenElement of this.currentExpertTokenElements[htmlId]) { tokenElement.classList.add("chip", "hoverable", "expert-view"); - tokenElement.onmouseover = this.tooltipEventCreateBind; - tokenElement.onmouseout = this.tooltipEventDestroyBind; + const eventCreate = (event, arg) => this.tooltipEventCreate(event, arg); + tokenElement.onmouseover = (event) => eventCreate(event, client); + tokenElement.onmouseout = () => this.tooltipEventDestroy(); this.eventTokens[htmlId].push(tokenElement); } } // fuction that creates Tooltip for one token and extracts the corresponding // infos from the result JSON - addToolTipToTokenElement(tokenElement, token) { + addToolTipToTokenElement(tokenElement, token, client) { this.currentTooltipElement; this.currentTooltipElement = M.Tooltip.init(tokenElement, {"html": ` @@ -1013,11 +1022,11 @@ class ResultsList extends List { NER: ${token.ner}
- Title: ${results.data.text_lookup[token.text].title} + Title: ${client.results.data.text_lookup[token.text].title}
- Author: ${results.data.text_lookup[token.text].author} + Author: ${client.results.data.text_lookup[token.text].author}
- Publishing year: ${results.data.text_lookup[token.text].publishing_year} + Publishing year: ${client.results.data.text_lookup[token.text].publishing_year}
`} diff --git a/web/app/templates/corpora/analyse_corpus.html.j2 b/web/app/templates/corpora/analyse_corpus.html.j2 index b59c5171..674603cf 100644 --- a/web/app/templates/corpora/analyse_corpus.html.j2 +++ b/web/app/templates/corpora/analyse_corpus.html.j2 @@ -69,22 +69,45 @@ diff --git a/web/app/templates/interactions/create.html.j2 b/web/app/templates/interactions/create.html.j2 index 353db592..5cf72d4f 100644 --- a/web/app/templates/interactions/create.html.j2 +++ b/web/app/templates/interactions/create.html.j2 @@ -10,8 +10,7 @@ results.--> Sub-Results creation: