mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-01-18 22:00:35 +00:00
Push before rework part 2
This commit is contained in:
parent
b3e8976c1c
commit
21ce07f3ef
@ -1,8 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* This class is used to create an CorpusAnalysisClient object.
|
* This class is used to create a CorpusAnalysisClient object.
|
||||||
* The client handels the client server communication.
|
* The client handels the client server communication.
|
||||||
* It requests data (e.g. the analysis session or query results) from the
|
* It requests data (e.g. the analysis session or query results) from the
|
||||||
* the server and recieves them.
|
* the server and recieves them, if it dynamicMode is true.
|
||||||
|
* If dynamicMode is false, the client can also handle data that is already
|
||||||
|
* loaded and not coming in in chunks.
|
||||||
*/
|
*/
|
||||||
class CorpusAnalysisClient {
|
class CorpusAnalysisClient {
|
||||||
constructor({corpusId = null,
|
constructor({corpusId = null,
|
||||||
@ -19,11 +21,9 @@ class CorpusAnalysisClient {
|
|||||||
this.socketEventListeners = {};
|
this.socketEventListeners = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set client into imported mode if SOME THIGN IS INDICATING it
|
* Disable all console logging.
|
||||||
*
|
* Credits to https://gist.github.com/kmonsoor/0244fdb4ad79a4826371e58a1a5fa984
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Disable all console logging. Credits to https://gist.github.com/kmonsoor/0244fdb4ad79a4826371e58a1a5fa984
|
|
||||||
if (!logging) {
|
if (!logging) {
|
||||||
(() => {
|
(() => {
|
||||||
let console = (window.console = window.console || {});
|
let console = (window.console = window.console || {});
|
||||||
@ -32,7 +32,7 @@ class CorpusAnalysisClient {
|
|||||||
'error', 'exception', 'group', 'groupCollapsed', 'groupEnd',
|
'error', 'exception', 'group', 'groupCollapsed', 'groupEnd',
|
||||||
'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'table',
|
'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'table',
|
||||||
'time', 'timeEnd', 'timeStamp', 'trace', 'warn'
|
'time', 'timeEnd', 'timeStamp', 'trace', 'warn'
|
||||||
].forEach(method => {
|
].forEach((method) => {
|
||||||
console[method] = () => {};
|
console[method] = () => {};
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
@ -41,14 +41,18 @@ class CorpusAnalysisClient {
|
|||||||
|
|
||||||
// Registers one or more SocketEventListeners to the CorpusAnalysisClient.
|
// Registers one or more SocketEventListeners to the CorpusAnalysisClient.
|
||||||
setSocketEventListeners(socketEventListeners) {
|
setSocketEventListeners(socketEventListeners) {
|
||||||
for (let listener of socketEventListeners) {
|
for (let socketEventListener of socketEventListeners) {
|
||||||
this.socketEventListeners[listener.type] = listener.listenerFunction;
|
this.socketEventListeners[socketEventListener.type] = socketEventListener;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the SocketEventListeners so they will be triggered on their assigned
|
||||||
|
* type strings because they double as the socket event event names.
|
||||||
|
*/
|
||||||
loadSocketEventListeners() {
|
loadSocketEventListeners() {
|
||||||
for (let [type, listener] of Object.entries(this.socketEventListeners)) {
|
for (let [type, listener] of Object.entries(this.socketEventListeners)) {
|
||||||
listener(this);
|
listener.listenerFunction(type, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,12 +68,14 @@ class CorpusAnalysisClient {
|
|||||||
* string as the key. The selector will be converted to a valid JavaScript
|
* string as the key. The selector will be converted to a valid JavaScript
|
||||||
* Field name i. e. #html-id-string -> this.htmlIdString
|
* Field name i. e. #html-id-string -> this.htmlIdString
|
||||||
* The value will be the identifed element fetched with the querySelector
|
* The value will be the identifed element fetched with the querySelector
|
||||||
* method. MutlipleResults and atattchSomeCallback not yet implemented.
|
* method.
|
||||||
*/
|
*/
|
||||||
getHTMLElements(arrayOfSelectors, multipleResults=false, atattchSomeCallback=false) {
|
// TODO: multipleResults=false, atattchSomeCallback=false ?
|
||||||
|
getHTMLElements(arrayOfSelectors) {
|
||||||
for (let selector of arrayOfSelectors) {
|
for (let selector of arrayOfSelectors) {
|
||||||
let element = document.querySelector(selector);
|
let element = document.querySelector(selector);
|
||||||
let cleanKey = [];
|
let cleanKey = [];
|
||||||
|
selector = selector.replace('_', '-');
|
||||||
selector.match(/\w+/g).forEach((word) => {
|
selector.match(/\w+/g).forEach((word) => {
|
||||||
let tmp = word[0].toUpperCase() + word.slice(1);
|
let tmp = word[0].toUpperCase() + word.slice(1);
|
||||||
cleanKey.push(tmp);
|
cleanKey.push(tmp);
|
||||||
@ -82,7 +88,8 @@ class CorpusAnalysisClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests a corpus analysis session via socket.io.
|
* Requests a corpus analysis session via socket.io.
|
||||||
* Opens a loading modal at the start of the request
|
* Opens a loading modal at the start of the request.
|
||||||
|
* Will be closed if session has been successfully recieved.
|
||||||
*/
|
*/
|
||||||
requestSession() {
|
requestSession() {
|
||||||
console.info('corpus_analysis_init: Client requesting session via',
|
console.info('corpus_analysis_init: Client requesting session via',
|
||||||
@ -95,7 +102,7 @@ class CorpusAnalysisClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the query string to the server.
|
* Request query data for the query string that has been sent to the server.
|
||||||
*/
|
*/
|
||||||
requestQueryData(queryStr) {
|
requestQueryData(queryStr) {
|
||||||
console.info('corpus_analysis_query: Client requesting query data via',
|
console.info('corpus_analysis_query: Client requesting query data via',
|
||||||
@ -180,16 +187,57 @@ class CorpusAnalysisDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to create an CorpusAnalysisDisplay object.
|
* This class is used to create an SocketEventListener.
|
||||||
* Input is one HTMLElement that can then be hidden or shown depending on
|
* Input are an identifying type string, the listener function and callbacks
|
||||||
* its CSS classes.
|
* which will be executed as part of the listener function. The identifying
|
||||||
|
* type string is also used as the socket event event identifier.
|
||||||
*/
|
*/
|
||||||
class SocketEventListener {
|
class SocketEventListener {
|
||||||
constructor(type, listenerFunction) {
|
constructor(type, listenerFunction) {
|
||||||
|
this.listenerCallbacks = {};
|
||||||
this.listenerFunction = listenerFunction;
|
this.listenerFunction = listenerFunction;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Registers callbacks to this SocketEventListener
|
||||||
|
setCallbacks(listenerCallbacks) {
|
||||||
|
for (let listenerCallback of listenerCallbacks) {
|
||||||
|
this.listenerCallbacks[listenerCallback.type] = listenerCallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shorthand to execute all registered callbacks with same args in insertion
|
||||||
|
* order.
|
||||||
|
* NOTE:
|
||||||
|
* Since ECMAScript 2015, objects do preserve creation order for
|
||||||
|
* string and Symbol keys. In JavaScript engines that comply with the
|
||||||
|
* ECMAScript 2015 spec, iterating over an object with only string keys will
|
||||||
|
* yield the keys in order of insertion.
|
||||||
|
* So all modern Browsers.
|
||||||
|
*/
|
||||||
|
executeCallbacks(args) {
|
||||||
|
for (let [type, listenerCallback] of Object.entries(this.listenerCallbacks)) {
|
||||||
|
listenerCallback.callbackFunction(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to create an ListenerCallback which will be registered
|
||||||
|
* to an SocketEventListener so the Listener cann invoke the ListenerCallback
|
||||||
|
* callback functions.
|
||||||
|
*/
|
||||||
|
class ListenerCallback {
|
||||||
|
constructor(type, callbackFunction) {
|
||||||
|
this.callbackFunction = callbackFunction;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// export Classes from this module
|
// export Classes from this module
|
||||||
export {CorpusAnalysisClient, CorpusAnalysisDisplay, SocketEventListener};
|
export {
|
||||||
|
CorpusAnalysisClient,
|
||||||
|
CorpusAnalysisDisplay,
|
||||||
|
SocketEventListener,
|
||||||
|
ListenerCallback,
|
||||||
|
};
|
@ -62,3 +62,6 @@ class InteractionElements {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export Classes
|
||||||
|
export { InteractionElement, InteractionElements };
|
@ -1,10 +1,8 @@
|
|||||||
// This callback is called on socket.on "query".
|
|
||||||
// Does some hiding, showing disabling etc.
|
|
||||||
/**
|
/**
|
||||||
* This call back is called when the SocketEventListener 'recieveQueryStatus'
|
* This callback should be registered to the SocketEventListener 'recieveQueryStatus'.
|
||||||
* has been triggered. It just gets the incoming status data of the issued
|
* It just gets the incoming status data of the issued query
|
||||||
* and does some preperation work like hiding or showing elements and deleting
|
* and does some preperation work like hiding or showing elements and deleting
|
||||||
* the date from the last query.
|
* the data from the last query.
|
||||||
*/
|
*/
|
||||||
function querySetup(payload, client) {
|
function querySetup(payload, client) {
|
||||||
// deletes old data from query issued before this new query
|
// deletes old data from query issued before this new query
|
||||||
@ -12,23 +10,29 @@ function querySetup(payload, client) {
|
|||||||
// load necessary HTMLElements with selectory syntax and save them as fields
|
// load necessary HTMLElements with selectory syntax and save them as fields
|
||||||
client.getHTMLElements(['#query-progress-bar', '#query-results-user-feedback',
|
client.getHTMLElements(['#query-progress-bar', '#query-results-user-feedback',
|
||||||
'#recieved-match-count', '#total-match-count',
|
'#recieved-match-count', '#total-match-count',
|
||||||
'#text-lookup-count', '#text-lookup-titles']);
|
'#text-lookup-count', '#text-lookup-titles',
|
||||||
|
'#query-results-create', '#add-to-sub-results']);
|
||||||
client.requestQueryProgress = 0;
|
client.requestQueryProgress = 0;
|
||||||
client.recievedMatchCount.textContent = 0;
|
client.recievedMatchCount.textContent = 0;
|
||||||
client.totalMatchCount.textContent = `${payload.match_count}`;
|
client.totalMatchCount.textContent = `${payload.match_count}`;
|
||||||
client.queryResultsUserFeedback.classList.toggle('hide');
|
client.queryResultsUserFeedback.classList.toggle('hide');
|
||||||
client.queryProgressBar.classList.toggle('hide');
|
client.queryProgressBar.classList.toggle('hide');
|
||||||
client.queryProgressBar.lastElementChild.style.width = '0%'
|
client.queryProgressBar.lastElementChild.style.width = '0%';
|
||||||
|
if (client.dynamicMode) {
|
||||||
|
client.addToSubResults.toggleAttribute('disabled');
|
||||||
|
client.queryResultsCreate.classList.toggle('disabled');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This callback is called when the SocketEventListener 'recieveQueryData'
|
* This callback should be registered to the SocketEventListener 'recieveQueryData'
|
||||||
* has been triggered. It takes the incoming chunk and renders the results in a
|
* It takes the incoming chunk and renders the results using the
|
||||||
* the results.jsList. It can either handle live incoming data or imported
|
* results.jsList object. It can either handle live incoming data chunks or
|
||||||
* results data.
|
* already loaded/imported results data.
|
||||||
*/
|
*/
|
||||||
function queryRenderResults(payload, client) {
|
function queryRenderResults(payload, client) {
|
||||||
client.getHTMLElements(['#recieved-match-count', '#match-count'])
|
client.getHTMLElements(['#recieved-match-count', '#match-count',
|
||||||
|
'#display-options-form-expert_mode']);
|
||||||
const renderResults = (data) => {
|
const renderResults = (data) => {
|
||||||
/**
|
/**
|
||||||
* resultItem saves the incoming chunk matches as objects to add those later
|
* resultItem saves the incoming chunk matches as objects to add those later
|
||||||
@ -59,11 +63,10 @@ function queryRenderResults(payload, client) {
|
|||||||
* activate, hide or show elements if all reults have been recieved
|
* activate, hide or show elements if all reults have been recieved
|
||||||
* also load some new elements taht have not ben loaded before
|
* also load some new elements taht have not ben loaded before
|
||||||
*/
|
*/
|
||||||
client.getHTMLElements(['#query-results-create']);
|
|
||||||
client.queryProgressBar.classList.toggle('hide');
|
client.queryProgressBar.classList.toggle('hide');
|
||||||
client.queryResultsUserFeedback.classList.toggle('hide');
|
client.queryResultsUserFeedback.classList.toggle('hide');
|
||||||
client.queryResultsCreate.classList.toggle('disabled');
|
client.queryResultsCreate.classList.toggle('disabled');
|
||||||
// resultsExportElement.classList.remove("disabled");
|
client.addToSubResults.toggleAttribute('disabled');
|
||||||
// addToSubResultsElement.removeAttribute("disabled");
|
// addToSubResultsElement.removeAttribute("disabled");
|
||||||
// // inital expert mode check and sub results activation
|
// // inital expert mode check and sub results activation
|
||||||
// client.results.jsList.activateInspect();
|
// client.results.jsList.activateInspect();
|
||||||
@ -75,19 +78,21 @@ function queryRenderResults(payload, client) {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
} else if (!client.dynamicMode) {
|
} else if (!client.dynamicMode) {
|
||||||
client.requestQueryProgress === 100;
|
|
||||||
client.queryResultsUserFeedback.classList.toggle('hide');
|
|
||||||
renderResults(payload);
|
renderResults(payload);
|
||||||
helperQueryRenderResults({'chunk': payload}, client);
|
helperQueryRenderResults({'chunk': payload}, client);
|
||||||
client.queryProgressBar.classList.toggle('hide');
|
client.queryProgressBar.classList.toggle('hide');
|
||||||
client.queryResultsUserFeedback.classList.toggle('hide');
|
client.queryResultsUserFeedback.classList.toggle('hide');
|
||||||
client.results.jsList.activateInspect();
|
client.results.jsList.activateInspect();
|
||||||
if (expertModeSwitchElement.checked) {
|
if (client.displayOptionsFormExpertMode.checked) {
|
||||||
client.results.jsList.expertModeOn("query-display");
|
client.results.jsList.expertModeOn("query-display");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that saves result data into the client.results.data object.
|
||||||
|
* Also does some showing and hiding of Elements and user feedback text.
|
||||||
|
*/
|
||||||
function helperQueryRenderResults (payload, client) {
|
function helperQueryRenderResults (payload, client) {
|
||||||
// updating table on finished item creation callback via createResultRowElement
|
// updating table on finished item creation callback via createResultRowElement
|
||||||
client.results.jsList.update();
|
client.results.jsList.update();
|
||||||
@ -111,6 +116,5 @@ function helperQueryRenderResults (payload, client) {
|
|||||||
client.requestQueryProgress = payload.progress;
|
client.requestQueryProgress = payload.progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add data to data objekt using its own socket on event?
|
// export callbacks
|
||||||
|
export { querySetup, queryRenderResults };
|
||||||
export { querySetup, queryRenderResults }
|
|
@ -5,10 +5,10 @@ import { querySetup, queryRenderResults } from './nopaque.listenerCallbacks.js'
|
|||||||
* Closes the loading modal that has been opend with requestSession at the
|
* Closes the loading modal that has been opend with requestSession at the
|
||||||
* start of the request.
|
* start of the request.
|
||||||
*/
|
*/
|
||||||
function recieveSession(client) {
|
function recieveSession(type, client) {
|
||||||
client.socket.on('corpus_analysis_init', (response) => {
|
client.socket.on(type, (response) => {
|
||||||
/**
|
/**
|
||||||
* Check if request for session was ok.
|
* Check if request for session was OK.
|
||||||
* If OK execute callbacks and hide/show displays.
|
* If OK execute callbacks and hide/show displays.
|
||||||
*/
|
*/
|
||||||
console.group('recieve session')
|
console.group('recieve session')
|
||||||
@ -44,11 +44,11 @@ function recieveSession(client) {
|
|||||||
* was invalid etc.
|
* was invalid etc.
|
||||||
* Also prepares the result.jsList for the incoming data.
|
* Also prepares the result.jsList for the incoming data.
|
||||||
*/
|
*/
|
||||||
function recieveQueryStatus(client) {
|
function recieveQueryStatus(type, client) {
|
||||||
client.socket.on('corpus_analysis_query', (response) => {
|
client.socket.on(type, (response) => {
|
||||||
/**
|
/**
|
||||||
* Check if issued query was ok.
|
* Check if issued query was OK.
|
||||||
* If OK execute callbacks and hide/show displays.
|
* If OK execute registered callbacks and hide/show displays.
|
||||||
*/
|
*/
|
||||||
console.group('corpus_analysis_query: Client recieving query process',
|
console.group('corpus_analysis_query: Client recieving query process',
|
||||||
'status via socket.on');
|
'status via socket.on');
|
||||||
@ -59,8 +59,8 @@ function recieveQueryStatus(client) {
|
|||||||
if (client.displays.query != undefined) {
|
if (client.displays.query != undefined) {
|
||||||
client.displays.query.setVisibilityByStatus("success");
|
client.displays.query.setVisibilityByStatus("success");
|
||||||
}
|
}
|
||||||
// executing the callbacks
|
// executing the registered callbacks
|
||||||
querySetup(response.payload, client);
|
client.socketEventListeners[type].executeCallbacks([response.payload, client]);
|
||||||
} else {
|
} else {
|
||||||
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
|
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
|
||||||
if (response.payload.code == 1281) {
|
if (response.payload.code == 1281) {
|
||||||
@ -83,20 +83,22 @@ function recieveQueryStatus(client) {
|
|||||||
/**
|
/**
|
||||||
* Recieves the query data from the request and handles it.
|
* Recieves the query data from the request and handles it.
|
||||||
*/
|
*/
|
||||||
function recieveQueryData(client) {
|
function recieveQueryData(type, client) {
|
||||||
console.group('corpus_analysis_query_results: Client recieving or loading',
|
console.group('corpus_analysis_query_results: Client recieving or loading',
|
||||||
'query data.');
|
'query data.');
|
||||||
if (client.dynamicMode) {
|
if (client.dynamicMode) {
|
||||||
console.info('Client recieving query data via socket.on');
|
console.info('Client recieving query data via socket.on');
|
||||||
client.socket.on('corpus_analysis_query_results', (response) => {
|
client.socket.on(type, (response) => {
|
||||||
console.info('Recieved chunk', response);
|
console.info('Recieved chunk', response);
|
||||||
queryRenderResults(response.payload, client);
|
// executing the registered callbacks
|
||||||
|
client.socketEventListeners[type].executeCallbacks([response.payload, client]);
|
||||||
console.info('Added chunk data to results.data and rendered it with',
|
console.info('Added chunk data to results.data and rendered it with',
|
||||||
'results.jsList');
|
'results.jsList');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.info('Client loading imported query data from database.');
|
console.info('Client loading imported query data from database.');
|
||||||
queryRenderResults(client.results.data, client);
|
// executing the registered callbacks
|
||||||
|
client.socketEventListeners[type].executeCallbacks([client.results.data, client]);
|
||||||
}
|
}
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
|
21
web/app/static/js/modules/nopaque.scrollToTop.js
Normal file
21
web/app/static/js/modules/nopaque.scrollToTop.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Function to show a scrol lto top button if the user has scrolled down
|
||||||
|
* 250 pixels from teh headline element.
|
||||||
|
*/
|
||||||
|
function scrollToTop() {
|
||||||
|
let headline = document.querySelector(".headline");
|
||||||
|
let scrollToTop = document.querySelector("#menu-scroll-to-top-div");
|
||||||
|
window.addEventListener("scroll", (event) => {
|
||||||
|
if (pageYOffset > 250) {
|
||||||
|
scrollToTop.classList.toggle("hide", false);
|
||||||
|
} else {
|
||||||
|
scrollToTop.classList.toggle("hide", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
scrollToTop.onclick = () => {
|
||||||
|
headline.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function
|
||||||
|
export { scrollToTop };
|
@ -425,6 +425,10 @@ RessourceList.options = {
|
|||||||
|
|
||||||
|
|
||||||
class ResultsList extends List {
|
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 = {
|
static options = {
|
||||||
page: 10,
|
page: 10,
|
||||||
pagination: [{
|
pagination: [{
|
||||||
@ -443,12 +447,19 @@ class ResultsList extends List {
|
|||||||
constructor(idOrElement, options) {
|
constructor(idOrElement, options) {
|
||||||
super(idOrElement, options);
|
super(idOrElement, options);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.eventTokens = {}; // all span tokens which are holdeing events if expert
|
/**
|
||||||
// mode is on. Collected here to delete later on
|
* All span tokens which are holding events if expert
|
||||||
this.currentExpertTokenElements = {}; // all token elements which have added
|
* mode is on. Collected here to delete later on.
|
||||||
// classes like chip and hoverable for expert view. Collected
|
*/
|
||||||
//here to delete later on
|
this.eventTokens = {};
|
||||||
this.addToSubResultsStatus = {}; // holds True/false for check buttons used to add matches tu sub-results. If checked, it is True. If unchecked, it is false. Buttons for this have the class add. Those little round check buttons.
|
/**
|
||||||
|
* all token elements which have added
|
||||||
|
* classes like chip and hoverable for expert view. Collected
|
||||||
|
* here to delete later on
|
||||||
|
*/
|
||||||
|
this.currentExpertTokenElements = {};
|
||||||
|
// holds True/false for check buttons used to add matches tu sub-results. If checked, it is True. If unchecked, it is false. Buttons for this have the class add. Those little round check buttons.
|
||||||
|
this.addToSubResultsStatus = {};
|
||||||
this.addToSubResultsIdsToShow = new Set(); // If check button is pressed its corresponding data_index is saved in this set. The set is shown to the user.
|
this.addToSubResultsIdsToShow = new Set(); // If check button is pressed its corresponding data_index is saved in this set. The set is shown to the user.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,7 +631,7 @@ class ResultsList extends List {
|
|||||||
// ###### Functions to inspect one match, to show more details ######
|
// ###### Functions to inspect one match, to show more details ######
|
||||||
// activate inspect buttons if progress is 100
|
// activate inspect buttons if progress is 100
|
||||||
activateInspect() {
|
activateInspect() {
|
||||||
if (progress === 100) {
|
if (this.requestQueryProgress === 100) {
|
||||||
let inspectBtnElements;
|
let inspectBtnElements;
|
||||||
inspectBtnElements = document.getElementsByClassName("inspect");
|
inspectBtnElements = document.getElementsByClassName("inspect");
|
||||||
for (let inspectBtn of inspectBtnElements) {
|
for (let inspectBtn of inspectBtnElements) {
|
||||||
@ -958,45 +969,43 @@ class ResultsList extends List {
|
|||||||
|
|
||||||
// ###### Expert view event functions ######
|
// ###### Expert view event functions ######
|
||||||
// function to create a tooltip for the current hovered token
|
// function to create a tooltip for the current hovered token
|
||||||
tooltipEventCreate(event) {
|
tooltipEventCreate(event, client) {
|
||||||
// console.log("Create Tooltip on mouseover.");
|
// console.log("Create Tooltip on mouseover.");
|
||||||
let token;
|
let token = client.results.data.cpos_lookup[event.target.dataset.cpos];
|
||||||
token = results.data.cpos_lookup[event.target.dataset.cpos];
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
token = this.contextData.cpos_lookup[event.target.dataset.cpos];
|
token = this.contextData.cpos_lookup[event.target.dataset.cpos];
|
||||||
}
|
}
|
||||||
this.addToolTipToTokenElement(event.target, token);
|
this.addToolTipToTokenElement(event.target, token, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to destroy the current Tooltip for the current hovered tooltip
|
// Function to destroy the current Tooltip for the current hovered tooltip
|
||||||
// on mouse leave
|
// on mouse leave
|
||||||
tooltipEventDestroy(event) {
|
tooltipEventDestroy() {
|
||||||
// console.log("Tooltip destroy on leave.");
|
// console.log("Tooltip destroy on leave.");
|
||||||
this.currentTooltipElement.destroy();
|
this.currentTooltipElement.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
expertModeOn(htmlId) {
|
|
||||||
// turn the expert mode on for all tokens in the DOM element identified by its htmlID
|
// turn the expert mode on for all tokens in the DOM element identified by its htmlID
|
||||||
|
expertModeOn(htmlId, client) {
|
||||||
if (!Array.isArray(this.currentExpertTokenElements[htmlId])) {
|
if (!Array.isArray(this.currentExpertTokenElements[htmlId])) {
|
||||||
this.currentExpertTokenElements[htmlId] = [];
|
this.currentExpertTokenElements[htmlId] = [];
|
||||||
}
|
}
|
||||||
let container = document.getElementById(htmlId);
|
let container = document.getElementById(htmlId);
|
||||||
let tokens = container.querySelectorAll("span.token");
|
let tokens = container.querySelectorAll("span.token");
|
||||||
this.currentExpertTokenElements[htmlId].push(...tokens);
|
this.currentExpertTokenElements[htmlId].push(...tokens);
|
||||||
this.tooltipEventCreateBind = this.tooltipEventCreate.bind(this);
|
|
||||||
this.tooltipEventDestroyBind = this.tooltipEventDestroy.bind(this);
|
|
||||||
this.eventTokens[htmlId] = [];
|
this.eventTokens[htmlId] = [];
|
||||||
for (let tokenElement of this.currentExpertTokenElements[htmlId]) {
|
for (let tokenElement of this.currentExpertTokenElements[htmlId]) {
|
||||||
tokenElement.classList.add("chip", "hoverable", "expert-view");
|
tokenElement.classList.add("chip", "hoverable", "expert-view");
|
||||||
tokenElement.onmouseover = this.tooltipEventCreateBind;
|
const eventCreate = (event, arg) => this.tooltipEventCreate(event, arg);
|
||||||
tokenElement.onmouseout = this.tooltipEventDestroyBind;
|
tokenElement.onmouseover = (event) => eventCreate(event, client);
|
||||||
|
tokenElement.onmouseout = () => this.tooltipEventDestroy();
|
||||||
this.eventTokens[htmlId].push(tokenElement);
|
this.eventTokens[htmlId].push(tokenElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fuction that creates Tooltip for one token and extracts the corresponding
|
// fuction that creates Tooltip for one token and extracts the corresponding
|
||||||
// infos from the result JSON
|
// infos from the result JSON
|
||||||
addToolTipToTokenElement(tokenElement, token) {
|
addToolTipToTokenElement(tokenElement, token, client) {
|
||||||
this.currentTooltipElement;
|
this.currentTooltipElement;
|
||||||
this.currentTooltipElement = M.Tooltip.init(tokenElement,
|
this.currentTooltipElement = M.Tooltip.init(tokenElement,
|
||||||
{"html": `<table>
|
{"html": `<table>
|
||||||
@ -1013,11 +1022,11 @@ class ResultsList extends List {
|
|||||||
NER: ${token.ner}
|
NER: ${token.ner}
|
||||||
</td>
|
</td>
|
||||||
<td class="left-align">
|
<td class="left-align">
|
||||||
Title: ${results.data.text_lookup[token.text].title}
|
Title: ${client.results.data.text_lookup[token.text].title}
|
||||||
<br>
|
<br>
|
||||||
Author: ${results.data.text_lookup[token.text].author}
|
Author: ${client.results.data.text_lookup[token.text].author}
|
||||||
<br>
|
<br>
|
||||||
Publishing year: ${results.data.text_lookup[token.text].publishing_year}
|
Publishing year: ${client.results.data.text_lookup[token.text].publishing_year}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>`}
|
</table>`}
|
||||||
|
@ -69,22 +69,45 @@
|
|||||||
<script type="module">
|
<script type="module">
|
||||||
/**
|
/**
|
||||||
* First Phase:
|
* First Phase:
|
||||||
* document content is loaded and scripts are being imported and executed
|
* Document content is loaded and scripts are being imported and executed.
|
||||||
*/
|
*/
|
||||||
import { CorpusAnalysisClient,
|
import {
|
||||||
|
CorpusAnalysisClient,
|
||||||
CorpusAnalysisDisplay,
|
CorpusAnalysisDisplay,
|
||||||
SocketEventListener } from '../../static/js/modules/nopaque.CorpusAnalysisClient.js';
|
SocketEventListener,
|
||||||
import { recieveSession, recieveQueryStatus,
|
ListenerCallback,
|
||||||
recieveQueryData } from '../../static/js/modules/nopaque.listenerFunctions.js';
|
} from '../../static/js/modules/nopaque.CorpusAnalysisClient.js';
|
||||||
import { Results, Data, MetaData } from '../../static/js/nopaque.Results.js';
|
import {
|
||||||
import { ResultsList } from '../../static/js/nopaque.lists.js';
|
recieveSession,
|
||||||
|
recieveQueryStatus,
|
||||||
|
recieveQueryData,
|
||||||
|
} from '../../static/js/modules/nopaque.listenerFunctions.js';
|
||||||
|
import {
|
||||||
|
querySetup,
|
||||||
|
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';
|
||||||
|
import {
|
||||||
|
InteractionElement,
|
||||||
|
InteractionElements,
|
||||||
|
} from '../../static/js/modules/nopaque.InteractionElement.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Second Phase:
|
* Second Phase:
|
||||||
* Asynchronus and event driven code
|
* Asynchronus and event driven code
|
||||||
*/
|
*/
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Initialize the CorpusAnalysisClient
|
// Initialize the CorpusAnalysisClient dynamic mode
|
||||||
let corpusId = {{ corpus_id}}
|
let corpusId = {{ corpus_id}}
|
||||||
const client = new CorpusAnalysisClient({'corpusId': corpusId,
|
const client = new CorpusAnalysisClient({'corpusId': corpusId,
|
||||||
'socket': nopaque.socket,
|
'socket': nopaque.socket,
|
||||||
@ -95,7 +118,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const initLoadingElement = document.getElementById("init-display");
|
const initLoadingElement = document.getElementById("init-display");
|
||||||
const initLoadingModal = M.Modal.init(initLoadingElement,
|
const initLoadingModal = M.Modal.init(initLoadingElement,
|
||||||
{"dismissible": false});
|
{"dismissible": false});
|
||||||
// Set up display elements which hare show depending on the client status
|
// Set up display elements which are shown depending on the client status
|
||||||
const initLoadingDisplay = new CorpusAnalysisDisplay(initLoadingModal);
|
const initLoadingDisplay = new CorpusAnalysisDisplay(initLoadingModal);
|
||||||
client.getHTMLElements(['#query-display']);
|
client.getHTMLElements(['#query-display']);
|
||||||
const queryDisplay = new CorpusAnalysisDisplay(client.queryDisplay);
|
const queryDisplay = new CorpusAnalysisDisplay(client.queryDisplay);
|
||||||
@ -105,8 +128,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
/**
|
/**
|
||||||
* Initializing the results object holding all the data of a query.
|
* Initializing the results object holding all the data of a query.
|
||||||
* Also holds the metadata of one query.
|
* Also holds the metadata of one query.
|
||||||
* resultsListOptions is set to determine how many results per page are
|
|
||||||
* shown etc.
|
|
||||||
* Lastly it contains the object ResultsList which is a list.js
|
* Lastly it contains the object ResultsList which is a list.js
|
||||||
* subclass which handles the visual representation of the query data.
|
* subclass which handles the visual representation of the query data.
|
||||||
*/
|
*/
|
||||||
@ -116,16 +137,70 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
let resultsList = new ResultsList("result-list", ResultsList.options);
|
let resultsList = new ResultsList("result-list", ResultsList.options);
|
||||||
let resultsMetaData = new MetaData();
|
let resultsMetaData = new MetaData();
|
||||||
let results = new Results(data, resultsList, resultsMetaData);
|
let results = new Results(data, resultsList, resultsMetaData);
|
||||||
// make results part of the client
|
// Make results part of the client
|
||||||
client.results = results;
|
client.results = results;
|
||||||
console.info('Initialized the Results object.')
|
console.info('Initialized the Results object.')
|
||||||
// register listeners listening to socket.io events and load them
|
/**
|
||||||
|
* 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).
|
||||||
|
*/
|
||||||
|
let interactionElements = new InteractionElements();
|
||||||
|
const expertModeInteraction = new InteractionElement("display-options-form-expert_mode");
|
||||||
|
expertModeInteraction.setCallback('on',
|
||||||
|
results.jsList.expertModeOn,
|
||||||
|
results.jsList,
|
||||||
|
['query-display', client]);
|
||||||
|
expertModeInteraction.setCallback('off',
|
||||||
|
results.jsList.expertModeOff,
|
||||||
|
results.jsList,
|
||||||
|
['query-display', client]);
|
||||||
|
const subResultsInteraction = new InteractionElement("add-to-sub-results");
|
||||||
|
subResultsInteraction.setCallback('on',
|
||||||
|
results.jsList.activateAddToSubResults,
|
||||||
|
results.jsList);
|
||||||
|
subResultsInteraction.setCallback('off',
|
||||||
|
results.jsList.deactivateAddToSubResults,
|
||||||
|
results.jsList);
|
||||||
|
|
||||||
|
const activateInspectInteraction = new InteractionElement('inspect',
|
||||||
|
false);
|
||||||
|
activateInspectInteraction.setCallback('noCheck',
|
||||||
|
results.jsList.activateInspect,
|
||||||
|
results.jsList);
|
||||||
|
const 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();
|
||||||
|
/**
|
||||||
|
* Register listeners listening to socket.io events and their callbacks
|
||||||
|
* Afterwards load them.
|
||||||
|
*/
|
||||||
const listenForSession = new SocketEventListener('corpus_analysis_init',
|
const listenForSession = new SocketEventListener('corpus_analysis_init',
|
||||||
recieveSession);
|
recieveSession);
|
||||||
const listenForQueryStatus = new SocketEventListener('corpus_analysis_query',
|
const listenForQueryStatus = new SocketEventListener('corpus_analysis_query',
|
||||||
recieveQueryStatus);
|
recieveQueryStatus);
|
||||||
|
const queryStatusCallback = new ListenerCallback('corpus_analysis_query',
|
||||||
|
querySetup);
|
||||||
|
listenForQueryStatus.setCallbacks([queryStatusCallback]);
|
||||||
const listenForQueryData = new SocketEventListener('corpus_analysis_query_results',
|
const listenForQueryData = new SocketEventListener('corpus_analysis_query_results',
|
||||||
recieveQueryData);
|
recieveQueryData);
|
||||||
|
const queryDataCallback = new ListenerCallback('corpus_analysis_query_results',
|
||||||
|
queryRenderResults);
|
||||||
|
listenForQueryData.setCallbacks([queryDataCallback]);
|
||||||
client.setSocketEventListeners([listenForSession, listenForQueryStatus,
|
client.setSocketEventListeners([listenForSession, listenForQueryStatus,
|
||||||
listenForQueryData]);
|
listenForQueryData]);
|
||||||
client.loadSocketEventListeners();
|
client.loadSocketEventListeners();
|
||||||
@ -153,6 +228,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
// Get query string and send query to server
|
// Get query string and send query to server
|
||||||
results.data.getQueryStr(queryFormElement);
|
results.data.getQueryStr(queryFormElement);
|
||||||
client.requestQueryData(results.data.query);
|
client.requestQueryData(results.data.query);
|
||||||
|
|
||||||
|
// Add scrollToTop functionality
|
||||||
|
scrollToTop();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,8 +10,7 @@ results.-->
|
|||||||
Sub-Results creation:
|
Sub-Results creation:
|
||||||
<label>
|
<label>
|
||||||
Off
|
Off
|
||||||
<input disabled
|
<input type="checkbox"
|
||||||
type="checkbox"
|
|
||||||
id="add-to-sub-results">
|
id="add-to-sub-results">
|
||||||
<span class="lever"></span>
|
<span class="lever"></span>
|
||||||
On
|
On
|
||||||
|
@ -9,7 +9,6 @@ the selected sub results.-->
|
|||||||
<button class="waves-effect
|
<button class="waves-effect
|
||||||
waves-light
|
waves-light
|
||||||
btn-flat
|
btn-flat
|
||||||
disabled
|
|
||||||
flat-interaction"
|
flat-interaction"
|
||||||
type="submit"
|
type="submit"
|
||||||
id="query-results-create">Create Results
|
id="query-results-create">Create Results
|
||||||
@ -30,7 +29,6 @@ the selected sub results.-->
|
|||||||
waves-light
|
waves-light
|
||||||
btn-flat
|
btn-flat
|
||||||
hide
|
hide
|
||||||
disabled
|
|
||||||
flat-interaction"
|
flat-interaction"
|
||||||
type="submit"
|
type="submit"
|
||||||
id="sub-results-create">Create Sub-Results
|
id="sub-results-create">Create Sub-Results
|
||||||
|
@ -59,21 +59,39 @@
|
|||||||
* First Phase:
|
* First Phase:
|
||||||
* document content is loaded and scripts are being imported and executed
|
* document content is loaded and scripts are being imported and executed
|
||||||
*/
|
*/
|
||||||
import { CorpusAnalysisClient,
|
import {
|
||||||
|
CorpusAnalysisClient,
|
||||||
CorpusAnalysisDisplay,
|
CorpusAnalysisDisplay,
|
||||||
SocketEventListener } from '../../static/js/modules/nopaque.CorpusAnalysisClient.js';
|
SocketEventListener,
|
||||||
import { recieveSession, recieveQueryStatus,
|
ListenerCallback,
|
||||||
recieveQueryData } from '../../static/js/modules/nopaque.listenerFunctions.js';
|
} from '../../static/js/modules/nopaque.CorpusAnalysisClient.js';
|
||||||
import { Results, Data, MetaData } from '../../static/js/nopaque.Results.js';
|
import {
|
||||||
import { ResultsList } from '../../static/js/nopaque.lists.js';
|
recieveSession,
|
||||||
import { queryRenderResults, querySetup } from '../../static/js/modules/nopaque.listenerCallbacks.js'
|
recieveQueryStatus,
|
||||||
|
recieveQueryData,
|
||||||
|
} from '../../static/js/modules/nopaque.listenerFunctions.js';
|
||||||
|
import {
|
||||||
|
querySetup,
|
||||||
|
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Second Phase:
|
* Second Phase:
|
||||||
* Asynchronus and event driven code
|
* Asynchronus and event driven code
|
||||||
*/
|
*/
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Initialize the CorpusAnalysisClient
|
// Initialize the CorpusAnalysisClient with dynamicMode as false
|
||||||
const client = new CorpusAnalysisClient({'logging': true,
|
const client = new CorpusAnalysisClient({'logging': true,
|
||||||
'dynamicMode': false});
|
'dynamicMode': false});
|
||||||
console.info("CorpusAnalysisClient created as client:", client);
|
console.info("CorpusAnalysisClient created as client:", client);
|
||||||
@ -85,8 +103,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
/**
|
/**
|
||||||
* Initializing the results object holding all the data of a query.
|
* Initializing the results object holding all the data of a query.
|
||||||
* Also holds the metadata of one query.
|
* Also holds the metadata of one query.
|
||||||
* resultsListOptions is set to determine how many results per page are
|
|
||||||
* shown etc.
|
|
||||||
* Lastly it contains the object ResultsList which is a list.js
|
* Lastly it contains the object ResultsList which is a list.js
|
||||||
* subclass which handles the visual representation of the query data.
|
* subclass which handles the visual representation of the query data.
|
||||||
*/
|
*/
|
||||||
@ -114,7 +130,22 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
'onOpenEnd': deleteOverlay});
|
'onOpenEnd': deleteOverlay});
|
||||||
// saving imported data into client object
|
// saving imported data into client object
|
||||||
const payload = {{ query_result_file_content|tojson|safe }};
|
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]);
|
||||||
|
|
||||||
//
|
//
|
||||||
// // Initialization of interactionElemnts
|
// // Initialization of interactionElemnts
|
||||||
@ -160,9 +191,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// render results directly with callbacks because we are not in dynamic mode
|
// render results directly with callbacks because we are not in dynamic mode
|
||||||
querySetup(payload, client);
|
listenForQueryStatus.listenerCallbacks['corpus_analysis_query'].callbackFunction(payload, client);
|
||||||
queryRenderResults(payload, client);
|
listenForQueryData.listenerCallbacks['corpus_analysis_query_results'].callbackFunction(payload, client);
|
||||||
|
|
||||||
// // ### Show corpus Metadata
|
// // ### Show corpus Metadata
|
||||||
// showMetaDataButton.onclick = () => {
|
// showMetaDataButton.onclick = () => {
|
||||||
// metaDataModal.open();
|
// metaDataModal.open();
|
||||||
@ -185,19 +215,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
//
|
//
|
||||||
// // scroll to top button if user scrolled down the list
|
// Add scrollToTop functionality
|
||||||
// let headline = document.querySelector(".headline");
|
scrollToTop();
|
||||||
// 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 %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user