diff --git a/web/app/corpora/events.py b/web/app/corpora/events.py index 58b2d9a0..e941ba5b 100644 --- a/web/app/corpora/events.py +++ b/web/app/corpora/events.py @@ -30,46 +30,63 @@ def init_corpus_analysis(corpus_id): corpus_id, current_user.id, request.sid) -@socketio.on('corpus_analysis_get_meta_data') +@socketio.on('corpus_analysis_meta_data') @socketio_login_required def corpus_analysis_get_meta_data(corpus_id): # get meta data from db db_corpus = Corpus.query.get(corpus_id) - # TODO: Check if current user is actually the creator of the corpus? metadata = {} metadata['corpus_name'] = db_corpus.title metadata['corpus_description'] = db_corpus.description metadata['corpus_creation_date'] = db_corpus.creation_date.isoformat() metadata['corpus_last_edited_date'] = db_corpus.last_edited_date.isoformat() - # get meta data from corpus in cqp server client = corpus_analysis_clients.get(request.sid) - client_corpus = client.corpora.get('CORPUS') - metadata['corpus_properties'] = client_corpus.attrs['properties'] - metadata['corpus_size_tokens'] = client_corpus.attrs['size'] + if client is None: + response = {'code': 424, 'desc': 'No client found for this session', + 'msg': 'Failed Dependency'} + socketio.emit('corpus_analysis_query', response, room=request.sid) + return + # check if client is busy or not + if client.status == 'running': + client.status = 'abort' + while client.status != 'ready': + socketio.sleep(0.1) + # get meta data from corpus in cqp server + client.status = 'running' + try: + client_corpus = client.corpora.get('CORPUS') + metadata['corpus_properties'] = client_corpus.attrs['properties'] + metadata['corpus_size_tokens'] = client_corpus.attrs['size'] - text_attr = client_corpus.structural_attributes.get('text') - struct_attrs = client_corpus.structural_attributes.list(filters={'part_of': text_attr}) - text_ids = range(0, (text_attr.attrs['size'])) - texts_metadata = {} - for text_id in text_ids: - texts_metadata[text_id] = {} - for struct_attr in struct_attrs: - texts_metadata[text_id][struct_attr.attrs['name'][(len(text_attr.attrs['name']) + 1):]] = struct_attr.values_by_ids(list(range(struct_attr.attrs['size'])))[text_id] - metadata['corpus_all_texts'] = texts_metadata - metadata['corpus_analysis_date'] = datetime.utcnow().isoformat() - metadata['corpus_cqi_py_protocol_version'] = client.api.version - metadata['corpus_cqi_py_package_version'] = cqi.__version__ - metadata['corpus_cqpserver_version'] = 'CQPserver v3.4.22' # TODO: make this dynamically + text_attr = client_corpus.structural_attributes.get('text') + struct_attrs = client_corpus.structural_attributes.list(filters={'part_of': text_attr}) + text_ids = range(0, (text_attr.attrs['size'])) + texts_metadata = {} + for text_id in text_ids: + texts_metadata[text_id] = {} + for struct_attr in struct_attrs: + texts_metadata[text_id][struct_attr.attrs['name'][(len(text_attr.attrs['name']) + 1):]] = struct_attr.values_by_ids(list(range(struct_attr.attrs['size'])))[text_id] + metadata['corpus_all_texts'] = texts_metadata + metadata['corpus_analysis_date'] = datetime.utcnow().isoformat() + metadata['corpus_cqi_py_protocol_version'] = client.api.version + metadata['corpus_cqi_py_package_version'] = cqi.__version__ + metadata['corpus_cqpserver_version'] = 'CQPserver v3.4.22' # TODO: make this dynamically - # write some metadata to the db - db_corpus.current_nr_of_tokens = metadata['corpus_size_tokens'] - db.session.commit() + # write some metadata to the db + db_corpus.current_nr_of_tokens = metadata['corpus_size_tokens'] + db.session.commit() - # emit data - payload = metadata - response = {'code': 200, 'desc': 'Corpus meta data', 'msg': 'OK', - 'payload': payload} - socketio.emit('corpus_analysis_send_meta_data', response, room=request.sid) + # emit data + payload = metadata + response = {'code': 200, 'desc': 'Corpus meta data', 'msg': 'OK', + 'payload': payload} + socketio.emit('corpus_analysis_meta_data', response, room=request.sid) + except cqi.errors.CQiException as e: + payload = {'code': e.code, 'desc': e.description, 'msg': e.name} + response = {'code': 500, 'desc': None, 'msg': 'Internal Server Error', + 'payload': payload} + socketio.emit('corpus_analysis_meta_data', response, room=request.sid) + client.status = 'ready' @socketio.on('corpus_analysis_query') @@ -100,7 +117,6 @@ def corpus_analysis_query(query): 'match_count': results.attrs['size']} response = {'code': 200, 'desc': None, 'msg': 'OK', 'payload': payload} socketio.emit('corpus_analysis_query', response, room=request.sid) - # TODO: Stop here and add a new method for transmission chunk_size = 100 chunk_start = 0 context = 50 @@ -142,6 +158,11 @@ def corpus_analysis_inspect_match(payload): socketio.emit('corpus_analysis_inspect_match', response, room=request.sid) return + if client.status == 'running': + client.status = 'abort' + while client.status != 'ready': + socketio.sleep(0.1) + client.status = 'running' try: corpus = client.corpora.get('CORPUS') s = corpus.structural_attributes.get('s') @@ -171,6 +192,7 @@ def corpus_analysis_inspect_match(payload): 'type': type, 'data_indexes': data_indexes} socketio.emit('corpus_analysis_inspect_match', response, room=request.sid) + client.status = 'ready' def corpus_analysis_session_handler(app, corpus_id, user_id, session_id): diff --git a/web/app/static/js/modules/corpus_analysis/client/Client.js b/web/app/static/js/modules/corpus_analysis/client/Client.js new file mode 100644 index 00000000..4c7acc31 --- /dev/null +++ b/web/app/static/js/modules/corpus_analysis/client/Client.js @@ -0,0 +1,188 @@ +/** + * This class is used to create a Client object. + * The client handels the client server communication. + * It communicates with the server (e.g. connection or query) + * and recieves data from it, if dynamicMode is true. + * If dynamicMode is false, the client can also handle data that is already + * loaded and is not coming in in chunks. + */ +class Client { + constructor({corpusId = null, + socket = null, + logging = true, + dynamicMode = true} = {}) { + this.corpusId = corpusId; + this.dynamicMode = dynamicMode; + this.logging = logging; + this.requestQueryProgress = 0; + this.socket = socket; + this.eventListeners = {}; + this.connected = false; + + + /** + * Disable all console logging. + * This is global. So every other log message in every other Class or + * function used in conjunction with the client either logs or does not + * log depending on the logging flag. + * 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] = () => {}; + }); + })(); + } + console.info("Client initialized:", this); + } + + // Registers one or more SocketEventListeners to the Client. + setSocketEventListeners(eventListeners) { + for (let eventListener of eventListeners) { + this.eventListeners[eventListener.type] = eventListener; + } + } + + /** + * 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.eventListeners)) { + listener.listenerFunction(type, this); + } + } + + /** + * This functions sends events to the View to trigger specific functions that + * are handleing the representation of data stored in the model. + */ + notifyView(caseIdentifier, detailObject={}) { + detailObject.caseIdentifier = caseIdentifier; + const event = new CustomEvent('notify-view', { detail: detailObject }); + console.info('Client dispatching Notification:', caseIdentifier); + document.dispatchEvent(event); + } + + // Registers a CorpusAnalysisDisplay object to the Client. + setDisplay(type, corpusAnalysisDisplay) { + this.displays[type] = corpusAnalysisDisplay; + } + + /** + * Connects to the corpus analysis session for the specified corpus via + * socket.io. + */ + connect() { + console.info('corpus_analysis_init: Client connecting to session via', + 'socket.emit'); + this.socket.emit('corpus_analysis_init', this.corpusId); + } + + getMetaData() { + console.info('corpus_analysis_meta_data: Client getting meta data via', + 'socket.emit.'); + this.socket.emit('corpus_analysis_meta_data', this.corpusId); + } + + /** + * Emits query to the server via socket.io. Server will send the results + * back. + */ + query(queryStr) { + console.info('corpus_analysis_query: Client sending query via', + 'socket.emit for the query', queryStr); + this.socket.emit('corpus_analysis_query', queryStr); + } + + /** + * Create results data either from all results or from all marked sub results + * Triggers emit to get full match context from server for a number of + * matches identified by their data_index. + **/ + getResultsData(resultsType, dataIndexes, results) { + let tmp_first_cpos = []; + let tmp_last_cpos = []; + for (let dataIndex of dataIndexes) { + tmp_first_cpos.push(results.data.matches[dataIndex].c[0]); + tmp_last_cpos.push(results.data.matches[dataIndex].c[1]); + } + nopaque.socket.emit("corpus_analysis_inspect_match", + {type: resultsType, + data_indexes: dataIndexes, + first_cpos: tmp_first_cpos, + last_cpos: tmp_last_cpos,}); + } + +} + + +/** + * 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 ClientEventListener { + 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 defaultArgs + * 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(defaultArgs) { + for (let [type, listenerCallback] of Object.entries(this.listenerCallbacks)) { + listenerCallback.callbackFunction(...defaultArgs, + ...listenerCallback.args); + } + } + // use this if you only want to execute a specific registered callback + executeCallback(defaultArgs, type) { + let listenerCallback = this.listenerCallbacks[type]; + listenerCallback.callbackFunction(...defaultArgs, + ...listenerCallback.args); + } +} + +/** + * This class is used to create an ListenerCallback which will be registered + * to an SocketEventListener so the Listener can invoke the ListenerCallback + * callback functions. + */ +class ListenerCallback { + constructor(type, callbackFunction, argsList) { + this.args = argsList; + this.callbackFunction = callbackFunction; + this.type = type; + } +} + +// export Classes from this module +export { + Client, + ClientEventListener, + ListenerCallback, +}; \ No newline at end of file diff --git a/web/app/static/js/modules/corpus_analysis/client/callbacks.js b/web/app/static/js/modules/corpus_analysis/client/callbacks.js new file mode 100644 index 00000000..64bb24d4 --- /dev/null +++ b/web/app/static/js/modules/corpus_analysis/client/callbacks.js @@ -0,0 +1,90 @@ +/** + * This callback is called on a socket.on "corpus_analysis_send_meta_data". + * Handels incoming corpus metadata + */ +function saveMetaData() { + let [payload, client, results, rest] = arguments; + results.metaData.init(payload) + console.info('Metada saved:', results); +} + +/** + * 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 data from the last query. + */ +function prepareQueryData() { + // deletes old data from query issued before this new query + let [payload, client, results, rest] = arguments; + // always initialize the results to delete data from the query issued before + results.init(); + results.data.match_count = payload.match_count; + client.requestQueryProgress = 0; + client.notifyView('query-data-prepareing', { results: results }); +} + +function saveQueryData(args) { + let [payload, client, results, rest] = arguments; + // get data matches length before new chun kdata is being inserted + let dataLength = results.data.matches.length; + // incorporating new chunk data into full results + results.data.matches.push(...payload.chunk.matches); + results.data.addData(payload.chunk.cpos_lookup, 'cpos_lookup'); + results.data.addData(payload.chunk.text_lookup, 'text_lookup'); + results.data.cpos_ranges = payload.chunk.cpos_ranges; + let queryFormElement = document.querySelector('#query-form'); + results.data.getQueryStr(queryFormElement); + client.requestQueryProgress = payload.progress; + client.notifyView('query-data-recieving', + { results: results, + client: client, + dataLength: dataLength }); + console.info('Query data chunk saved', results.data); + if (client.requestQueryProgress === 100) { + client.notifyView('query-data-recieved'); + } +} + +function getResultsData(args) { + let [resultsType, dataIndexes, client, results, rest] = arguments; + client.notifyView('results-data-recieving'); + client.getResultsData(resultsType, dataIndexes, results); +} + +function saveResultsData(args) { + let [payload, type, client, results, rest] = arguments; + let objectKey = ''; + if (type === 'full-results') { + console.info('Saving full-results data.'); + objectKey = 'fullResultsData'; + } else if (type === 'sub-results') { + console.info('Saving sub-results data.'); + objectKey = 'subResultsData'; + } else if (type = 'inspect-results') { + objectKey = 'inspectResultsData' + console.info('Saving inspect-results data'); + } + // Save incoming data + results[objectKey].init(); + results[objectKey].matches.push(...payload.matches); + results[objectKey].addData(payload.cpos_lookup, "cpos_lookup"); + results[objectKey].addData(payload.text_lookup, "text_lookup"); + results[objectKey].addData(results.metaData); + results[objectKey].query = results.data.query; + results[objectKey].corpus_type = type; + results[objectKey].match_count = [...payload.matches].length; + results[objectKey].cpos_ranges = payload.cpos_ranges; + console.info('Results data has been saved.', results); + client.notifyView('results-data-recieved', {type: type, + results: results}); +} + +// export callbacks +export { + prepareQueryData, + saveMetaData, + saveQueryData, + getResultsData, + saveResultsData, +}; \ No newline at end of file diff --git a/web/app/static/js/modules/corpus_analysis/client/listeners.js b/web/app/static/js/modules/corpus_analysis/client/listeners.js new file mode 100644 index 00000000..0edbdde2 --- /dev/null +++ b/web/app/static/js/modules/corpus_analysis/client/listeners.js @@ -0,0 +1,202 @@ +/** + * This file contains the listener functions which can be assigned to the + * coprus_analysis client. So that the incoming data/status informations will + * be handled. There are several listeners listening for socket .io events. + * Further below one javascript custom event listener is specified. This + * listener listens for javascript custom events which are being dispatched by + * the View (resultsList). + */ + +// Listeners for socket io events + +/** + * Recieves a corpus analysis connected signal via socket.io. + */ +function recieveConnected(type, client) { + client.socket.on(type, (response) => { + /** + * Check if request for session was OK. + * If OK execute registered callbacks and notify View. + */ + if (response.code === 200) { + console.group('Connected!') + console.info('corpus_analysis_init: Client recieving connected codes', + 'codes via socket.on'); + console.info(`corpus_analysis_init: ${response.code} - ${response.msg}`); + console.info('corpus_analysis_init: Initialization succeeded'); + console.info(response); + client.notifyView('connected'); + console.groupEnd(); + // get meta data immediately + client.getMetaData(); + } else { + let errorText = `Error ${response.code} - ${response.msg}`; + console.group('Connection failed!') + console.error(`corpus_analysis_init: ${errorText}`); + client.notifyView('connecting-failed', { msg: errorText }); + console.groupEnd(); + } + }); +} + +/** + * Recieves meta data from the server via socket.io. + */ +function recieveMetaData(type, client) { + client.socket.on(type, (response) => { + /** + * Check if request for session was OK. + * If OK execute registered callbacks and notify View. + */ + if (response.code === 200) { + console.group('Client recieving meta data') + console.info('corpus_analysis_meta_data: Client recieving meta data', + 'via socket.on'); + console.info(`corpus_analysis_meta_data: ${response.code} - ${response.msg}`); + console.info(response); + // executing the registered callbacks + client.eventListeners[type].executeCallbacks([response.payload]); + console.groupEnd(); + } else { + console.group('Failed to recieve meta data.'); + console.error('corpus_analysis_meta_data: Client failed to recieve', + 'meta data via socket.on'); + let errorText = `Error ${response.payload.code} - ${response.payload.msg}`; + console.error(`corpus_analysis_meta_data: ${errorText}`); + console.groupEnd(); + } + }); +} + +/** + * 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(type, client) { + client.socket.on(type, (response) => { + /** + * Check if request for session was OK. + * If OK execute registered callbacks and notify View. + */ + if (response.code === 200) { + console.group('corpus_analysis_query: Client recieving query process', + 'status via socket.on'); + console.info(`corpus_analysis_query: ${response.code} - ${response.msg}`); + console.info(response); + // executing the registered callbacks + client.eventListeners[type].executeCallbacks([response.payload]); + console.groupEnd(); + } else { + console.group('corpus_analysis_query: Client failed recieving', + 'query process status via socket.on'); + let errorText = `Error ${response.payload.code} - ${response.payload.msg}`; + if (response.payload.code == 1281) { + errorText += ' - Invalid Query'; + } + console.error(`corpus_analysis_query: ${errorText}`); + console.groupEnd(); + } + }); +} + +/** + * Recieves the query data from the request and handles it. + */ +function recieveQueryData(type, client) { + /** + * Check if request for session was OK. + * If OK execute registered callbacks and notify View. + */ + if (client.dynamicMode) { + client.socket.on(type, (response) => { + if (response.code === 200) { + console.group('corpus_analysis_query_results: Recieveing query data') + console.info('Client recieving query data via socket.on'); + console.info('Recieved chunk', response); + /** + * Execute registered callbacks and notify View. + */ + client.eventListeners[type].executeCallbacks([response.payload]); + console.info('Added chunk data to results.data.'); + console.groupEnd(); + } else { + console.group('corpus_analysis_query_results: Client failed recieving', + 'the results via socket.on'); + let errorText = `Error ${response.payload.code} - ${response.payload.msg}`; + console.error(`corpus_analysis_query: ${errorText}`); + console.groupEnd(); + } + }); + } else { + console.group('corpus_analysis_query_results: Loading query data.'); + console.info('Client loading imported query data from database.'); + // executing the registered callbacks + client.eventListeners[type].executeCallbacks(); + console.groupEnd(); + } +} + +/** + * Recieves the data requested by the create Results or sub results button + */ +function recieveResultsData(type, client) { + client.socket.on(type, (response) => { + /** + * Check if request for session was OK. + * If OK execute registered callbacks and notify View. + */ + if (response.code === 200) { + console.group('Client recieving results data') + console.info('corpus_analysis_inspect_match: Client recieving results data', + 'via socket.on'); + console.info(`corpus_analysis_inspect_match: ${response.code} - ${response.msg}`); + console.info(response); + // executing the registered callbacks + client.eventListeners[type].executeCallbacks([response.payload, + response.type]); + console.groupEnd(); + } else { + console.group('Failed to recieve results data.'); + console.error('corpus_analysis_inspect_match: Client failed to recieve', + 'results data via socket.on'); + let errorText = `Error ${response.payload.code} - ${response.payload.msg}`; + console.error(`corpus_analysis_inspect_match: ${errorText}`); + console.groupEnd(); + } + }); +} + +/* + * This is the javascript custom event listener, listening for events + * dispatched by the View. + */ +function recieveViewNotification(type, client) { + document.addEventListener(type, (event) => { + let caseIdentifier = event.detail.caseIdentifier; + switch(caseIdentifier) { + case 'get-results': + console.info('Client getting full results for export.'); + // execute callback or functions + client.eventListeners[type].executeCallback([event.detail.resultsType, + event.detail.dataIndexes], + caseIdentifier); + break + default: + console.error('Recieved unkown notification case identifier from View'); + // do something to not crash the analysis session? + // maybe unnecessary + } + }); +} + +// export listeners from this module +export { + recieveConnected, + recieveMetaData, + recieveQueryStatus, + recieveQueryData, + recieveViewNotification, + recieveResultsData, +}; \ No newline at end of file diff --git a/web/app/static/js/modules/corpus_analysis/model/Results.js b/web/app/static/js/modules/corpus_analysis/model/Results.js new file mode 100644 index 00000000..ea43c696 --- /dev/null +++ b/web/app/static/js/modules/corpus_analysis/model/Results.js @@ -0,0 +1,104 @@ +/** + * These classes are implementing the data store of the corpus_analysis + * package. If we follow the idea of the Model View Controller Pattern these + * classes combined in the Results class define the Model. + */ + +class Results { + constructor() { + this.data = new Data(); + this.metaData = new MetaData(); + this.fullResultsData = new Data(); + this.subResultsData = new Data(); + this.inspectResultsData = new Data(); + console.info('Initialized the Results object.'); + } + + init() { + this.data.init(); + this.metaData.init(); + this.fullResultsData.init(); + this.subResultsData.init(); + this.inspectResultsData.init(); + } + +} + +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.cpos_ranges = null; + this.query = ''; + } + + addData(jsonData, key=null) { + if (key !== null) { + Object.assign(this[key], jsonData); + } else if (key === null) { + Object.assign(this, jsonData) + } + } + + // get query as string from form Element + getQueryStr(queryFormElement) { + // gets query + let queryFormData; + let queryStr; + queryFormData = new FormData(queryFormElement); + queryStr = queryFormData.get('query-form-query'); + this["query"] = queryStr; + } + + // function creates a unique and safe filename for the download + createDownloadFilename(suffix) { + let today = new Date(); + let currentDate = `${today.getUTCFullYear()}` + + `-${(today.getUTCMonth() + 1)}` + + `-${today.getUTCDate()}`; + let currentTime = `${today.getUTCHours()}h` + + `${today.getUTCMinutes()}m` + + `${today.getUTCSeconds()}s`; + let safeFilename = this.query.replace(/[^a-z0-9_-]/gi, "_"); + let resultFilename = `UTC-${currentDate}_${currentTime}_${safeFilename}_${suffix}`; + return resultFilename + } + /** + * 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) { + filename += filenameSlug; + let file = new Blob([dataStr], {type: type}); + var url = URL.createObjectURL(file); + downloadElement.href = url; + downloadElement.download = filename; + } + + // function to download the results as JSON + downloadJSONRessource(resultFilename, downloadData, downloadElement) { + /** + * Stringify JSON object for json download. + * Use tabs to save some space. + */ + let dataStr = JSON.stringify(downloadData, undefined, "\t"); + // Start actual download + this.download(downloadElement, dataStr, resultFilename, "text/json", ".json") + } + +} + +class MetaData { + // Sets empty object structure when no input is given. + // if json object like input is given class fields are created from this + init(json={}) { + Object.assign(this, json); + } +} + +export {Results, Data, MetaData}; \ No newline at end of file diff --git a/web/app/static/js/modules/corpus_analysis/view/ResultsView.js b/web/app/static/js/modules/corpus_analysis/view/ResultsView.js new file mode 100644 index 00000000..2fe6ba1b --- /dev/null +++ b/web/app/static/js/modules/corpus_analysis/view/ResultsView.js @@ -0,0 +1,832 @@ +/** + * This class implements a NotificationListener that is listening for the + * specified + */ +class ViewEventListener { + constructor(type, listenerFunction) { + this.listenerFunction = listenerFunction; + this.type = type; + } +} + +/** + * This class is implements a View which handles the reprensentation of the + * data that has been fetched by the Client of the corpus_analysis. This view + * only handles how the data is shown to the user. View extends the list.js + * List class. + */ +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: 30, + 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); + /** + * 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 = {}; + // TODO: Rename both variables to something more descreptive and clear + // 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. + // notification listeners listening for client notifications (or other in the future?) + this.notificationListeners = {}; + this.clientIsBusy = false; + } + + /** + * // TODO: + * Init function that gets all needed HTML Elements. Implement this, or not? + * Or build a check into the get HTMLElements function if element already exists. + * Also think about saving alle elements in resultsList.es.nameOfElement + */ + + /** + * Function to clear/reset some class field values. Usefull if a new query + * hase been issued by the user. + */ + resetFields() { + this.addToSubResultsIdsToShow = new Set(); + this.addToSubResultsStatus = {}; + } + + + /** + * Function that takes one or more query selector + * strings in an array as an input. The function then creates a + * class field in the ResultsList 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 or elements fetched with the querySelector + * method. + */ + getHTMLElements(arrayOfSelectors) { + for (let selector of arrayOfSelectors) { + let element; + let elements; + if (selector.startsWith('#')) { + element = document.querySelector(selector); + } else { + elements = document.querySelectorAll(selector); + elements = [...elements]; + } + let cleanKey = []; + selector = selector.replace(/_/g, '-'); + 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 ? element: elements; + } + } + + /** + * Register notificationListeners to the ResultsList. Which will listen for + * the specified event. + */ + setNotificationListeners(notificationListeners) { + for (let notificationListener of notificationListeners) { + this.notificationListeners[notificationListener.type] = notificationListener; + } + } + + /** + * Loads the notificationListeners so that hey will be listening to their + * assigned custom events. + */ + loadNotificationListeners() { + for (let [type, listener] of Object.entries(this.notificationListeners)) { + listener.listenerFunction(type, this); + } + } + + /** + * This functions sends events to the Client to trigger specific functions to + * trigger new data requests from the server. + */ + notifyClient(caseIdentifier, detailObject={}) { + detailObject.caseIdentifier = caseIdentifier; + const event = new CustomEvent('notify-client', { detail: detailObject }); + console.info('Client dispatching Notification:', caseIdentifier); + document.dispatchEvent(event); + } + + /** + * Creates cpos either from ranges or not. + */ + helperCreateCpos(cpos_ranges, cpos_values) { + let lc; + let c; + let rc; + if (cpos_ranges) { + // python range like function from MDN + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Sequence_generator_(range) + const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step)); + lc = range(cpos_values.lc[0], cpos_values.lc[1], 1) + c = range(cpos_values.c[0], cpos_values.c[1], 1) + rc = range(cpos_values.rc[0], cpos_values.rc[1], 1) + } else { + lc = cpos_values.lc; + c = cpos_values.c; + rc = cpos_values.rc; + } + return {lc: lc, c: c, rc: rc}; + } + + // handels interactionElements during a pagination navigation + // loops over interactionElements and executes callback functions accordingly + pageChangeEventInteractionHandler(interactionElements) { + // get elements to check thier status + for (let interaction of interactionElements.interactions) { + if (interaction.checkStatus) { + if (interaction.element.checked) { + let f_on = interaction.bindThisToCallback("on"); + let args_on = interaction.callbacks.on.args; + f_on(...args_on); + } else { + let f_off = interaction.bindThisToCallback("off"); + let args_off = interaction.callbacks.off.args; + f_off(...args_off); + } + } else { + let f = interaction.bindThisToCallback("noCheck"); + let args = interaction.callbacks.noCheck.args; + f(...args); + } + } + } + + // get display options from display options form element + static getDisplayOptions(htmlId) { + // gets display options parameters + 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"), + "expertMode": displayOptionsFormData.get("display-options-form-expert_mode") + }; + return displayOptionsData + } + + // Used in addToSubResults and inspect to toggle the design of the check + // buttons according to its checked unchecked status. + helperActivateAddBtn(btn) { + btn.classList.remove("grey"); + btn.classList.add("green"); + btn.textContent = "check"; + } + + // Used in addToSubResults and inspect to toggle the design of the check + // buttons according to its checked unchecked status. + helperDeactivateAddBtn(btn) { + btn.classList.remove("green"); + btn.classList.add("grey"); + btn.textContent = "add"; + } + + // Either adds or removes a match to the sub-results. For this it checks + // onclick if the current button has been checked or not. For this the + // function checks if its status in addToSubResultsStatus is either flase or + // true. Adds match to sub-results if status is false if status is true it + // removes it. + addToSubResults(dataIndex, tableCall=true) { + if (!this.addToSubResultsStatus[dataIndex] + || this.addToSubResultsStatus === undefined) { + // add button is activated because status is either false or undefined + this.helperActivateAddBtn(event.target); + this.addToSubResultsStatus[dataIndex] = true; // sets status to true + this.addToSubResultsIdsToShow.add(dataIndex + 1); // + 1 because user does not see zero indexd data indexes + this.subResultsMatchIds.textContent = [...this.addToSubResultsIdsToShow].sort(function(a, b){return a-b}).join(", "); // automaticalle sorts ids into the textarea in ascending order + M.textareaAutoResize(this.subResultsMatchIds); // after an insert textarea has to be resized manually + this.nrMarkedMatches.textContent = [...this.addToSubResultsIdsToShow].length; + } else if (this.addToSubResultsStatus[dataIndex]) { + // add button is deactivated because status is true + this.helperDeactivateAddBtn(event.target); + this.addToSubResultsStatus[dataIndex] = false; // sets status to false + this.addToSubResultsIdsToShow.delete(dataIndex + 1); // + 1 because user does not see zero indexd data indexes + this.subResultsMatchIds.textContent = [...this.addToSubResultsIdsToShow].sort(function(a, b){return a-b}).join(", "); // automaticalle sorts ids into the textarea in ascending order + this.nrMarkedMatches.textContent = [...this.addToSubResultsIdsToShow].length; + M.textareaAutoResize(this.subResultsMatchIds); // after an insert textarea has to be resized manually + } + // Toggles the create button according to the number of ids in addToSubResultsIdsToShow + if ([...this.addToSubResultsIdsToShow].length > 0) { + this.subResultsCreate.classList.toggle('disabled', false); + } else if ([...this.addToSubResultsIdsToShow].length === 0) { + this.subResultsCreate.classList.toggle('disabled', true); + } + /** + * After a match as been added or removed the export button will be + * hidden because the sub-results have been altered and have to be built + * again. Thus subResultsCreateElement has to be shown again. + */ + this.subResultsExport.classList.add("hide"); + this.subResultsCreate.classList.remove("hide"); + /** + * Also activate/deactivate buttons in the table/jsList results accordingly + * if button in inspect was activated/deactivated. + * This part only runs if tableCall is set to false when this function is + * called. + */ + if (!tableCall) { + this.getHTMLElements(['#query-results-table']); + let container = this.queryResultsTable.querySelector(`[data-index="${dataIndex}"]`); + let tableAddBtn = container.querySelector('.add-btn'); // gets the add button from the list view + if (this.addToSubResultsStatus[dataIndex]) { + this.helperActivateAddBtn(tableAddBtn); + } else { + this.helperDeactivateAddBtn(tableAddBtn); + } + } + } + + // ###### Functions to inspect one match, to show more details ###### + // activate inspect buttons if progress is 100 + activateInspect() { + if (!this.clientIsBusy) { + let inspectBtnElements; + inspectBtnElements = document.querySelectorAll('.inspect'); + for (let inspectBtn of inspectBtnElements) { + inspectBtn.classList.toggle('disabled', false); + } + } + } + + // deactivate inspect buttons + deactivateInspect() { + let inspectBtnElements; + inspectBtnElements = document.querySelectorAll('.inspect'); + for (let inspectBtn of inspectBtnElements) { + inspectBtn.classList.toggle('disabled', true); + } + } + + // ### functions to inspect imported Matches + // This function creates an object that is similar to the object that is + // being recieved as an answere to the getMatchWithContext Method, which is + // triggering an socket.io event. + // It is used as an input for show match context in the context of imported + // results to be able to inspect matches. + createFakeResponse() { + contextModal.open(); + // match nr for user to display derived from data_index + let contextMatchNrElement = document.getElementById("context-match-nr"); + contextMatchNrElement.textContent = this.contextId + 1; + let cpos_lookup; + let fake_response = {}; + let contextResultsElement; + // function to create one match object from entire imported results + // that is passed into the results.jsList.showMatchContext() function + fake_response["payload"] = {}; + let dataIndex = event.target.closest("tr").dataset.index; + this.contextId = dataIndex; + fake_response.payload["matches"] = [results.data.matches[dataIndex]]; + contextResultsElement = document.getElementById("context-results"); + contextResultsElement.innerHTML = ""; + let {lc, c, rc} = this.helperCreateCpos(results.data.cpos_ranges, + fake_response.payload.matches[0]); + cpos_lookup = {}; + for (let cpos of lc) { + cpos_lookup[cpos] = results.data.cpos_lookup[cpos]; + } + for (let cpos of c) { + cpos_lookup[cpos] = results.data.cpos_lookup[cpos]; + } + for (let cpos of rc) { + cpos_lookup[cpos] = results.data.cpos_lookup[cpos]; + } + fake_response.payload["cpos_lookup"] = cpos_lookup + fake_response.payload["cpos_ranges"] = results.data.cpos_ranges; + fake_response.payload["query"] = results.data.query; + fake_response.payload["context_id"] = dataIndex + 1; + fake_response.payload["match_count"] = fake_response.payload.matches.length + fake_response.payload["corpus_type"] = "inspect-result" + return fake_response + } + + // gets result cpos infos for one dataIndex (list of length 1) to send back to + // the server + inspect(dataIndex, type) { + // initialize context modal + this.getHTMLElements([ + '#context-modal', + '#context-results', + '#create-inspect-menu', + ]); + this.contextModal = M.Modal.init(this.contextModal); + // get result infos from server and show them in context modal + this.contextId = dataIndex[0]; + this.contextResults.innerHTML = ""; // clear it from old inspects + this.notifyClient('get-results', {resultsType: 'inspect-results', + dataIndexes: [dataIndex]}); + // match nr for user to display derived from data_index + let contextMatchNrElement = document.getElementById("context-match-nr"); + contextMatchNrElement.textContent = this.contextId + 1; + this.contextModal.open(); + // add a button to add this match to sub results with onclick event + let classes = `btn-floating btn waves-effect` + + ` waves-light grey right` + let addToSubResultsIdsBtn = document.createElement("a"); + addToSubResultsIdsBtn.setAttribute("class", classes + ` add`); + addToSubResultsIdsBtn.innerHTML = 'add'; + addToSubResultsIdsBtn.onclick= () => {this.addToSubResults(dataIndex[0], false)}; + // checks if the match has or has not been added to sub results yet + // sets the color and status of the button accordingly + if (this.addToSubResultsStatus[dataIndex[0]]) { + this.helperActivateAddBtn(addToSubResultsIdsBtn.firstElementChild); + } else if (!this.addToSubResultsStatus[dataIndex[0]]) { + this.helperDeactivateAddBtn(addToSubResultsIdsBtn.firstElementChild); + } + this.createInspectMenu.innerHTML = ''; + this.createInspectMenu.appendChild(addToSubResultsIdsBtn); + } + + // create Element from HTML String helper function + HTMLTStrToElement(htmlStr) { + // https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518 + let template = document.createElement("template"); + htmlStr = htmlStr.trim(); + template.innerHTML = htmlStr; + return template.content.firstChild; + } + + // Used as a callback to handle incoming match context results when inspect + // has been used. + showMatchContext(results) { + + this.getHTMLElements([ + '#context-results', + '#inspect-display-options-form-expert_mode_inspect', + '#inspect-display-options-form-highlight_sentences', + '#context-sentences' + ]) + + let uniqueS = new Set(); + let uniqueContextS = new Set(); + let {lc, c, rc} = this.helperCreateCpos(results.inspectResultsData.cpos_ranges, + results.inspectResultsData.matches[0]); + // create sentence strings as tokens + let tokenHTMLArray = []; + let htmlTokenStr = ``; + let tokenHTMlElement; + let token; + for (let cpos of lc) { + token = results.inspectResultsData.cpos_lookup[cpos]; + uniqueS.add(token.s) + htmlTokenStr = `` + + `${token.word}` + + ``; + tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr) + tokenHTMLArray.push(tokenHTMlElement); + } + for (let cpos of c) { + token = results.inspectResultsData.cpos_lookup[cpos]; + uniqueContextS.add(token.s); + uniqueS.add(token.s); + htmlTokenStr = `` + + `${token.word}` + + ``; + tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr) + tokenHTMLArray.push(tokenHTMlElement); + } + results.inspectResultsData["context_s_ids"] = Array.from(uniqueContextS); + for (let cpos of rc) { + token = results.inspectResultsData.cpos_lookup[cpos]; + uniqueS.add(token.s) + htmlTokenStr = `` + + `${token.word}` + + ``; + tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr) + tokenHTMLArray.push(tokenHTMlElement); + } + for (let sId of uniqueS) { + let htmlSentence = ``; + let sentenceElement = this.HTMLTStrToElement(htmlSentence); + for (let tokenElement of tokenHTMLArray) { + if (tokenElement.dataset.sid == sId) { + sentenceElement.appendChild(tokenElement); + sentenceElement.insertAdjacentHTML("beforeend", ` `); + } else { + continue; + } + } + this.contextResults.appendChild(sentenceElement); + } + + + // add inspect display options events + this.inspectDisplayOptionsFormExpertModeInspect.onchange = (event) => { + if (event.target.checked) { + this.expertModeOn("context-results", results); + } else { + this.expertModeOff("context-results") + } + }; + + this.inspectDisplayOptionsFormHighlightSentences.onchange = (event) => { + if (event.target.checked) { + this.higlightContextSentences(); + } else { + this.unhighlightContextSentences(); + } + }; + + this.contextSentences.onchange = (event) => { + // console.log(event.target.value); + this.changeSentenceContext(event.target.value); + } + + // checks on new modal opening if switches are checked + // if switches are checked functions are executed + if (this.inspectDisplayOptionsFormExpertModeInspect.checked) { + this.expertModeOn("context-results", results); + } + + if (this.inspectDisplayOptionsFormHighlightSentences.checked) { + this.higlightContextSentences(); + } + + // checks the value of the number of sentences to show on modal opening + // sets context sentences accordingly + this.changeSentenceContext(this.contextSentences.value); + } + + // splits context text into sentences based on spacy sentence split + higlightContextSentences() { + let sentences; + sentences = document.getElementById("context-results").getElementsByClassName("sentence"); + for (let s of sentences) { + s.insertAdjacentHTML("beforeend", `

`) + } + } + + unhighlightContextSentences() { + let sentences; + let br; + sentences = document.getElementById("context-results").getElementsByClassName("sentence"); + for (let s of sentences) { + br = s.lastChild; + br.remove(); + } + } + + // changes how many context sentences in inspect view are shown + changeSentenceContext(sValue, maxSValue=10) { + let array; + let sentences; + let toHideArray; + let toShowArray; + sValue = maxSValue - sValue; + // console.log(sValue); + sentences = document.getElementById("context-results").getElementsByClassName("sentence"); + array = Array.from(sentences); + if (sValue != 0) { + toHideArray = array.slice(0, sValue).concat(array.slice(-(sValue))); + toShowArray = array.slice(sValue, 9).concat(array.slice(9, -(sValue))) + } else { + toHideArray = []; + toShowArray = array; + } + // console.log(array); + // console.log("#######"); + // console.log(toHideArray); + for (let s of toHideArray) { + s.classList.add("hide"); + } + for (let s of toShowArray) { + s.classList.remove("hide"); + } + } + + // ###### Display options changing live how the matches are being displayed ###### + + // Event function that changes the shown hits per page. + // Just alters the resultsList.page property + changeHitsPerPage() { + try { + if (event.type === "change") { + nopaque.flash("Updated matches per page.", "corpus") + } + } catch (e) { + + } finally { + this.page = this.displayOptionsFormResultsPerPage.value; + this.update(); + } + this.activateInspect(); + if (this.displayOptionsFormExpertMode.checked) { + this.expertModeOn("query-display"); + } + } + + // Event function triggered on context select change + // also if pagination is clicked + changeContext() { + try { + if (event.type === "change") { + nopaque.flash("Updated context per match!", "corpus"); + } + } catch (e) { + + } finally { + let newContextValue = this.displayOptionsFormResultContext.value; + let lc = document.querySelectorAll(".left-context"); + let rc = document.querySelectorAll(".right-context"); + for (let element of lc) { + let arrayLc = Array.from(element.childNodes); + for (let element of arrayLc.reverse().slice(newContextValue)) { + element.classList.add("hide"); + } + for (let element of arrayLc.slice(0, newContextValue)) { + element.classList.remove("hide"); + } + } + for (let element of rc) { + let arrayRc = Array.from(element.childNodes); + for (let element of arrayRc.slice(newContextValue)) { + element.classList.add("hide"); + } + for (let element of arrayRc.slice(0, newContextValue)) { + element.classList.remove("hide"); + } + } + } + } + + // ###### Expert view event functions ###### + // function to create a tooltip for the current hovered token + tooltipEventCreate(event, results) { + // console.log("Create Tooltip on mouseover."); + let token; + token = results.data.cpos_lookup[event.target.dataset.cpos]; + if (!token) { + token = results.inspectResultsData.cpos_lookup[event.target.dataset.cpos]; + } + this.addToolTipToTokenElement(event.target, token, results); + } + + // Function to destroy the current Tooltip for the current hovered tooltip + // on mouse leave + tooltipEventDestroy(event) { + // console.log("Tooltip destroy on leave."); + this.currentTooltipElement.destroy(); + } + + // turn the expert mode on for all tokens in the DOM element identified by its htmlID + expertModeOn(htmlId, results) { + 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.eventTokens[htmlId] = []; + for (let tokenElement of this.currentExpertTokenElements[htmlId]) { + tokenElement.classList.add("chip", "hoverable", "expert-view"); + const eventCreate = (event, arg) => this.tooltipEventCreate(event, arg); + tokenElement.onmouseover = (event) => eventCreate(event, results); + tokenElement.onmouseout = (event) => this.tooltipEventDestroy(event); + this.eventTokens[htmlId].push(tokenElement); + } + } + + // fuction that creates Tooltip for one token and extracts the corresponding + // infos from the result JSON + addToolTipToTokenElement(tokenElement, token, results) { + this.currentTooltipElement; + this.currentTooltipElement = M.Tooltip.init(tokenElement, + {"html": ` + + + + + + + + +
Token informationSource information
+ Word: ${token.word}
+ Lemma: ${token.lemma}
+ POS: ${token.pos}
+ Simple POS: ${token.simple_pos}
+ NER: ${token.ner} +
+ Title: ${results.data.text_lookup[token.text].title} +
+ Author: ${results.data.text_lookup[token.text].author} +
+ Publishing year: ${results.data.text_lookup[token.text].publishing_year} +
`} + ); + } + + // function to remove extra informations and animations from tokens + expertModeOff(htmlId) { + // console.log("Expert mode is off."); + if (!Array.isArray(this.currentExpertTokenElements[htmlId])) { + this.currentExpertTokenElements[htmlId] = []; + } + if (!Array.isArray(this.eventTokens[htmlId])) { + this.eventTokens[htmlId] = []; + } + for (let tokenElement of this.currentExpertTokenElements[htmlId]) { + tokenElement.classList.remove("chip", "hoverable", "expert-view"); + } + this.currentExpertTokenElements[htmlId] = []; + + for (let eventToken of this.eventTokens[htmlId]) { + eventToken.onmouseover = ""; + eventToken.onmouseout = ""; + } + this.eventTokens[htmlId] = []; + } + + createResultRowElement(item, chunk, imported=false) { + let aCellElement; + let addToSubResultsBtn; + let cCellElement; + let cpos; + let fakeResponse; // used if imported results are being created; + let inspectBtn + let lcCellElement; + let matchNrElement; + let matchRowElement; + let rcCellElement; + let textTitles; + let textTitlesCellElement; + let token; + let values; + // gather values from item + values = item.values(); + let {lc, c, rc} = this.helperCreateCpos(chunk.cpos_ranges, + values) + // get infos for full match row + matchRowElement = document.createElement("tr"); + matchRowElement.setAttribute("data-index", values.index) + lcCellElement = document.createElement("td"); + lcCellElement.classList.add("left-context"); + matchRowElement.appendChild(lcCellElement); + for (cpos of lc) { + token = chunk.cpos_lookup[cpos]; + lcCellElement.insertAdjacentHTML("beforeend", + `${token.word} `); + } + + // get infos for hit of match and set actions + textTitles = new Set(); + aCellElement = document.createElement("td"); + aCellElement.classList.add("actions"); + cCellElement = document.createElement("td"); + cCellElement.classList.add("match-hit"); + textTitlesCellElement = document.createElement("td"); + textTitlesCellElement.classList.add("titles"); + matchNrElement = document.createElement("td"); + matchNrElement.classList.add("match-nr"); + matchRowElement.appendChild(cCellElement); + matchRowElement.appendChild(aCellElement); + for (cpos of c) { + token = chunk.cpos_lookup[cpos]; + cCellElement.insertAdjacentHTML("beforeend", + `${token.word} `); + // get text titles of every hit cpos token + textTitles.add(chunk.text_lookup[token.text].title); + } + // add some interaction buttons + // # some btn css rules and classes + let css = `margin-right: 5px; margin-bottom: 5px;` + let classes = `btn-floating btn waves-effect` + + ` waves-light grey` + // # add button to trigger more context to every match td + inspectBtn = document.createElement("a"); + inspectBtn.setAttribute("style", css); + inspectBtn.setAttribute("class", classes + ` disabled inspect` + ); + inspectBtn.innerHTML = 'search'; + // # add btn to add matches to sub-results. hidden per default + addToSubResultsBtn = document.createElement("a"); + addToSubResultsBtn.setAttribute("style", css); + addToSubResultsBtn.setAttribute("class", classes + ` add` + ); + addToSubResultsBtn.innerHTML = 'add'; + aCellElement.appendChild(inspectBtn); + aCellElement.appendChild(addToSubResultsBtn); + // add text titles at front as first td of one row + textTitlesCellElement.textContent = [...textTitles].join(", "); + matchRowElement.insertAdjacentHTML("afterbegin", textTitlesCellElement.outerHTML); + matchNrElement.textContent = values.index + 1; + matchRowElement.insertAdjacentHTML("afterbegin", matchNrElement.outerHTML); + + // get infos for right context of match + rcCellElement = document.createElement("td"); + rcCellElement.classList.add("right-context"); + matchRowElement.appendChild(rcCellElement); + for (cpos of rc) { + token = chunk.cpos_lookup[cpos]; + rcCellElement.insertAdjacentHTML("beforeend", + `${token.word} `); + } + return matchRowElement + } + + // creates the HTML table code for the metadata view in the corpus analysis interface + createMetaDataForModal(metaDataObject) { + let html = `
+ + + + + + + + ` + for (let [outerKey, outerValue] of Object.entries(metaDataObject)) { + html += ` + ` + if (outerKey === "corpus_all_texts" || outerKey === "text_lookup") { + html += `` + } else { + html += `` + } + html += `` + } + html += ` +
Metadata DescriptionValue
${outerKey.replace(/_/g, " ")} +
    ` + for (let [innerKey, innerValue] of Object.entries(outerValue)) { + html += `` + } + html += `
+
${outerValue}
` + return html + } + + // Creates the text details for the texts shown in the corpus analysis metadata modal. + createTextDetails(metaData) { + let metadataKey = event.target.dataset.metadataKey; + let textKey = event.target.dataset.textKey; + let textData = metaData[metadataKey][textKey]; + let bibliographicData = document.querySelector(`#bibliographic-data-${metadataKey}-${textKey}`); + bibliographicData.textContent = ''; + for (let [key, value] of Object.entries(textData)) { + bibliographicData.insertAdjacentHTML("afterbegin", + ` +
  • ${key}: ${value}
  • + `); + } + } +}; + +// export classses +export { ViewEventListener, ResultsList }; \ No newline at end of file diff --git a/web/app/static/js/modules/corpus_analysis/view/callbacks.js b/web/app/static/js/modules/corpus_analysis/view/callbacks.js new file mode 100644 index 00000000..d9c85926 --- /dev/null +++ b/web/app/static/js/modules/corpus_analysis/view/callbacks.js @@ -0,0 +1,183 @@ +/** + * This file contains all the callbacks triggered by the notificationListener. + * Also general callbacks are defined which are doing some hiding/disabling and + * showing/enabling of common elements when data is being transmitted or not. + */ + +function disableElementsGeneralCallback(resultsList, detail) { + if (detail.type === 'full-results') { + resultsList.fullResultsCreate.classList.toggle('hide', false); + resultsList.fullResultsExport.classList.toggle('hide', true); + } else if (detail.type === 'sub-results') { + resultsList.subResultsCreate.classList.toggle('hide', false); + resultsList.subResultsExport.classList.toggle('hide', true); + } else { + resultsList.fullResultsCreate.classList.toggle('disabled', true); + resultsList.subResultsCreate.classList.toggle('disabled', true); + } + resultsList.deactivateInspect(); +} + +function enableElementsGeneralCallback(resultsList, detail) { + resultsList.fullResultsCreate.classList.toggle('disabled'); + resultsList.subResultsCreate.classList.toggle('disabled'); + resultsList.activateInspect(); +} + +function connectingCallback(resultsList, detail) { + resultsList.getHTMLElements(['#analysis-init-modal']); + resultsList.analysisInitModal = M.Modal.init(resultsList.analysisInitModal, + {dismissible: false}); + resultsList.analysisInitModal.open(); +} + +function connectedCallback(resultsList, detail) { + resultsList.analysisInitModal.close(); +} + +function connectingFaildeCallback(resultsList, detail) { + resultsList.getHTMLElements([ + '#analysis-init-progress', + '#analysis-init-error' + ]); + resultsList.analysisInitProgress.classList.toggle('hide'); + resultsList.analysisInitError.classList.toggle('hide'); + resultsList.analysisInitError.textContent = detail.msg; +} + +function queryDataPreparingCallback(resultsList, detail) { + resultsList.clientIsBusy = true; + // remove all items from resultsList, like from the query issued before + resultsList.clear() + // get needed HTML Elements + let results = detail.results; + resultsList.getHTMLElements([ + '#interactions-menu', + '#recieved-match-count', + '#total-match-count', + '#text-lookup-count', + '#text-lookup-titles', + '#query-results-user-feedback', + '#query-progress-bar', + '#query-results-create', + '#sub-results-match-ids', + '#nr-marked-matches', + ]); + // show or enable some things for the user + resultsList.interactionsMenu.classList.toggle('hide', false) + resultsList.queryResultsUserFeedback.classList.toggle('hide', false); + resultsList.queryProgressBar.classList.toggle('hide', false); + /** + * Set some initial values for the user feedback + * or reset values for new issued query + */ + resultsList.recievedMatchCount.textContent = 0; + resultsList.totalMatchCount.textContent = results.data.match_count; + resultsList.textLookupTitles.textContent = ''; + resultsList.textLookupCount.textContent = 0; + resultsList.nrMarkedMatches.textContent = 0; + resultsList.subResultsMatchIds.textContent = ''; + resultsList.resetFields(); +} + +function queryDataRecievingCallback(resultsList, detail) { + // load the data into the resultsList and show them to the user + let results = detail.results; + let client = detail.client; + let start = detail.dataLength; + let resultItems = []; + for (let [index, match] of Object.entries(results.data.matches).slice(start)) { + resultItems.push({ ...match, ...{ 'index': parseInt(index) } }); + } + if (client.dynamicMode) { + resultsList.add(resultItems, (items) => { + for (let item of items) { + item.elm = resultsList.createResultRowElement(item, results.data); + } + }); + // update user feedback about query status + resultsList.recievedMatchCount.textContent = results.data.matches.length; + resultsList.queryProgressBar.firstElementChild.style.width = `${client.requestQueryProgress}%`; + resultsList.textLookupCount.textContent = `${Object.keys(results.data.text_lookup).length}`; + let titles = new Array(); + for (let [key, value] of Object.entries(results.data.text_lookup)) { + titles.push(`${value.title} (${value.publishing_year})`); + } + resultsList.textLookupTitles.textContent = `${titles.join(', ')}`; + // updating table on finished item creation callback via createResultRowElement + resultsList.update(); + resultsList.changeHitsPerPage(); + resultsList.changeContext(); + //activate expertMode of switch is checked + if (resultsList.displayOptionsFormExpertMode.checked) { + resultsList.expertModeOn('query-display', results); + } + } else if (!client.dynamicMode) { + results.jsList.add(resultItems, (items) => { + for (let item of items) { + item.elm = results.jsList.createResultRowElement(item, payload.chunk, + true); + } + }); + } +} + +function queryDataRecievedCallback(resultsList, detail) { + // hide or disable some things for the user + resultsList.queryResultsUserFeedback.classList.toggle('hide'); + resultsList.queryProgressBar.classList.toggle('hide'); + // reset bar progress for next query + resultsList.queryProgressBar.firstElementChild.style.width = '0%'; + resultsList.clientIsBusy = false; +} + +function resultsDataRecievingCallback(resultsList, detail) { + resultsList.clientIsBusy = true; +} + +function resultsDataRecievedCallback(resultsList, detail) { + resultsList.clientIsBusy = false; + // create strings for create buttons depending on type + const handleType = (keyPrefix, text) => { + // hides the create element after results have been recieved and reset it + resultsList[`${keyPrefix}Create`].classList.toggle('hide'); + resultsList[`${keyPrefix}Create`].textContent = `Create ${text}`; + resultsList[`${keyPrefix}Create`].insertAdjacentHTML('beforeend', + `build`); + // show and highlight export button + resultsList[`${keyPrefix}Export`].classList.toggle('hide', false); + resultsList[`${keyPrefix}Export`].classList.toggle('pulse', true); + setTimeout(() => { + resultsList[`${keyPrefix}Export`].classList.toggle('pulse', false); + clearTimeout(); + }, 3000) + } + if (detail.type === 'full-results') { + handleType('fullResults', 'Results'); + } else if (detail.type ==='sub-results') { + handleType('subResults', 'Sub-Results'); + } else if (detail.type ==='inspect-results') { + if (resultsList.addToSubResultsIdsToShow.size === 0) { + /** + * Prevent create sub results button from being activated if it is disabled + * and no matches have been marked by the user for sub results creation. + */ + resultsList.subResultsCreate.classList.toggle('disabled', true); + } + resultsList.showMatchContext(detail.results); + } +} + +// export the callbacks +export { + connectingCallback, + connectedCallback, + connectingFaildeCallback, + queryDataPreparingCallback, + queryDataRecievingCallback, + queryDataRecievedCallback, + resultsDataRecievingCallback, + resultsDataRecievedCallback, + disableElementsGeneralCallback, + enableElementsGeneralCallback, +}; \ No newline at end of file diff --git a/web/app/static/js/modules/corpus_analysis/view/listeners.js b/web/app/static/js/modules/corpus_analysis/view/listeners.js new file mode 100644 index 00000000..469bcaf2 --- /dev/null +++ b/web/app/static/js/modules/corpus_analysis/view/listeners.js @@ -0,0 +1,87 @@ +/** + * This file contains the listener function that will be assigned to the + * corpus_analysis ResultsView. The listener is listening for the notification + * event which is being dispatched by the corpus_analysis Client. The + * notification Event triggers the listener whiche will call different + * callback functions depending on the detail information of the notification + * event. + */ + +import { + connectingCallback, + connectedCallback, + connectingFaildeCallback, + queryDataPreparingCallback, + queryDataRecievingCallback, + queryDataRecievedCallback, + resultsDataRecievingCallback, + resultsDataRecievedCallback, + disableElementsGeneralCallback, + enableElementsGeneralCallback, +} from './callbacks.js'; + +function recieveClientNotification(eventType, resultsList) { + document.addEventListener(eventType, (event) => { + let caseIdentifier = event.detail.caseIdentifier; + switch (caseIdentifier) { + case 'connecting': + console.info('View recieved notification:', caseIdentifier); + connectingCallback(resultsList, event.detail); + // execute callbacks + break; + case 'connected': + console.info('View recieved notification:', caseIdentifier); + connectedCallback(resultsList, event.detail); + break; + case 'connecting-failed': + console.info('View recieved notification:', caseIdentifier); + // execute callbacks + connectingFaildeCallback(resultsList, event.detail); + break; + case 'query-data-prepareing': + console.info('View recieved notification:', caseIdentifier); + // some extra hiding and showing (this should be done less confusing) + resultsList.fullResultsExport.classList.toggle('hide', true); + resultsList.subResultsExport.classList.toggle('hide', true); + resultsList.fullResultsCreate.classList.toggle('hide', false); + resultsList.subResultsCreate.classList.toggle('hide', false); + // execute callbacks + disableElementsGeneralCallback(resultsList, event.detail); + queryDataPreparingCallback(resultsList, event.detail); + break; + case 'query-data-recieving': + console.info('View recieved notification:', caseIdentifier); + // execute callbacks + queryDataRecievingCallback(resultsList, event.detail); + break; + case 'query-data-recieved': + console.info('View recieved notification:', caseIdentifier); + // execute callbacks + queryDataRecievedCallback(resultsList, event.detail); + enableElementsGeneralCallback(resultsList, event.detail); + // create sub-results is disabled per default until matches have been added + resultsList.subResultsCreate.classList.toggle('disabled', true); + break; + case 'results-data-recieving': + console.info('View recieved notification:', caseIdentifier); + // execute callbacks + disableElementsGeneralCallback(resultsList, event.detail); + resultsDataRecievedCallback(resultsList, event.detail); + break; + case 'results-data-recieved': + console.info('View recieved notification:', caseIdentifier); + // execute callbacks + console.info(event.detail); + enableElementsGeneralCallback(resultsList, event.detail); + resultsDataRecievedCallback(resultsList, event.detail); + break; + default: + console.error('Recieved unkown notification case identifier from Client'); + // do something to not crash the analysis session? + // maybe unnecessary + } + }); +} + +// export listeners +export { recieveClientNotification }; \ No newline at end of file diff --git a/web/app/static/js/modules/corpus_analysis/view/scrollToTop.js b/web/app/static/js/modules/corpus_analysis/view/scrollToTop.js new file mode 100644 index 00000000..302cdf4f --- /dev/null +++ b/web/app/static/js/modules/corpus_analysis/view/scrollToTop.js @@ -0,0 +1,21 @@ +/** + * Function to show a scroll to top button if the user has scrolled down + * 250 pixels from the headline element. + */ +function scrollToTop(scrollToElementSelector, triggerElementSelector) { + let headline = document.querySelector(scrollToElementSelector); + let scrollToTop = document.querySelector(triggerElementSelector); + 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/modules/corpus_analysis/view/spinner.js b/web/app/static/js/modules/corpus_analysis/view/spinner.js new file mode 100644 index 00000000..5c4b7422 --- /dev/null +++ b/web/app/static/js/modules/corpus_analysis/view/spinner.js @@ -0,0 +1,17 @@ +// loading spinner animation HTML +const loadingSpinnerHTML = ` +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + `; + +//export +export { loadingSpinnerHTML }; diff --git a/web/app/static/js/nopaque.CorpusAnalysisClient.js b/web/app/static/js/nopaque.CorpusAnalysisClient.js deleted file mode 100644 index 592d61de..00000000 --- a/web/app/static/js/nopaque.CorpusAnalysisClient.js +++ /dev/null @@ -1,205 +0,0 @@ -class CorpusAnalysisClient { - constructor(corpusId, socket) { - this.callbacks = {}; - this.corpusId = corpusId; - this.displays = {}; - this.socket = socket; - - // socket on event for corpous analysis initialization - socket.on("corpus_analysis_init", (response) => { - let errorText; - - if (response.code === 200) { - console.log(`corpus_analysis_init: ${response.code} - ${response.msg}`); - if (this.callbacks.init != undefined) { - this.callbacks.init(response.payload); - this.callbacks.get_metadata(); // should hold the function getMetaData - } - if (this.displays.init != undefined) { - this.displays.init.setVisibilityByStatus("success"); - } - } else { - 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}`); - } - }); - - // socket on event for recieving meta - socket.on('corpus_analysis_send_meta_data', (response) => { - let errorText; - - if (response.code === 200) { - console.log(`corpus_analysis_send_meta_data: ${response.code} - ${response.msg} - ${response.desc}`); - if (this.callbacks.recv_meta_data != undefined) { - this.callbacks.recv_meta_data(response.payload); - } - } else { - 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_send_meta_data: ${errorText}`); - } - }); - - // socket on event for recieveing query results - socket.on("corpus_analysis_query", (response) => { - let errorText; - - if (response.code === 200) { - console.log(`corpus_analysis_query: ${response.code} - ${response.msg}`); - if (this.callbacks.query != undefined) { - this.callbacks.query(response.payload); - } - if (this.displays.query != undefined) { - this.displays.query.setVisibilityByStatus("success"); - } - } else { - errorText = `Error ${response.payload.code} - ${response.payload.msg}`; - nopaque.flash(errorText, "error"); - if (this.displays.query.errorContainer != undefined) { - this.displays.query.errorContainer.innerHTML = `

    `+ - `error ${errorText}

    `; - } - if (this.displays.query != undefined) { - this.displays.query.setVisibilityByStatus("error"); - } - console.error(`corpus_analysis_query: ${errorText}`); - } - }); - - - socket.on("corpus_analysis_query_results", (response) => { - if (this.callbacks.query_results != undefined) { - this.callbacks.query_results(response.payload); - } - }); - - // inspect callback handeling based on type - socket.on("corpus_analysis_inspect_match", (response) => { - console.log(response); - if (response.type === "inspect") { - if (this.callbacks.query_match_context != undefined) { - this.callbacks.query_match_context(response); - } - } else if (response.type === "sub-results" - || response.type ==="results"){ - if (this.callbacks.save_sub_results_choices != undefined) { - this.callbacks.save_sub_results_choices(response); - } - } - }); - } - - init() { - if (this.displays.init.errorContainer != undefined) { - this.displays.init.errorContainer.innerHTML == ""; - } - if (this.displays.init != undefined) { - this.displays.init.setVisibilityByStatus("waiting"); - } - this.socket.emit("corpus_analysis_init", this.corpusId); - } - - getMetaData() { - // just emits this to tell the server to gather all meta data infos and send - // those back - this.socket.emit("corpus_analysis_get_meta_data", this.corpusId); - } - - query(queryStr) { - let displayOptionsData; - let resultListOptions; - - if (this.displays.query.errorContainer != undefined) { - this.displays.query.errorContainer.innerHTML == ""; - } - if (this.displays.query != undefined) { - this.displays.query.setVisibilityByStatus("waiting"); - } - nopaque.socket.emit("corpus_analysis_query", queryStr); - } - - setCallback(type, callback) { - // saves callback functions into an object. Key is function type, callback - // is the callback function - this.callbacks[type] = callback; - } - - setDisplay(type, display) { - this.displays[type] = display; - } -} - - -class CorpusAnalysisDisplay { - constructor(element) { - this.element = element; - 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"); - } - } - } -} diff --git a/web/app/static/js/nopaque.InteractionElement.js b/web/app/static/js/nopaque.InteractionElement.js deleted file mode 100644 index a1d9064b..00000000 --- a/web/app/static/js/nopaque.InteractionElement.js +++ /dev/null @@ -1,64 +0,0 @@ -class InteractionElement { - constructor(htmlId="", - checkStatus=true, - disabledBefore=true, - disabledAfter=false, - hideBefore=true, - hideAfter=false) { - this.htmlId = htmlId; - this.element = (htmlId) => {this.element = document.getElementById(htmlId);} - this.checkStatus = checkStatus; - this.callbacks = {}; - this.disabledBefore = disabledBefore; - this.disabledAfter = disabledAfter; - this.hideBefore = hideBefore; - this.hideAfter = hideAfter; - this.element(this.htmlId); - } - - setCallback(trigger, callback, bindThis, args=[]) { - this.callbacks[trigger] = { - "function": callback, - "bindThis": bindThis, - "args": args - }; - } - - bindThisToCallback(trigger) { - let callback = this.callbacks[trigger]; - let boundedCallback = callback["function"].bind(callback.bindThis); - return boundedCallback; - } -} - -class InteractionElements { - constructor() { - this.interactions = []; - } - - addInteractions (interactionsArray) { - this.interactions.push(...interactionsArray); - } - - onChangeExecute() { - // checks if a change for every interactionElement happens and executes - // the callbacks accordingly - for (let interaction of this.interactions) { - if (interaction.checkStatus) { - interaction.element.addEventListener("change", (event) => { - if (event.target.checked) { - let f_on = interaction.bindThisToCallback("on"); - let args_on = interaction.callbacks.on.args; - f_on(...args_on); - } else if (!event.target.checked){ - let f_off = interaction.bindThisToCallback("off"); - let args_off = interaction.callbacks.off.args; - f_off(...args_off); - } - }); - } else { - continue - } - }; - } -} diff --git a/web/app/static/js/nopaque.Results.js b/web/app/static/js/nopaque.Results.js deleted file mode 100644 index 2b3c1caa..00000000 --- a/web/app/static/js/nopaque.Results.js +++ /dev/null @@ -1,125 +0,0 @@ -class Results { - constructor(data, jsList , metaData) { - this.data = data; - this.jsList = jsList; - this.metaData = metaData - this.resultsData = new Data(); - this.subResultsData = new Data(); - } - - clearAll() { - this.jsList.clear(); - this.jsList.update(); - this.data.init(); - this.metaData.init(); - this.resultsData.init() - this.subResultsData.init(); - } - -} - - -class Data { - // Sets empty object structure. Also usefull to delete old results. - // matchCount default is 0 - init(matchCount = 0) { - 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) { - if (key !== null) { - Object.assign(this[key], jsonData); - } else if (key === null) { - Object.assign(this, jsonData) - } - } - - // get query as string from form Element - getQueryStr(queryFormElement) { - // gets query - let queryFormData; - let queryStr; - queryFormData = new FormData(queryFormElement); - queryStr = queryFormData.get("query-form-query"); - this["query"] = queryStr; - } - - // function creates a unique and safe filename for the download - createDownloadFilename(suffix) { - let today; - let currentDate; - let currentTime; - let safeFilename; - let resultFilename; - // get and create metadata - today = new Date(); - currentDate = `${today.getUTCFullYear()}` + - `-${(today.getUTCMonth() + 1)}` + - `-${today.getUTCDate()}`; - currentTime = `${today.getUTCHours()}h` + - `${today.getUTCMinutes()}m` + - `${today.getUTCSeconds()}s`; - safeFilename = this.query.replace(/[^a-z0-9_-]/gi, "_"); - resultFilename = `UTC-${currentDate}_${currentTime}_${safeFilename}_${suffix}`; - return resultFilename - } - - // 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!"); - let file; - filename += filenameSlug; - file = new Blob([dataStr], {type: type}); - var url = URL.createObjectURL(file); - downloadElement.href = url; - downloadElement.download = filename; - } - - // function to download the results as JSON - downloadJSONRessource(resultFilename, downloadData, downloadElement) { - let dataStr; - // stringify JSON object for json download - // use tabs to save some space - dataStr = JSON.stringify(downloadData, undefined, "\t"); - // start actual download - this.download(downloadElement, dataStr, resultFilename, "text/json", ".json") - } - - // create results data either from all results or from al lmarked sub results - createResultsData(type) { - // deactivate inspect, because cqp server cannot handle multiple requests - results.jsList.deactivateInspect(); - activateInspectInteraction.setCallback("noCheck", - results.jsList.deactivateInspect, - results.jsList); - // set flag that results are being created to avoid reactivation of - // sub results creation if marked matches are changed - resultCreationRunning = true; - console.log(resultCreationRunning); - if (type === "sub-results") { - resultsCreateElement.classList.add("disabled"); // cqp server cannot handle more than one request at a time. Thus we deactivate the resultsCreateElement - let tmp = [...results.jsList.addToSubResultsIdsToShow].sort(function(a, b){return a-b}); - let dataIndexes = []; - tmp.forEach((index) => dataIndexes.push(index - 1)); - results.jsList.getMatchWithContext(dataIndexes, "sub-results"); - } else if (type === "results") { - subResultsCreateElement.classList.add("disabled"); // cqp server cannot handle more than one request at a time. Thus we deactivate the subResultsCreateElement - let dataIndexes = [...Array(results.data.match_count).keys()]; - results.jsList.getMatchWithContext(dataIndexes, "results"); - } - } -} - -class MetaData { - // Sets empty object structure when no input is given. - // if json object like input is given class fields are created from this - init(json = {}) { - Object.assign(this, json); - } -} \ 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..306d706e 100644 --- a/web/app/static/js/nopaque.lists.js +++ b/web/app/static/js/nopaque.lists.js @@ -423,764 +423,4 @@ 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. - } - - helperCreateCpos(cpos_ranges, cpos_values) { - let lc; - let c; - let rc; - if (cpos_ranges) { - // python range like function from MDN - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Sequence_generator_(range) - const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step)); - lc = range(cpos_values.lc[0], cpos_values.lc[1], 1) - c = range(cpos_values.c[0], cpos_values.c[1], 1) - rc = range(cpos_values.rc[0], cpos_values.rc[1], 1) - } else { - lc = cpos_values.lc; - c = cpos_values.c; - rc = cpos_values.rc; - } - return {lc: lc, c: c, rc: rc}; - } - - // handels interactionElements during a pagination navigation - // loops over interactionElements and executes callback functions accordingly - pageChangeEventInteractionHandler(interactionElements) { - // get elements to check thier status - for (let interaction of interactionElements.interactions) { - if (interaction.checkStatus) { - if (interaction.element.checked) { - let f_on = interaction.bindThisToCallback("on"); - let args_on = interaction.callbacks.on.args; - f_on(...args_on); - } else { - let f_off = interaction.bindThisToCallback("off"); - let args_off = interaction.callbacks.off.args; - f_off(...args_off); - } - } else { - let f = interaction.bindThisToCallback("noCheck"); - let args = interaction.callbacks.noCheck.args; - f(...args); - } - } - } - - // get display options from display options form element - static getDisplayOptions(displayOptionsFormElement) { - // gets display options parameters - let displayOptionsFormData - let displayOptionsData; - displayOptionsFormData = new FormData(displayOptionsFormElement); - displayOptionsData = - { - "resultsPerPage": displayOptionsFormData.get("display-options-form-results_per_page"), - "resultsContex": displayOptionsFormData.get("display-options-form-result_context"), - "expertMode": displayOptionsFormData.get("display-options-form-expert_mode") - }; - return displayOptionsData - } - - // ###### Functions to add one match to a sub-results ###### - // activate the add buttons - activateAddToSubResults() { - subResultsIdListElement.classList.remove("hide"); - if (subResultsExportElement.classList.contains("hide")) { - subResultsCreateElement.classList.remove("hide"); - } - let addToSubResultsBtnElements = document.getElementsByClassName("add"); - for (let addToSubResultsBtn of addToSubResultsBtnElements) { - addToSubResultsBtn.classList.remove("hide"); - } - } - // deactivate the add buttons - deactivateAddToSubResults() { - subResultsIdListElement.classList.add("hide"); - subResultsCreateElement.classList.add("hide"); - let addToSubResultsBtnElements = document.getElementsByClassName("add"); - for (let addToSubResultsBtn of addToSubResultsBtnElements) { - addToSubResultsBtn.classList.add("hide"); - } - } - - // Used in addToSubResults and inspect to toggle the design of the check - // buttons according to its checked unchecked status. - helperActivateBtn(btn) { - btn.classList.remove("grey"); - btn.classList.add("green"); - btn.textContent = "check"; - } - - // Used in addToSubResults and inspect to toggle the design of the check - // buttons according to its checked unchecked status. - helperDeactivateBtn(btn) { - btn.classList.remove("green"); - btn.classList.add("grey"); - btn.textContent = "add"; - } - - // Either adds or removes a match to the sub-results. For this it checks - // onclick if the current button has been checked or not. For this the - // function checks if its status in addToSubResultsStatus is either flase or - // true. Adds match to sub-results if status is false if status is true it - // removes it. - addToSubResults(dataIndex, tableCall=true) { - let textarea = subResultsIdListElement.getElementsByTagName("textarea")[0]; - if (!this.addToSubResultsStatus[dataIndex] - || this.addToSubResultsStatus === undefined) { - // add button is activated because status is either false or undefined - this.helperActivateBtn(event.target); - this.addToSubResultsStatus[dataIndex] = true; // sets status to true - this.addToSubResultsIdsToShow.add(dataIndex + 1); // + 1 because user does not see zero indexd data indexes - textarea.textContent = [...this.addToSubResultsIdsToShow].sort(function(a, b){return a-b}).join(", "); // automaticalle sorts ids into the textarea in ascending order - M.textareaAutoResize(textarea); // after an insert textarea has to be resized manually - nrMarkedMatches.textContent = [...this.addToSubResultsIdsToShow].length; - } else if (this.addToSubResultsStatus[dataIndex]) { - // add button is deactivated because status is true - this.helperDeactivateBtn(event.target); - this.addToSubResultsStatus[dataIndex] = false; // sets status to false - this.addToSubResultsIdsToShow.delete(dataIndex + 1); // + 1 because user does not see zero indexd data indexes - textarea.textContent = [...this.addToSubResultsIdsToShow].sort(function(a, b){return a-b}).join(", "); // automaticalle sorts ids into the textarea in ascending order - nrMarkedMatches.textContent = [...this.addToSubResultsIdsToShow].length; - M.textareaAutoResize(textarea); // after an insert textarea has to be resized manually - } - // Toggles the create button according to the number of ids in addToSubResultsIdsToShow - if ([...this.addToSubResultsIdsToShow].length > 0) { - subResultsCreateElement.classList.remove("disabled"); - } else if ([...this.addToSubResultsIdsToShow].length === 0) { - subResultsCreateElement.classList.add("disabled"); - } - if (resultCreationRunning) { - subResultsCreateElement.classList.add("disabled"); - } - // After a match as been added or removed the export button will be - // hidden because the sub-results have been altered and have to be built - // again. Thus subResultsCreateElement has to be shown again. - subResultsExportElement.classList.add("hide"); - subResultsCreateElement.classList.remove("hide"); - // Also activate/deactivate buttons in the table/jsList results accordingly - //if button in inspect was activated/deactivated. - // This part only runs if tableCall is false. - if (!tableCall) { - let tableAddBtn = document.getElementById("query-results").querySelectorAll(`[data-index="${dataIndex}"]`)[0].getElementsByClassName('add')[0].firstElementChild; // gets the add button from the list view - if (this.addToSubResultsStatus[dataIndex]) { - this.helperActivateBtn(tableAddBtn); - } else { - this.helperDeactivateBtn(tableAddBtn); - } - } - } - - // Triggers emit to get full match context from server for a number of - // matches identified by their data_index. - getMatchWithContext(dataIndexes, type) { - let tmp_first_cpos = []; - let tmp_last_cpos = []; - for (let dataIndex of dataIndexes) { - tmp_first_cpos.push(results.data.matches[dataIndex].c[0]); - tmp_last_cpos.push(results.data.matches[dataIndex].c[1]); - } - nopaque.socket.emit("corpus_analysis_inspect_match", - { - type: type, - data_indexes: dataIndexes, - first_cpos: tmp_first_cpos, - last_cpos: tmp_last_cpos, - } - ); - } - - // ###### Functions to inspect one match, to show more details ###### - // activate inspect buttons if progress is 100 - activateInspect() { - if (progress === 100) { - let inspectBtnElements; - inspectBtnElements = document.getElementsByClassName("inspect"); - for (let inspectBtn of inspectBtnElements) { - inspectBtn.classList.remove("disabled"); - } - } else { - return - } - } - - // deactivate inspect buttons - deactivateInspect() { - let inspectBtnElements; - inspectBtnElements = document.getElementsByClassName("inspect"); - for (let inspectBtn of inspectBtnElements) { - inspectBtn.classList.add("disabled"); - } - } - - // ### functions to inspect imported Matches - // This function creates an object that is similar to the object that is - // being recieved as an answere to the getMatchWithContext Method, which is - // triggering an socket.io event. - // It is used as an input for show match context in the context of imported - // results to be able to inspect matches. - createFakeResponse() { - contextModal.open(); - // match nr for user to display derived from data_index - let contextMatchNrElement = document.getElementById("context-match-nr"); - contextMatchNrElement.textContent = this.contextId + 1; - let cpos_lookup; - let fake_response = {}; - let contextResultsElement; - // function to create one match object from entire imported results - // that is passed into the results.jsList.showMatchContext() function - fake_response["payload"] = {}; - let dataIndex = event.target.closest("tr").dataset.index; - this.contextId = dataIndex; - fake_response.payload["matches"] = [results.data.matches[dataIndex]]; - contextResultsElement = document.getElementById("context-results"); - contextResultsElement.innerHTML = ""; - let {lc, c, rc} = this.helperCreateCpos(results.data.cpos_ranges, - fake_response.payload.matches[0]); - cpos_lookup = {}; - for (let cpos of lc) { - cpos_lookup[cpos] = results.data.cpos_lookup[cpos]; - } - for (let cpos of c) { - cpos_lookup[cpos] = results.data.cpos_lookup[cpos]; - } - for (let cpos of rc) { - cpos_lookup[cpos] = results.data.cpos_lookup[cpos]; - } - fake_response.payload["cpos_lookup"] = cpos_lookup - fake_response.payload["cpos_ranges"] = results.data.cpos_ranges; - fake_response.payload["query"] = results.data.query; - fake_response.payload["context_id"] = dataIndex + 1; - fake_response.payload["match_count"] = fake_response.payload.matches.length - fake_response.payload["corpus_type"] = "inspect-result" - return fake_response - } - - // gets result cpos infos for one dataIndex (list of length 1) to send back to - // the server - inspect(dataIndex, type) { - let contextResultsElement; - // get result infos from server and show them in context modal - this.contextId = dataIndex[0]; - contextResultsElement = document.getElementById("context-results"); - contextResultsElement.innerHTML = ""; // clear it from old inspects - this.getMatchWithContext(dataIndex, type); - // match nr for user to display derived from data_index - let contextMatchNrElement = document.getElementById("context-match-nr"); - contextMatchNrElement.textContent = this.contextId + 1; - contextModal.open(); - // add a button to add this match to sub results with onclick event - let classes = `btn-floating btn waves-effect` + - `waves-light grey right` - let addToSubResultsIdsBtn = document.createElement("a"); - addToSubResultsIdsBtn.setAttribute("class", classes + ` add`); - addToSubResultsIdsBtn.innerHTML = 'add'; - addToSubResultsIdsBtn.onclick= () => {this.addToSubResults(dataIndex[0], false)}; - // checks if a button has already been added to the inspect modal and removes it - if (addToSubResultsFromInspectElement.children.length > 0) { - addToSubResultsFromInspectElement.firstElementChild.remove(); - } - // Changes the design of the add button according to its checked status - // upon opening the inspect modal. - if (this.addToSubResultsStatus[dataIndex[0]]) { - this.helperActivateBtn(addToSubResultsIdsBtn.firstElementChild); - } else if (!this.addToSubResultsStatus[dataIndex[0]]) { - this.helperDeactivateBtn(addToSubResultsIdsBtn.firstElementChild); - } - addToSubResultsFromInspectElement.appendChild(addToSubResultsIdsBtn); - } - - // create Element from HTML String helper function - HTMLTStrToElement(htmlStr) { - // https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518 - let template = document.createElement("template"); - htmlStr = htmlStr.trim(); - template.innerHTML = htmlStr; - return template.content.firstChild; - } - - // Used as a callback to handle incoming match context results when inspect - // has been used. - showMatchContext(response) { - this.contextData; - let contextModalLoading; - let contextModalReady; - let contextResultsElement; - let highlightSentencesSwitchElement; - let htmlTokenStr; - let modalExpertModeSwitchElement; - let modalTokenElements; - let nrOfContextSentences; - let partElement; - let token; - let tokenHTMLArray; - let tokenHTMlElement; - let uniqueContextS; - let uniqueS; - - this.contextData = response.payload; - console.log(this.contextData); - this.contextData["cpos_ranges"] = response.payload.cpos_ranges; - this.contextData["query"] = results.data.query; - this.contextData["context_id"] = this.contextId; - this.contextData["match_count"] = this.contextData.matches.length - this.contextData["corpus_type"] = "inspect-result" - Object.assign(this.contextData, results.metaData); - contextResultsElement = document.getElementById("context-results"); - modalExpertModeSwitchElement = document.getElementById("inspect-display-options-form-expert_mode_inspect"); - highlightSentencesSwitchElement = document.getElementById("inspect-display-options-form-highlight_sentences"); - nrOfContextSentences = document.getElementById("context-sentences"); - uniqueS = new Set(); - uniqueContextS = new Set(); - let {lc, c, rc} = this.helperCreateCpos(this.contextData.cpos_ranges, - this.contextData.matches[0]) - // create sentence strings as tokens - tokenHTMLArray = []; - for (let cpos of lc) { - token = this.contextData.cpos_lookup[cpos]; - uniqueS.add(token.s) - htmlTokenStr = `` + - `${token.word}` + - ``; - tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr) - tokenHTMLArray.push(tokenHTMlElement); - } - for (let cpos of c) { - token = this.contextData.cpos_lookup[cpos]; - uniqueContextS.add(token.s); - uniqueS.add(token.s); - htmlTokenStr = `` + - `${token.word}` + - ``; - tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr) - tokenHTMLArray.push(tokenHTMlElement); - } - this.contextData["context_s_ids"] = Array.from(uniqueContextS); - for (let cpos of rc) { - token = this.contextData.cpos_lookup[cpos]; - uniqueS.add(token.s) - htmlTokenStr = `` + - `${token.word}` + - ``; - tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr) - tokenHTMLArray.push(tokenHTMlElement); - } - for (let sId of uniqueS) { - let htmlSentence = ``; - let sentenceElement = this.HTMLTStrToElement(htmlSentence); - for (let tokenElement of tokenHTMLArray) { - if (tokenElement.dataset.sid == sId) { - sentenceElement.appendChild(tokenElement); - sentenceElement.insertAdjacentHTML("beforeend", ` `); - } else { - continue; - } - } - contextResultsElement.appendChild(sentenceElement); - } - - - // add inspect display options events - modalExpertModeSwitchElement.onchange = (event) => { - if (event.target.checked) { - this.expertModeOn("context-results"); - } else { - this.expertModeOff("context-results") - } - }; - - highlightSentencesSwitchElement.onchange = (event) => { - if (event.target.checked) { - this.higlightContextSentences(); - } else { - this.unhighlightContextSentences(); - } - }; - - nrOfContextSentences.onchange = (event) => { - // console.log(event.target.value); - this.changeSentenceContext(event.target.value); - } - - // checks on new modal opening if switches are checked - // if switches are checked functions are executed - if (modalExpertModeSwitchElement.checked) { - this.expertModeOn("context-results"); - } - - if (highlightSentencesSwitchElement.checked) { - this.higlightContextSentences(); - } - - // checks the value of the number of sentences to show on modal opening - // sets context sentences accordingly - this.changeSentenceContext(nrOfContextSentences.value) - } - - // splits context text into sentences based on spacy sentence split - higlightContextSentences() { - let sentences; - sentences = document.getElementById("context-results").getElementsByClassName("sentence"); - for (let s of sentences) { - s.insertAdjacentHTML("beforeend", `

    `) - } - } - - unhighlightContextSentences() { - let sentences; - let br; - sentences = document.getElementById("context-results").getElementsByClassName("sentence"); - for (let s of sentences) { - br = s.lastChild; - br.remove(); - } - } - - // changes how many context sentences in inspect view are shown - changeSentenceContext(sValue, maxSValue=10) { - let array; - let sentences; - let toHideArray; - let toShowArray; - sValue = maxSValue - sValue; - // console.log(sValue); - sentences = document.getElementById("context-results").getElementsByClassName("sentence"); - array = Array.from(sentences); - if (sValue != 0) { - toHideArray = array.slice(0, sValue).concat(array.slice(-(sValue))); - toShowArray = array.slice(sValue, 9).concat(array.slice(9, -(sValue))) - } else { - toHideArray = []; - toShowArray = array; - } - // console.log(array); - // console.log("#######"); - // console.log(toHideArray); - for (let s of toHideArray) { - s.classList.add("hide"); - } - for (let s of toShowArray) { - s.classList.remove("hide"); - } - } - - // ###### Display options changing live how the matches are being displayed ###### - - // Event function that changes the shown hits per page. - // Just alters the resultsList.page property - changeHitsPerPage(event) { - try { - // console.log(this); - this.page = event.target.value; - this.update(); - this.activateInspect(); - this.pageChangeEventInteractionHandler(interactionElements); - if (expertModeSwitchElement.checked) { - this.expertModeOn("query-display"); // page holds new result rows, so add new tooltips - } - nopaque.flash("Updated matches per page.", "corpus") - } catch (e) { - // console.log(e); - // console.log("resultsList has no results right now."); - } - } - - // Event function triggered on context select change - // also if pagination is clicked - changeContext(event) { - let array; - let lc; - let newContextValue; - let rc; - try { - if (event.type === "change") { - nopaque.flash("Updated context per match!", "corpus"); - } - } catch (e) { - } finally { - newContextValue = document.getElementById("display-options-form-result_context").value; - lc = document.getElementsByClassName("left-context"); - rc = document.getElementsByClassName("right-context"); - for (let element of lc) { - array = Array.from(element.childNodes); - for (let element of array.reverse().slice(newContextValue)) { - element.classList.add("hide"); - } - for (let element of array.slice(0, newContextValue)) { - element.classList.remove("hide"); - } - } - for (let element of rc) { - array = Array.from(element.childNodes); - for (let element of array.slice(newContextValue)) { - element.classList.add("hide"); - } - for (let element of array.slice(0, newContextValue)) { - element.classList.remove("hide"); - } - } - } - } - - // ###### Expert view event functions ###### - // function to create a tooltip for the current hovered token - tooltipEventCreate(event) { - // console.log("Create Tooltip on mouseover."); - let token; - token = results.data.cpos_lookup[event.target.dataset.cpos]; - if (!token) { - token = this.contextData.cpos_lookup[event.target.dataset.cpos]; - } - this.addToolTipToTokenElement(event.target, token); - } - - // Function to destroy the current Tooltip for the current hovered tooltip - // on mouse leave - tooltipEventDestroy(event) { - // 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 - 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; - this.eventTokens[htmlId].push(tokenElement); - } - } - - // fuction that creates Tooltip for one token and extracts the corresponding - // infos from the result JSON - addToolTipToTokenElement(tokenElement, token) { - this.currentTooltipElement; - this.currentTooltipElement = M.Tooltip.init(tokenElement, - {"html": ` - - - - - - - - -
    Token informationSource information
    - Word: ${token.word}
    - Lemma: ${token.lemma}
    - POS: ${token.pos}
    - Simple POS: ${token.simple_pos}
    - NER: ${token.ner} -
    - Title: ${results.data.text_lookup[token.text].title} -
    - Author: ${results.data.text_lookup[token.text].author} -
    - Publishing year: ${results.data.text_lookup[token.text].publishing_year} -
    `} - ); - } - - // function to remove extra informations and animations from tokens - expertModeOff(htmlId) { - // console.log("Expert mode is off."); - if (!Array.isArray(this.currentExpertTokenElements[htmlId])) { - this.currentExpertTokenElements[htmlId] = []; - } - if (!Array.isArray(this.eventTokens[htmlId])) { - this.eventTokens[htmlId] = []; - } - for (let tokenElement of this.currentExpertTokenElements[htmlId]) { - tokenElement.classList.remove("chip", "hoverable", "expert-view"); - } - this.currentExpertTokenElements[htmlId] = []; - - for (let eventToken of this.eventTokens[htmlId]) { - eventToken.onmouseover = ""; - eventToken.onmouseout = ""; - } - this.eventTokens[htmlId] = []; - } - - createResultRowElement(item, chunk, imported=false) { - let aCellElement; - let addToSubResultsBtn; - let cCellElement; - let cpos; - let fakeResponse; // used if imported results are being created; - let inspectBtn - let lcCellElement; - let matchNrElement; - let matchRowElement; - let rcCellElement; - let textTitles; - let textTitlesCellElement; - let token; - let values; - // gather values from item - values = item.values(); - let {lc, c, rc} = this.helperCreateCpos(chunk.cpos_ranges, - values) - // get infos for full match row - matchRowElement = document.createElement("tr"); - matchRowElement.setAttribute("data-index", values.index) - lcCellElement = document.createElement("td"); - lcCellElement.classList.add("left-context"); - matchRowElement.appendChild(lcCellElement); - for (cpos of lc) { - token = chunk.cpos_lookup[cpos]; - lcCellElement.insertAdjacentHTML("beforeend", - `${token.word} `); - } - - // get infos for hit of match and set actions - textTitles = new Set(); - aCellElement = document.createElement("td"); - aCellElement.classList.add("actions"); - cCellElement = document.createElement("td"); - cCellElement.classList.add("match-hit"); - textTitlesCellElement = document.createElement("td"); - textTitlesCellElement.classList.add("titles"); - matchNrElement = document.createElement("td"); - matchNrElement.classList.add("match-nr"); - matchRowElement.appendChild(cCellElement); - matchRowElement.appendChild(aCellElement); - for (cpos of c) { - token = chunk.cpos_lookup[cpos]; - cCellElement.insertAdjacentHTML("beforeend", - `${token.word} `); - // get text titles of every hit cpos token - textTitles.add(chunk.text_lookup[token.text].title); - } - // add some interaction buttons - // # some btn css rules and classes - let css = `margin-right: 5px; margin-bottom: 5px;` - let classes = `btn-floating btn waves-effect` + - `waves-light grey` - // # add button to trigger more context to every match td - inspectBtn = document.createElement("a"); - inspectBtn.setAttribute("style", css); - inspectBtn.setAttribute("class", classes + ` disabled inspect` - ); - inspectBtn.innerHTML = 'search'; - // # add btn to add matches to sub-results. hidden per default - addToSubResultsBtn = document.createElement("a"); - addToSubResultsBtn.setAttribute("style", css); - addToSubResultsBtn.setAttribute("class", classes + ` hide add` - ); - addToSubResultsBtn.innerHTML = 'add'; - aCellElement.appendChild(inspectBtn); - aCellElement.appendChild(addToSubResultsBtn); - // add text titles at front as first td of one row - textTitlesCellElement.textContent = [...textTitles].join(", "); - matchRowElement.insertAdjacentHTML("afterbegin", textTitlesCellElement.outerHTML); - matchNrElement.textContent = values.index + 1; - matchRowElement.insertAdjacentHTML("afterbegin", matchNrElement.outerHTML); - - // get infos for right context of match - rcCellElement = document.createElement("td"); - rcCellElement.classList.add("right-context"); - matchRowElement.appendChild(rcCellElement); - for (cpos of rc) { - token = chunk.cpos_lookup[cpos]; - rcCellElement.insertAdjacentHTML("beforeend", - `${token.word} `); - } - return matchRowElement - } - - // creates the HTML table code for the metadata vie in the corpus analysis interface - createMetaDataForModal(metaDataObject) { - let html = `
    - - - - - - - - ` - for (let [outerKey, outerValue] of Object.entries(metaDataObject)) { - html += ` - ` - if (outerKey === "corpus_all_texts" || outerKey === "text_lookup") { - html += `` - } else { - html += `` - } - html += `` - } - html += ` -
    Metadata DescriptionValue
    ${outerKey.replace(/_/g, " ")} -
      ` - for (let [innerKey, innerValue] of Object.entries(outerValue)) { - html += `` - } - html += `
    -
    ${outerValue}
    ` - return html - } - - // Creates the text details for the texts shown in the corpus analysis metadata modal. - createTextDetails(metaDataObject) { - let metadataKey = event.target.dataset.metadataKey; - let textKey = event.target.dataset.textKey; - let textData = metaDataObject[metadataKey][textKey]; - let bibliographicData = document.getElementById(`bibliographic-data-${metadataKey}-${textKey}`); - bibliographicData.innerHTML = ""; - for (let [key, value] of Object.entries(textData)) { - bibliographicData.insertAdjacentHTML("afterbegin", - ` -
  • ${key}: ${value}
  • - `); - } - } - -} +export { RessourceList, }; 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 1ca61ba3..d940e6ff 100644 --- a/web/app/templates/corpora/analyse_corpus.html.j2 +++ b/web/app/templates/corpora/analyse_corpus.html.j2 @@ -41,8 +41,7 @@
    -
    -
    +
    {% include 'interactions/infos.html.j2' %} {% include 'interactions/export.html.j2' %} {% include 'interactions/create.html.j2' %} @@ -65,376 +64,304 @@ {% include 'modals/export_query_results.html.j2' %} {% include 'modals/context_modal.html.j2' %} + + - - - - {% 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/modals/analysis_init.html.j2 b/web/app/templates/modals/analysis_init.html.j2 index dfe2b948..b7e54026 100644 --- a/web/app/templates/modals/analysis_init.html.j2 +++ b/web/app/templates/modals/analysis_init.html.j2 @@ -1,12 +1,17 @@ -