src/UXClient/Components/ModelSearch/ModelSearch.ts (354 lines of code) (raw):

import * as d3 from "d3"; import "./ModelSearch.scss"; import Utils from "../../Utils"; import { Component } from "./../../Interfaces/Component"; import "awesomplete"; import Hierarchy from "../Hierarchy/Hierarchy"; import ModelAutocomplete from "../ModelAutocomplete/ModelAutocomplete"; import { HierarchyDelegate } from "../../../ServerClient/HierarchyDelegate"; class ModelSearch extends Component { private delegate: HierarchyDelegate; private hierarchies; private clickedInstance; private wrapper; private types; private instanceResults; private usedContinuationTokens = {}; private contextMenu; private currentResultIndex = -1; constructor(renderTarget: Element, delegate: HierarchyDelegate) { super(renderTarget); this.delegate = delegate; d3.select("html").on("click." + Utils.guid(), () => { if ( this.clickedInstance && d3.event.target != this.clickedInstance && this.contextMenu ) { this.closeContextMenu(); this.clickedInstance = null; } }); } ModelSearch() {} public async render(hierarchyData: any, chartOptions?: any) { this.chartOptions.setOptions(chartOptions); let self = this; let continuationToken, searchText; let targetElement = d3.select(this.renderTarget); targetElement.html(""); this.wrapper = targetElement .append("div") .attr("class", "tsi-modelSearchWrapper"); super.themify(this.wrapper, this.chartOptions.theme); let inputWrapper = this.wrapper .append("div") .attr("class", "tsi-modelSearchInputWrapper"); let autocompleteOnInput = (st, event) => { self.usedContinuationTokens = {}; // blow results away if no text if (st.length === 0) { searchText = st; self.instanceResults.html(""); self.currentResultIndex = -1; (hierarchyElement.node() as any).style.display = "block"; (showMore.node() as any).style.display = "none"; noResults.style("display", "none"); } else if (event.which === 13 || event.keyCode === 13) { (hierarchyElement.node() as any).style.display = "none"; self.instanceResults.html(""); self.currentResultIndex = -1; noResults.style("display", "none"); searchInstances(st); searchText = st; } }; let modelAutocomplete = new ModelAutocomplete( inputWrapper.node(), this.delegate ); modelAutocomplete.render({ onInput: autocompleteOnInput, onKeydown: (event, ap) => { this.handleKeydown(event, ap); }, ...chartOptions, }); var ap = modelAutocomplete.ap; let results = this.wrapper .append("div") .attr("class", "tsi-modelSearchResults") .on("scroll", function () { self.closeContextMenu(); let that = this as any; if ( that.scrollTop + that.clientHeight + 150 > (self.instanceResults.node() as any).clientHeight && searchText.length !== 0 ) { searchInstances(searchText, continuationToken); } }); let noResults = results .append("div") .text(this.getString("No results")) .classed("tsi-noResults", true) .style("display", "none"); let instanceResultsWrapper = results .append("div") .attr("class", "tsi-modelSearchInstancesWrapper"); this.instanceResults = instanceResultsWrapper .append("div") .attr("class", "tsi-modelSearchInstances"); let showMore = instanceResultsWrapper .append("div") .attr("class", "tsi-showMore") .text(this.getString("Show more") + "...") .on("click", () => searchInstances(searchText, continuationToken)) .style("display", "none"); let hierarchyElement = this.wrapper .append("div") .attr("class", "tsi-hierarchyWrapper"); let hierarchy = new Hierarchy(hierarchyElement.node() as any); hierarchy.render(hierarchyData, { ...this.chartOptions, withContextMenu: true, }); let searchInstances = async (searchText, ct = null) => { var self = this; if (ct === "END") return; if (ct === null || !self.usedContinuationTokens[ct]) { self.usedContinuationTokens[ct] = true; const r = await this.delegate.getInstancesSearch(searchText); let instances = r.instances.hits; //ontinuationToken = r.instances.hits.continuationToken; if (!continuationToken) continuationToken = "END"; (showMore.node() as any).style.display = continuationToken !== "END" ? "block" : "none"; if (instances.length == 0) { noResults.style("display", "block"); } else { noResults.style("display", "none"); } instances.forEach((i) => { let handleClick = ( elt, wrapperMousePos, eltMousePos, fromKeyboard = false ) => { self.closeContextMenu(); if (self.clickedInstance != elt) { self.clickedInstance = elt; (i as any).type = self.types.filter((t) => { return ( t.name.replace(/\s/g, "") === i.highlights.typeName .split("<hit>") .join("") .split("</hit>") .join("") .replace(/\s/g, "") ); })[0]; let contextMenuActions = self.chartOptions.onInstanceClick(i); self.contextMenu = self.wrapper.append("div"); if (!Array.isArray(contextMenuActions)) { contextMenuActions = [contextMenuActions]; } let totalActionCount = contextMenuActions .map((cma) => Object.keys(cma).length) .reduce((p, c) => p + c, 0); let currentActionIndex = 0; contextMenuActions.forEach((cma, cmaGroupIdx) => { Object.keys(cma).forEach((k, kIdx, kArray) => { let localActionIndex = currentActionIndex; self.contextMenu .append("div") .text(k) .on("click", cma[k]) .on("keydown", function () { let evt = d3.event; if (evt.keyCode === 13) { this.click(); } if (evt.keyCode === 13 || evt.keyCode === 37) { self.closeContextMenu(); let results = self.instanceResults.selectAll( ".tsi-modelResultWrapper" ); results.nodes()[self.currentResultIndex].focus(); } if ( evt.keyCode === 40 && localActionIndex + 1 < totalActionCount ) { // down self.contextMenu .node() .children[ localActionIndex + 1 + cmaGroupIdx + (kIdx === kArray.length - 1 ? 1 : 0) ].focus(); } if (evt.keyCode === 38 && localActionIndex > 0) { // up self.contextMenu .node() .children[ localActionIndex - 1 + cmaGroupIdx - (kIdx === 0 ? 1 : 0) ].focus(); } }) .attr("tabindex", "0"); currentActionIndex++; }); self.contextMenu.append("div").classed("tsi-break", true); }); self.contextMenu.attr( "style", () => `top: ${wrapperMousePos - eltMousePos}px` ); self.contextMenu.classed("tsi-modelSearchContextMenu", true); d3.select(elt).classed("tsi-resultSelected", true); if (self.contextMenu.node().children.length > 0 && fromKeyboard) { self.contextMenu.node().children[0].focus(); } } else { self.clickedInstance = null; } }; this.instanceResults .append("div") .html(self.getInstanceHtml(i)) // known unsafe usage of .html .on("click", function () { let mouseWrapper = d3.mouse(self.wrapper.node()); let mouseElt = d3.mouse(this as any); handleClick(this, mouseWrapper[1], mouseElt[1]); }) .on("keydown", () => { let evt = d3.event; if (evt.keyCode === 13) { let resultsNodes = this.instanceResults .selectAll(".tsi-modelResultWrapper") .nodes(); let height = 0; for (var i = 0; i < this.currentResultIndex; i++) { height += resultsNodes[0].clientHeight; } handleClick( this.instanceResults .select(".tsi-modelResultWrapper:focus") .node(), height - results.node().scrollTop + 48, 0, true ); } self.handleKeydown(evt, ap); }) .attr("tabindex", "0") .classed("tsi-modelResultWrapper", true); }); } }; this.hierarchies = await this.delegate.getHierarchies(); // get types this.types = await this.delegate.getTimeSeriesTypes(); } public handleKeydown(event, ap) { if (!ap.isOpened) { let results = this.instanceResults.selectAll(".tsi-modelResultWrapper"); if (results.size()) { if ( event.keyCode === 40 && this.currentResultIndex < results.nodes().length - 1 ) { this.currentResultIndex++; results.nodes()[this.currentResultIndex].focus(); } else if (event.keyCode === 38) { this.currentResultIndex--; if (this.currentResultIndex <= -1) { this.currentResultIndex = -1; ap.input.focus(); } else { results.nodes()[this.currentResultIndex].focus(); } } } } } private closeContextMenu() { if (this.contextMenu) { this.contextMenu.remove(); } d3.selectAll(".tsi-resultSelected").classed("tsi-resultSelected", false); } private stripHits = (str) => { return str .split("<hit>") .map((h) => h .split("</hit>") .map((h2) => Utils.strip(h2)) .join("</hit>") ) .join("<hit>"); }; private getInstanceHtml(i) { return `<div class="tsi-modelResult"> <div class="tsi-modelPK"> ${ i.highlights.name ? this.stripHits(i.highlights.name) : this.stripHits( i.highlights.timeSeriesIds ? i.highlights.timeSeriesIds.join(" ") : i.highlights.timeSeriesId.join(" ") ) } </div> <div class="tsi-modelHighlights"> ${this.stripHits( i.highlights.description && i.highlights.description.length ? i.highlights.description : this.getString("No description") )} <br/><table> ${ i.highlights.name ? "<tr><td>" + this.getString("Time Series ID") + "</td><td>" + this.stripHits( i.highlights.timeSeriesIds ? i.highlights.timeSeriesIds.join(" ") : i.highlights.timeSeriesId.join(" ") ) + "</td></tr>" : "" } ${i.highlights.instanceFieldNames .map((ifn, idx) => { var val = i.highlights.instanceFieldValues[idx]; if ( ifn.indexOf("<hit>") !== -1 || val.indexOf("<hit>") !== -1 ) { return val.length === 0 ? "" : "<tr><td>" + this.stripHits(ifn) + "</td><td>" + this.stripHits(val) + "</tr>"; } }) .join("")} </table> </div> </div>`; } } export default ModelSearch;