class RessourceList extends List { constructor(idOrElement, subscriberList, type, options={}) { if (!["corpus", "job", "result", "user"].includes(type)) { console.error("Unknown Type!"); return; } if (subscriberList) { super(idOrElement, {...RessourceList.options['common'], ...RessourceList.options[type], ...options}); this.type = type; subscriberList.push(this); } else { super(idOrElement, {...RessourceList.options['extended'], ...RessourceList.options[type], ...options}); this.type = type; } } _init(ressources) { this.clear(); this.addRessources(Object.values(ressources)); this.sort("creation_date", {order: "desc"}); } _update(patch) { let item, pathArray; for (let operation of patch) { /* "/{ressourceName}/{ressourceId}/..." -> ["{ressourceId}", "..."] */ pathArray = operation.path.split("/").slice(2); switch(operation.op) { case "add": if (pathArray.includes("results")) {break;} this.addRessources([operation.value]); break; case "remove": this.remove("id", pathArray[0]); break; case "replace": item = this.get("id", pathArray[0])[0]; switch(pathArray[1]) { case "status": item.values({status: operation.value, "analyse-link": ["analysing", "prepared", "start analysis"].includes(operation.value) ? `/corpora/${pathArray[0]}/analyse` : ""}); break; default: break; } default: break; } } } addRessources(ressources) { this.add(ressources.map(x => RessourceList.dataMapper[this.type](x))); } } RessourceList.dataMapper = { corpus: corpus => ({creation_date: corpus.creation_date, description: corpus.description, id: corpus.id, "analyse-link": ["analysing", "prepared", "start analysis"].includes(corpus.status) ? `/corpora/${corpus.id}/analyse` : "", "edit-link": `/corpora/${corpus.id}`, status: corpus.status, title: corpus.title}), job: job => ({creation_date: job.creation_date, description: job.description, id: job.id, link: `/jobs/${job.id}`, service: job.service, status: job.status, title: job.title}), result: result => ({ query: result.query, match_count: result.match_count, corpus_name: result.corpus_name, corpus_creation_date: result.corpus_creation_date, corpus_analysis_date: result.corpus_analysis_date, corpus_type : result.corpus_type, "details-link": `${result.id}/details`, "inspect-link": `${result.id}/inspect`, "delete-modal": `delete-result-${result.id}-modal`}), user: user => ({username: user.username, email: user.email, role_id: user.role_id, confirmed: user.confirmed, id: user.id, "profile-link": `user/${user.id}`}) }; RessourceList.options = { common: {page: 4, pagination: {innerWindow: 8, outerWindow: 1}}, extended: {page: 10, pagination: [ { name: "paginationTop", paginationClass: "paginationTop", innerWindow: 8, outerWindow: 1 }, { paginationClass: "paginationBottom", innerWindow: 8, outerWindow: 1 } ]}, corpus: {item: ` book
edit search `, valueNames: ["creation_date", "description", "title", {data: ["id"]}, {name: "analyse-link", attr: "href"}, {name: "edit-link", attr: "href"}, {name: "status", attr: "data-status"}]}, job: {item: `
send `, valueNames: ["creation_date", "description", "title", {data: ["id"]}, {name: "link", attr: "href"}, {name: "service", attr: "data-service"}, {name: "status", attr: "data-status"}]}, result: {item: ` info_outline search delete `, valueNames: ["query", "match_count", "corpus_name", "corpus_creation_date", "corpus_analysis_date", "corpus_type", {name: "details-link", attr: "href"}, {name: "inspect-link", attr: "href"}, {name: "delete-modal", attr: "data-target"}]}, user: {item: ` edit `, valueNames: ["username", "email", "role_id", "confirmed", "id", {name: "profile-link", attr: "href"}]} }; class ResultsList extends List { constructor(idOrElement, options={}) { super(idOrElement, options); this.eventTokens = {}; // all span tokens which are holdeing events if expert // mode is on. Collected here to delete later on this.currentExpertTokenElements = {}; // all token elements which have added // classes like chip and hoverable for expert view. Collected //here to delete later on this.addToSubResultsStatus = {}; // holds True/false for check buttons used to add matches tu sub-results. If checked, it is True. If unchecked, it is false. Buttons for this have the class add. Those little round check buttons. this.addToSubResultsIdsToShow = new Set(); // If check button is pressed its corresponding data_index is saved in this set. The set is shown to the user. } // handels interactionElements during a pagination navigation // loops over interactionElements and executes callback functions accordingly pageChangeEventInteractionHandler(interactionElements) { // get elements to check thier status for (let interaction of interactionElements) { if (interaction.checkStatus) { if (interaction.element.checked) { let f_on = interaction.bindThisToCallback("on"); let args_on = interaction.callbacks.on.args; f_on(...args_on); } else { let f_off = interaction.bindThisToCallback("off"); let args_off = interaction.callbacks.off.args; f_off(...args_off); } } else { let f = interaction.bindThisToCallback("noCheck"); let args = interaction.callbacks.noCheck.args; f(...args); } } } // get display options from display options form element static getDisplayOptions(displayOptionsFormElement) { // gets display options parameters let displayOptionsFormData let displayOptionsData; displayOptionsFormData = new FormData(displayOptionsFormElement); displayOptionsData = { "resultsPerPage": displayOptionsFormData.get("display-options-form-results_per_page"), "resultsContex": displayOptionsFormData.get("display-options-form-result_context"), "expertMode": displayOptionsFormData.get("display-options-form-expert_mode") }; return displayOptionsData } // ###### Functions to add one match to a sub-results ###### // activate the add buttons activateAddToSubResults() { subResultsIdListElement.classList.remove("hide"); subResultsCreateElement.classList.remove("hide"); let addToSubResultsBtnElements = document.getElementsByClassName("add"); for (let addToSubResultsBtn of addToSubResultsBtnElements) { addToSubResultsBtn.classList.remove("hide"); } } // deactivate the add buttons deactivateAddToSubResults() { subResultsIdListElement.classList.add("hide"); subResultsCreateElement.classList.add("hide"); let addToSubResultsBtnElements = document.getElementsByClassName("add"); for (let addToSubResultsBtn of addToSubResultsBtnElements) { addToSubResultsBtn.classList.add("hide"); } } // Used in addToSubResults and inspect to toggle the design of the check // buttons according to its checked unchecked status. helperActivateBtn(btn) { btn.classList.remove("grey"); btn.classList.add("green"); btn.innerText = "check"; } // Used in addToSubResults and inspect to toggle the design of the check // buttons according to its checked unchecked status. helperDeactivateBtn(btn) { btn.classList.remove("green"); btn.classList.add("grey"); btn.innerText = "add"; } // Either adds or removes a match to the sub-results. For this it checks // onclick if the current button has been checked or not. For this the // function checks if its status in addToSubResultsStatus is either flase or // true. Adds match to sub-results if status is false if status is true it // removes it. addToSubResults(dataIndex, tableCall=true) { let textarea = subResultsIdListElement.getElementsByTagName("textarea")[0]; if (!this.addToSubResultsStatus[dataIndex] || this.addToSubResultsStatus === undefined) { // add button is activated because status is either false or undefined this.helperActivateBtn(event.target); this.addToSubResultsStatus[dataIndex] = true; // sets status to true this.addToSubResultsIdsToShow.add(dataIndex + 1); // + 1 because user does not see zero indexd data indexes textarea.innerText = [...this.addToSubResultsIdsToShow].sort(function(a, b){return a-b}).join(", "); // automaticalle sorts ids into the textarea in ascending order M.textareaAutoResize(textarea); // after an insert textarea has to be resized manually nrMarkedMatches.innerText = [...this.addToSubResultsIdsToShow].length; } else if (this.addToSubResultsStatus[dataIndex]) { // add button is deactivated because status is true this.helperDeactivateBtn(event.target); this.addToSubResultsStatus[dataIndex] = false; // sets status to false this.addToSubResultsIdsToShow.delete(dataIndex + 1); // + 1 because user does not see zero indexd data indexes textarea.innerText = [...this.addToSubResultsIdsToShow].sort(function(a, b){return a-b}).join(", "); // automaticalle sorts ids into the textarea in ascending order nrMarkedMatches.innerText = [...this.addToSubResultsIdsToShow].length; M.textareaAutoResize(textarea); // after an insert textarea has to be resized manually } // Toggles the create button accoring to the number of ids in addToSubResultsIdsToShow if ([...this.addToSubResultsIdsToShow].length > 0) { subResultsCreateElement.firstElementChild.classList.remove("disabled"); } else if ([...this.addToSubResultsIdsToShow].length === 0) { subResultsCreateElement.firstElementChild.classList.add("disabled"); } // After a match as been added or removed the export button will be // disabled because the sub-results have been alterd and have to be built //// again. subResultsExportElement.classList.add("disabled"); // Also activate/deactivate buttons in the table/jsList results accordingly //if button in inspect was activated/deactivated. // This part only runs if tableCall is false. if (!tableCall) { let tableAddBtn = document.getElementById("query-results").querySelectorAll(`[data-index="${dataIndex}"]`)[0].getElementsByClassName('add')[0].firstElementChild; // gets the add button from the list view if (this.addToSubResultsStatus[dataIndex]) { this.helperActivateBtn(tableAddBtn); } else { this.helperDeactivateBtn(tableAddBtn); } } } // Triggers emit to get full match context from server for a number of // matches identified by their data_index. getMatchWithContext(dataIndexes, type) { let tmp_first_cpos = []; let tmp_last_cpos = []; for (let dataIndex of dataIndexes) { tmp_first_cpos.push(results.data.matches[dataIndex].c[0]); tmp_last_cpos.push(results.data.matches[dataIndex].c[1]); } nopaque.socket.emit("corpus_analysis_inspect_match", { type: type, data_indexes: dataIndexes, first_cpos: tmp_first_cpos, last_cpos: tmp_last_cpos, } ); } // ###### Functions to inspect one match, to show more details ###### // activate inspect buttons if progress is 100 activateInspect() { if (progress === 100) { let inspectBtnElements; inspectBtnElements = document.getElementsByClassName("inspect"); for (let inspectBtn of inspectBtnElements) { inspectBtn.classList.remove("disabled"); } } else { return } } // gets result cpos infos for one dataIndex (list of length 1) to send back to // the server inspect(dataIndex, type) { let contextMatchNrElement; // get result infos from server and show them in context modal this.contextId = dataIndex[0]; // match nr for user to display derived from data_index contextMatchNrElement = document.getElementById("context-match-nr"); contextMatchNrElement.innerText = this.contextId + 1; let contextResultsElement; contextResultsElement = document.getElementById("context-results"); contextResultsElement.innerHTML = ""; // clear it from old inspects this.getMatchWithContext(dataIndex, type); contextModal.open(); // add a button to add this match to sub results with onclick event let classes = `btn-floating btn waves-effect` + `waves-light grey right` let addToSubResultsIdsBtn = document.createElement("a"); addToSubResultsIdsBtn.setAttribute("class", classes + ` add`); addToSubResultsIdsBtn.innerHTML = 'add'; addToSubResultsIdsBtn.onclick= () => {this.addToSubResults(dataIndex[0], false)}; // checks if a button has already been added to the inspect modal and removes it if (addToSubResultsFromInspectElement.children.length > 0) { addToSubResultsFromInspectElement.firstElementChild.remove(); } // Changes the design of the add button according to its checked status // upon opening the inspect modal. if (this.addToSubResultsStatus[dataIndex[0]]) { this.helperActivateBtn(addToSubResultsIdsBtn.firstElementChild); } else if (!this.addToSubResultsStatus[dataIndex[0]]) { this.helperDeactivateBtn(addToSubResultsIdsBtn.firstElementChild); } addToSubResultsFromInspectElement.appendChild(addToSubResultsIdsBtn); } // create Element from HTML String helper function HTMLTStrToElement(htmlStr) { // https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518 let template = document.createElement("template"); htmlStr = htmlStr.trim(); template.innerHTML = htmlStr; return template.content.firstChild; } // Used as a callback to handle incoming match context results when inspect // has been used. showMatchContext(response) { this.contextData; let c; let contextModalLoading; let contextModalReady; let contextResultsElement; let highlightSentencesSwitchElement; let htmlTokenStr; let lc; let modalExpertModeSwitchElement; let modalTokenElements; let nrOfContextSentences; let partElement; let rc; let token; let tokenHTMLArray; let tokenHTMlElement; let uniqueContextS; let uniqueS; this.contextData = response.payload; console.log(this.contextData); this.contextData["cpos_ranges"] = response.payload.cpos_ranges; this.contextData["query"] = results.data.query; this.contextData["context_id"] = this.contextId; this.contextData["match_count"] = this.contextData.matches.length this.contextData["corpus_type"] = "inspect-result" Object.assign(this.contextData, results.metaData); contextResultsElement = document.getElementById("context-results"); modalExpertModeSwitchElement = document.getElementById("inspect-display-options-form-expert_mode_inspect"); highlightSentencesSwitchElement = document.getElementById("inspect-display-options-form-highlight_sentences"); nrOfContextSentences = document.getElementById("context-sentences"); uniqueS = new Set(); uniqueContextS = new Set(); // check if cpos ranges are used or not if (this.contextData.cpos_ranges == true) { // python range like function from MDN // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Sequence_generator_(range) const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step)); lc = range(this.contextData.matches[0].lc[0], this.contextData.matches[0].lc[1], 1) c = range(this.contextData.matches[0].c[0], this.contextData.matches[0].c[1], 1) rc = range(this.contextData.matches[0].rc[0], this.contextData.matches[0].rc[1], 1) } else { lc = this.contextData.matches[0].lc; c = this.contextData.matches[0].c; rc = this.contextData.matches[0].rc; } // create sentence strings as tokens tokenHTMLArray = []; for (let cpos of lc) { token = this.contextData.cpos_lookup[cpos]; uniqueS.add(token.s) htmlTokenStr = `` + `${token.word}` + ``; tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr) tokenHTMLArray.push(tokenHTMlElement); } for (let cpos of c) { token = this.contextData.cpos_lookup[cpos]; uniqueContextS.add(token.s); uniqueS.add(token.s); htmlTokenStr = `` + `${token.word}` + ``; tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr) tokenHTMLArray.push(tokenHTMlElement); } this.contextData["context_s_ids"] = Array.from(uniqueContextS); for (let cpos of rc) { token = this.contextData.cpos_lookup[cpos]; uniqueS.add(token.s) htmlTokenStr = `` + `${token.word}` + ``; tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr) tokenHTMLArray.push(tokenHTMlElement); } for (let sId of uniqueS) { let htmlSentence = ``; let sentenceElement = this.HTMLTStrToElement(htmlSentence); for (let tokenElement of tokenHTMLArray) { if (tokenElement.dataset.sid == sId) { sentenceElement.appendChild(tokenElement); sentenceElement.insertAdjacentHTML("beforeend", ` `); } else { continue; } } contextResultsElement.appendChild(sentenceElement); } // add inspect display options events modalExpertModeSwitchElement.onchange = (event) => { if (event.target.checked) { this.expertModeOn("context-results"); } else { this.expertModeOff("context-results") } }; highlightSentencesSwitchElement.onchange = (event) => { if (event.target.checked) { this.higlightContextSentences(); } else { this.unhighlightContextSentences(); } }; nrOfContextSentences.onchange = (event) => { // console.log(event.target.value); this.changeSentenceContext(event.target.value); } // checks on new modal opening if switches are checked // if switches are checked functions are executed if (modalExpertModeSwitchElement.checked) { this.expertModeOn("context-results"); } if (highlightSentencesSwitchElement.checked) { this.higlightContextSentences(); } // checks the value of the number of sentences to show on modal opening // sets context sentences accordingly this.changeSentenceContext(nrOfContextSentences.value) } // splits context text into sentences based on spacy sentence split higlightContextSentences() { let sentences; sentences = document.getElementById("context-results").getElementsByClassName("sentence"); for (let s of sentences) { s.insertAdjacentHTML("beforeend", `

`) } } unhighlightContextSentences() { let sentences; let br; sentences = document.getElementById("context-results").getElementsByClassName("sentence"); for (let s of sentences) { br = s.lastChild; br.remove(); } } // changes how many context sentences in inspect view are shown changeSentenceContext(sValue, maxSValue=10) { let array; let sentences; let toHideArray; let toShowArray; sValue = maxSValue - sValue; // console.log(sValue); sentences = document.getElementById("context-results").getElementsByClassName("sentence"); array = Array.from(sentences); if (sValue != 0) { toHideArray = array.slice(0, sValue).concat(array.slice(-(sValue))); toShowArray = array.slice(sValue, 9).concat(array.slice(9, -(sValue))) } else { toHideArray = []; toShowArray = array; } // console.log(array); // console.log("#######"); // console.log(toHideArray); for (let s of toHideArray) { s.classList.add("hide"); } for (let s of toShowArray) { s.classList.remove("hide"); } } // ###### Display options changing live how the matches are being displayed ###### // Event function that changes the shown hits per page. // Just alters the resultsList.page property changeHitsPerPage(event) { try { // console.log(this); this.page = event.target.value; this.update(); this.activateInspect(); this.pageChangeEventInteractionHandler(interactionElements); if (expertModeSwitchElement.checked) { this.expertModeOn("query-display"); // page holds new result rows, so add new tooltips } nopaque.flash("Updated matches per page.", "corpus") } catch (e) { // console.log(e); // console.log("resultsList has no results right now."); } } // Event function triggered on context select change // also if pagination is clicked changeContext(event) { let array; let lc; let newContextValue; let rc; try { if (event.type === "change") { nopaque.flash("Updated context per match!", "corpus"); } } catch (e) { } finally { newContextValue = document.getElementById("display-options-form-result_context").value; lc = document.getElementsByClassName("left-context"); rc = document.getElementsByClassName("right-context"); for (let element of lc) { array = Array.from(element.childNodes); for (let element of array.reverse().slice(newContextValue)) { element.classList.add("hide"); } for (let element of array.slice(0, newContextValue)) { element.classList.remove("hide"); } } for (let element of rc) { array = Array.from(element.childNodes); for (let element of array.slice(newContextValue)) { element.classList.add("hide"); } for (let element of array.slice(0, newContextValue)) { element.classList.remove("hide"); } } } } // ###### Expert view event functions ###### // function to create a tooltip for the current hovered token tooltipEventCreate(event) { // console.log("Create Tooltip on mouseover."); let token; token = results.data.cpos_lookup[event.target.dataset.cpos]; if (!token) { token = this.contextData.cpos_lookup[event.target.dataset.cpos]; } this.addToolTipToTokenElement(event.target, token); } // Function to destroy the current Tooltip for the current hovered tooltip // on mouse leave tooltipEventDestroy(event) { // console.log("Tooltip destroy on leave."); this.currentTooltipElement.destroy(); } expertModeOn(htmlId) { // turn the expert mode on for all tokens in the DOM element identified by its htmlID if (!Array.isArray(this.currentExpertTokenElements[htmlId])) { this.currentExpertTokenElements[htmlId] = []; } this.currentExpertTokenElements[htmlId].push( ...document.getElementById(htmlId).getElementsByClassName("token")); this.tooltipEventCreateBind = this.tooltipEventCreate.bind(this); this.tooltipEventDestroyBind = this.tooltipEventDestroy.bind(this); this.eventTokens[htmlId] = []; for (let tokenElement of this.currentExpertTokenElements[htmlId]) { tokenElement.classList.add("chip", "hoverable", "expert-view"); tokenElement.onmouseover = this.tooltipEventCreateBind; tokenElement.onmouseout = this.tooltipEventDestroyBind; this.eventTokens[htmlId].push(tokenElement); } } // fuction that creates Tooltip for one token and extracts the corresponding // infos from the result JSON addToolTipToTokenElement(tokenElement, token) { this.currentTooltipElement; this.currentTooltipElement = M.Tooltip.init(tokenElement, {"html": `
Token information Source information
Word: ${token.word}
Lemma: ${token.lemma}
POS: ${token.pos}
Simple POS: ${token.simple_pos}
NER: ${token.ner}
Title: ${results.data.text_lookup[token.text].title}
Author: ${results.data.text_lookup[token.text].author}
Publishing year: ${results.data.text_lookup[token.text].publishing_year}
`} ); } // function to remove extra informations and animations from tokens expertModeOff(htmlId) { // console.log("Expert mode is off."); if (!Array.isArray(this.currentExpertTokenElements[htmlId])) { this.currentExpertTokenElements[htmlId] = []; } if (!Array.isArray(this.eventTokens[htmlId])) { this.eventTokens[htmlId] = []; } for (let tokenElement of this.currentExpertTokenElements[htmlId]) { tokenElement.classList.remove("chip", "hoverable", "expert-view"); } this.currentExpertTokenElements[htmlId] = []; for (let eventToken of this.eventTokens[htmlId]) { eventToken.onmouseover = ""; eventToken.onmouseout = ""; } this.eventTokens[htmlId] = []; } createResultRowElement(item, chunk) { let aCellElement; let addToSubResultsBtn; let c; let cCellElement; let cpos; let inspectBtn let lc; let lcCellElement; let matchNrElement; let matchRowElement; let rc; let rcCellElement; let textTitles; let textTitlesCellElement; let token; let values; // gather values from item values = item.values(); if (chunk.cpos_ranges == true) { // python range like function from MDN // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Sequence_generator_(range) const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step)); lc = range(values.lc[0], values.lc[1], 1) c = range(values.c[0], values.c[1], 1) rc = range(values.rc[0], values.rc[1], 1) } else { lc = values.lc; c = values.c; rc = values.rc; } // get infos for full match row matchRowElement = document.createElement("tr"); matchRowElement.setAttribute("data-index", values.index) lcCellElement = document.createElement("td"); lcCellElement.classList.add("left-context"); matchRowElement.appendChild(lcCellElement); for (cpos of lc) { token = chunk.cpos_lookup[cpos]; lcCellElement.insertAdjacentHTML("beforeend", `${token.word} `); } // get infos for hit of match and set actions textTitles = new Set(); aCellElement = document.createElement("td"); aCellElement.classList.add("actions"); cCellElement = document.createElement("td"); cCellElement.classList.add("match-hit"); textTitlesCellElement = document.createElement("td"); textTitlesCellElement.classList.add("titles"); matchNrElement = document.createElement("td"); matchNrElement.classList.add("match-nr"); matchRowElement.appendChild(cCellElement); matchRowElement.appendChild(aCellElement); for (cpos of c) { token = chunk.cpos_lookup[cpos]; cCellElement.insertAdjacentHTML("beforeend", `${token.word} `); // get text titles of every hit cpos token textTitles.add(chunk.text_lookup[token.text].title); } // add some interaction buttons // # some btn css rules and classes let css = `margin-right: 5px; margin-bottom: 5px;` let classes = `btn-floating btn waves-effect` + `waves-light grey` // # add button to trigger more context to every match td inspectBtn = document.createElement("a"); inspectBtn.setAttribute("style", css); inspectBtn.setAttribute("class", classes + ` disabled inspect` ); inspectBtn.innerHTML = 'search'; inspectBtn.onclick = () => {this.inspect([values.index], "inspect")}; // # add btn to add matches to sub-results. hidden per default addToSubResultsBtn = document.createElement("a"); addToSubResultsBtn.setAttribute("style", css); addToSubResultsBtn.setAttribute("class", classes + ` hide add` ); addToSubResultsBtn.innerHTML = 'add'; addToSubResultsBtn.onclick = (event) => {this.addToSubResults(values.index)} aCellElement.appendChild(inspectBtn); aCellElement.appendChild(addToSubResultsBtn); // add text titles at front as first td of one row textTitlesCellElement.innerText = [...textTitles].join(", "); matchRowElement.insertAdjacentHTML("afterbegin", textTitlesCellElement.outerHTML); matchNrElement.innerText = values.index + 1; matchRowElement.insertAdjacentHTML("afterbegin", matchNrElement.outerHTML); // get infos for right context of match rcCellElement = document.createElement("td"); rcCellElement.classList.add("right-context"); matchRowElement.appendChild(rcCellElement); for (cpos of rc) { token = chunk.cpos_lookup[cpos]; rcCellElement.insertAdjacentHTML("beforeend", `${token.word} `); } return matchRowElement } }