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: