diff options
| author | ab <abdullah_ahmed@brown.edu> | 2019-07-23 11:38:10 -0400 |
|---|---|---|
| committer | ab <abdullah_ahmed@brown.edu> | 2019-07-23 11:38:10 -0400 |
| commit | 13c016d7f7765acda7f6ce2d69c14597469f55d7 (patch) | |
| tree | 6fcf409ee4f0035443aa4fb67a05d24aae09689d /src/client/views/search/SearchBox.tsx | |
| parent | bd841fe56540e1a9177d2872310b10fefcb4acd1 (diff) | |
| parent | d880e4b2fcb4e7bab3ee25d63209b173efcf37c0 (diff) | |
merged
Diffstat (limited to 'src/client/views/search/SearchBox.tsx')
| -rw-r--r-- | src/client/views/search/SearchBox.tsx | 225 |
1 files changed, 186 insertions, 39 deletions
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 393a5a924..2214ac8af 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,38 +1,49 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { observable, action, runInAction } from 'mobx'; +import { observable, action, runInAction, flow, computed } from 'mobx'; import "./SearchBox.scss"; import "./FilterBox.scss"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { SetupDrag } from '../../util/DragManager'; import { Docs } from '../../documents/Documents'; -import { NumCast } from '../../../new_fields/Types'; +import { NumCast, Cast } from '../../../new_fields/Types'; import { Doc } from '../../../new_fields/Doc'; import { SearchItem } from './SearchItem'; -import { DocServer } from '../../DocServer'; import * as rp from 'request-promise'; import { Id } from '../../../new_fields/FieldSymbols'; import { SearchUtil } from '../../util/SearchUtil'; import { RouteStore } from '../../../server/RouteStore'; import { FilterBox } from './FilterBox'; +import { Utils } from '../../../Utils'; + @observer export class SearchBox extends React.Component { @observable private _searchString: string = ""; @observable private _resultsOpen: boolean = false; - @observable private _results: Doc[] = []; + @observable private _searchbarOpen: boolean = false; + @observable private _results: [Doc, string[]][] = []; + private _resultsSet = new Map<Doc, number>(); @observable private _openNoResults: boolean = false; - @observable public _pageNum: number = 0; - //temp - @observable public _maxNum: number = 10; + @observable private _visibleElements: JSX.Element[] = []; + + private resultsRef = React.createRef<HTMLDivElement>(); + + private _isSearch: ("search" | "placeholder" | undefined)[] = []; + private _numTotalResults = -1; + private _endIndex = -1; static Instance: SearchBox; + private _maxSearchIndex: number = 0; + private _curRequest?: Promise<any> = undefined; + constructor(props: any) { super(props); SearchBox.Instance = this; + this.resultsScrolled = this.resultsScrolled.bind(this); } @action @@ -49,19 +60,21 @@ export class SearchBox extends React.Component { onChange(e: React.ChangeEvent<HTMLInputElement>) { this._searchString = e.target.value; - if (this._searchString === "") { - this._results = []; - this._openNoResults = false; - } + this._openNoResults = false; + this._results = []; + this._resultsSet.clear(); + this._visibleElements = []; + this._numTotalResults = -1; + this._endIndex = -1; + this._curRequest = undefined; + this._maxSearchIndex = 0; } - enter = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { this.submitSearch(); } - } + enter = (e: React.KeyboardEvent) => { if (e.key === "Enter") { this.submitSearch(); } }; public static async convertDataUri(imageUri: string, returnedFilename: string) { try { - let posting = DocServer.prepend(RouteStore.dataUriToImage); + let posting = Utils.prepend(RouteStore.dataUriToImage); const returnedUri = await rp.post(posting, { body: { uri: imageUri, @@ -78,38 +91,97 @@ export class SearchBox extends React.Component { @action submitSearch = async () => { - let query = this._searchString; // searchbox gets query - let results: Doc[]; - + let query = this._searchString; query = FilterBox.Instance.getFinalQuery(query); + this._results = []; + this._resultsSet.clear(); + this._isSearch = []; + this._visibleElements = []; + FilterBox.Instance.closeFilter(); //if there is no query there should be no result if (query === "") { - results = []; + return; } else { - //gets json result into a list of documents that can be used - //these are filtered by type - results = await this.getResults(query); + this._endIndex = 12; + this._maxSearchIndex = 0; + this._numTotalResults = -1; + await this.getResults(query); } runInAction(() => { this._resultsOpen = true; - this._results = results; + this._searchbarOpen = true; this._openNoResults = true; + this.resultsScrolled(); }); } - @action + getAllResults = async (query: string) => { + return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 }); + } + + private get filterQuery() { + const types = FilterBox.Instance.filterTypes; + const includeDeleted = FilterBox.Instance.getDataStatus(); + return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); + } + + + private lockPromise?: Promise<void>; getResults = async (query: string) => { - const { docs } = await SearchUtil.Search(query, true); - return FilterBox.Instance.filterDocsByType(docs); + if (this.lockPromise) { + await this.lockPromise; + } + this.lockPromise = new Promise(async res => { + while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { + this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: 10, hl: true, "hl.fl": "*" }).then(action(async (res: SearchUtil.DocSearchResult) => { + + // happens at the beginning + if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { + this._numTotalResults = res.numFound; + } + + const highlighting = res.highlighting || {}; + const highlightList = res.docs.map(doc => highlighting[doc[Id]]); + const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); + const highlights: typeof res.highlighting = {}; + docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); + let filteredDocs = FilterBox.Instance.filterDocsByType(docs); + runInAction(() => { + // this._results.push(...filteredDocs); + filteredDocs.forEach(doc => { + const index = this._resultsSet.get(doc); + const highlight = highlights[doc[Id]]; + const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : []; + if (index === undefined) { + this._resultsSet.set(doc, this._results.length); + this._results.push([doc, hlights]); + } else { + this._results[index][1].push(...hlights); + } + }); + }); + + this._curRequest = undefined; + })); + this._maxSearchIndex += 10; + + await this._curRequest; + } + this.resultsScrolled(); + res(); + }); + return this.lockPromise; } collectionRef = React.createRef<HTMLSpanElement>(); startDragCollection = async () => { - const results = await this.getResults(FilterBox.Instance.getFinalQuery(this._searchString)); - const docs = results.map(doc => { + let res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString)); + let filtered = FilterBox.Instance.filterDocsByType(res.docs); + // console.log(this._results) + const docs = filtered.map(doc => { const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); if (isProto) { return Doc.MakeDelegate(doc); @@ -141,7 +213,8 @@ export class SearchBox extends React.Component { y += 300; } } - return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); + return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); + } @action.bound @@ -150,6 +223,7 @@ export class SearchBox extends React.Component { this._openNoResults = false; FilterBox.Instance.closeFilter(); this._resultsOpen = true; + this._searchbarOpen = true; FilterBox.Instance._pointerTime = e.timeStamp; } @@ -157,34 +231,107 @@ export class SearchBox extends React.Component { closeSearch = () => { FilterBox.Instance.closeFilter(); this.closeResults(); + this._searchbarOpen = false; } @action.bound closeResults() { this._resultsOpen = false; this._results = []; + this._resultsSet.clear(); + this._visibleElements = []; + this._numTotalResults = -1; + this._endIndex = -1; + this._curRequest = undefined; } + @action + resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { + let scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0; + let buffer = 4; + let startIndex = Math.floor(Math.max(0, scrollY / 70 - buffer)); + let endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (560 / 70) + buffer)); + + this._endIndex = endIndex === -1 ? 12 : endIndex; + + if ((this._numTotalResults === 0 || this._results.length === 0) && this._openNoResults) { + this._visibleElements = [<div className="no-result">No Search Results</div>]; + return; + } + + if (this._numTotalResults <= this._maxSearchIndex) { + this._numTotalResults = this._results.length; + } + + // only hit right at the beginning + // visibleElements is all of the elements (even the ones you can't see) + else if (this._visibleElements.length !== this._numTotalResults) { + // undefined until a searchitem is put in there + this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults); + // indicates if things are placeholders + this._isSearch = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); + } + + for (let i = 0; i < this._numTotalResults; i++) { + //if the index is out of the window then put a placeholder in + //should ones that have already been found get set to placeholders? + if (i < startIndex || i > endIndex) { + if (this._isSearch[i] !== "placeholder") { + this._isSearch[i] = "placeholder"; + this._visibleElements[i] = <div className="searchBox-placeholder" key={`searchBox-placeholder-${i}`}>Loading...</div>; + } + } + else { + if (this._isSearch[i] !== "search") { + let result: [Doc, string[]] | undefined = undefined; + if (i >= this._results.length) { + this.getResults(this._searchString); + if (i < this._results.length) result = this._results[i]; + if (result) { + this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />; + this._isSearch[i] = "search"; + } + } + else { + result = this._results[i]; + if (result) { + this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />; + this._isSearch[i] = "search"; + } + } + } + } + } + if (this._maxSearchIndex >= this._numTotalResults) { + this._visibleElements.length = this._results.length; + this._isSearch.length = this._results.length; + } + } + + @computed + get resFull() { return this._numTotalResults <= 8; } + + @computed + get resultHeight() { return this._numTotalResults * 70; } + render() { return ( <div className="searchBox-container"> <div className="searchBox-bar"> - <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}> + <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef} title="Drag Results as Collection"> <FontAwesomeIcon icon="object-group" size="lg" /> </span> <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} - style={{ width: this._resultsOpen ? "500px" : "100px" }} /> + style={{ width: this._searchbarOpen ? "500px" : "100px" }} /> + <button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button> <button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button> </div> - <div className="searchBox-results" style={this._resultsOpen ? { display: "flex" } : { display: "none" }}> - {(this._results.length !== 0) ? ( - this._results.map(result => <SearchItem doc={result} query={this._searchString} key={result[Id]} />) - ) : - this._openNoResults ? (<div className="no-result">No Search Results</div>) : null} - </div> - <div style={this._results.length !== 0 ? { display: "flex" } : { display: "none" }}> - {/* <Pager /> */} + <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ + display: this._resultsOpen ? "flex" : "none", + height: this.resFull ? "560px" : this.resultHeight, overflow: this.resFull ? "auto" : "visible" + }} ref={this.resultsRef}> + {this._visibleElements} </div> </div> ); |
