Some cleanup (css, html, js)

This commit is contained in:
Patrick Jentsch 2021-12-09 12:50:14 +01:00
parent 067273df6d
commit 40bd169101
45 changed files with 36 additions and 6478 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -71,7 +71,8 @@ class App {
usersPatchHandler(patch) { usersPatchHandler(patch) {
let listener; let listener;
this.data = jsonpatch.apply_patch(this.data, patch); this.data = jsonpatch.applyPatch(this.data, patch).newDocument;
//this.data = jsonpatch.apply_patch(this.data, patch);
for (listener of this.eventListeners['users.patch']) {listener(patch);} for (listener of this.eventListeners['users.patch']) {listener(patch);}
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"list.min.js","sources":["webpack://List/list.min.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1,282 +0,0 @@
/**
* 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,
fullContext = null} = {}) {
this.corpusId = corpusId;
this.dynamicMode = dynamicMode;
this.logging = logging;
this.requestQueryProgress = 0;
this.socket = socket;
this.eventListeners = {};
this.isBusy = false;
this.fullContext = fullContext;
/**
* Disables 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. It is kind of hacky but not bad.
* 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 event listeners to the Client. Either socket or
* custom javascript events. Event listeners are class instances of
* ClientEventListener implemented further below.
*/
setSocketEventListeners(eventListeners) {
for (let eventListener of eventListeners) {
this.eventListeners[eventListener.type] = eventListener;
}
}
/**
* Loads the event listeners that have been registered with the function
* above so that they will be triggered on their assigned
* type strings. Type strings double as the socket event event names or
* javascript custom event names.
*/
loadSocketEventListeners() {
for (let [type, listener] of Object.entries(this.eventListeners)) {
listener.listenerFunction(type, this);
}
}
/**
* This functions emits the 'notify-view' custom javascript event. This
* triggers specific functions in the View depending on the caseIdentifier.
* The detail object can hold any type of data the View needs to know about
* to represent those to the user.
*/
notifyView(caseIdentifier, detailObject={}, notificationType='info',
raiseModalFeedback=true) {
detailObject.caseIdentifier = caseIdentifier;
detailObject.client = this;
detailObject.notificationType = notificationType;
detailObject.raiseModalFeedback = raiseModalFeedback;
const event = new CustomEvent('notify-view', { detail: detailObject });
console[notificationType]('Client dispatching Notification with details:',
detailObject);
document.dispatchEvent(event);
}
/**
* Connects to the corpus analysis session running on the server side 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);
}
// Gets the meta data of the current corpus.
getMetaData() {
this.isBusy = true;
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) {
this.isBusy = true;
console.info('corpus_analysis_query: Client sending query via',
'socket.emit for the query', queryStr);
this.socket.emit('corpus_analysis_query', queryStr);
}
/**
* Requests results data either for, 'full-results', 'sub-results' or
* 'inspect-results' (resultsType).
* Gets full results for evere provided dataIndex (one match).
* Full results means with full context. So the Client has to request all
* matches from the server again!
**/
getResultsData(resultsType, dataIndexes, results) {
let tmp_first_cpos = [];
let tmp_last_cpos = [];
let objectKey = '';
if (resultsType === 'full-results') {
objectKey = 'fullResultsData';
} else if (resultsType === 'sub-results') {
objectKey = 'subResultsData';
} else if (resultsType === 'inspect-results') {
objectKey = 'inspectResultsData';
}
// Delete old data before new data is coming in.
results[objectKey].init();
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]);
}
this.socket.emit('corpus_analysis_get_match_with_full_context',
{type: resultsType,
data_indexes: dataIndexes,
first_cpos: tmp_first_cpos,
last_cpos: tmp_last_cpos,});
}
/**
* Gets results data either for, 'full-results' or 'sub-results'
* Gets results for every provided dataIndex (one match) without full
* context. Because no context is needed the results data is gathered locally
* from results.data and not from the server.
**/
getResultsDataWithoutContext(resultsType, dataIndexes, results, resultsList) {
this.notifyView('results-data-recieving', {fullContext: false});
let objectKey = '';
if (resultsType === 'full-results') {
console.info('Saving full-results data without full context.');
objectKey = 'fullResultsData';
} else if (resultsType === 'sub-results') {
console.info('Saving sub-results data without full context.');
objectKey = 'subResultsData';
}
// Get matches from results.data.
let matches = [];
let cpos = [];
let match;
for (let index of dataIndexes) {
match = results.data.matches[index]
matches.push(match)
// Get cpos from match.
let {lc, c, rc} = resultsList.helperCreateCpos(results.data.cpos_ranges,
match);
cpos.push(...lc);
cpos.push(...c);
cpos.push(...rc);
}
// Get cpos_lookups from cposes.
let cpos_lookup = {};
let textIds = new Set;
for (let single_cpos of cpos) {
textIds.add(results.data.cpos_lookup[single_cpos].text);
Object.assign(cpos_lookup, { [single_cpos]: results.data.cpos_lookup[single_cpos]});
}
let text = {};
let text_lookup = {};
for (let id of textIds) {
text[id] = results.data.text_lookup[id];
Object.assign(text_lookup, text);
}
/**
* Save the data from results.dat either in results.fullResultsData or
* results.subResultsData.
*/
results[objectKey].init();
results[objectKey].matches.push(...matches);
results[objectKey].addData(cpos_lookup, "cpos_lookup");
results[objectKey].addData(text_lookup, "text_lookup");
results[objectKey].addData(results.metaData);
results[objectKey].query = results.data.query;
results[objectKey].corpus_type = resultsType;
results[objectKey].match_count = matches.length;
results[objectKey].cpos_ranges = results.data.cpos_ranges;
results[objectKey].fullContext = false;
if (objectKey === 'subResultsData') {
// Remove match_count from texts, because they are useless in sub results
for (let [key, value] of Object.entries(results[objectKey].text_lookup)) {
delete results[objectKey].text_lookup[key].match_count;
}
}
console.info('Results data without context has been saved.', results);
this.isBusy = false;
this.notifyView('results-data-recieved', {type: resultsType,
results: results,
fullContext: false});
}
}
/**
* This class is used to create an event listener listening for socket or
* javascript custom events.
* 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 or custom javascript event name
* identifier.
*/
class ClientEventListener {
constructor(type, listenerFunction) {
this.listenerCallbacks = {};
this.listenerFunction = listenerFunction;
this.type = type;
}
// Registers callbacks to this ClientEventListener.
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);
}
}
// Executes a specific registered callback by provoding a type string.
executeCallback(defaultArgs, type) {
let listenerCallback = this.listenerCallbacks[type];
let args = defaultArgs.concat(listenerCallback.args) ;
listenerCallback.callbackFunction(...args);
}
}
/**
* This class is used to create an ListenerCallback which will be registered
* to an ClientEventListener so the listener can invoke the associated
* 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

@ -1,161 +0,0 @@
/**
* This callback is called on a socket.on "corpus_analysis_send_meta_data".
* Handels incoming corpus metadata
*/
function saveMetaData(...args) {
let [payload, client, results, rest] = args;
client.notifyView('meta-data-recieving');
results.metaData.init(payload)
console.info('Metada saved:', results);
client.isBusy = false;
client.notifyView('meta-data-recieved');
}
/**
* 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(...args) {
// deletes old data from query issued before this new query
let [payload, client, results, rest] = args;
// 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 });
}
/**
* This callbacks saves the incoming query data chunks into the model results.
*/
function saveQueryData(...args) {
let [payload, client, results, rest] = args;
// Get data matches length before new chunk data is being inserted
let dataLength = results.data.matches.length;
if (client.dynamicMode) {
// 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');
/**
* Increment match_counts per text in a global results varaible because
* they are coming in chunkwise.
*/
if (payload.chunk.text_lookup) {
for (let [text_key, value] of Object.entries(payload.chunk.text_lookup)) {
if (!(text_key in results.tmp_match_counts)) {
results.tmp_match_counts[text_key] = {match_count: 0};
}
results.tmp_match_counts[text_key].match_count += payload.chunk.text_lookup[text_key].match_count;
}
}
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.isBusy = false;
// Update text_lookup with tmp_match_counts.
for (let [text_key, value] of Object.entries(results.tmp_match_counts)) {
results.data.text_lookup[text_key].match_count = results.tmp_match_counts[text_key].match_count;
}
client.notifyView('query-data-recieved');
}
} else {
results.data.matches.push(...payload.matches);
results.data.addData(payload.cpos_lookup, 'cpos_lookup');
results.data.addData(payload.text_lookup, 'text_lookup');
results.data.cpos_ranges = payload.cpos_ranges;
let queryFormElement = document.querySelector('#query-form');
results.data.getQueryStr(queryFormElement);
client.requestQueryProgress = 100;
client.notifyView('query-data-recieving',
{ results: results,
client: client,
dataLength: dataLength });
console.info('Query data chunk saved', results.data);
if (client.requestQueryProgress === 100) {
console.log(results.data);
client.notifyView('query-data-recieved');
}
}
}
/**
* This callback gets the results data for the export. Either requesting it
* whith full context from the server or gets it locally without full context
* from the already present results.data. Result data is identified with the
* dataIndexes. On index is one match.
*/
function getResultsData(...args) {
let [resultsType, dataIndexes, resultsList, client, results, rest] = args;
client.isBusy = true;
if (resultsList.exportFullInspectContext.checked
|| resultsType === 'inspect-results') {
console.info('Get results with full context');
client.getResultsData(resultsType, dataIndexes, results);
} else {
console.info('Get results without full context');
client.getResultsDataWithoutContext(resultsType, dataIndexes, results,
resultsList);
}
}
/**
* Handles incoming results which have been requested via getResultsData(). and
* saves the data accorindgly into the results object.
*/
function saveResultsData(...args) {
let [payload, type, client, results, rest] = args;
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. Data is incoming one match at a time.
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 += 1;
results[objectKey].cpos_ranges = payload.cpos_ranges;
results[objectKey].fullContext = true;
console.info('Results data has been saved.', results);
// Notify view to update progress bar
client.notifyView('results-data-recieving', {type: type,
progress: payload.progress})
if (payload.progress === 100) {
client.isBusy = false;
if (objectKey === 'fullResultsData') {
// Get match count per text from results.data only for fullResultsData
results[objectKey].text_lookup = results.data.text_lookup;
}
client.notifyView('results-data-recieved', {type: type,
results: results,
fullContext: true});
}
}
// export callbacks
export {
prepareQueryData,
saveMetaData,
saveQueryData,
getResultsData,
saveResultsData,
};

View File

@ -1,201 +0,0 @@
/**
* 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('client-failed', { msg: errorText }, 'error');
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 {
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.group('Failed to recieve meta data.');
console.error('corpus_analysis_meta_data: Client failed to recieve',
'meta data via socket.on');
console.error(`corpus_analysis_meta_data: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error');
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) {
/**
* Check if request for session was OK.
* If OK execute registered callbacks and notify View.
*/
client.socket.on(type, (response) => {
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 {
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.group('corpus_analysis_query: Client failed recieving',
'query process status via socket.on');
if (response.payload.code == 1281) {
errorText += ' - Invalid Query';
console.error(`corpus_analysis_query: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error', false);
} else {
console.error(`corpus_analysis_query: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error');
}
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.
*/
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 {
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.group('corpus_analysis_query_results: Client failed recieving',
'the results via socket.on');
console.error(`corpus_analysis_query: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error');
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_get_match_with_full_context: Client recieving results data',
'via socket.on');
console.info(`corpus_analysis_get_match_with_full_context: ${response.code} - ${response.msg}`);
console.info(response);
// executing the registered callbacks
client.eventListeners[type].executeCallbacks([response.payload,
response.type]);
console.groupEnd();
} else {
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.group('Failed to recieve results data.');
console.error('corpus_analysis_get_match_with_full_context: Client failed to recieve',
'results data via socket.on');
console.error(`corpus_analysis_get_match_with_full_context: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error');
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,
event.detail.resultsList],
caseIdentifier);
break
default:
console.error('Recieved unkown notification case identifier from View');
// do something to not crash the analysis session?
}
});
}
// export listeners from this module
export {
recieveConnected,
recieveMetaData,
recieveQueryStatus,
recieveQueryData,
recieveViewNotification,
recieveResultsData,
};

View File

@ -1,141 +0,0 @@
/**
* 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.
*/
// Results class bundleing the different data objects.
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.');
}
// Reset all the data objects in the results class and thus emptying them.
init() {
this.data.init();
this.metaData.init();
this.fullResultsData.init();
this.subResultsData.init();
this.inspectResultsData.init();
// Temporarly save match counts per text
this.tmp_match_counts = {};
}
}
/**
* Class that defines the actual data objects holding the results data
* requested by the client. Data can be the results of a query, full results
* data for the export, sub results data for the export or inspect results data.
* All kinds are structured the same way.
*/
class Data {
/**
* Sets empty object structure. Also usefull to delete old results.
* MatchCount default is 0.
*/
init(matchCount=0, type="results") {
// List of all c with lc and rc CPOS.
this.matches = [];
/**
* CPOS lookup object. CPOS are the key and value are infos about the CPOS
* like lemma, ner, pos, text ID etc. CPOS from the matches correspond to
* exactly one object in the cpos_lookup.
*/
this.cpos_lookup = {};
/**
* Same like above but for text IDs. One CPOS object always has a text ID
* referencing on text object in the text_lookup. Text ID is the key. Values
* are author, publishing year etc.
*/
this.text_lookup = {};
this.match_count = matchCount;
this.corpus_type = 'results';
this.cpos_ranges = null;
this.query = '';
}
/**
* Function to add json data/object data to this data instance.
* If no key is specified the entire data will be assigned to this data
* instance.
*/
addData(jsonData, key=null) {
if (key !== null) {
Object.assign(this[key], jsonData);
} else if (key === null) {
Object.assign(this, jsonData)
}
}
// Get query as a string from the 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")
}
}
/**
* Similar to the data class but just intended for meta data about the current
* corpus the client is working with.
*/
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 the classes
export {
Results,
Data,
MetaData
};

View File

@ -1,837 +0,0 @@
/**
* This class implements a ViewEventListener that is listening for the
* specified
*/
class ViewEventListener {
constructor(type, listenerFunction, args=[]) {
this.listenerFunction = listenerFunction;
this.type = type;
this.args = args;
}
}
/**
* 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 = {};
/**
* Holds True/false for check buttons used to add matches to sub-results.
* If checked, it is True. If unchecked, it is false. Buttons for this
* have the class add. The ittle round check buttons to add matches to sub
* results. Key is match index. Value is true or false as mentioned above.
*/
this.subResultsIndexes = {};
// ViewEventListeners listening for client notifications.
this.notificationListeners = {};
this.knownHTMLElements = new Set();
}
/**
* Function to clear/reset some class field values. Usefull if a new query
* hase been issued by the user.
*/
resetFields() {
this.subResultsIndexes = {};
}
/**
* 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 respectively querySelectorAll method.
* If the query selector is passed as an Array of length 2, with the second
* element defining modal options, teh identified element will be initialized
* as a modal with the given options.
*/
getHTMLElements(arrayOfSelectors) {
for (let selector of arrayOfSelectors) {
// Check if identified Element should be initialized as a modal.
let modalInit = false;
let options;
if (Array.isArray(selector)) {
options = selector[1];
selector = selector[0];
modalInit = true;
}
// Check if the current selector has already been used.
if (this.knownHTMLElements.has(selector)) {
continue;
} else {
// Get element or elements.
let element;
let elements;
if (selector.startsWith('#')) {
element = document.querySelector(selector);
} else {
elements = document.querySelectorAll(selector);
elements = [...elements];
}
// Create valid javascript instance field name.
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;
// Initialize current element as modal if modalInit true.
if (modalInit) {
this[cleanKey] = M.Modal.init(this[cleanKey], options);
}
}
// Add current selector to knwonHTMLElements.
this.knownHTMLElements.add(selector);
}
}
/**
* Register ViewEventListeners to the ResultsList. Which will listen for
* the specified event.
*/
setViewEventListeners(notificationListeners) {
for (let notificationListener of notificationListeners) {
this.notificationListeners[notificationListener.type] = notificationListener;
}
}
/**
* Loads the ViewEventListeners so that hey will be listening to their
* assigned custom events.
*/
loadViewEventListeners() {
for (let [type, listener] of Object.entries(this.notificationListeners)) {
if (listener.args.length > 0) {
listener.listenerFunction(...listener.args);
} else {
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_values) {
let lc, c, rc;
/**
* 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 = cpos_values.lc ? range(cpos_values.lc[0], cpos_values.lc[1], 1) : [];
c = range(cpos_values.c[0], cpos_values.c[1], 1);
rc = cpos_values.rc ? range(cpos_values.rc[0], cpos_values.rc[1], 1) : [];
return {lc: lc, c: c, rc: rc};
}
// 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("corpus-analysis-color.lighten");
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("corpus-analysis-color.lighten");
btn.textContent = "add";
}
/**
* This function is invoked when the users adds or removes a match using the
* add-btn (+ button/or green checkmark) to/from sub-results. When the button
* is clicked the function checks if the current dataIndex ID is already
* saved in subResultsIndexes or not. If it is not the dataIndex will be used
* as a key in subResultsIndexes with the value true. If it is already added
* the entry with the key dataIndex will be deleted from subResultsIndexes.
* Visual feedback (green checkmark if a match has been added etc.) is also
* handled on the basis of the information stored in subResultsIndexes.
*/
addToSubResults(dataIndex, client, tableCall=true) {
let toShowArray;
dataIndex = parseInt(dataIndex);
if (!this.subResultsIndexes[dataIndex]
|| this.subResultsIndexes[dataIndex] === undefined) {
// add button is activated because status is false or undefined
this.helperActivateAddBtn(event.target);
this.subResultsIndexes[dataIndex] = true;
toShowArray = Object.keys(this.subResultsIndexes).map(index => parseInt(index));
// Add 1 because indexes are zero based. User sees 1 based numbering.
toShowArray = toShowArray.map(index => index + 1);
// Allways sort the shown indexes for the user if new match is added.
toShowArray = toShowArray.sort(function(a, b){return a-b});
this.subResultsIndexesDisplay.textContent = toShowArray.join(', ');
M.textareaAutoResize(this.subResultsIndexesDisplay);
this.nrMarkedMatches.textContent = Object.keys(this.subResultsIndexes).length;
} else if (this.subResultsIndexes[dataIndex]) {
// add button is deactivated because status is true
this.helperDeactivateAddBtn(event.target);
delete this.subResultsIndexes[dataIndex];
toShowArray = Object.keys(this.subResultsIndexes).map(index => parseInt(index));
// Add 1 because indexes are zero based. User sees 1 based numbering.
toShowArray = toShowArray.map(index => index + 1);
// Allways sort the shown indexes for the user if new match is added.
toShowArray = toShowArray.sort(function(a, b){return a-b});
this.subResultsIndexesDisplay.textContent = toShowArray.join(', ');
this.nrMarkedMatches.textContent = Object.keys(this.subResultsIndexes).length;
M.textareaAutoResize(this.subResultsIndexesDisplay);
}
// Toggles create button according to the number of ids in addToSubResultsIdsToShow
if (Object.keys(this.subResultsIndexes).length > 0 && !client.isBusy) {
this.subResultsCreate.classList.toggle('disabled', false);
} else if (Object.keys(this.subResultsIndexes).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/resultsList 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.subResultsIndexes[dataIndex]) {
this.helperActivateAddBtn(tableAddBtn);
} else {
this.helperDeactivateAddBtn(tableAddBtn);
}
}
}
// Toggle inspect buttons depending on the Client status
toggleInspectButtons(client) {
if (!client.isBusy) {
this.activateInspectButtons();
} else if (client.isBusy) {
this.deactivateInspectButtons();
}
}
// Helper function. Should be private if feature is available.
activateInspectButtons() {
let inspectBtnElements;
inspectBtnElements = document.querySelectorAll('.inspect');
for (let inspectBtn of inspectBtnElements) {
inspectBtn.classList.toggle('disabled', false);
}
}
// Helper function. Should be private if feature is available.
deactivateInspectButtons() {
let inspectBtnElements;
inspectBtnElements = document.querySelectorAll('.inspect');
for (let inspectBtn of inspectBtnElements) {
inspectBtn.classList.toggle('disabled', true);
}
}
/**
* Function to inspect one match in more detail (Showing more context).
* If in dynamic mode the view notifies the client to requests the new
* context for the one match identified by the given dataIndex.
* If not in dynamic mode the the needed context will be gathered from the
* already present results in results.data.
*/
inspect(client, results, dataIndex, type) {
// initialize context modal
this.getHTMLElements([
['#context-modal', true],
'#context-results',
'#create-inspect-menu',
'#create-from-inspect',
]);
// Clear fields from old data on every new inspect() call.
this.contextId = dataIndex[0];
this.contextResults.innerHTML = '';
// Open modal.
this.contextModal.open();
this.contextResults.insertAdjacentHTML('afterbegin', `
<div class="progress">
<div class="indeterminate"></div>
</div>
`);
if (client.dynamicMode) {
// Notify Client to get results from server.
this.notifyClient('get-results', {resultsType: 'inspect-results',
dataIndexes: dataIndex,
resultsList: this});
} else {
// Gather results data from already present data.
results.inspectResultsData.matches = [results.data.matches[dataIndex[0]]];
results.inspectResultsData.cpos_ranges = results.data.cpos_ranges;
this.showMatchContext(results, client)
}
// Match nr for user to display derived from data_index.
let contextMatchNrElement = document.getElementById("context-match-nr");
contextMatchNrElement.textContent = this.contextId + 1;
// Add the add button to add this match to sub results with onclick event.
let classes = `btn-floating btn waves-effect` +
` waves-light corpus-analysis-color.lighten 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], client, 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.subResultsIndexes[dataIndex[0]]) {
this.helperActivateAddBtn(addToSubResultsIdsBtn.firstElementChild);
} else if (!this.subResultsIndexes[dataIndex[0]]) {
this.helperDeactivateAddBtn(addToSubResultsIdsBtn.firstElementChild);
}
this.createInspectMenu.innerHTML = '';
this.createInspectMenu.appendChild(addToSubResultsIdsBtn);
// Hide create menu if not in dynamic mode.
if (!client.dynamicMode) {
this.createFromInspect.classList.add('hide');
}
}
/**
* Create Element from HTML String. Helper function should be private.
* https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
*/
HTMLTStrToElement(htmlStr) {
let template = document.createElement("template");
htmlStr = htmlStr.trim();
template.innerHTML = htmlStr;
return template.content.firstChild;
}
/**
* Used either as a callback if the client has been notified to get new
* results with new full context. Or just directly invoced as a function
* with the according input data.
*/
showMatchContext(results, client) {
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.matches[0]);
// Create sentence strings as tokens.
let tokenHTMLArray = [];
let htmlTokenStr = ``;
let tokenHTMlElement;
let token;
for (let cpos of lc) {
if (client.dynamicMode) {
token = results.inspectResultsData.cpos_lookup[cpos];
// If client is not in dynamic mode use cpos_lookup from results.data
} else {
token = results.data.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) {
if (client.dynamicMode) {
token = results.inspectResultsData.cpos_lookup[cpos];
// If client is not in dynamic mode use cpos_lookup from results.data
} else {
token = results.data.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) {
if (client.dynamicMode) {
token = results.inspectResultsData.cpos_lookup[cpos];
// If client is not in dynamic mode use cpos_lookup from results.data
} else {
token = results.data.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);
}
// Remove loading indeterminate HTML before context is inserted.
this.contextResults.innerHTML = '';
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 expert mode switch event for the modal to toggle expert mode.
this.inspectDisplayOptionsFormExpertModeInspect.onchange = (event) => {
if (event.target.checked) {
this.expertModeOn("context-results", results);
} else {
this.expertModeOff("context-results")
}
};
// Add switch event to toggle Sentence highlighting.
this.inspectDisplayOptionsFormHighlightSentences.onchange = (event) => {
if (event.target.checked) {
this.higlightContextSentences();
} else {
this.unhighlightContextSentences();
}
};
// Add range event to change nr of context sentences.
this.contextSentences.onchange = (event) => {
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 = document.getElementById("context-results").getElementsByClassName("sentence");
for (let s of sentences) {
s.insertAdjacentHTML("beforeend", `<span><br><br></span>`)
}
}
// Reverse operation of above function.
unhighlightContextSentences() {
let sentences = document.getElementById("context-results").getElementsByClassName("sentence");
let br;
for (let s of sentences) {
br = s.lastChild;
br.remove();
}
}
// Changes how many context sentences in inspect view are shown.
changeSentenceContext(sValue, maxSValue=10) {
sValue = maxSValue - sValue;
// console.log(sValue);
let sentences = document.getElementById("context-results").getElementsByClassName("sentence");
let array = Array.from(sentences);
let toHideArray;
let toShowArray;
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;
}
for (let s of toHideArray) {
s.classList.add("hide");
}
for (let s of toShowArray) {
s.classList.remove("hide");
}
}
// ###### Display options functions changing how results are being displayed ######
/**
* Event function that changes the shown hits per page.
* Just alters the resultsList.page property.
*/
changeHitsPerPage(client, results) {
this.page = this.displayOptionsFormResultsPerPage.value;
this.update();
this.changeContext();
this.toggleInspectButtons(client);
if (this.displayOptionsFormExpertMode.checked) {
this.expertModeOn('query-display', results);
}
}
/**
* Event function triggered on context select change also if pagination is
* clicked.
*/
changeContext() {
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) {
let token = results.data.cpos_lookup[event.target.dataset.cpos];
if (!token) {
token = results.inspectResultsData.cpos_lookup[event.target.dataset.cpos];
}
this.currentTooltipElement = M.Tooltip.init(event.target, {
'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 destroy the current Tooltip for the current hovered tooltip
* on mouse leave
*/
tooltipEventDestroy(event) {
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);
}
}
/**
* Turn the expert mode off for all tokens in the DOM element identified by
* its htmlID.
*/
expertModeOff(htmlId) {
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, client, imported=false) {
// Gather values from item.
let values = item.values();
let {lc, c, rc} = this.helperCreateCpos(values)
// Get infos for full match row.
let matchRowElement = document.createElement("tr");
matchRowElement.setAttribute("data-index", values.index)
let lcCellElement = document.createElement("td");
lcCellElement.classList.add("left-context");
matchRowElement.appendChild(lcCellElement);
for (let cpos of lc) {
let 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.
let textTitles = new Set();
let aCellElement = document.createElement("td");
aCellElement.classList.add("actions");
let cCellElement = document.createElement("td");
cCellElement.classList.add("match-hit");
let textTitlesCellElement = document.createElement("td");
textTitlesCellElement.classList.add("titles");
let matchNrElement = document.createElement("td");
matchNrElement.classList.add("match-nr");
matchRowElement.appendChild(cCellElement);
matchRowElement.appendChild(aCellElement);
for (let cpos of c) {
let 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.
let css = `margin-right: 5px; margin-bottom: 5px;`
let classes = `btn-floating btn waves-effect` +
` waves-light corpus-analysis-color.lighten`
// Add inspect button to trigger inspect view with more context.
let 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 be able add matches to sub-results.
let addToSubResultsBtn = document.createElement("a");
addToSubResultsBtn.setAttribute("style", css);
addToSubResultsBtn.setAttribute("class", classes + ` add`);
addToSubResultsBtn.innerHTML = '<i class="material-icons add-btn">add</i>';
if (client.dynamicMode || client.fullContext) {
aCellElement.appendChild(inspectBtn);
}
if (client.dynamicMode) {
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
let rcCellElement = document.createElement("td");
rcCellElement.classList.add("right-context");
matchRowElement.appendChild(rcCellElement);
for (let cpos of rc) {
let 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>Meta Data Description</th>
<th>Value</th>
</tr>
</thead>
<tbody>`
for (let [outerKey, outerValue] of Object.entries(metaDataObject)) {
// Use more descriptive names.
if (outerKey === 'corpus_all_texts') {
let tmpName = 'All texts in this corpus';
html += `<tr>
<td style="text-transform: uppercase;">${tmpName.replace(/_/g, " ")}
</td>`
} else if (outerKey === 'text_lookup') {
let tmpName = 'Texts where the query resulted in matches'
html += `<tr>
<td style="text-transform: uppercase;">${tmpName.replace(/_/g, " ")}
</td>`
} else {
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 table.
*/
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 classes.
export {
ViewEventListener,
ResultsList
};

View File

@ -1,244 +0,0 @@
/**
* 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.
*/
// Callback to disable some elements for the user when the client is busy.
function disableElementsGeneralCallback(resultsList, detail) {
if (detail.client.isBusy) {
resultsList.fullResultsCreate.classList.toggle('disabled', true);
resultsList.subResultsCreate.classList.toggle('disabled', true);
resultsList.toggleInspectButtons(detail.client);
}
}
// Callback to enable some elements for the user when the client is not busy.
function enableElementsGeneralCallback(resultsList, detail) {
if (!detail.client.isBusy) {
resultsList.fullResultsCreate.classList.toggle('disabled', false);
if (Object.keys(resultsList.subResultsIndexes).length > 0) {
resultsList.subResultsCreate.classList.toggle('disabled', false);
}
resultsList.toggleInspectButtons(detail.client);
}
}
/**
* Callback opening the loading modal when the client is connecting to the
* CQP server.
*/
function connectingCallback(resultsList, detail) {
resultsList.getHTMLElements(['#analysis-init-modal']);
resultsList.analysisInitModal = M.Modal.init(resultsList.analysisInitModal,
{dismissible: false});
resultsList.analysisInitModal.open();
}
// Callback that closes the loading modal from above.
function connectedCallback(resultsList, detail) {
/**
* In the past this closed the init modal. But the init modal is now being
* closed when the meta data has also been recieved. See below.
*/
}
// Callback that closes the loading modal from above.
function metaDataRecievedCallback(resultsList, detail) {
resultsList.analysisInitModal.close();
}
// Callback that shows the user some feedback if the client raised an error.
function clientFailedCallback(resultsList, detail) {
resultsList.getHTMLElements([
'#analysis-init-progress',
'#analysis-init-error',
'#user-feedback',
]);
if (detail.raiseModalFeedback) {
resultsList.analysisInitModal.open();
resultsList.analysisInitProgress.classList.toggle('hide');
resultsList.analysisInitError.classList.toggle('hide');
resultsList.analysisInitError.textContent = detail.msg;
} else {
nopaque.appClient.flash(detail.msg, 'error')
}
}
// Callback doing some preperation work if a query has been issued by the user.
function queryDataPreparingCallback(resultsList, detail) {
// 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-titles',
'#text-lookup-count',
'#query-results-user-feedback',
'#query-progress-bar',
'#query-results-create',
'#sub-results-indexes-display',
'#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);
resultsList.showCorpusFiles.classList.toggle('disabled', true);
/**
* 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.textLookupCount.textContent = 0;
resultsList.nrMarkedMatches.textContent = 0;
resultsList.subResultsIndexesDisplay.textContent = '';
resultsList.resetFields();
}
/**
* Callback handling the incoming results of an issued query. It renders
* the incoming matches using the resultsList for the user.
*/
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,
client);
}
});
// 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}`;
// updating table on finished item creation callback via createResultRowElement
resultsList.update();
resultsList.changeHitsPerPage(client, results);
resultsList.changeContext();
//activate expertMode of switch is checked
if (resultsList.displayOptionsFormExpertMode.checked) {
resultsList.expertModeOn('query-display', results);
}
} else if (!client.dynamicMode) {
resultsList.add(resultItems, (items) => {
for (let item of items) {
item.elm = resultsList.createResultRowElement(item,
results.data,
client,
true);
}
});
// 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}`;
// updating table on finished item creation callback via createResultRowElement
resultsList.update();
resultsList.changeHitsPerPage(client, results);
resultsList.changeContext();
}
}
// Callback that is executed when all results from an issued query have been recieved
function queryDataRecievedCallback(resultsList, detail) {
// hide or disable some things for the user
resultsList.queryResultsUserFeedback.classList.toggle('hide', true);
resultsList.queryProgressBar.classList.toggle('hide', true);
// reset bar progress for next query
resultsList.queryProgressBar.firstElementChild.style.width = '0%';
resultsList.showCorpusFiles.classList.toggle('disabled', false);
}
/**
* Callback that is handling incoming results data. Results data is needed for
* the export and download of the data.
*/
function resultsDataRecievingCallback(resultsList, detail) {
resultsList.getHTMLElements([
'#full-results-progress-bar',
'#sub-results-progress-bar',
]);
// Disable the full context switch when results are being recieved.
resultsList.exportFullInspectContext.setAttribute('disabled', '');
if (detail.type === 'full-results' && detail.progress) {
resultsList.fullResultsProgressBar.firstElementChild.style.width = `${detail.progress}%`;
resultsList.fullResultsProgressBar.classList.toggle('hide', false);
} else if (detail.type === 'sub-results' && detail.progress) {
resultsList.subResultsProgressBar.firstElementChild.style.width = `${detail.progress}%`;
resultsList.subResultsProgressBar.classList.toggle('hide', false);
}
}
/**
* Callback is executed when all results data has been recieved.
* Reactivates the resutls create buttons etc.
*/
function resultsDataRecievedCallback(resultsList, detail) {
// create strings for create buttons depending on type
const handleType = (keyPrefix, text) => {
// Enable the full context switch when results have been recieved
resultsList.exportFullInspectContext.removeAttribute('disabled', '');
// 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');
if (detail.fullContext) {
resultsList.fullResultsProgressBar.firstElementChild.style.width = `0%`;
resultsList.fullResultsProgressBar.classList.toggle('hide', true);
}
} else if (detail.type ==='sub-results') {
handleType('subResults', 'Sub-Results');
if (detail.fullContext) {
resultsList.subResultsProgressBar.firstElementChild.style.width = `0%`;
resultsList.subResultsProgressBar.classList.toggle('hide', true);
}
} else if (detail.type ==='inspect-results') {
if (Object.keys(resultsList.subResultsIndexes).length === 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, detail.client);
}
}
// export the callbacks
export {
connectingCallback,
connectedCallback,
metaDataRecievedCallback,
clientFailedCallback,
queryDataPreparingCallback,
queryDataRecievingCallback,
queryDataRecievedCallback,
resultsDataRecievingCallback,
resultsDataRecievedCallback,
disableElementsGeneralCallback,
enableElementsGeneralCallback,
};

View File

@ -1,378 +0,0 @@
/**
* 1.)
* 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 which will call different
* callback functions depending on the detail information of the notification
* event.
* 2.)
* This file also contains vanilla javascript Event listeners which are
* listening for button clicks etc. the user is doing for page interaction.
* They will be registered the same way as teh listeners above.
*/
import {
connectingCallback,
connectedCallback,
metaDataRecievedCallback,
clientFailedCallback,
queryDataPreparingCallback,
queryDataRecievingCallback,
queryDataRecievedCallback,
resultsDataRecievingCallback,
resultsDataRecievedCallback,
disableElementsGeneralCallback,
enableElementsGeneralCallback,
} from './callbacks.js';
// Import the script that implements a spinner animation for buttons.
import {
loadingSpinnerHTML,
} from './spinner.js';
/**
* The Listener listening for the notification event 'notify-view' dispatched
* by the client and execeutes callbacks accordingly.
*/
function recieveClientNotification(eventType, resultsList) {
document.addEventListener(eventType, (event) => {
let caseIdentifier = event.detail.caseIdentifier;
switch (caseIdentifier) {
case 'client-failed':
console.error('View recieved notification:', caseIdentifier);
// execute callbacks
clientFailedCallback(resultsList, event.detail);
break;
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 'meta-data-recieving':
console.info('View recieved notification:', caseIdentifier);
break;
case 'meta-data-recieved':
console.info('View recieved notification:', caseIdentifier);
// execute
metaDataRecievedCallback(resultsList, event.detail);
break;
case 'query-data-prepareing':
console.info('View recieved notification:', caseIdentifier);
// Hide all download buttons
resultsList.fullResultsExport.classList.toggle('hide', true);
resultsList.subResultsExport.classList.toggle('hide', true);
// Show all create buttons
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);
resultsDataRecievingCallback(resultsList, event.detail);
break;
case 'results-data-recieved':
console.info('View recieved notification:', caseIdentifier);
// execute callbacks
console.info(event.detail);
resultsDataRecievedCallback(resultsList, event.detail);
enableElementsGeneralCallback(resultsList, event.detail);
break;
default:
console.error('Recieved unkown notification case identifier from Client');
// do something to not crash the analysis session?
}
});
}
/**
* This are some vanilla javascript Event listeners which are listening
* for button clicks etc. the user is doing to interact with the page.
* They will be registered the same way as the listeners above.
*/
/**
* The following listener handles what functions are called when the user
* does use the page navigation to navigate to a new page.
*/
function pageNavigation(resultsList, results, client) {
for (let element of resultsList.pagination) {
element.addEventListener("click", (event) => {
// Shows match context according to the user picked value on a new page.
resultsList.changeContext();
// De- or activates expertMode on new page depending on switch value.
if (resultsList.displayOptionsFormExpertMode.checked) {
resultsList.expertModeOn('query-display', results);
} else {
resultsList.expertModeOff('query-display');
}
// Activates inspect buttons on new page if client is not busy.
resultsList.toggleInspectButtons(client);
});
}
}
/**
* The following event Listener handles the expert mode switch for the list.
*/
function expertModeSwitch(resultsList, results) {
resultsList.displayOptionsFormExpertMode.onchange = (event) => {
if (event.target.checked) {
resultsList.expertModeOn('query-display', results);
} else {
resultsList.expertModeOff('query-display');
}
};
}
/**
* The following event Listener handles the add-btn and the inspect-btn
* onclick events via bubbleing.
*/
function actionButtons(resultsList, results, client) {
resultsList.queryResultsTable.addEventListener('click', (event) => {
let dataIndex;
if (event.target.classList.contains('inspect-btn')) {
dataIndex = parseInt(event.target.closest('tr').dataset.index);
resultsList.inspect(client, results, [dataIndex], 'inspect');
} else if (event.target.classList.contains('add-btn')) {
dataIndex = parseInt(event.target.closest('tr').dataset.index);
resultsList.addToSubResults(dataIndex, client);
}
})
}
/**
* Following event listeners handle the change of Context size per match and
* the number of matches shown per page.
*/
function displayOptions(resultsList, results, client) {
resultsList.displayOptionsFormResultsPerPage.onchange = (event) => {
resultsList.changeHitsPerPage(client, results);
};
resultsList.displayOptionsFormResultContext.onchange = (event) => {
resultsList.changeContext();
};
}
/**
* The following event listener handles the show metadata button and its
* functionality.
*/
function showMetaData(resultsList, results) {
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 listener handles the button showing infos about matches
* and their corresponding corpus files
*/
function showCorpusFiles(resultsList, results) {
resultsList.showCorpusFiles.onclick = () => {
resultsList.showCorpusFilesModalContent.innerHTML = '';
let htmlString = `
<div id="corpus-file-table">
<ul class="pagination paginationTop"></ul>
<table class="responsive-table highlight">
<thead>
<tr>
<th class="sort" data-sort="title">Title</th>
<th class="sort" data-sort="year">Year</th>
<th class="sort" data-sort="match-count">Match count in this text</th>
</tr>
</thead>
<tbody class="list">
`
for (let [key, value] of Object.entries(results.data.text_lookup)) {
htmlString += `
<tr>
<td class="title">${value.title}</td>
<td class="year">${value.publishing_year}</td>
<td class="match-count">${value.match_count}</td>
</tr>
`
}
htmlString += `
</tbody>
</table>
<ul class="pagination paginationBottom"></ul>
</div>
`
resultsList.showCorpusFilesModalContent.insertAdjacentHTML('afterbegin', htmlString);
resultsList.showCorpusFilesModal.open();
let options = {
page: 10,
pagination: [{
name: "paginationTop",
paginationClass: "paginationTop",
innerWindow: 8,
outerWindow: 1
}, {
paginationClass: "paginationBottom",
innerWindow: 8,
outerWindow: 1
}],
valueNames: ["title", "year", "match-count"],
};
let corpusFileTable = new List('corpus-file-table', options);
}
}
/**
* Checks if resultsList.exportFullInspectContext switch is changed.
* If it has been changed reset all Download buttons.
*/
function exportFullContextSwitch(resultsList) {
resultsList.exportFullInspectContext.onchange = (event) => {
// Hide all download buttons.
resultsList.fullResultsExport.classList.toggle('hide', true);
resultsList.subResultsExport.classList.toggle('hide', true);
// Show result create buttons.
resultsList.fullResultsCreate.classList.toggle('hide', false);
resultsList.subResultsCreate.classList.toggle('hide', false);
}
}
/**
* 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
* 5. Download single inspect-results
*/
// 1. Add events for full-results create
function createFullResults(resultsList, results) {
resultsList.fullResultsCreate.onclick = (event) => {
resultsList.fullResultsCreate.querySelector('i').classList.toggle('hide');
resultsList.fullResultsCreate.textContent = 'Creating...';
resultsList.fullResultsCreate.insertAdjacentHTML('afterbegin',
loadingSpinnerHTML);
// .keys() is for a zero based array. I think...
let dataIndexes = [...Array(results.data.match_count).keys()];
// Empty fullResultsData so that no previous data is used.
results.fullResultsData.init();
resultsList.notifyClient('get-results', {resultsType: 'full-results',
dataIndexes: dataIndexes,
resultsList: resultsList,});
}
}
// 2. Add events for sub-results create
function createSubResults(resultsList, results) {
resultsList.subResultsCreate.onclick = (event) => {
let dataIndexes = [];
Object.keys(resultsList.subResultsIndexes).forEach((id) => {
dataIndexes.push(id);
});
resultsList.subResultsCreate.querySelector('i').classList.toggle('hide');
resultsList.subResultsCreate.textContent = 'Creating...';
resultsList.subResultsCreate.insertAdjacentHTML('afterbegin',
loadingSpinnerHTML);
// Empty subResultsData so that no previous data is used.
results.subResultsData.init();
resultsList.notifyClient('get-results', {resultsType: 'sub-results',
dataIndexes: dataIndexes,
resultsList: resultsList,});
}
}
// 3. Open download modal when full results export button is pressed
function exportFullResults(resultsList, results) {
resultsList.fullResultsExport.onclick = (event) => {
resultsList.queryResultsDownloadModal.open();
// add onclick to download JSON button and download the file
resultsList.downloadResultsJson.onclick = (event) => {
let suffix = 'full-results'
if (resultsList.exportFullInspectContext.checked) {
suffix += '_full-context';
}
let filename = results.fullResultsData.createDownloadFilename(suffix);
results.fullResultsData.addData(results.metaData);
results.fullResultsData.downloadJSONRessource(filename,
results.fullResultsData,
resultsList.downloadResultsJson)};
}
}
// 4. Open download modal when sub results export button is pressed
function exportSubResults(resultsList, results) {
resultsList.subResultsExport.onclick = (event) => {
resultsList.queryResultsDownloadModal.open();
// add onclick to download JSON button and download the file
resultsList.downloadResultsJson.onclick = (event) => {
let suffix = 'sub-results'
if (resultsList.exportFullInspectContext.checked) {
suffix += '_full-context';
}
let filename = results.subResultsData.createDownloadFilename(suffix);
results.subResultsData.addData(results.metaData);
results.subResultsData.downloadJSONRessource(filename,
results.subResultsData,
resultsList.downloadResultsJson)};
}
}
// 5. Open download modal when inspect-results-export button is pressed
function exportSingleMatch(resultsList, results) {
resultsList.inspectResultsExport.onclick = (event) => {
resultsList.queryResultsDownloadModal.open();
// add onclick to download JSON button and download the file
resultsList.downloadResultsJson.onclick = (event) => {
let filename = results.subResultsData.createDownloadFilename('inspect-results_full-context');
results.subResultsData.addData(results.metaData);
results.subResultsData.downloadJSONRessource(filename,
results.inspectResultsData,
resultsList.downloadResultsJson)};
}
}
// export listeners
export {
recieveClientNotification,
pageNavigation,
expertModeSwitch,
actionButtons,
displayOptions,
showMetaData,
showCorpusFiles,
exportFullContextSwitch,
createFullResults,
createSubResults,
exportFullResults,
exportSubResults,
exportSingleMatch,
};

View File

@ -1,27 +0,0 @@
/**
* Function to show a scroll to top button if the user has scrolled down
* 250 pixels from the with scrollToElementSelector specified Element.
*/
function scrollToTop(scrollToElementSelector, triggerElementSelector) {
let scrollToThis = document.querySelector(scrollToElementSelector);
let scrolltoTopTrigger = document.querySelector(triggerElementSelector);
window.addEventListener('scroll', (event) => {
if (pageYOffset > 250) {
scrolltoTopTrigger.classList.toggle('hide', false);
} else {
scrolltoTopTrigger.classList.toggle('hide', true);
}
});
scrolltoTopTrigger.onclick = () => {
scrollToThis.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
};
}
// Export function.
export {
scrollToTop
};

View File

@ -1,19 +0,0 @@
// 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 const.
export {
loadingSpinnerHTML
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,33 +1,31 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/darkreader/4.9.40/darkreader.min.js" integrity="sha512-0Jbi9gWSyU5SvNS16za0aILl6l+MgM8N+TGlZxy4qPQEzqKoU9egh4h56Kz0xp2R+ZFPQMfeDn26Gh6cqu2WAg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fast-json-patch/3.1.0/fast-json-patch.min.js" integrity="sha512-KrvLlmKBiDoTa0Fke92aFoEv4xS0+cuYGP27nt39w0yLZWvVOhArmZ29uuOe3uOOBcbnkpvnLhkvYcYjahSOwg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js" integrity="sha512-93wYgwrIFL+b+P3RvYxi/WUFRXXUDSLCT2JQk9zhVGXuS2mHl2axj6d+R6pP+gcU5isMHRj1u0oYE/mWyt/RjA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.4.0/socket.io.js" integrity="sha512-nYuHvSAhY5lFZ4ixSViOwsEKFvlxHMU2NHts1ILuJgOS6ptUmAGt/0i5czIgMOahKZ6JN84YFDA+mCdky7dD8A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="{{ url_for('static', filename='js/App.js') }}"></script>
<script src="{{ url_for('static', filename='js/JobStatusNotifier.js') }}"></script>
{% assets filters='rjsmin', output="js/RessourceDisplays.min.bundle.js",
"js/RessourceDisplays/RessourceDisplay.js",
"js/RessourceDisplays/CorpusDisplay.js",
"js/RessourceDisplays/JobDisplay.js" %}
<script src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets filters='rjsmin', output="js/RessourceLists.min.bundle.js",
"js/RessourceLists/RessourceList.js",
"js/RessourceLists/CorpusList.js",
"js/RessourceLists/CorpusFileList.js",
"js/RessourceLists/JobList.js",
"js/RessourceLists/JobInputList.js",
"js/RessourceLists/JobResultList.js",
"js/RessourceLists/QueryResultList.js",
"js/RessourceLists/UserList.js" %}
<script src="{{ ASSET_URL }}"></script>
{% endassets %}
<script src="{{ url_for('static', filename='js/UploadForm.js') }}"></script>
<script>
{% if current_user.setting_dark_mode %} {% if current_user.setting_dark_mode %}
<script src="{{ url_for('static', filename='js/darkreader.js') }}"></script>
<script>
DarkReader.enable({brightness: 150, contrast: 100, sepia: 0}); DarkReader.enable({brightness: 150, contrast: 100, sepia: 0});
</script>
{% endif %} {% endif %}
<script src="{{ url_for('static', filename='js/jsonpatch.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/list.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/socket.io.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/nopaque/App.js') }}"></script>
<script src="{{ url_for('static', filename='js/nopaque/JobStatusNotifier.js') }}"></script>
{% assets filters='rjsmin', output="js/nopaque/RessourceDisplays.min.bundle.js",
"js/nopaque/RessourceDisplays/RessourceDisplay.js",
"js/nopaque/RessourceDisplays/CorpusDisplay.js",
"js/nopaque/RessourceDisplays/JobDisplay.js" %}
<script src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets filters='rjsmin', output="js/nopaque/RessourceLists.min.bundle.js",
"js/nopaque/RessourceLists/RessourceList.js",
"js/nopaque/RessourceLists/CorpusList.js",
"js/nopaque/RessourceLists/CorpusFileList.js",
"js/nopaque/RessourceLists/JobList.js",
"js/nopaque/RessourceLists/JobInputList.js",
"js/nopaque/RessourceLists/JobResultList.js",
"js/nopaque/RessourceLists/QueryResultList.js",
"js/nopaque/RessourceLists/UserList.js" %}
<script src="{{ ASSET_URL }}"></script>
{% endassets %}
<script src="{{ url_for('static', filename='js/nopaque/UploadForm.js') }}"></script>
<script>
// Disable all option elements with no value // Disable all option elements with no value
for (let optionElement of document.querySelectorAll('option[value=""]')) {optionElement.disabled = true;} for (let optionElement of document.querySelectorAll('option[value=""]')) {optionElement.disabled = true;}
M.AutoInit(); M.AutoInit();

View File

@ -57,11 +57,11 @@
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
{% assets output="js/nopaque/CorpusAnalysis.min.bundle.js", {% assets output="js/CorpusAnalysis.min.bundle.js",
"js/nopaque/CorpusAnalysis/CQiClient.js", "js/CorpusAnalysis/CQiClient.js",
"js/nopaque/CorpusAnalysis/CorpusAnalysisApp.js", "js/CorpusAnalysis/CorpusAnalysisApp.js",
"js/nopaque/CorpusAnalysis/CorpusAnalysisConcordance.js", "js/CorpusAnalysis/CorpusAnalysisConcordance.js",
"js/nopaque/CorpusAnalysis/CorpusAnalysisReader.js" %} "js/CorpusAnalysis/CorpusAnalysisReader.js" %}
<script src="{{ ASSET_URL }}"></script> <script src="{{ ASSET_URL }}"></script>
{% endassets %} {% endassets %}
<script> <script>

View File

@ -11,8 +11,8 @@
{% endblock metas %} {% endblock metas %}
{% block styles %} {% block styles %}
<link href="{{ url_for('static', filename='css/material_icons.css') }}" rel="stylesheet"> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" crossorigin="anonymous" referrerpolicy="no-referrer" >
<link href="{{ url_for('static', filename='css/materialize.min.css') }}" media="screen,projection" rel="stylesheet"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" integrity="sha512-UJfAaOlIRtdR+0P6C3KUoTDAxVTuy3lnSXLyLKlHYJlcSU8Juge/mjeaxDNMlw9LgeIotgz5FP8eUQPhX1q10A==" crossorigin="anonymous" referrerpolicy="no-referrer">
{% endblock styles %} {% endblock styles %}
{% endblock head %} {% endblock head %}
</head> </head>
@ -36,7 +36,7 @@
</footer> </footer>
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/materialize.min.js') }}"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js" integrity="sha512-NiWqa2rceHnN3Z5j6mSAvbwwg3tiwVNxiAQaaSMSXnRRDh5C2mk/+sKQRw8qjV1vN4nf8iK2a0b048PnHbyx+Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
{% endblock scripts %} {% endblock scripts %}
{% endblock body %} {% endblock body %}
</body> </body>

View File

@ -1,15 +0,0 @@
<!-- Analysis init modal. User feedback showing that the analysis session is loading. -->
<div class="modal no-autoinit" id="analysis-init-modal">
<div class="modal-content">
<h4>Initializing your corpus analysis session...</h4>
<p>If the loading takes to long or an error occured,
<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>
<p id="analysis-init-error" class="hide red-text"></p>
</div>
</div>

View File

@ -1,75 +0,0 @@
<!-- Modal showing detailed context info for one match. -->
<div id="context-modal" class="modal">
<div class="modal-content">
<form>
<div class="row" style="margin-bottom: 0px; margin-top: -20px;">
<div class="col s12 m6 l6">
<div class="section">
<h6 style="margin-top: 0px;">Display</h6>
<div class="divider" style="margin-bottom: 10px;"></div>
<div class="col s12" style="margin-bottom: 10px;">
{{ inspect_display_options_form.expert_mode_inspect.label.text }}
<div class="switch right">
<label>
{{ inspect_display_options_form.expert_mode_inspect() }}
<span class="lever"></span>
</label>
</div>
</div>
<div class="col s12" style="margin-bottom: 10px;">
{{ inspect_display_options_form.highlight_sentences.label.text }}
<div class="switch right">
<label>
{{ inspect_display_options_form.highlight_sentences() }}
<span class="lever"></span>
</label>
</div>
</div>
<div class="col s12" style="margin-bottom: 10px;">
Sentences around match
<div class="input-field right" style="margin-top: -2rem;
margin-bottom: -2rem;
height: 0px;">
<p class="range-field">
<input type="range"
id="context-sentences"
min="1"
max="10"
value="3" />
</p>
</div>
</div>
</div>
</div>
<div class="col s12 m6 l6" id="create-from-inspect">
<div class="section">
<h6 style="margin-top: 0px;">Create</h6>
<div class="divider" style="margin-bottom: 10px;"></div>
<div class="col s12">
Add to Sub Results
<div class="secondary-content right" id="create-inspect-menu">
{# The needed button is created and added via javascript #}
</div>
</div>
</div>
</div>
</div>
</form>
<div class="row section">
<h5 style="margin-top: 0px;">Context for match:
<span id="context-match-nr"></span></h5>
<div class="divider" style="margin-bottom: 10px;"></div>
<div class="col s12" >
<div id="context-results">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<a id="inspect-results-export" class="left waves-effect waves-light btn">
Export Single Context
<i class="material-icons right">file_download</i>
</a>
<a href="#!" class="modal-close waves-effect waves-light red btn">Close</a>
</div>
</div>

View File

@ -1,52 +0,0 @@
<!-- Export query results modal. Allos the user to download the results in different file formats. WIP -->
<div id="query-results-download-modal"
class="modal modal-fixed-footer no-autoinit">
<div class="modal-content">
<h4>Download your results</h4>
<p>Download your results or sub results in different file formats like JSON
or CSV, so you can reuse them in Excel or other software.
You can also simply publish your results as raw data using one of these
formats. We recommend JSON!
</p>
<table>
<tr>
<td>JSON</td>
<td>
<a class="btn waves-effect waves-light" id="download-results-json">
Download
<i class="material-icons right">file_download</i>
</a>
</td>
</tr>
<tr>
<td>CSV</td>
<td>
<a class="btn waves-effect waves-light disabled"
id="download-results-csv">
Download
<i class="material-icons right">file_download</i>
</a>
</td>
</tr>
<tr>
<td>EXCEL</td>
<td>
<a class="btn waves-effect waves-light disabled">Download
<i class="material-icons right">file_download</i>
</a>
</td>
</tr>
<tr>
<td>HTML</td>
<td>
<a class="btn waves-effect waves-light disabled">Download
<i class="material-icons right">file_download</i>
</a>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-light red btn">Close</a>
</div>
</div>

View File

@ -1,18 +0,0 @@
<!-- Modal showing the corpus files for the current query results including title ant match count per corpus file. -->
<div id="query-builder-modal" class="modal">
<div class="modal-content">
<h4>Query builder</h4>
<p>Comming soon...</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-green btn" id="query-builder-submit">Done</a>
</div>
</div>
<script>
let queryBuilderSubmitElement = document.querySelector('#query-builder-submit');
queryBuilderSubmitElement.addEventListener('click', () => {
let queryElement = document.querySelector('#query-form-query');
// queryElement.value = 'Inhalt';
});
</script>

View File

@ -1,17 +0,0 @@
<!-- Modal showing the corpus files for the current query results including title ant match count per corpus file. -->
<div id="show-corpus-files-modal" class="modal bottom-sheet">
<div class="container">
<div class="row">
<div class="section">
<div class="col s12 right-align">
<a href="#!" class="modal-close waves-effect waves-green btn red"
style="left: 5px;">Close</a>
</div>
</div>
<div class="col s12 modal-content" id="show-corpus-files-modal-content"></div>
<div class="col s12 modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn red">Close</a>
</div>
</div>
</div>
</div>

View File

@ -1,17 +0,0 @@
<!-- Modal showing the meta data for the current query results or the imported results -->
<div id="meta-data-modal" class="modal bottom-sheet">
<div class="container">
<div class="row">
<div class="section">
<div class="col s12 right-align">
<a href="#!" class="modal-close waves-effect waves-green btn red"
style="left: 5px;">Close</a>
</div>
</div>
<div class="col s12 modal-content" id="meta-data-modal-content"></div>
<div class="col s12 modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn red">Close</a>
</div>
</div>
</div>
</div>

View File

@ -1,10 +0,0 @@
<!-- Modal to show all metadata of one text/corpus file. Used in conjunction with the show_meta_data.html.j2 template. -->
<div id="modal-text-details" class="modal modal-fixed-footer">
<div class="modal-content">
<h4>Bibliographic data</h4>
<p id="bibliographic-data"></p>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green red btn">Close</a>
</div>
</div>