Merge branch 'javascript-rework' into development

This commit is contained in:
Stephan Porada 2020-09-08 10:48:48 +02:00
commit 3d76a585a8
28 changed files with 2593 additions and 1770 deletions

View File

@ -30,46 +30,63 @@ def init_corpus_analysis(corpus_id):
corpus_id, current_user.id, request.sid) corpus_id, current_user.id, request.sid)
@socketio.on('corpus_analysis_get_meta_data') @socketio.on('corpus_analysis_meta_data')
@socketio_login_required @socketio_login_required
def corpus_analysis_get_meta_data(corpus_id): def corpus_analysis_get_meta_data(corpus_id):
# get meta data from db # get meta data from db
db_corpus = Corpus.query.get(corpus_id) db_corpus = Corpus.query.get(corpus_id)
# TODO: Check if current user is actually the creator of the corpus?
metadata = {} metadata = {}
metadata['corpus_name'] = db_corpus.title metadata['corpus_name'] = db_corpus.title
metadata['corpus_description'] = db_corpus.description metadata['corpus_description'] = db_corpus.description
metadata['corpus_creation_date'] = db_corpus.creation_date.isoformat() metadata['corpus_creation_date'] = db_corpus.creation_date.isoformat()
metadata['corpus_last_edited_date'] = db_corpus.last_edited_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_analysis_clients.get(request.sid)
client_corpus = client.corpora.get('CORPUS') if client is None:
metadata['corpus_properties'] = client_corpus.attrs['properties'] response = {'code': 424, 'desc': 'No client found for this session',
metadata['corpus_size_tokens'] = client_corpus.attrs['size'] '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') text_attr = client_corpus.structural_attributes.get('text')
struct_attrs = client_corpus.structural_attributes.list(filters={'part_of': text_attr}) struct_attrs = client_corpus.structural_attributes.list(filters={'part_of': text_attr})
text_ids = range(0, (text_attr.attrs['size'])) text_ids = range(0, (text_attr.attrs['size']))
texts_metadata = {} texts_metadata = {}
for text_id in text_ids: for text_id in text_ids:
texts_metadata[text_id] = {} texts_metadata[text_id] = {}
for struct_attr in struct_attrs: 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] 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_all_texts'] = texts_metadata
metadata['corpus_analysis_date'] = datetime.utcnow().isoformat() metadata['corpus_analysis_date'] = datetime.utcnow().isoformat()
metadata['corpus_cqi_py_protocol_version'] = client.api.version metadata['corpus_cqi_py_protocol_version'] = client.api.version
metadata['corpus_cqi_py_package_version'] = cqi.__version__ metadata['corpus_cqi_py_package_version'] = cqi.__version__
metadata['corpus_cqpserver_version'] = 'CQPserver v3.4.22' # TODO: make this dynamically metadata['corpus_cqpserver_version'] = 'CQPserver v3.4.22' # TODO: make this dynamically
# write some metadata to the db # write some metadata to the db
db_corpus.current_nr_of_tokens = metadata['corpus_size_tokens'] db_corpus.current_nr_of_tokens = metadata['corpus_size_tokens']
db.session.commit() db.session.commit()
# emit data # emit data
payload = metadata payload = metadata
response = {'code': 200, 'desc': 'Corpus meta data', 'msg': 'OK', response = {'code': 200, 'desc': 'Corpus meta data', 'msg': 'OK',
'payload': payload} 'payload': payload}
socketio.emit('corpus_analysis_send_meta_data', response, room=request.sid) 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') @socketio.on('corpus_analysis_query')
@ -100,7 +117,6 @@ def corpus_analysis_query(query):
'match_count': results.attrs['size']} 'match_count': results.attrs['size']}
response = {'code': 200, 'desc': None, 'msg': 'OK', 'payload': payload} response = {'code': 200, 'desc': None, 'msg': 'OK', 'payload': payload}
socketio.emit('corpus_analysis_query', response, room=request.sid) socketio.emit('corpus_analysis_query', response, room=request.sid)
# TODO: Stop here and add a new method for transmission
chunk_size = 100 chunk_size = 100
chunk_start = 0 chunk_start = 0
context = 50 context = 50
@ -142,6 +158,11 @@ def corpus_analysis_inspect_match(payload):
socketio.emit('corpus_analysis_inspect_match', response, socketio.emit('corpus_analysis_inspect_match', response,
room=request.sid) room=request.sid)
return return
if client.status == 'running':
client.status = 'abort'
while client.status != 'ready':
socketio.sleep(0.1)
client.status = 'running'
try: try:
corpus = client.corpora.get('CORPUS') corpus = client.corpora.get('CORPUS')
s = corpus.structural_attributes.get('s') s = corpus.structural_attributes.get('s')
@ -171,6 +192,7 @@ def corpus_analysis_inspect_match(payload):
'type': type, 'type': type,
'data_indexes': data_indexes} 'data_indexes': data_indexes}
socketio.emit('corpus_analysis_inspect_match', response, room=request.sid) 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): def corpus_analysis_session_handler(app, corpus_id, user_id, session_id):

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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};

View File

@ -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: `<span></span>`
};
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 = '<i class="material-icons">add</i>';
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 = `<span class="token"` +
`data-sid="${token.s}"` +
`data-cpos="${cpos}">` +
`${token.word}` +
`</span>`;
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 = `<span class="token bold light-green"` +
`data-sid="${token.s}"` +
`data-cpos="${cpos}"` +
`style="text-decoration-line: underline;">` +
`${token.word}` +
`</span>`;
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 = `<span class="token"` +
`data-sid="${token.s}"` +
`data-cpos="${cpos}">` +
`${token.word}` +
`</span>`;
tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr)
tokenHTMLArray.push(tokenHTMlElement);
}
for (let sId of uniqueS) {
let htmlSentence = `<span class="sentence" data-sid="${sId}"></span>`;
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", `<span><br><br></span>`)
}
}
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": `<table>
<tr>
<th>Token information</th>
<th>Source information</th>
</tr>
<tr>
<td class="left-align">
Word: ${token.word}<br>
Lemma: ${token.lemma}<br>
POS: ${token.pos}<br>
Simple POS: ${token.simple_pos}<br>
NER: ${token.ner}
</td>
<td class="left-align">
Title: ${results.data.text_lookup[token.text].title}
<br>
Author: ${results.data.text_lookup[token.text].author}
<br>
Publishing year: ${results.data.text_lookup[token.text].publishing_year}
</td>
</tr>
</table>`}
);
}
// 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",
`<span class="token" data-cpos="${cpos}">${token.word} </span>`);
}
// 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",
`<span class="token" data-cpos="${cpos}">${token.word} </span>`);
// 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 = '<i class="material-icons inspect-btn">search</i>';
// # 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 = '<i class="material-icons add-btn">add</i>';
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",
`<span class="token" data-cpos="${cpos}">${token.word} </span>`);
}
return matchRowElement
}
// creates the HTML table code for the metadata view in the corpus analysis interface
createMetaDataForModal(metaDataObject) {
let html = `<div class="col s12">
<table class="highlight">
<thead>
<tr>
<th>Metadata Description</th>
<th>Value</th>
</tr>
</thead>
<tbody>`
for (let [outerKey, outerValue] of Object.entries(metaDataObject)) {
html += `<tr>
<td style="text-transform: uppercase;">${outerKey.replace(/_/g, " ")}</td>`
if (outerKey === "corpus_all_texts" || outerKey === "text_lookup") {
html += `<td>
<ul class="collapsible">`
for (let [innerKey, innerValue] of Object.entries(outerValue)) {
html += `<li class="text-metadata"
data-metadata-key="${outerKey}"
data-text-key="${innerKey}">
<div class="collapsible-header"
data-metadata-key="${outerKey}"
data-text-key="${innerKey}">
<i class="material-icons"
data-metadata-key="${outerKey}"
data-text-key="${innerKey}">info_outline</i>
${innerValue['author']} - ${innerValue['publishing_year']} -
${innerValue['title']}
</div>
<div class="collapsible-body">
<span>
<ul id="bibliographic-data-${outerKey}-${innerKey}"
style="column-count: 2;">
</ul>
</span>
</div>
</li>`
}
html += `</ul>
</td>`
} else {
html += `<td>${outerValue}</td>`
}
html += `</tr>`
}
html += `</tbody>
</table>`
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",
`
<li><span style="text-transform: capitalize;">${key}:</span> ${value}</li>
`);
}
}
};
// export classses
export { ViewEventListener, ResultsList };

View File

@ -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',
`<i class="material-icons left">build</i>`);
// 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,
};

View File

@ -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 };

View File

@ -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 };

View File

@ -0,0 +1,17 @@
// loading spinner animation HTML
const loadingSpinnerHTML = `
<div class="preloader-wrapper button-icon-spinner small active">
<div class="spinner-layer spinner-green-only">
<div class="circle-clipper left">
<div class="circle"></div>
</div><div class="gap-patch">
<div class="circle"></div>
</div><div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
`;
//export
export { loadingSpinnerHTML };

View File

@ -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 = `<p class="red-text">` +
`<i class="material-icons tiny">error</i> ${errorText}</p>`;
}
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 = `<p class="red-text">` +
`<i class="material-icons tiny">error</i> ${errorText}</p>`;
}
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 = `<p class="red-text">`+
`<i class="material-icons tiny">error</i> ${errorText}</p>`;
}
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");
}
}
}
}

View File

@ -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
}
};
}
}

View File

@ -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);
}
}

View File

@ -423,764 +423,4 @@ RessourceList.options = {
}, },
}; };
export { RessourceList, };
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 = '<i class="material-icons">add</i>';
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 = `<span class="token"` +
`data-sid="${token.s}"` +
`data-cpos="${cpos}">` +
`${token.word}` +
`</span>`;
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 = `<span class="token bold light-green"` +
`data-sid="${token.s}"` +
`data-cpos="${cpos}"` +
`style="text-decoration-line: underline;">` +
`${token.word}` +
`</span>`;
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 = `<span class="token"` +
`data-sid="${token.s}"` +
`data-cpos="${cpos}">` +
`${token.word}` +
`</span>`;
tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr)
tokenHTMLArray.push(tokenHTMlElement);
}
for (let sId of uniqueS) {
let htmlSentence = `<span class="sentence" data-sid="${sId}"></span>`;
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", `<span><br><br></span>`)
}
}
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": `<table>
<tr>
<th>Token information</th>
<th>Source information</th>
</tr>
<tr>
<td class="left-align">
Word: ${token.word}<br>
Lemma: ${token.lemma}<br>
POS: ${token.pos}<br>
Simple POS: ${token.simple_pos}<br>
NER: ${token.ner}
</td>
<td class="left-align">
Title: ${results.data.text_lookup[token.text].title}
<br>
Author: ${results.data.text_lookup[token.text].author}
<br>
Publishing year: ${results.data.text_lookup[token.text].publishing_year}
</td>
</tr>
</table>`}
);
}
// 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",
`<span class="token" data-cpos="${cpos}">${token.word} </span>`);
}
// 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",
`<span class="token" data-cpos="${cpos}">${token.word} </span>`);
// 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 = '<i class="material-icons inspect-btn">search</i>';
// # 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 = '<i class="material-icons add-btn">add</i>';
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",
`<span class="token" data-cpos="${cpos}">${token.word} </span>`);
}
return matchRowElement
}
// creates the HTML table code for the metadata vie in the corpus analysis interface
createMetaDataForModal(metaDataObject) {
let html = `<div class="col s12">
<table class="highlight">
<thead>
<tr>
<th>Metadata Description</th>
<th>Value</th>
</tr>
</thead>
<tbody>`
for (let [outerKey, outerValue] of Object.entries(metaDataObject)) {
html += `<tr>
<td style="text-transform: uppercase;">${outerKey.replace(/_/g, " ")}</td>`
if (outerKey === "corpus_all_texts" || outerKey === "text_lookup") {
html += `<td>
<ul class="collapsible">`
for (let [innerKey, innerValue] of Object.entries(outerValue)) {
html += `<li class="text-metadata"
data-metadata-key="${outerKey}"
data-text-key="${innerKey}">
<div class="collapsible-header"
data-metadata-key="${outerKey}"
data-text-key="${innerKey}">
<i class="material-icons"
data-metadata-key="${outerKey}"
data-text-key="${innerKey}">info_outline</i>
${innerValue['author']} - ${innerValue['publishing_year']} -
${innerValue['title']}
</div>
<div class="collapsible-body">
<span>
<ul id="bibliographic-data-${outerKey}-${innerKey}"
style="column-count: 2;">
</ul>
</span>
</div>
</li>`
}
html += `</ul>
</td>`
} else {
html += `<td>${outerValue}</td>`
}
html += `</tr>`
}
html += `</tbody>
</table>`
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",
`
<li><span style="text-transform: capitalize;">${key}:</span> ${value}</li>
`);
}
}
}

View File

@ -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 = `
<link rel="stylesheet" href="../../static/fonts/Material_design_icons/material-icons.css">
<link rel="stylesheet" href="../../static/css/Materialize/materialize.min.css">
<link rel="stylesheet" href="../../static/css/nopaque.css">
<div class="col">
<h6 style="margin-top: 0px;">Infos</h6>
<div class="divider" style="margin-bottom: 10px;"></div>
<div class="row">
<div class="col s12">
<button id="show-metadata"
class="waves-effect
waves-light
btn-flat
flat-interaction"
type="submit">Corpus Metadata
<i class="material-icons left">info_outline</i>
</button>
</div>
<div class="col s12">
<p>
<slot name="received-match-count">
0
</slot> of
<slot name="match-count">(to be determined)</slot>
matches loaded.
<br>
Matches occured in
<slot name="text-lookup-count">(to be determined)</slot>
corpus files:
<br>
<slot name=text-titles>(to be determined)</slot>
</p>
<p id="query-results-user-feedback">
<i class="material-icons">help</i>
The Server is still sending your results.
Functions like "Export Results" and "Match Inspect" will be
available after all matches have been loaded.
</p>
<div class="progress" id="query-progress-bar">
<div class="determinate"></div>
</div>
</div>
</div>
</div>
`;
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 };

View File

@ -41,8 +41,7 @@
<div class="col s12" id="query-display"> <div class="col s12" id="query-display">
<div class="card"> <div class="card">
<div class="card-content" id="result-list" style="overflow: hidden;"> <div class="card-content" id="result-list" style="overflow: hidden;">
<div class="error-container hide show-on-error"></div> <div id="interactions-menu" class="row hide">
<div class=" row hide show-on-success">
{% include 'interactions/infos.html.j2' %} {% include 'interactions/infos.html.j2' %}
{% include 'interactions/export.html.j2' %} {% include 'interactions/export.html.j2' %}
{% include 'interactions/create.html.j2' %} {% include 'interactions/create.html.j2' %}
@ -65,376 +64,304 @@
{% include 'modals/export_query_results.html.j2' %} {% include 'modals/export_query_results.html.j2' %}
{% include 'modals/context_modal.html.j2' %} {% include 'modals/context_modal.html.j2' %}
<!-- import modules -->
<script type="module">
/**
* First Phase:
* Document content is loaded and scripts are being imported and executed.
*/
// import Client classes
import {
Client,
ClientEventListener,
ListenerCallback,
} from '../../static/js/modules/corpus_analysis/client/Client.js';
// import client listener functions
import {
recieveConnected,
recieveMetaData,
recieveQueryStatus,
recieveQueryData,
recieveViewNotification,
recieveResultsData,
} from '../../static/js/modules/corpus_analysis/client/listeners.js';
// import client listener callbacks
import {
prepareQueryData,
saveQueryData,
saveMetaData,
getResultsData,
saveResultsData,
} from '../../static/js/modules/corpus_analysis/client/callbacks.js';
import {
Results,
} from '../../static/js/modules/corpus_analysis/model/Results.js';
import {
ViewEventListener,
ResultsList,
} from '../../static/js/modules/corpus_analysis/view/ResultsView.js';
import {
recieveClientNotification,
} from '../../static/js/modules/corpus_analysis/view/listeners.js';
import {
scrollToTop,
} from '../../static/js/modules/corpus_analysis/view/scrollToTop.js'
import {
loadingSpinnerHTML,
} from '../../static/js/modules/corpus_analysis/view/spinner.js'
<script src="{{ url_for('static', filename='js/nopaque.CorpusAnalysisClient.js') }}"> /**
</script> * Second Phase:
<script src="{{ url_for('static', filename='js/nopaque.Results.js') }}"> * Asynchronus and event driven code
</script> */
<script src="{{ url_for('static', filename='js/nopaque.callbacks.js') }}"> document.addEventListener("DOMContentLoaded", () => {
</script> // Initialize the client for server client communication in dynamic mode
<script src="{{ url_for('static', filename='js/nopaque.InteractionElement.js') }}"> let corpusId = {{ corpus_id }}
</script> const client = new Client({'corpusId': corpusId,
<script> 'socket': nopaque.socket,
// ###### Defining global variables used in other functions ###### 'logging': true,
var addToSubResultsElement; // Button to start adding matches to sub-results 'dynamicMode': true});
var addToSubResultsFromInspectElement; // button in inspect mdoal to add this match to the sub results /**
var client; // CorpusAnalysisClient first undefined on DOMContentLoaded defined * Initializing the results object as a model holding all the data of a query.
var collapsibleElements; // All collapsibleElements on this page * Also holds the metadata of one query.
var contextModal; // Modal to open on inspect for further match context * After that initialize the ResultsList object as the View handeling the
var data; // full JSON object holding match results * represnetation of the data for the user.
var expertModeSwitchElement; // Expert mode switch Element */
var initDisplay; // CorpusAnalysisDisplay object first undfined on DOMContentLoaded defined let results = new Results();
var interactionElements; // Interaction elements and their parameters let resultsList = new ResultsList('result-list', ResultsList.options);
var matchCountElement; // Total nr. of matches will be displayed in this element /**
var progress; // global progress value * Register listeners listening to socket.io events and their callbacks
var queryDisplay; // CorpusAnalysisDisplay object first undfined on DOMContentLoaded defined * Afterwards load them. Also registers listeners listening for custom
var queryFormElement; // the query form * javascript events.
var queryResultsDeterminateElement; // The progress bar for recieved results */
var resultsExportElement; // Download button opens download modal const listenForConnected = new ClientEventListener('corpus_analysis_init',
var queryResultsProgressElement; // Div element holding the progress bar recieveConnected);
var queryResultsUserFeedbackElement; // Element showing match count|total etc const listenForMetaData = new ClientEventListener('corpus_analysis_meta_data',
var receivedMatchCountElement; // Nr. of loaded matches will be displayed in this element recieveMetaData);
var results; // results object const metaDataCallback = new ListenerCallback('corpus_analysis_meta_data',
var resultsList; // resultsList object saveMetaData,
var resultsListOptions; // specifies ResultsList options [client, results]);
var subResultsCreateElement; // if pressed sub results will be created from ids listenForMetaData.setCallbacks([metaDataCallback]);
var resultsCreateElement; // if pressed results will pe created for all matches const listenForQueryStatus = new ClientEventListener('corpus_analysis_query',
var subResultsExportElement; // button to download sub results recieveQueryStatus);
var subResultsIdListElement; // list showing marked matches for sub corpus creation const queryStatusCallback = new ListenerCallback('corpus_analysis_query',
var textLookupCountElement // Nr of texts the matches occured in will be shown in this element prepareQueryData,
var textTitlesElement; // matched text titles [client, results]);
var nrMarkedMatches; // count of matches marked for subresults listenForQueryStatus.setCallbacks([queryStatusCallback]);
var showMetaDataButton; // Button to show corpus metadata const listenForQueryData = new ClientEventListener('corpus_analysis_query_results',
var activateInspectInteraction; // global interaction element recieveQueryData);
var expertModeInteraction; // global interaction element const queryDataCallback = new ListenerCallback('corpus_analysis_query_results',
var subResultsInteraction; // global interaction element saveQueryData,
var changeContextInteraction; // global interaction element [client, results]);
var resultCreationRunning; listenForQueryData.setCallbacks([queryDataCallback]);
const listenForResults = new ClientEventListener('corpus_analysis_inspect_match',
// ###### Defining local scope variables ###### recieveResultsData);
let contextPerItemElement; // Form Element for display option const resultsDataCallback = new ListenerCallback('corpus_analysis_inspect_match',
let contextSentencesElement; // Form Element for display option in inspect saveResultsData,
let displayOptionsData; // Getting form data from display options [client, results]);
let displayOptionsFormElement; // Form holding the display informations listenForResults.setCallbacks([resultsDataCallback]);
let downloadResultsJSONElement; // button for downloading results as JSON // listen for javascript custom notifications
let downloadInspectContextElement; // button for downloading inspect context const listenForViewNotification = new ClientEventListener('notify-client',
let exportModal; // Download options modal recieveViewNotification);
let firstPageElement; // first page element of resultsList pagination const getResultsCallback = new ListenerCallback('get-results',
let hitsPerPageInputElement; getResultsData,
let initDisplayElement; // Element for initialization using initDisplay [client, results]);
let initModal; listenForViewNotification.setCallbacks([getResultsCallback]);
let paginationElements; client.setSocketEventListeners([listenForConnected,
let queryDisplayElement; // Element for initialization using queryDisplay listenForQueryStatus,
let xpath; // xpath to grab first resultsList page pagination element listenForQueryData,
let metaDataModal; // modal showing corpus meta data listenForMetaData,
listenForViewNotification,
// ###### Initialize variables ###### listenForResults]);
addToSubResultsElement = document.getElementById("add-to-sub-results"); client.loadSocketEventListeners();
addToSubResultsFromInspectElement = document.getElementById("add-to-sub-results-from-inspect"); /**
client = undefined; * Register resultsList listeners listening to notification events.
collapsibleElements = document.querySelector('.collapsible.expandable'); */
contextModal = document.getElementById("context-modal"); const listenForClientNotification = new ViewEventListener('notify-view',
contextPerItemElement = document.getElementById("display-options-form-result_context"); recieveClientNotification);
contextSentencesElement = document.getElementById("context-sentences"); resultsList.setNotificationListeners([listenForClientNotification]);
displayOptionsFormElement = document.getElementById("display-options-form"); resultsList.loadNotificationListeners();
expertModeSwitchElement = document.getElementById("display-options-form-expert_mode"); // Connect client to server
exportModal = document.getElementById("query-results-download-modal"); client.notifyView('connecting');
hitsPerPageInputElement = document.getElementById("display-options-form-results_per_page"); client.connect();
initDisplay = undefined; // Send a query and recieve its answer data
initDisplayElement = document.getElementById("init-display"); let queryFormElement = document.querySelector('#query-form');
matchCountElement = document.getElementById("match-count"); queryFormElement.addEventListener('submit', (event) => {
paginationElements = document.getElementsByClassName("pagination"); try {
queryDisplay = undefined; /**
queryDisplayElement = document.getElementById("query-display"); * Selects first page of result list if pagination is already available
queryFormElement = document.getElementById("query-form"); * from an query submitted before.
queryResultsDeterminateElement = document.getElementById("query-results-determinate"); * This avoids confusion for the user e.g.: The user was on page 24
resultsExportElement = document.getElementById("query-results-export"); * reviewing the results and issues a new query. He would not see any
queryResultsProgressElement = document.getElementById("query-results-progress"); * results until the new results reach page 24 or he clicks on another
queryResultsUserFeedbackElement = document.getElementById("query-results-user-feedback"); * valid result page element from the new pagination.
receivedMatchCountElement = document.getElementById("received-match-count"); */
subResultsCreateElement = document.getElementById("sub-results-create"); let firstPageElement = document.querySelector('a.page');
resultsCreateElement = document.getElementById("results-create"); firstPageElement.click();
subResultsExportElement = document.getElementById("sub-results-export"); } catch (e) {
subResultsIdListElement = document.getElementById("sub-results-match-ids-div"); // No page element is present if first query is submitted.
textLookupCountElement = document.getElementById("text-lookup-count");
textTitlesElement = document.getElementById("text-titles");
nrMarkedMatches = document.getElementById("nr-marked-matches");
showMetaDataButton = document.getElementById("show-metadata");
metaDataModal = document.getElementById("meta-data-modal");
// ###### js list options and intialization ######
displayOptionsData = ResultsList.getDisplayOptions(displayOptionsFormElement);
resultsListOptions = {page: displayOptionsData["resultsPerPage"],
pagination: [{
name: "paginationTop",
paginationClass: "paginationTop",
innerWindow: 8,
outerWindow: 1
}, {
paginationClass: "paginationBottom",
innerWindow: 8,
outerWindow: 1
}],
valueNames: ["titles", "lc", "c", "rc", {data: ["index"]}],
item: `<span></span>`};
// ###### event on DOMContentLoaded ######
document.addEventListener("DOMContentLoaded", () => {
// creates some modals on DOMContentLoaded
let defaultOptions = {"dismissible": true,
"preventScrolling": false};
contextModal = M.Modal.init(contextModal, defaultOptions);
exportModal = M.Modal.init(exportModal, defaultOptions);
initModal = M.Modal.init(initDisplayElement, {"dismissible": false});
let deleteOverlay = () => {
let overlay = document.getElementsByClassName("modal-overlay")[0];
overlay.remove();
};
metaDataModal = M.Modal.init(metaDataModal, {"preventScrolling": false,
"opacity": 0.0,
"dismissible": false,
"onOpenEnd": deleteOverlay});
// Init corpus analysis components
data = new Data();
resultsList = new ResultsList("result-list", resultsListOptions);
resultsMetaData = new MetaData();
results = new Results(data, resultsList, resultsMetaData);
initDisplay = new CorpusAnalysisDisplay(initDisplayElement);
queryDisplay = new CorpusAnalysisDisplay(queryDisplayElement);
client = new CorpusAnalysisClient({{ corpus_id }}, nopaque.socket);
initModal.open();
// set displays and callback functions
client.setDisplay("init", initDisplay);
client.setCallback("init", () => {
initModal.close();
});
client.setCallback('get_metadata', () => {
client.getMetaData();
})
client.setCallback('recv_meta_data', (response) => {
recvMetaData(response);
})
client.setDisplay("query", queryResultsUserFeedbackElement);
client.setDisplay("query", queryDisplay);
client.setCallback("query", (payload) => {
querySetup(payload);
});
client.setCallback("query_results", (payload) => {
queryRenderResults(payload);
});
client.setCallback("query_match_context", (payload) => {
results.jsList.showMatchContext(payload);
});
client.setCallback("save_sub_results_choices", (payload) => {
saveSubResultsChoices(payload);
});
// Trigger corpus analysis initialization on server side
client.init();
// start a query request on submit
queryFormElement.addEventListener("submit", (event) => {
try {
// Selects first page of result list if pagination is already available
// from an query submitted before.
// This avoids confusion for the user eg: The user was on page 24
// reviewing the results and issues a new query. He would not see any
// results until the new results reach page 24 or he clicks on another
// valid result page element from the new pagination.
firstPageElement;
xpath = '//a[@class="page" and text()=1]';
firstPageElement = document.evaluate(xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null).singleNodeValue;
firstPageElement.click();
} catch (e) {
}
// Prevent page from reloading on submit
event.preventDefault();
// Get query string and send query to server
results.data.getQueryStr(queryFormElement);
client.query(results.data.query);
});
// live update of hits per page if hits per page value is changed
let changeHitsPerPageBind = results.jsList.changeHitsPerPage.bind(results.jsList);
hitsPerPageInputElement.onchange = changeHitsPerPageBind;
// live update of lr context per item if context value is changed
contextPerItemElement.onchange = results.jsList.changeContext;
// Initialization of interactionElemnts
// An interactionElement is an object identifing a switch or button via
// htmlID. Callbacks are set for these elements which will be triggered on
// a pagination interaction by the user or if the status of the element has
// been altered. (Like the switche has ben turned on or off).
interactionElements = new InteractionElements();
expertModeInteraction = new InteractionElement("display-options-form-expert_mode");
expertModeInteraction.setCallback("on",
results.jsList.expertModeOn,
results.jsList,
["query-display"])
expertModeInteraction.setCallback("off",
results.jsList.expertModeOff,
results.jsList,
["query-display"])
subResultsInteraction = new InteractionElement("add-to-sub-results");
subResultsInteraction.setCallback("on",
results.jsList.activateAddToSubResults,
results.jsList);
subResultsInteraction.setCallback("off",
results.jsList.deactivateAddToSubResults,
results.jsList);
activateInspectInteraction = new InteractionElement("inspect",
false);
activateInspectInteraction.setCallback("noCheck",
results.jsList.activateInspect,
results.jsList);
changeContextInteraction = new InteractionElement("display-options-form-results_per_page",
false);
changeContextInteraction.setCallback("noCheck",
results.jsList.changeContext,
results.jsList)
interactionElements.addInteractions([expertModeInteraction, subResultsInteraction, activateInspectInteraction, changeContextInteraction]);
// checks if a change for every interactionElement happens and executes
// the callbacks accordingly
interactionElements.onChangeExecute();
// eventListener if pagination is used to apply new context size to new page
// and also activate inspect match if progress is 100
// also adds more interaction buttons like add to sub results
for (let element of paginationElements) {
element.addEventListener("click", (event) => {
results.jsList.pageChangeEventInteractionHandler(interactionElements);
});
} }
// Prevent page from reloading on submit
// ### Show corpus Metadata event.preventDefault();
showMetaDataButton.onclick = () => { // Get query string and send query to server
let metaDataObject = {}; results.data.getQueryStr(queryFormElement);
Object.assign(metaDataObject, results.metaData); client.query(results.data.query);
metaDataObject["query"] = results.data.query;
metaDataObject["text_lookup"] = results.data.text_lookup;
metaDataModalContent = document.getElementById("meta-data-modal-content");
metaDataModalContent.innerHTML = "";
let table = results.jsList.createMetaDataForModal(metaDataObject);
metaDataModalContent.insertAdjacentHTML("afterbegin", table);
metaDataModal.open();
let collapsibles = document.getElementsByClassName("text-metadata");
for (let collapsible of collapsibles) {
collapsible.onclick = () => {
let elems = document.querySelectorAll('.collapsible');
let instances = M.Collapsible.init(elems, {accordion: false});
results.jsList.createTextDetails(metaDataObject);
}
}
};
}); });
// Get all needed HTMLElements for the following event listeners
resultsList.getHTMLElements([
'#display-options-form-results_per_page',
'#display-options-form-result_context',
'#show-meta-data',
'#meta-data-modal',
'#meta-data-modal-content',
'#full-results-create',
'#sub-results-create',
'#full-results-export',
'#sub-results-export',
'#download-results-json',
'#query-results-download-modal',
'#query-results-table',
'#display-options-form-expert_mode',
'.pagination',
]);
// new insepct event listener makeing use of javascript bubbleing /**
let resultsTable = document.getElementById("query-results"); * The following listener handles what functions are called when the user
console.log(resultsTable); * does use the page navigation to navigate to a new page.
resultsTable.addEventListener("click", (event) => { */
let dataIndex; for (let element of resultsList.pagination) {
if (event.target.classList.contains("inspect-btn")) { element.addEventListener("click", (event) => {
dataIndex = parseInt(event.target.closest("tr").dataset.index); // shows match context according to the user picked value
console.log(dataIndex); resultsList.changeContext();
results.jsList.inspect([dataIndex], "inspect"); // activates or deactivates expertMode on new page depending switch value
} else if (event.target.classList.contains("add-btn")) { if (resultsList.displayOptionsFormExpertMode.checked) {
dataIndex = parseInt(event.target.closest("tr").dataset.index); resultsList.expertModeOn('query-display', results);
results.jsList.addToSubResults(dataIndex); } else {
} resultsList.expertModeOff('query-display');
}) }
// activates inspect buttons on new page if client is not busy
// ### Download events and sub-results creation ### resultsList.activateInspect();
var loadingSpinnerHTML = ` });
<div class="preloader-wrapper button-icon-spinner small active">
<div class="spinner-layer spinner-green-only">
<div class="circle-clipper left">
<div class="circle"></div>
</div><div class="gap-patch">
<div class="circle"></div>
</div><div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
`
// create results on click from all match ids
resultsCreateElement.onclick = () => {
resultsCreateElement.getElementsByTagName("i")[0].classList.add("hide");
resultsCreateElement.innerText = "Creating...";
resultsCreateElement.insertAdjacentHTML("afterbegin", loadingSpinnerHTML);
results.data.createResultsData("results");
} }
// Add onclick to open download modal when Export Results button is pressed /**
resultsExportElement.onclick = () => { * The following event Listener handles the expert mode switch for the list
exportModal.open(); */
// add onclick to download JSON button and download the file resultsList.displayOptionsFormExpertMode.onchange = (event) => {
downloadResultsJSONElement = document.getElementById("download-results-json") if (event.target.checked) {
downloadResultsJSONElement.onclick = () => { resultsList.expertModeOn('query-display', results);
let filename = results.resultsData.createDownloadFilename("matches-results");
results.resultsData.addData(results.metaData);
results.resultsData.downloadJSONRessource(filename, results.resultsData,
downloadResultsJSONElement
)};
}
// create sub results on click from shown marked match ids
subResultsCreateElement.onclick = () => {
subResultsCreateElement.getElementsByTagName("i")[0].remove();
subResultsCreateElement.innerText = "Creating...";
subResultsCreateElement.insertAdjacentHTML("afterbegin", loadingSpinnerHTML);
results.data.createResultsData("sub-results");
}
// Add onclick to open download modal when sub-results button is pressed
subResultsExportElement.onclick = () => {
exportModal.open();
console.log(results.subResultsData);
// add onclick to download JSON button and download the file
downloadResultsJSONElement = document.getElementById("download-results-json")
downloadResultsJSONElement.onclick = () => {
let filename = results.subResultsData.createDownloadFilename("matches-sub-results");
results.subResultsData.downloadJSONRessource(filename,
results.subResultsData,
downloadResultsJSONElement
)};
}
// add onclick to download JSON button and download the file
downloadInspectContextElement = document.getElementById("inspect-download-context")
downloadInspectContextElement.onclick = () => {
let filename = results.data.createDownloadFilename(`context-id-${results.jsList.contextId}`);
results.data.addData(results.metaData);
results.data.downloadJSONRessource(filename,
results.jsList.contextData,
downloadInspectContextElement);
};
// scroll to top button if user scrolled down the list
let headline = document.querySelector(".headline");
let scrollToTop = document.querySelector("#menu-scroll-to-top-div");
window.addEventListener("scroll", (event) => {
if (pageYOffset > 250) {
scrollToTop.classList.toggle("hide", false);
} else { } else {
scrollToTop.classList.toggle("hide", true); resultsList.expertModeOff('query-display');
} }
});
scrollToTop.onclick = () => {
headline.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
}; };
/**
* The following event Listener handles the add-btn and the inspect-btn
* onclick events via bubbleing.
*/
resultsList.queryResultsTable.addEventListener('click', (event) => {
let dataIndex;
if (event.target.classList.contains('inspect-btn')) {
dataIndex = parseInt(event.target.closest('tr').dataset.index);
resultsList.inspect([dataIndex], 'inspect');
} else if (event.target.classList.contains('add-btn')) {
dataIndex = parseInt(event.target.closest('tr').dataset.index);
resultsList.addToSubResults(dataIndex);
}
})
/**
* Display events: Following event listeners are handling the
* live update of hits per page if hits per page value is changed and the
* context size of every match.
*/
resultsList.displayOptionsFormResultsPerPage.onchange = () => {
resultsList.changeHitsPerPage();
};
resultsList.displayOptionsFormResultContext.onchange = () => {
resultsList.changeContext();
};
/**
* The following event listener handel the Show metadata button and its
* functionality. Before the needed modal is initialized.
*/
resultsList.metaDataModal= M.Modal.init(resultsList.metaDataModal, {
'preventScrolling': false,
'opacity': 0.0,
'dismissible': false,
'onOpenEnd': (() => {document.querySelector(".modal-overlay").remove()})
});
resultsList.showMetaData.onclick = () => {
resultsList.metaDataModalContent.textContent = '';
let table = resultsList.createMetaDataForModal(results.metaData);
resultsList.metaDataModalContent.insertAdjacentHTML('afterbegin', table);
resultsList.metaDataModal.open();
let collapsibles = resultsList.metaDataModalContent.querySelectorAll(".text-metadata");
for (let collapsible of collapsibles) {
collapsible.onclick = () => {
let elems = resultsList.metaDataModalContent.querySelectorAll('.collapsible');
let instances = M.Collapsible.init(elems, {accordion: false});
resultsList.createTextDetails(results.metaData);
}
}
};
/**
* The following event listeners are handeling the data export.
* 1. Create full-results
* 2. Create sub-results
* 3. Download full-results
* 4. Download sub-results
*/
resultsList.fullResultsCreate.onclick = () => {
resultsList.fullResultsCreate.querySelector('i').classList.toggle('hide');
resultsList.fullResultsCreate.innerText = 'Creating...';
resultsList.fullResultsCreate.insertAdjacentHTML('afterbegin',
loadingSpinnerHTML);
let dataIndexes = [...Array(results.data.match_count).keys()];
resultsList.notifyClient('get-results', { resultsType: 'full-results',
dataIndexes: dataIndexes});
}
resultsList.subResultsCreate.onclick = () => {
let dataIndexes = [];
resultsList.addToSubResultsIdsToShow.forEach((id) => {
dataIndexes.push(id - 1);
});
resultsList.subResultsCreate.querySelector('i').classList.toggle('hide');
resultsList.subResultsCreate.innerText = 'Creating...';
resultsList.subResultsCreate.insertAdjacentHTML('afterbegin',
loadingSpinnerHTML);
resultsList.notifyClient('get-results', { resultsType: 'sub-results',
dataIndexes: dataIndexes});
}
/**
* Before the downland events are added the needed modal is initialized.
*/
resultsList.queryResultsDownloadModal = M.Modal.init(resultsList.queryResultsDownloadModal);
// Open download modal when full results export button is pressed
resultsList.fullResultsExport.onclick = () => {
resultsList.queryResultsDownloadModal.open();
// add onclick to download JSON button and download the file
resultsList.downloadResultsJson.onclick = () => {
let filename = results.fullResultsData.createDownloadFilename('full-results');
results.fullResultsData.addData(results.metaData);
results.fullResultsData.downloadJSONRessource(filename, results.fullResultsData,
resultsList.downloadResultsJson)};
}
// Open download modal when sub results export button is pressed
resultsList.subResultsExport.onclick = () => {
resultsList.queryResultsDownloadModal.open();
// add onclick to download JSON button and download the file
resultsList.downloadResultsJson.onclick = () => {
let filename = results.subResultsData.createDownloadFilename('sub-results');
results.subResultsData.addData(results.metaData);
results.subResultsData.downloadJSONRessource(filename, results.subResultsData,
resultsList.downloadResultsJson)};
}
// enable scroll to Top
scrollToTop('.headline', '#menu-scroll-to-top-div');
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -107,8 +107,8 @@
</div> </div>
</div> </div>
<script type="module">
<script> import {RessourceList} from '../../static/js/nopaque.lists.js';
class InformationUpdater { class InformationUpdater {
constructor(corpusId, foreignCorpusFlag) { constructor(corpusId, foreignCorpusFlag) {
this.corpusId = corpusId; this.corpusId = corpusId;
@ -196,7 +196,8 @@
nopaque.socket.emit("foreign_user_data_stream_init", {{ corpus.user_id }}); nopaque.socket.emit("foreign_user_data_stream_init", {{ corpus.user_id }});
}); });
{% endif %} {% endif %}
var corpusFilesList = new RessourceList("corpus-files", null, "CorpusFile");
let corpusFilesList = new RessourceList("corpus-files", null, "CorpusFile");
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
corpusFilesList._add({{ corpus_files|tojson|safe }}); corpusFilesList._add({{ corpus_files|tojson|safe }});
}); });

View File

@ -6,22 +6,14 @@ results.-->
<div class="divider" style="margin-bottom: 10px;"></div> <div class="divider" style="margin-bottom: 10px;"></div>
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
<div class="switch"> <p>Add matches to Sub-Results with the
Sub-Results creation: <i class="material-icons tiny">add</i>
<label> button in the list or inspect view.
Off </p>
<input disabled
type="checkbox"
id="add-to-sub-results">
<span class="lever"></span>
On
</label>
</div>
</div> </div>
<div class="col s12 hide" id="sub-results-match-ids-div"> <div class="col s12">
<div class="input-field"> <div class="input-field">
<p><span id="nr-marked-matches"></span> matches marked <p><span id="nr-marked-matches"></span> matches added for sub-results:</p>
for Sub-Results:</p>
<textarea id="sub-results-match-ids" <textarea id="sub-results-match-ids"
class="materialize-textarea" class="materialize-textarea"
disabled> disabled>

View File

@ -12,10 +12,10 @@ the selected sub results.-->
disabled disabled
flat-interaction" flat-interaction"
type="submit" type="submit"
id="results-create">Create Results id="full-results-create">Create Results
<i class="material-icons left">build</i> <i class="material-icons left">build</i>
</button> </button>
<button id="query-results-export" <button id="full-results-export"
class="waves-effect class="waves-effect
waves-light waves-light
btn-flat btn-flat
@ -29,7 +29,6 @@ the selected sub results.-->
<button class="waves-effect <button class="waves-effect
waves-light waves-light
btn-flat btn-flat
hide
disabled disabled
flat-interaction" flat-interaction"
type="submit" type="submit"

View File

@ -7,7 +7,7 @@ result.-->
<div class="divider" style="margin-bottom: 10px;"></div> <div class="divider" style="margin-bottom: 10px;"></div>
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
<button id="show-metadata" <button id="show-meta-data"
class="waves-effect class="waves-effect
waves-light waves-light
btn-flat btn-flat
@ -17,29 +17,29 @@ result.-->
</button> </button>
</div> </div>
<div class="col s12"> <div class="col s12">
<div class="progress hide" id="query-progress-bar">
<div class="determinate"></div>
</div>
<p> <p>
<span id="received-match-count"> <span id="recieved-match-count">
</span> of </span> of
<span id="match-count"></span> <span id="total-match-count"></span>
matches loaded. matches loaded.
<br> <br>
<br>
Matches occured in Matches occured in
<span id="text-lookup-count"></span> <span id="text-lookup-count"></span>
corpus files: corpus files:
<br> <br>
<span id=text-titles></span> <span id=text-lookup-titles></span>
</p> </p>
{% if not imported %} <br>
<p id="query-results-user-feedback"> <p class="hide" id="query-results-user-feedback">
<i class="material-icons">help</i> <i class="material-icons tiny">help</i>
The Server is still sending your results. Server is sending your results.
Functions like "Export Results" and "Match Inspect" will be Functions like "Export Results" and "Match Inspect" will be
available after all matches have been loaded. available after all matches have been loaded.
</p> </p>
<div class="progress" id="query-results-progress">
<div class="determinate" id="query-results-determinate"></div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -162,11 +162,11 @@
</div> </div>
</div> </div>
</div> </div>
<script type="module">
<script> import {RessourceList} from '../../static/js/nopaque.lists.js';
var corpusList = new RessourceList("corpora", nopaque.corporaSubscribers, "Corpus"); let corpusList = new RessourceList("corpora", nopaque.corporaSubscribers, "Corpus");
var jobList = new RessourceList("jobs", nopaque.jobsSubscribers, "Job"); let jobList = new RessourceList("jobs", nopaque.jobsSubscribers, "Job");
var queryResultList = new RessourceList("query-results", nopaque.queryResultsSubscribers, "QueryResult"); let queryResultList = new RessourceList("query-results", nopaque.queryResultsSubscribers, "QueryResult");
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,12 +1,17 @@
<!-- Analysis init modal. User feedback showing that the analysis session is <!-- Analysis init modal. User feedback showing that the analysis session is
loading. --> loading. -->
<div class="modal no-autoinit" id="init-display"> <div class="modal no-autoinit" id="analysis-init-modal">
<div class="modal-content"> <div class="modal-content">
<h4>Initializing your corpus analysis session...</h4> <h4>Initializing your corpus analysis session...</h4>
<div class="error-container hide show-on-error"></div> <p>If the loading takes to long or an error occured,
<div class="hide progress show-while-waiting"> <a onclick="window.location.reload()" href="#">click here</a>
to refresh your session or
<a href="{{ url_for('corpora.corpus', corpus_id=corpus_id) }}">go back</a>!
</p>
<div id="analysis-init-progress" class="progress">
<div class="indeterminate"></div> <div class="indeterminate"></div>
</div> </div>
<p id="analysis-init-error" class="hide red-text"></p>
</div> </div>
</div> </div>

View File

@ -8,7 +8,7 @@
<div class="section"> <div class="section">
<h6 style="margin-top: 0px;">Display</h6> <h6 style="margin-top: 0px;">Display</h6>
<div class="divider" style="margin-bottom: 10px;"></div> <div class="divider" style="margin-bottom: 10px;"></div>
<div class="col s12" style="margin-bottom: 10px;" id="display-inspect"> <div class="col s12" style="margin-bottom: 10px;">
{{ inspect_display_options_form.expert_mode_inspect.label.text }} {{ inspect_display_options_form.expert_mode_inspect.label.text }}
<div class="switch right"> <div class="switch right">
<label> <label>
@ -17,7 +17,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="col s12" style="margin-bottom: 10px;" id="create-inspect"> <div class="col s12" style="margin-bottom: 10px;">
{{ inspect_display_options_form.highlight_sentences.label.text }} {{ inspect_display_options_form.highlight_sentences.label.text }}
<div class="switch right"> <div class="switch right">
<label> <label>
@ -42,19 +42,18 @@
</div> </div>
</div> </div>
</div> </div>
{% if not imported %}
<div class="col s12 m6 l6"> <div class="col s12 m6 l6">
<div class="section"> <div class="section">
<h6 style="margin-top: 0px;">Create</h6> <h6 style="margin-top: 0px;">Create</h6>
<div class="divider" style="margin-bottom: 10px;"></div> <div class="divider" style="margin-bottom: 10px;"></div>
<div class="col s12"> <div class="col s12">
Add to Sub Results Add to Sub Results
<div class="secondary-content right" id="add-to-sub-results-from-inspect"> <div class="secondary-content right" id="create-inspect-menu">
{# The needed button is created and added via javascript #}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
</form> </form>
<div class="row section"> <div class="row section">

View File

@ -121,7 +121,9 @@
<script src="{{ url_for('static', filename='js/List.js/list.min.js') }}"></script> <script src="{{ url_for('static', filename='js/List.js/list.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/Socket.IO/socket.io.slim.js') }}"></script> <script src="{{ url_for('static', filename='js/Socket.IO/socket.io.slim.js') }}"></script>
<script src="{{ url_for('static', filename='js/nopaque.js') }}"></script> <script src="{{ url_for('static', filename='js/nopaque.js') }}"></script>
<script src="{{ url_for('static', filename='js/nopaque.lists.js') }}"></script> <script type="module">
import {RessourceList} from '../../static/js/nopaque.lists.js'
</script>
<script> <script>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{% if current_user.setting_dark_mode %} {% if current_user.setting_dark_mode %}

View File

@ -34,7 +34,7 @@
<div class="col s12" id="query-display"> <div class="col s12" id="query-display">
<div class="card"> <div class="card">
<div class="card-content" id="result-list" style="overflow: hidden;"> <div class="card-content" id="result-list" style="overflow: hidden;">
<div class=" row show-on-success"> <div class="row">
{% include 'interactions/infos.html.j2' %} {% include 'interactions/infos.html.j2' %}
{% include 'interactions/display.html.j2' %} {% include 'interactions/display.html.j2' %}
{% include 'interactions/analysis.html.j2' %} {% include 'interactions/analysis.html.j2' %}
@ -54,180 +54,169 @@
{% include 'modals/context_modal.html.j2' %} {% include 'modals/context_modal.html.j2' %}
<script src="{{ url_for('static', filename='js/nopaque.Results.js') }}"> <script type="module">
</script> /**
<script src="{{ url_for('static', filename='js/nopaque.callbacks.js') }}"> * First Phase:
</script> * document content is loaded and scripts are being imported and executed
<script src="{{ url_for('static', filename='js/nopaque.InteractionElement.js') }}"> */
</script> import {
<script> CorpusAnalysisClient,
// ###### global variables ###### CorpusAnalysisDisplay,
var full_result_json; SocketEventListener,
var result_json; ListenerCallback,
var receivedMatchCountElement; // Nr. of loaded matches will be displayed in this element } from '../../static/js/modules/nopaque.CorpusAnalysisClient.js';
var textLookupCountElement // Nr of texts the matches occured in will be shown in this element import {
var textTitlesElement; // matched text titles recieveSession,
var progress; // global progress value recieveQueryStatus,
var queryResultsProgressElement; // Div element holding the progress bar recieveQueryData,
var expertModeSwitchElement; // Expert mode switch Element } from '../../static/js/modules/nopaque.listenerFunctions.js';
var matchCountElement; // Total nr. of matches will be displayed in this element import {
var interactionElements; // Interaction elements and their parameters querySetup,
var contextModal; // Modal to open on inspect for further match context queryRenderResults,
} from '../../static/js/modules/nopaque.listenerCallbacks.js'
import {
Results,
Data,
MetaData,
} from '../../static/js/nopaque.Results.js';
import {
ResultsList,
} from '../../static/js/nopaque.lists.js';
import {
scrollToTop,
} from '../../static/js/modules/nopaque.scrollToTop.js';
// ###### Defining local scope variables /**
let displayOptionsFormElement; // Form holding the display informations * Second Phase:
let resultItems; // array of built html result items row element. This is called when results are transmitted and being recieved * Asynchronus and event driven code
let hitsPerPageInputElement;let contextPerItemElement; // Form Element for display option */
let paginationElements; document.addEventListener("DOMContentLoaded", () => {
let inspectBtnElements; // Initialize the CorpusAnalysisClient with dynamicMode as false
let metaDataModal; const client = new CorpusAnalysisClient({'logging': true,
let showMetaDataButton 'dynamicMode': false});
console.info("CorpusAnalysisClient created as client:", client);
// Set up display elements which hare show depending on the client status
client.getHTMLElements(['#query-display']);
const queryDisplay = new CorpusAnalysisDisplay(client.queryDisplay);
// Register those display elements to client
client.setDisplay("query", queryDisplay);
/**
* Initializing the results object holding all the data of a query.
* Also holds the metadata of one query.
* Lastly it contains the object ResultsList which is a list.js
* subclass which handles the visual representation of the query data.
*/
let displayOptionsData = ResultsList.getDisplayOptions('display-options-form');
ResultsList.options.page = displayOptionsData["resultsPerPage"];
let data = new Data();
let resultsList = new ResultsList("result-list", ResultsList.options);
let resultsMetaData = new MetaData();
let results = new Results(data, resultsList, resultsMetaData);
// make results part of the client
client.results = results;
// inits some object keys and values
client.results.clearAll();
console.info('Initialized the Results object.')
// init some modals
let deleteOverlay = () => {
let overlay = document.getElementsByClassName("modal-overlay")[0];
overlay.remove();
};
client.getHTMLElements(['#meta-data-modal'])
client.metaDataModal = M.Modal.init(client.metaDataModal,
{'preventScrolling': false,
'opacity': 0.0,
'dismissible': false,
'onOpenEnd': deleteOverlay});
// saving imported data into client object
const payload = {{ query_result_file_content|tojson|safe }};
/**
* Register listeners and their callbacks. Because we are using the client
* not in dynamic mode we will not load the listeners. We just call the
* callbacks of the listeners manually. This is done to keep the setup of
* the client in dynamic or not dynamic mode similarish.
*/
const listenForQueryStatus = new SocketEventListener('corpus_analysis_query',
recieveQueryStatus);
const queryStatusCallback = new ListenerCallback('corpus_analysis_query',
querySetup);
listenForQueryStatus.setCallbacks([queryStatusCallback]);
const listenForQueryData = new SocketEventListener('corpus_analysis_query_results',
recieveQueryData);
const queryDataCallback = new ListenerCallback('corpus_analysis_query_results',
queryRenderResults);
listenForQueryData.setCallbacks([queryDataCallback]);
// ###### Initializing variables ###### //
displayOptionsFormElement = document.getElementById("display-options-form"); // // Initialization of interactionElemnts
resultItems = []; // // An interactionElement is an object identifing a switch or button via
receivedMatchCountElement = document.getElementById("received-match-count"); // // htmlID. Callbacks are set for these elements which will be triggered on
textLookupCountElement = document.getElementById("text-lookup-count"); // // a pagination interaction by the user or if the status of the element has
textTitlesElement = document.getElementById("text-titles"); // // been altered. (Like the switche has ben turned on or off).
queryResultsProgressElement = document.getElementById("query-results-progress"); // interactionElements = new InteractionElements();
expertModeSwitchElement = document.getElementById("display-options-form-expert_mode"); // let expertModeInteraction = new InteractionElement("display-options-form-expert_mode");
matchCountElement = document.getElementById("match-count"); // expertModeInteraction.setCallback("on",
hitsPerPageInputElement = document.getElementById("display-options-form-results_per_page"); // results.jsList.expertModeOn,
contextPerItemElement = document.getElementById("display-options-form-result_context"); // results.jsList,
paginationElements = document.getElementsByClassName("pagination"); // ["query-display"])
contextModal = document.getElementById("context-modal"); // expertModeInteraction.setCallback("off",
metaDataModal = document.getElementById("meta-data-modal"); // results.jsList.expertModeOff,
showMetaDataButton = document.getElementById("show-metadata"); // results.jsList,
// ["query-display"])
// js list options //
displayOptionsData = ResultsList.getDisplayOptions(displayOptionsFormElement); // let activateInspectInteraction = new InteractionElement("inspect",
resultsListOptions = {page: displayOptionsData["resultsPerPage"], // false);
pagination: [{ // activateInspectInteraction.setCallback("noCheck",
name: "paginationTop", // results.jsList.activateInspect,
paginationClass: "paginationTop", // results.jsList);
innerWindow: 8, //
outerWindow: 1 // let changeContextInteraction = new InteractionElement("display-options-form-results_per_page",
}, { // false);
paginationClass: "paginationBottom", // changeContextInteraction.setCallback("noCheck",
innerWindow: 8, // results.jsList.changeContext,
outerWindow: 1 // results.jsList)
}], // interactionElements.addInteractions([expertModeInteraction, activateInspectInteraction, changeContextInteraction]);
valueNames: ["titles", "lc", "c", "rc", {data: ["index"]}], //
item: `<span></span>` // // checks if a change for every interactionElement happens and executes
}; // // the callbacks accordingly
// interactionElements.onChangeExecute();
document.addEventListener("DOMContentLoaded", () => { //
// Initialize some Modals // // eventListener if pagination is used to apply new context size to new page
contextModal = M.Modal.init(contextModal, {"dismissible": true}); // // and also activate inspect match if progress is 100
// // also adds more interaction buttons like add to sub results
// ###### recreating chunk structure to reuse callback queryRenderResults() // for (let element of paginationElements) {
full_result_json = {{ query_result_file_content|tojson|safe }}; // element.addEventListener("click", (event) => {
result_json = {}; // results.jsList.pageChangeEventInteractionHandler(interactionElements);
result_json["chunk"] = {}; // });
result_json.chunk["cpos_lookup"] = full_result_json.cpos_lookup; // }
result_json.chunk["cpos_ranges"] = full_result_json.cpos_ranges; //
result_json.chunk["matches"] = full_result_json.matches; // render results directly with callbacks because we are not in dynamic mode
result_json.chunk["text_lookup"] = full_result_json.text_lookup; listenForQueryStatus.listenerCallbacks['corpus_analysis_query'].callbackFunction(payload, client);
listenForQueryData.listenerCallbacks['corpus_analysis_query_results'].callbackFunction(payload, client);
// Init corpus analysis components // // ### Show corpus Metadata
data = new Data(); // showMetaDataButton.onclick = () => {
resultsList = new ResultsList("result-list", resultsListOptions); // metaDataModal.open();
resultsMetaData = new MetaData(); // };
results = new Results(data, resultsList, resultsMetaData); //
results.clearAll(); // inits some object keys and values // // live update of hits per page if hits per page value is changed
// init some modals // let changeHitsPerPageBind = results.jsList.changeHitsPerPage.bind(results.jsList);
let deleteOverlay = () => { // hitsPerPageInputElement.onchange = changeHitsPerPageBind;
let overlay = document.getElementsByClassName("modal-overlay")[0]; //
overlay.remove(); // // live update of lr context per item if context value is changed
}; // contextPerItemElement.onchange = results.jsList.changeContext;
metaDataModal = M.Modal.init(metaDataModal, {"preventScrolling": false, //
"opacity": 0.0, // // new insepct event listener makeing use of javascript bubbleing
"dismissible": false, // let resultsTable = document.getElementById("query-results");
"onOpenEnd": deleteOverlay}); // resultsTable.addEventListener("click", (event) => {
// if (event.target.classList.contains("inspect-btn")) {
// setting some initial values for user feedback // const dataIndex = event.target.closest("tr").dataset.index;
matchCountElement.innerText = full_result_json.match_count; // const fakeResponse = results.jsList.createFakeResponse();
// results.jsList.showMatchContext(fakeResponse);
// Initialization of interactionElemnts // }
// An interactionElement is an object identifing a switch or button via // });
// htmlID. Callbacks are set for these elements which will be triggered on //
// a pagination interaction by the user or if the status of the element has // Add scrollToTop functionality
// been altered. (Like the switche has ben turned on or off). scrollToTop();
interactionElements = new InteractionElements(); });
let expertModeInteraction = new InteractionElement("display-options-form-expert_mode");
expertModeInteraction.setCallback("on",
results.jsList.expertModeOn,
results.jsList,
["query-display"])
expertModeInteraction.setCallback("off",
results.jsList.expertModeOff,
results.jsList,
["query-display"])
let activateInspectInteraction = new InteractionElement("inspect",
false);
activateInspectInteraction.setCallback("noCheck",
results.jsList.activateInspect,
results.jsList);
let changeContextInteraction = new InteractionElement("display-options-form-results_per_page",
false);
changeContextInteraction.setCallback("noCheck",
results.jsList.changeContext,
results.jsList)
interactionElements.addInteractions([expertModeInteraction, activateInspectInteraction, changeContextInteraction]);
// checks if a change for every interactionElement happens and executes
// the callbacks accordingly
interactionElements.onChangeExecute();
// eventListener if pagination is used to apply new context size to new page
// and also activate inspect match if progress is 100
// also adds more interaction buttons like add to sub results
for (let element of paginationElements) {
element.addEventListener("click", (event) => {
results.jsList.pageChangeEventInteractionHandler(interactionElements);
});
}
// render results in table imported parameter is true
queryRenderResults(result_json, true);
// ### Show corpus Metadata
showMetaDataButton.onclick = () => {
metaDataModal.open();
};
// live update of hits per page if hits per page value is changed
let changeHitsPerPageBind = results.jsList.changeHitsPerPage.bind(results.jsList);
hitsPerPageInputElement.onchange = changeHitsPerPageBind;
// live update of lr context per item if context value is changed
contextPerItemElement.onchange = results.jsList.changeContext;
// new insepct event listener makeing use of javascript bubbleing
let resultsTable = document.getElementById("query-results");
resultsTable.addEventListener("click", (event) => {
if (event.target.classList.contains("inspect-btn")) {
const dataIndex = event.target.closest("tr").dataset.index;
const fakeResponse = results.jsList.createFakeResponse();
results.jsList.showMatchContext(fakeResponse);
}
});
// scroll to top button if user scrolled down the list
let headline = document.querySelector(".headline");
let scrollToTop = document.querySelector("#menu-scroll-to-top-div");
window.addEventListener("scroll", (event) => {
if (pageYOffset > 250) {
scrollToTop.classList.toggle("hide", false);
} else {
scrollToTop.classList.toggle("hide", true);
}
});
scrollToTop.onclick = () => {
headline.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
};
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,233 @@
{% extends "nopaque.html.j2" %}
{% set headline = ' ' %}
{% set full_width = True %}
{% set imported = True %}
{% block page_content %}
<div class="col s12">
<div class="card">
<div class="card-content" style="padding-top: 5px;
padding-bottom: 0px;">
<!-- Query form -->
<div class="row">
<form id="query-form">
<div class="col s12 m10">
<div class="input-field">
<i class="material-icons prefix">search</i>
<input disabled value="{{ query_metadata.query|escape }}" id="disabled" type="text" class="validate">
<label for="disabled">Query</label>
</div>
</div>
<div class="col s12 m2 right-align">
<br class="hide-on-small-only">
</div>
</form>
</div>
</div>
</div>
</div>
<!-- entire results div/card -->
<div class="col s12" id="query-display">
<div class="card">
<div class="card-content" id="result-list" style="overflow: hidden;">
<div class=" row show-on-success">
{% include 'interactions/infos.html.j2' %}
{% include 'interactions/display.html.j2' %}
{% include 'interactions/analysis.html.j2' %}
{% include 'interactions/cite.html.j2' %}
</div>
{% include 'tables/query_results.html.j2' %}
</div>
</div>
</div>
<!-- Scroll to top element -->
{% include 'interactions/scroll_to_top.html.j2' %}
<!-- Modals -->
{% include 'modals/show_metadata.html.j2' %}
{% include 'modals/show_text_details.html.j2' %}
{% include 'modals/context_modal.html.j2' %}
<script src="{{ url_for('static', filename='js/nopaque.Results.js') }}">
</script>
<script src="{{ url_for('static', filename='js/nopaque.callbacks.js') }}">
</script>
<script src="{{ url_for('static', filename='js/nopaque.InteractionElement.js') }}">
</script>
<script>
// ###### global variables ######
var full_result_json;
var result_json;
var receivedMatchCountElement; // Nr. of loaded matches will be displayed in this element
var textLookupCountElement // Nr of texts the matches occured in will be shown in this element
var textTitlesElement; // matched text titles
var progress; // global progress value
var queryResultsProgressElement; // Div element holding the progress bar
var expertModeSwitchElement; // Expert mode switch Element
var matchCountElement; // Total nr. of matches will be displayed in this element
var interactionElements; // Interaction elements and their parameters
var contextModal; // Modal to open on inspect for further match context
// ###### Defining local scope variables
let displayOptionsFormElement; // Form holding the display informations
let resultItems; // array of built html result items row element. This is called when results are transmitted and being recieved
let hitsPerPageInputElement;let contextPerItemElement; // Form Element for display option
let paginationElements;
let inspectBtnElements;
let metaDataModal;
let showMetaDataButton
// ###### Initializing variables ######
displayOptionsFormElement = document.getElementById("display-options-form");
resultItems = [];
receivedMatchCountElement = document.getElementById("received-match-count");
textLookupCountElement = document.getElementById("text-lookup-count");
textTitlesElement = document.getElementById("text-titles");
queryResultsProgressElement = document.getElementById("query-results-progress");
expertModeSwitchElement = document.getElementById("display-options-form-expert_mode");
matchCountElement = document.getElementById("match-count");
hitsPerPageInputElement = document.getElementById("display-options-form-results_per_page");
contextPerItemElement = document.getElementById("display-options-form-result_context");
paginationElements = document.getElementsByClassName("pagination");
contextModal = document.getElementById("context-modal");
metaDataModal = document.getElementById("meta-data-modal");
showMetaDataButton = document.getElementById("show-metadata");
// js list options
displayOptionsData = ResultsList.getDisplayOptions(displayOptionsFormElement);
resultsListOptions = {page: displayOptionsData["resultsPerPage"],
pagination: [{
name: "paginationTop",
paginationClass: "paginationTop",
innerWindow: 8,
outerWindow: 1
}, {
paginationClass: "paginationBottom",
innerWindow: 8,
outerWindow: 1
}],
valueNames: ["titles", "lc", "c", "rc", {data: ["index"]}],
item: `<span></span>`
};
document.addEventListener("DOMContentLoaded", () => {
// Initialize some Modals
contextModal = M.Modal.init(contextModal, {"dismissible": true});
// ###### recreating chunk structure to reuse callback queryRenderResults()
full_result_json = {{ query_result_file_content|tojson|safe }};
result_json = {};
result_json["chunk"] = {};
result_json.chunk["cpos_lookup"] = full_result_json.cpos_lookup;
result_json.chunk["cpos_ranges"] = full_result_json.cpos_ranges;
result_json.chunk["matches"] = full_result_json.matches;
result_json.chunk["text_lookup"] = full_result_json.text_lookup;
// Init corpus analysis components
data = new Data();
resultsList = new ResultsList("result-list", resultsListOptions);
resultsMetaData = new MetaData();
results = new Results(data, resultsList, resultsMetaData);
results.clearAll(); // inits some object keys and values
// init some modals
let deleteOverlay = () => {
let overlay = document.getElementsByClassName("modal-overlay")[0];
overlay.remove();
};
metaDataModal = M.Modal.init(metaDataModal, {"preventScrolling": false,
"opacity": 0.0,
"dismissible": false,
"onOpenEnd": deleteOverlay});
// setting some initial values for user feedback
matchCountElement.innerText = full_result_json.match_count;
// Initialization of interactionElemnts
// An interactionElement is an object identifing a switch or button via
// htmlID. Callbacks are set for these elements which will be triggered on
// a pagination interaction by the user or if the status of the element has
// been altered. (Like the switche has ben turned on or off).
interactionElements = new InteractionElements();
let expertModeInteraction = new InteractionElement("display-options-form-expert_mode");
expertModeInteraction.setCallback("on",
results.jsList.expertModeOn,
results.jsList,
["query-display"])
expertModeInteraction.setCallback("off",
results.jsList.expertModeOff,
results.jsList,
["query-display"])
let activateInspectInteraction = new InteractionElement("inspect",
false);
activateInspectInteraction.setCallback("noCheck",
results.jsList.activateInspect,
results.jsList);
let changeContextInteraction = new InteractionElement("display-options-form-results_per_page",
false);
changeContextInteraction.setCallback("noCheck",
results.jsList.changeContext,
results.jsList)
interactionElements.addInteractions([expertModeInteraction, activateInspectInteraction, changeContextInteraction]);
// checks if a change for every interactionElement happens and executes
// the callbacks accordingly
interactionElements.onChangeExecute();
// eventListener if pagination is used to apply new context size to new page
// and also activate inspect match if progress is 100
// also adds more interaction buttons like add to sub results
for (let element of paginationElements) {
element.addEventListener("click", (event) => {
results.jsList.pageChangeEventInteractionHandler(interactionElements);
});
}
// render results in table imported parameter is true
queryRenderResults(result_json, true);
// ### Show corpus Metadata
showMetaDataButton.onclick = () => {
metaDataModal.open();
};
// live update of hits per page if hits per page value is changed
let changeHitsPerPageBind = results.jsList.changeHitsPerPage.bind(results.jsList);
hitsPerPageInputElement.onchange = changeHitsPerPageBind;
// live update of lr context per item if context value is changed
contextPerItemElement.onchange = results.jsList.changeContext;
// new insepct event listener makeing use of javascript bubbleing
let resultsTable = document.getElementById("query-results");
resultsTable.addEventListener("click", (event) => {
if (event.target.classList.contains("inspect-btn")) {
const dataIndex = event.target.closest("tr").dataset.index;
const fakeResponse = results.jsList.createFakeResponse();
results.jsList.showMatchContext(fakeResponse);
}
});
// scroll to top button if user scrolled down the list
let headline = document.querySelector(".headline");
let scrollToTop = document.querySelector("#menu-scroll-to-top-div");
window.addEventListener("scroll", (event) => {
if (pageYOffset > 250) {
scrollToTop.classList.toggle("hide", false);
} else {
scrollToTop.classList.toggle("hide", true);
}
});
scrollToTop.onclick = () => {
headline.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
};
});
</script>
{% endblock %}

View File

@ -90,8 +90,9 @@
</div> </div>
</div> </div>
<script> <script type="module">
var corpusList = new RessourceList("corpora", nopaque.corporaSubscribers, "Corpus"); import {RessourceList} from '../../static/js/nopaque.lists.js';
var queryResultList = new RessourceList("query-results", nopaque.queryResultsSubscribers, "QueryResult"); let corpusList = new RessourceList("corpora", nopaque.corporaSubscribers, "Corpus");
let queryResultList = new RessourceList("query-results", nopaque.queryResultsSubscribers, "QueryResult");
</script> </script>
{% endblock %} {% endblock %}

View File

@ -14,7 +14,7 @@ results. -->
<th style="width: 25%">Right Context</th> <th style="width: 25%">Right Context</th>
</tr> </tr>
</thead> </thead>
<tbody class="list" id="query-results"> <tbody class="list" id="query-results-table">
</tbody> </tbody>
</table> </table>
<ul class="pagination paginationBottom"></ul> <ul class="pagination paginationBottom"></ul>