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 | |
| parent | bd841fe56540e1a9177d2872310b10fefcb4acd1 (diff) | |
| parent | d880e4b2fcb4e7bab3ee25d63209b173efcf37c0 (diff) | |
merged
Diffstat (limited to 'src/client/views/search')
| -rw-r--r-- | src/client/views/search/FieldFilters.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/search/FilterBox.tsx | 27 | ||||
| -rw-r--r-- | src/client/views/search/IconBar.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/search/IconButton.tsx | 42 | ||||
| -rw-r--r-- | src/client/views/search/Pager.scss | 47 | ||||
| -rw-r--r-- | src/client/views/search/SearchBox.scss | 6 | ||||
| -rw-r--r-- | src/client/views/search/SearchBox.tsx | 225 | ||||
| -rw-r--r-- | src/client/views/search/SearchItem.scss | 302 | ||||
| -rw-r--r-- | src/client/views/search/SearchItem.tsx | 58 |
9 files changed, 431 insertions, 282 deletions
diff --git a/src/client/views/search/FieldFilters.tsx b/src/client/views/search/FieldFilters.tsx index 648aac20a..7a33282d2 100644 --- a/src/client/views/search/FieldFilters.tsx +++ b/src/client/views/search/FieldFilters.tsx @@ -34,7 +34,7 @@ export class FieldFilters extends React.Component<FieldFilterProps> { <div className="field-filters"> <CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.titleFieldStatus} updateStatus={this.props.updateTitleStatus} title={Keys.TITLE} /> <CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.authorFieldStatus} updateStatus={this.props.updateAuthorStatus} title={Keys.AUTHOR} /> - <CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={Keys.DATA} /> + <CheckBox default={false} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={"Deleted Docs"} /> </div> ); } diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index 6053db595..4b1887339 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -6,7 +6,7 @@ import { faTimes, faCheckCircle, faObjectGroup } from '@fortawesome/free-solid-s import { library } from '@fortawesome/fontawesome-svg-core'; import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { DocTypes } from '../../documents/Documents'; +import { DocumentType } from '../../documents/Documents'; import { Cast, StrCast } from '../../../new_fields/Types'; import * as _ from "lodash"; import { ToggleBar } from './ToggleBar'; @@ -35,7 +35,7 @@ export enum Keys { export class FilterBox extends React.Component { static Instance: FilterBox; - public _allIcons: string[] = [DocTypes.AUDIO, DocTypes.COL, DocTypes.HIST, DocTypes.IMG, DocTypes.LINK, DocTypes.PDF, DocTypes.TEXT, DocTypes.VID, DocTypes.WEB]; + public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.HIST, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB]; //if true, any keywords can be used. if false, all keywords are required. //this also serves as an indicator if the word status filter is applied @@ -48,6 +48,7 @@ export class FilterBox extends React.Component { @observable private _authorFieldStatus: boolean = true; @observable private _dataFieldStatus: boolean = true; //this also serves as an indicator if the collection status filter is applied + @observable public _deletedDocsStatus: boolean = false; @observable private _collectionStatus = false; @observable private _collectionSelfStatus = true; @observable private _collectionParentStatus = true; @@ -94,6 +95,9 @@ export class FilterBox extends React.Component { } }); + + let el = acc[i] as HTMLElement; + el.click(); } }); } @@ -168,13 +172,13 @@ export class FilterBox extends React.Component { if (this._authorFieldStatus) { finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR); } - if (this._dataFieldStatus) { + if (this._deletedDocsStatus) { finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA); } return finalQuery; } - get fieldFiltersApplied() { return !(this._dataFieldStatus && this._authorFieldStatus && this._titleFieldStatus); } + get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); } //TODO: basically all of this //gets all of the collections of all the docviews that are selected @@ -244,12 +248,19 @@ export class FilterBox extends React.Component { return "+(" + finalColString + ")" + query; } + get filterTypes() { + return this._icons.length === 9 ? undefined : this._icons; + } + @action filterDocsByType(docs: Doc[]) { + if (this._icons.length === 9) { + return docs; + } let finalDocs: Doc[] = []; docs.forEach(doc => { let layoutresult = Cast(doc.type, "string"); - if (!layoutresult || this._icons.includes(layoutresult)) { + if (layoutresult && this._icons.includes(layoutresult)) { finalDocs.push(doc); } }); @@ -339,7 +350,7 @@ export class FilterBox extends React.Component { updateAuthorStatus(newStat: boolean) { this._authorFieldStatus = newStat; } @action.bound - updateDataStatus(newStat: boolean) { this._dataFieldStatus = newStat; } + updateDataStatus(newStat: boolean) { this._deletedDocsStatus = newStat; } @action.bound updateCollectionStatus(newStat: boolean) { this._collectionStatus = newStat; } @@ -355,7 +366,7 @@ export class FilterBox extends React.Component { getParentCollectionStatus() { return this._collectionParentStatus; } getTitleStatus() { return this._titleFieldStatus; } getAuthorStatus() { return this._authorFieldStatus; } - getDataStatus() { return this._dataFieldStatus; } + getDataStatus() { return this._deletedDocsStatus; } getActiveFilters() { console.log(this._authorFieldStatus, this._titleFieldStatus, this._dataFieldStatus); @@ -433,7 +444,7 @@ export class FilterBox extends React.Component { <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleFieldOpen} /></div> </div> <div className="filter-panel"><FieldFilters - titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._dataFieldStatus} authorFieldStatus={this._authorFieldStatus} + titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._deletedDocsStatus} authorFieldStatus={this._authorFieldStatus} updateAuthorStatus={this.updateAuthorStatus} updateDataStatus={this.updateDataStatus} updateTitleStatus={this.updateTitleStatus} /> </div> </div> </div> diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index 744dd898a..4712b0abc 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -4,7 +4,7 @@ import { observable, action } from 'mobx'; // import "./SearchBox.scss"; import "./IconBar.scss"; import "./IconButton.scss"; -import { DocTypes } from '../../documents/Documents'; +import { DocumentType } from '../../documents/Documents'; import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faTimesCircle, faCheckCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { library } from '@fortawesome/fontawesome-svg-core'; @@ -63,7 +63,7 @@ export class IconBar extends React.Component { <div className="type-outer"> <div className={"type-icon all"} onClick={this.selectAll}> - <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} /> + <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} /> </div> <div className="filter-description">Select All</div> </div> diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx index 23ab42de0..bfe2c7d0b 100644 --- a/src/client/views/search/IconButton.tsx +++ b/src/client/views/search/IconButton.tsx @@ -6,7 +6,7 @@ import "./IconButton.scss"; import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faVideo, faCaretDown } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { library, icon } from '@fortawesome/fontawesome-svg-core'; -import { DocTypes } from '../../documents/Documents'; +import { DocumentType } from '../../documents/Documents'; import '../globalCssVariables.scss'; import * as _ from "lodash"; import { IconBar } from './IconBar'; @@ -80,25 +80,25 @@ export class IconButton extends React.Component<IconButtonProps>{ @action.bound getIcon() { switch (this.props.type) { - case (DocTypes.NONE): + case (DocumentType.NONE): return faBan; - case (DocTypes.AUDIO): + case (DocumentType.AUDIO): return faMusic; - case (DocTypes.COL): + case (DocumentType.COL): return faObjectGroup; - case (DocTypes.HIST): + case (DocumentType.HIST): return faChartBar; - case (DocTypes.IMG): + case (DocumentType.IMG): return faImage; - case (DocTypes.LINK): + case (DocumentType.LINK): return faLink; - case (DocTypes.PDF): + case (DocumentType.PDF): return faFilePdf; - case (DocTypes.TEXT): + case (DocumentType.TEXT): return faStickyNote; - case (DocTypes.VID): + case (DocumentType.VID): return faVideo; - case (DocTypes.WEB): + case (DocumentType.WEB): return faGlobeAsia; default: return faCaretDown; @@ -149,25 +149,25 @@ export class IconButton extends React.Component<IconButtonProps>{ getFA = () => { switch (this.props.type) { - case (DocTypes.NONE): + case (DocumentType.NONE): return (<FontAwesomeIcon className="fontawesome-icon" icon={faBan} />); - case (DocTypes.AUDIO): + case (DocumentType.AUDIO): return (<FontAwesomeIcon className="fontawesome-icon" icon={faMusic} />); - case (DocTypes.COL): + case (DocumentType.COL): return (<FontAwesomeIcon className="fontawesome-icon" icon={faObjectGroup} />); - case (DocTypes.HIST): + case (DocumentType.HIST): return (<FontAwesomeIcon className="fontawesome-icon" icon={faChartBar} />); - case (DocTypes.IMG): + case (DocumentType.IMG): return (<FontAwesomeIcon className="fontawesome-icon" icon={faImage} />); - case (DocTypes.LINK): + case (DocumentType.LINK): return (<FontAwesomeIcon className="fontawesome-icon" icon={faLink} />); - case (DocTypes.PDF): + case (DocumentType.PDF): return (<FontAwesomeIcon className="fontawesome-icon" icon={faFilePdf} />); - case (DocTypes.TEXT): + case (DocumentType.TEXT): return (<FontAwesomeIcon className="fontawesome-icon" icon={faStickyNote} />); - case (DocTypes.VID): + case (DocumentType.VID): return (<FontAwesomeIcon className="fontawesome-icon" icon={faVideo} />); - case (DocTypes.WEB): + case (DocumentType.WEB): return (<FontAwesomeIcon className="fontawesome-icon" icon={faGlobeAsia} />); default: return (<FontAwesomeIcon className="fontawesome-icon" icon={faCaretDown} />); diff --git a/src/client/views/search/Pager.scss b/src/client/views/search/Pager.scss deleted file mode 100644 index 2b9c81b93..000000000 --- a/src/client/views/search/Pager.scss +++ /dev/null @@ -1,47 +0,0 @@ -@import "../globalCssVariables"; - -.search-pager { - background-color: $dark-color; - border-radius: 10px; - width: 500px; - display: flex; - justify-content: center; - // margin-left: 27px; - float: right; - margin-right: 74px; - margin-left: auto; - - // flex-direction: column; - - .search-arrows { - display: flex; - justify-content: center; - margin: 10px; - width: 50%; - - .arrow { - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - - .fontawesome-icon { - color: $light-color; - width: 20px; - height: 20px; - margin-right: 2px; - margin-left: 2px; - // opacity: .7; - } - } - - .pager-title { - text-align: center; - // font-size: 8px; - // margin-bottom: 10px; - color: $light-color; - // padding: 2px; - width: 40%; - } - } -}
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 7541b328a..fcdc79220 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -41,7 +41,7 @@ } .searchBox-results { - margin-left: 27px; + margin-right: 136px; top: 300px; display: flex; flex-direction: column; @@ -50,6 +50,9 @@ height: 100%; // overflow: hidden; // overflow-y: auto; + max-height: 560px; + overflow: hidden; + overflow-y: auto; .no-result { width: 500px; @@ -61,5 +64,6 @@ text-transform: uppercase; text-align: left; font-weight: bold; + margin-left: 28px; } }
\ No newline at end of file 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> ); diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss index fa4c9cb38..273d49349 100644 --- a/src/client/views/search/SearchItem.scss +++ b/src/client/views/search/SearchItem.scss @@ -6,20 +6,25 @@ justify-content: flex-end; height: 70px; z-index: 0; +} - .search-item { - width: 500px; - background: $light-color-secondary; - border-color: $intermediate-color; - border-bottom-style: solid; - padding: 10px; - height: 70px; - z-index: 0; - display: inline-block; - - .main-search-info { - display: flex; - flex-direction: row; +.searchBox-placeholder, +.search-overview .search-item { + width: 500px; + background: $light-color-secondary; + border-color: $intermediate-color; + border-bottom-style: solid; + padding: 10px; + height: 70px; + z-index: 0; + display: inline-block; + + .main-search-info { + display: flex; + flex-direction: row; + width: 100%; + + .search-title-container { width: 100%; .search-title { @@ -28,160 +33,183 @@ width: 100%; font-weight: bold; } + } - .search-info { + .search-info { + display: flex; + justify-content: flex-end; + + .link-container.item { + margin-left: auto; + margin-right: auto; + height: 26px; + width: 26px; + border-radius: 13px; + background: $dark-color; + color: $light-color-secondary; display: flex; - justify-content: flex-end; - - .link-container.item { - margin-left: auto; - margin-right: auto; - height: 26px; - width: 26px; - border-radius: 13px; - background: $dark-color; - color: $light-color-secondary; - display: flex; - justify-content: center; - align-items: center; - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - transform-origin: top right; - overflow: hidden; + justify-content: center; + align-items: center; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + transform-origin: top right; + overflow: hidden; + position: relative; + + .link-count { + opacity: 1; + position: absolute; + z-index: 1000; + text-align: center; + -webkit-transition: opacity 0.2s ease-in-out; + -moz-transition: opacity 0.2s ease-in-out; + -o-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; + } + + .link-extended { + // display: none; + visibility: hidden; + opacity: 0; position: relative; + z-index: 500; + overflow: hidden; + -webkit-transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s; + -moz-transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s; + -o-transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s; + transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s; + // transition-delay: 1s; + } - .link-count { - opacity: 1; - position: absolute; - z-index: 1000; - text-align: center; - -webkit-transition: opacity 0.2s ease-in-out; - -moz-transition: opacity 0.2s ease-in-out; - -o-transition: opacity 0.2s ease-in-out; - transition: opacity 0.2s ease-in-out; - } + } - .link-extended { - opacity: 0; - position: relative; - z-index: 500; - overflow: hidden; - -webkit-transition: opacity 0.2s ease-in-out; - -moz-transition: opacity 0.2s ease-in-out; - -o-transition: opacity 0.2s ease-in-out; - transition: opacity 0.2s ease-in-out; - } - } + .link-container.item:hover { + width: 70px; + } - .link-container.item:hover { - width: 70px; - } + .link-container.item:hover .link-count { + opacity: 0; + } - .link-container.item:hover .link-count { - opacity: 0; - } + .link-container.item:hover .link-extended { + opacity: 1; + visibility: visible; + // display: inline; + } - .link-container.item:hover .link-extended { - opacity: 1; - } - - .icon-icons { - width:50px - } - .icon-live { - width:175px; + .icon-icons { + width: 50px + } + + .icon-live { + width: 175px; + } + + .icon-icons, + .icon-live { + height: 50px; + margin: auto; + overflow: hidden; + + .search-type { + display: inline-block; + width: 100%; + position: absolute; + justify-content: center; + align-items: center; + position: relative; + margin-right: 5px; } - .icon-icons, .icon-live { - height:50px; - margin:auto; + .pdfBox-cont { overflow: hidden; - .search-type { - display: inline-block; - width:100%; - position: absolute; - justify-content: center; - align-items: center; - position: relative; - margin-right: 5px; - } - .pdfBox-cont { - overflow: hidden; - - img { - width:100% !important; - height:auto !important; - } - } - .search-type:hover+.search-label { - opacity: 1; + img { + width: 100% !important; + height: auto !important; } + } - .search-label { - font-size: 10; - position: relative; - right: 0px; - text-transform: capitalize; - opacity: 0; - -webkit-transition: opacity 0.2s ease-in-out; - -moz-transition: opacity 0.2s ease-in-out; - -o-transition: opacity 0.2s ease-in-out; - transition: opacity 0.2s ease-in-out; - } + .search-type:hover+.search-label { + opacity: 1; } - .icon-live:hover { - height:175px; - .pdfBox-cont { - img { - width:100% !important; - } - } + .search-label { + font-size: 10; + position: relative; + right: 0px; + text-transform: capitalize; + opacity: 0; + -webkit-transition: opacity 0.2s ease-in-out; + -moz-transition: opacity 0.2s ease-in-out; + -o-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; } } - .search-info:hover { - width:60%; + + .icon-live:hover { + height: 175px; + + .pdfBox-cont { + img { + width: 100% !important; + } + } } } - } - .search-item:hover~.searchBox-instances, - .searchBox-instances:hover, - .searchBox-instances:active { - opacity: 1; - background: $lighter-alt-accent; - -webkit-transform: scale(1); - -ms-transform: scale(1); - transform: scale(1); + .search-info:hover { + width: 60%; + } } +} - .search-item:hover { - transition: all 0.2s; - background: $lighter-alt-accent; - } +.search-item:hover~.searchBox-instances, +.searchBox-instances:hover, +.searchBox-instances:active { + opacity: 1; + background: $lighter-alt-accent; + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} - .searchBox-instances { - float: left; - opacity: 1; - width: 150px; - transition: all 0.2s ease; - color: black; - transform-origin: top right; - -webkit-transform: scale(0); - -ms-transform: scale(0); - transform: scale(0); - } +.search-item:hover { + transition: all 0.2s; + background: $lighter-alt-accent; +} +.searchBox-instances { + float: left; + opacity: 1; + width: 150px; + transition: all 0.2s ease; + color: black; + transform-origin: top right; + -webkit-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); } + + .search-overview:hover { z-index: 1; } + +.searchBox-placeholder { + min-height: 70px; + margin-left: 150px; + text-transform: uppercase; + text-align: left; + font-weight: bold; +} + .collection { - display:flex; + display: flex; } + .collection-item { - width:35px; + width: 35px; }
\ No newline at end of file diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 2b8d3eda3..5b0e20348 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -8,7 +8,7 @@ import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { emptyFunction, returnFalse, returnOne, Utils } from "../../../Utils"; -import { DocTypes } from "../../documents/Documents"; +import { DocumentType } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { SetupDrag, DragManager } from "../../util/DragManager"; import { LinkManager } from "../../util/LinkManager"; @@ -33,6 +33,7 @@ import { DocServer } from "../../DocServer"; export interface SearchItemProps { doc: Doc; query?: string; + highlighting: string[]; } library.add(faCaretUp); @@ -58,9 +59,9 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { async fetchDocuments() { let aliases = (await SearchUtil.GetViewsOfDocument(this.props.doc)).filter(doc => doc !== this.props.doc); - const { docs } = await SearchUtil.Search(`data_l:"${this.props.doc[Id]}"`, true); + const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.doc[Id]}"` }); const map: Map<Doc, Doc> = new Map; - const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true).then(result => result.docs))); + const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs))); allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index]))); docs.forEach(doc => map.delete(doc)); runInAction(() => { @@ -145,10 +146,12 @@ export class SearchItem extends React.Component<SearchItemProps> { private _previewDoc?: Doc; onClick = () => { + // I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like DocumentManager.Instance.jumpToDocument(this.props.doc, false); if (this.props.doc.data instanceof RichTextField) { this.highlightTextBox(this.props.doc); } + // CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined); } @observable _useIcons = true; @observable _displayDim = 50; @@ -167,7 +170,7 @@ export class SearchItem extends React.Component<SearchItemProps> { } fitToBox = () => { - let bounds = Doc.ComputeContentBounds(this.props.doc); + let bounds = Doc.ComputeContentBounds([this.props.doc]); return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / Math.max((bounds.b - bounds.y), (bounds.r - bounds.x)), this._displayDim]; } @@ -185,7 +188,7 @@ export class SearchItem extends React.Component<SearchItemProps> { if (!this._useIcons) { let renderDoc = this.props.doc; //let box: number[] = []; - if (layoutresult.indexOf(DocTypes.COL) !== -1) { + if (layoutresult.indexOf(DocumentType.COL) !== -1) { renderDoc = Doc.MakeDelegate(renderDoc); let bounds = DocListCast(renderDoc.data).reduce((bounds, doc) => { var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; @@ -210,8 +213,8 @@ export class SearchItem extends React.Component<SearchItemProps> { onPointerEnter={action(() => this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE))} onPointerLeave={action(() => this._displayDim = 50)} > <DocumentView - fitToBox={this.fitToBox} - Document={newRenderDoc} + fitToBox={StrCast(this.props.doc.type).indexOf(DocumentType.COL) !== -1} + Document={this.props.doc} addDocument={returnFalse} removeDocument={returnFalse} ScreenToLocalTransform={Transform.Identity} @@ -239,15 +242,15 @@ export class SearchItem extends React.Component<SearchItemProps> { if (this._previewDoc) { DocServer.DeleteDocument(this._previewDoc[Id]); } - let button = layoutresult.indexOf(DocTypes.PDF) !== -1 ? faFilePdf : - layoutresult.indexOf(DocTypes.IMG) !== -1 ? faImage : - layoutresult.indexOf(DocTypes.TEXT) !== -1 ? faStickyNote : - layoutresult.indexOf(DocTypes.VID) !== -1 ? faFilm : - layoutresult.indexOf(DocTypes.COL) !== -1 ? faObjectGroup : - layoutresult.indexOf(DocTypes.AUDIO) !== -1 ? faMusic : - layoutresult.indexOf(DocTypes.LINK) !== -1 ? faLink : - layoutresult.indexOf(DocTypes.HIST) !== -1 ? faChartBar : - layoutresult.indexOf(DocTypes.WEB) !== -1 ? faGlobeAsia : + let button = layoutresult.indexOf(DocumentType.PDF) !== -1 ? faFilePdf : + layoutresult.indexOf(DocumentType.IMG) !== -1 ? faImage : + layoutresult.indexOf(DocumentType.TEXT) !== -1 ? faStickyNote : + layoutresult.indexOf(DocumentType.VID) !== -1 ? faFilm : + layoutresult.indexOf(DocumentType.COL) !== -1 ? faObjectGroup : + layoutresult.indexOf(DocumentType.AUDIO) !== -1 ? faMusic : + layoutresult.indexOf(DocumentType.LINK) !== -1 ? faLink : + layoutresult.indexOf(DocumentType.HIST) !== -1 ? faChartBar : + layoutresult.indexOf(DocumentType.WEB) !== -1 ? faGlobeAsia : faCaretUp; return <div onPointerDown={action(() => { this._useIcons = false; this._displayDim = Number(SEARCH_THUMBNAIL_SIZE); })} > <FontAwesomeIcon icon={button} size="2x" /> @@ -281,7 +284,7 @@ export class SearchItem extends React.Component<SearchItemProps> { pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); } highlightDoc = (e: React.PointerEvent) => { - if (this.props.doc.type === DocTypes.LINK) { + if (this.props.doc.type === DocumentType.LINK) { if (this.props.doc.anchor1 && this.props.doc.anchor2) { let doc1 = Cast(this.props.doc.anchor1, Doc, null); @@ -298,18 +301,18 @@ export class SearchItem extends React.Component<SearchItemProps> { } unHighlightDoc = (e: React.PointerEvent) => { - if (this.props.doc.type === DocTypes.LINK) { + if (this.props.doc.type === DocumentType.LINK) { if (this.props.doc.anchor1 && this.props.doc.anchor2) { let doc1 = Cast(this.props.doc.anchor1, Doc, null); let doc2 = Cast(this.props.doc.anchor2, Doc, null); - doc1 && (doc1.libraryBrush = false); - doc2 && (doc2.libraryBrush = false); + doc1 && (doc1.libraryBrush = undefined); + doc2 && (doc2.libraryBrush = undefined); } } else { let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); docViews.forEach(element => { - element.props.Document.libraryBrush = false; + element.props.Document.libraryBrush = undefined; }); } } @@ -341,12 +344,15 @@ export class SearchItem extends React.Component<SearchItemProps> { <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result" onClick={this.onClick} onPointerDown={this.pointerDown} > <div className="main-search-info"> - <div title="Drag as document" onPointerDown={this.onPointerDown}> <FontAwesomeIcon icon="file" size="lg" /> </div> - <div className="search-title" id="result" >{this.props.doc.title}</div> + <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" /> </div> + <div className="search-title-container"> + <div className="search-title">{StrCast(this.props.doc.title)}</div> + <div className="search-highlighting">Matched fields: {this.props.highlighting.join(", ")}</div> + </div> <div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}> <div className={`icon-${this._useIcons ? "icons" : "live"}`}> - <div className="search-type" >{this.DocumentIcon()}</div> - <div className="search-label">{this.props.doc.type}</div> + <div className="search-type" title="Click to Preview">{this.DocumentIcon}</div> + <div className="search-label">{this.props.doc.type ? this.props.doc.type : "Other"}</div> </div> <div className="link-container item"> <div className="link-count">{this.linkCount}</div> @@ -356,7 +362,7 @@ export class SearchItem extends React.Component<SearchItemProps> { </div> </div> <div className="searchBox-instances"> - {this.props.doc.type === DocTypes.LINK ? <LinkContextMenu doc1={Cast(this.props.doc.anchor1, Doc, new Doc())} doc2={Cast(this.props.doc.anchor2, Doc, new Doc())} /> : + {this.props.doc.type === DocumentType.LINK ? <LinkContextMenu doc1={Cast(this.props.doc.anchor1, Doc, new Doc())} doc2={Cast(this.props.doc.anchor2, Doc, new Doc())} /> : <SelectorContextMenu {...this.props} />} </div> </div> |
