diff options
| author | Sam Wilkins <samwilkins333@gmail.com> | 2020-04-07 13:46:15 -0700 |
|---|---|---|
| committer | Sam Wilkins <samwilkins333@gmail.com> | 2020-04-07 13:46:15 -0700 |
| commit | 52ad8b3874419a76e6953c3bd698d9e68a3158a6 (patch) | |
| tree | a20156fa4453c817aa52d8d807dbc705066c5f6e /src/client/views/search | |
| parent | edd96f6d78ff066f0a14efefaa92a9710caff9bd (diff) | |
| parent | 9ad73769dd2f3a7c6598041f123dfd5cb7ef05d5 (diff) | |
pulled
Diffstat (limited to 'src/client/views/search')
| -rw-r--r-- | src/client/views/search/CheckBox.tsx | 146 | ||||
| -rw-r--r-- | src/client/views/search/FilterBox.scss | 9 | ||||
| -rw-r--r-- | src/client/views/search/FilterBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/search/IconBar.scss | 7 | ||||
| -rw-r--r-- | src/client/views/search/IconBar.tsx | 20 | ||||
| -rw-r--r-- | src/client/views/search/IconButton.scss | 1 | ||||
| -rw-r--r-- | src/client/views/search/IconButton.tsx | 16 | ||||
| -rw-r--r-- | src/client/views/search/SearchBox.scss | 263 | ||||
| -rw-r--r-- | src/client/views/search/SearchBox.tsx | 418 | ||||
| -rw-r--r-- | src/client/views/search/SearchItem.tsx | 3 |
10 files changed, 725 insertions, 160 deletions
diff --git a/src/client/views/search/CheckBox.tsx b/src/client/views/search/CheckBox.tsx index a9d48219a..8c97d5dbc 100644 --- a/src/client/views/search/CheckBox.tsx +++ b/src/client/views/search/CheckBox.tsx @@ -17,8 +17,8 @@ interface CheckBoxProps { export class CheckBox extends React.Component<CheckBoxProps>{ // true = checked, false = unchecked @observable private _status: boolean; - @observable private uncheckTimeline: anime.AnimeTimelineInstance; - @observable private checkTimeline: anime.AnimeTimelineInstance; + // @observable private uncheckTimeline: anime.AnimeTimelineInstance; + // @observable private checkTimeline: anime.AnimeTimelineInstance; @observable private checkRef: any; @observable private _resetReaction?: IReactionDisposer; @@ -28,87 +28,87 @@ export class CheckBox extends React.Component<CheckBoxProps>{ this._status = this.props.originalStatus; this.checkRef = React.createRef(); - this.checkTimeline = anime.timeline({ - loop: false, - autoplay: false, - direction: "normal", - }); this.uncheckTimeline = anime.timeline({ - loop: false, - autoplay: false, - direction: "normal", - }); + // this.checkTimeline = anime.timeline({ + // loop: false, + // autoplay: false, + // direction: "normal", + // }); this.uncheckTimeline = anime.timeline({ + // loop: false, + // autoplay: false, + // direction: "normal", + // }); } - componentDidMount = () => { - this.uncheckTimeline.add({ - targets: this.checkRef.current, - easing: "easeInOutQuad", - duration: 500, - opacity: 0, - }); - this.checkTimeline.add({ - targets: this.checkRef.current, - easing: "easeInOutQuad", - duration: 500, - strokeDashoffset: [anime.setDashoffset, 0], - opacity: 1 - }); + // componentDidMount = () => { + // this.uncheckTimeline.add({ + // targets: this.checkRef.current, + // easing: "easeInOutQuad", + // duration: 500, + // opacity: 0, + // }); + // this.checkTimeline.add({ + // targets: this.checkRef.current, + // easing: "easeInOutQuad", + // duration: 500, + // strokeDashoffset: [anime.setDashoffset, 0], + // opacity: 1 + // }); - if (this.props.originalStatus) { - this.checkTimeline.play(); - this.checkTimeline.restart(); - } + // if (this.props.originalStatus) { + // this.checkTimeline.play(); + // this.checkTimeline.restart(); + // } - this._resetReaction = reaction( - () => this.props.parent._resetBoolean, - () => { - if (this.props.parent._resetBoolean) { - runInAction(() => { - this.reset(); - this.props.parent._resetCounter++; - if (this.props.parent._resetCounter === this.props.numCount) { - this.props.parent._resetCounter = 0; - this.props.parent._resetBoolean = false; - } - }); - } - }, - ); - } + // this._resetReaction = reaction( + // () => this.props.parent._resetBoolean, + // () => { + // if (this.props.parent._resetBoolean) { + // runInAction(() => { + // this.reset(); + // this.props.parent._resetCounter++; + // if (this.props.parent._resetCounter === this.props.numCount) { + // this.props.parent._resetCounter = 0; + // this.props.parent._resetBoolean = false; + // } + // }); + // } + // }, + // ); + // } - @action.bound - reset() { - if (this.props.default) { - if (!this._status) { - this._status = true; - this.checkTimeline.play(); - this.checkTimeline.restart(); - } - } - else { - if (this._status) { - this._status = false; - this.uncheckTimeline.play(); - this.uncheckTimeline.restart(); - } - } + // @action.bound + // reset() { + // if (this.props.default) { + // if (!this._status) { + // this._status = true; + // this.checkTimeline.play(); + // this.checkTimeline.restart(); + // } + // } + // else { + // if (this._status) { + // this._status = false; + // this.uncheckTimeline.play(); + // this.uncheckTimeline.restart(); + // } + // } - this.props.updateStatus(this.props.default); - } + // this.props.updateStatus(this.props.default); + // } @action.bound onClick = () => { - if (this._status) { - this.uncheckTimeline.play(); - this.uncheckTimeline.restart(); - } - else { - this.checkTimeline.play(); - this.checkTimeline.restart(); + // if (this._status) { + // this.uncheckTimeline.play(); + // this.uncheckTimeline.restart(); + // } + // else { + // this.checkTimeline.play(); + // this.checkTimeline.restart(); - } - this._status = !this._status; - this.props.updateStatus(this._status); + // } + // this._status = !this._status; + // this.props.updateStatus(this._status); } diff --git a/src/client/views/search/FilterBox.scss b/src/client/views/search/FilterBox.scss index ebb39460d..094ea9cc5 100644 --- a/src/client/views/search/FilterBox.scss +++ b/src/client/views/search/FilterBox.scss @@ -4,7 +4,6 @@ .filter-form { padding: 25px; width: 440px; - background: whitesmoke; position: relative; right: 1px; color: grey; @@ -12,9 +11,7 @@ display: inline-block; transform-origin: top; overflow: auto; - border-radius: 15px; - box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; - border: solid #BBBBBBBB 1px; + border-bottom: solid black 3px; .top-filter-header { @@ -124,7 +121,7 @@ max-width: 40px; flex: initial; - &.icon{ + &.icon { width: 40px; text-align: center; margin-bottom: 5px; @@ -150,7 +147,7 @@ transition: all 0.2s ease-in-out; } - &.icon:hover + .description { + &.icon:hover+.description { opacity: 1; } } diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index d4c9e67fb..1c05ff864 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -387,7 +387,7 @@ export class FilterBox extends React.Component { {/* {this.getActiveFilters()} */} </div> {this._filterOpen ? ( - <div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex" } : { display: "none" }}> + <div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex", background: "black" } : { display: "none" }}> <div className="top-filter-header" style={{ display: "flex", width: "100%" }}> <div id="header">Filter Search Results</div> <div style={{ marginLeft: "auto" }}></div> diff --git a/src/client/views/search/IconBar.scss b/src/client/views/search/IconBar.scss index 2555ad271..013dcd57e 100644 --- a/src/client/views/search/IconBar.scss +++ b/src/client/views/search/IconBar.scss @@ -2,10 +2,9 @@ .icon-bar { display: flex; + flex-wrap: wrap; justify-content: space-evenly; - align-items: center; - height: 35px; + height: auto; width: 100%; - flex-wrap: wrap; - margin-bottom: 10px; + flex-direction: row-reverse; }
\ No newline at end of file diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index cff397407..46c109934 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -9,7 +9,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { library } from '@fortawesome/fontawesome-svg-core'; import * as _ from "lodash"; import { IconButton } from './IconButton'; -import { FilterBox } from './FilterBox'; +import { DocumentType } from "../../documents/DocumentTypes"; + library.add(faSearch); library.add(faObjectGroup); @@ -25,6 +26,9 @@ library.add(faBan); @observer export class IconBar extends React.Component { + public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB]; + + @observable private _icons: string[] = this._allIcons; static Instance: IconBar; @@ -33,16 +37,22 @@ export class IconBar extends React.Component { @observable public _reset: number = 0; @observable public _select: number = 0; + @action.bound + updateIcon(newArray: string[]) { this._icons = newArray; } + + @action.bound + getIcons(): string[] { return this._icons; } + constructor(props: any) { super(props); IconBar.Instance = this; } @action.bound - getList(): string[] { return FilterBox.Instance.getIcons(); } + getList(): string[] { return this.getIcons(); } @action.bound - updateList(newList: string[]) { FilterBox.Instance.updateIcon(newList); } + updateList(newList: string[]) { this.updateIcon(newList); } @action.bound resetSelf = () => { @@ -53,13 +63,13 @@ export class IconBar extends React.Component { @action.bound selectAll = () => { this._selectAllClicked = true; - this.updateList(FilterBox.Instance._allIcons); + this.updateList(this._allIcons); } render() { return ( <div className="icon-bar"> - {FilterBox.Instance._allIcons.map((type: string) => + {this._allIcons.map((type: string) => <IconButton key={type.toString()} type={type} /> )} </div> diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss index 4a3107676..4ec03c7c9 100644 --- a/src/client/views/search/IconButton.scss +++ b/src/client/views/search/IconButton.scss @@ -5,7 +5,6 @@ flex-direction: column; align-items: center; width: 30px; - height: 60px; .type-icon { height: 30px; diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx index f01508141..4f94139d9 100644 --- a/src/client/views/search/IconButton.tsx +++ b/src/client/views/search/IconButton.tsx @@ -11,7 +11,6 @@ import '../globalCssVariables.scss'; import * as _ from "lodash"; import { IconBar } from './IconBar'; import { props } from 'bluebird'; -import { FilterBox } from './FilterBox'; import { Search } from '../../../server/Search'; import { gravity } from 'sharp'; @@ -34,7 +33,7 @@ interface IconButtonProps { @observer export class IconButton extends React.Component<IconButtonProps>{ - @observable private _isSelected: boolean = FilterBox.Instance.getIcons().indexOf(this.props.type) !== -1; + @observable private _isSelected: boolean = IconBar.Instance.getIcons().indexOf(this.props.type) !== -1; @observable private _hover = false; private _resetReaction?: IReactionDisposer; private _selectAllReaction?: IReactionDisposer; @@ -108,7 +107,7 @@ export class IconButton extends React.Component<IconButtonProps>{ @action.bound onClick = () => { - const newList: string[] = FilterBox.Instance.getIcons(); + const newList: string[] = IconBar.Instance.getIcons(); if (!this._isSelected) { this._isSelected = true; @@ -119,21 +118,24 @@ export class IconButton extends React.Component<IconButtonProps>{ _.pull(newList, this.props.type); } - FilterBox.Instance.updateIcon(newList); + IconBar.Instance.updateIcon(newList); } selected = { opacity: 1, - backgroundColor: "rgb(128, 128, 128)" + backgroundColor: "#121721", + //backgroundColor: "rgb(128, 128, 128)" }; notSelected = { opacity: 0.2, + backgroundColor: "#121721", }; hoverStyle = { opacity: 1, - backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent + backgroundColor: "rgb(128, 128, 128)" + //backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent }; @action.bound @@ -186,7 +188,7 @@ export class IconButton extends React.Component<IconButtonProps>{ > {this.getFA()} </div> - <div className="filter-description">{this.props.type}</div> + {/* <div className="filter-description">{this.props.type}</div> */} </div> ); } diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index f492ea773..f0223ca76 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -4,20 +4,21 @@ .searchBox-container { display: flex; flex-direction: column; - width:100%; - height:100%; + width: 100%; + height: 100%; position: absolute; font-size: 10px; line-height: 1; - overflow: hidden; + overflow: auto; + background: lightgrey, } + .searchBox-bar { height: 32px; display: flex; justify-content: flex-end; align-items: center; padding-left: 2px; - padding-right: 2px; .searchBox-barChild { @@ -33,8 +34,7 @@ -webkit-transition: width 0.4s; transition: width 0.4s; align-self: stretch; - margin-left: 2px; - margin-right: 2px + } .searchBox-input:focus { @@ -44,6 +44,10 @@ &.searchBox-filter { align-self: stretch; + button:hover{ + transform:scale(1.0); + background:"#121721"; + } } &.searchBox-submit { @@ -65,7 +69,7 @@ } .searchBox-results { - display:flex; + display: flex; flex-direction: column; top: 300px; display: flex; @@ -83,6 +87,249 @@ text-transform: uppercase; text-align: left; font-weight: bold; - margin-left: 28px; + } +} + +.filter-form { + position: relative; + background: #121721; + flex-direction: column; + transform-origin: top; + transition: height 0.3s ease, display 0.6s ease; + height:0px; + overflow:hidden; + + + .filter-header { + display: flex; + position: relative; + flex-wrap:wrap; + right: 1px; + color: grey; + flex-direction: row-reverse; + transform-origin: top; + justify-content: space-evenly; + margin-bottom: 5px; + overflow:hidden; + transition:height 0.3s ease-out; + + + + .filter-item { + position: relative; + border:1px solid grey; + border-radius: 16px; + + } + } + + .filter-body { + position: relative; + right: 1px; + color: grey; + transform-origin: top; + border-top: 0px; + //padding-top: 5px; + margin-left: 10px; + margin-right: 10px; + overflow:hidden; + transition:height 0.3s ease-out; + height:0px; + + } + .filter-key { + position: relative; + right: 1px; + color: grey; + transform-origin: top; + border-top: 0px; + //padding-top: 5px; + margin-left: 10px; + margin-right: 10px; + overflow:hidden; + transition:height 0.3s ease-out; + height:0px; + .filter-keybar { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + height: auto; + width: 100%; + flex-direction: row-reverse; + margin-top:5px; + + .filter-item { + position: relative; + border:1px solid grey; + border-radius: 16px; + + } + } + + + } +} + +// .top-filter-header { + +// #header { +// text-transform: uppercase; +// letter-spacing: 2px; +// font-size: 13; +// width: 80%; +// } + +// .close-icon { +// width: 20%; +// opacity: .6; +// position: relative; +// display: block; + +// .line { +// display: block; +// background: $alt-accent; +// width: 20; +// height: 3; +// position: absolute; +// right: 0; +// border-radius: ($height-line / 2); + +// &.line-1 { +// transform: rotate(45deg); +// top: 45%; +// } + +// &.line-2 { +// transform: rotate(-45deg); +// top: 45%; +// } +// } +// } + +// .close-icon:hover { +// opacity: 1; +// } + +// } + +// .filter-options { + +// .filter-div { +// margin-top: 10px; +// margin-bottom: 10px; +// display: inline-block; +// width: 100%; +// border-color: rgba(178, 206, 248, .2); // $darker-alt-accent +// border-top-style: solid; + +// .filter-header { +// display: flex; +// align-items: center; +// margin-bottom: 10px; +// letter-spacing: 2px; + +// .filter-title { +// font-size: 13; +// text-transform: uppercase; +// margin-top: 10px; +// margin-bottom: 10px; +// -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; +// } +// } + +// .filter-header:hover .filter-title { +// transform: scale(1.05); +// } + +// .filter-panel { +// max-height: 0px; +// width: 100%; +// overflow: hidden; +// opacity: 0; +// transform-origin: top; +// -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; +// text-align: center; +// } +// } +// } + +// .filter-buttons { +// border-color: rgba(178, 206, 248, .2); // $darker-alt-accent +// border-top-style: solid; +// padding-top: 10px; +// } + + +.active-filters { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + width: 100%; + margin-right: 30px; + position: relative; + + .active-icon { + max-width: 40px; + flex: initial; + + &.icon { + width: 40px; + text-align: center; + margin-bottom: 5px; + position: absolute; + } + + &.container { + display: flex; + flex-direction: column; + width: 40px; + } + + &.description { + text-align: center; + top: 40px; + position: absolute; + width: 40px; + font-size: 9px; + opacity: 0; + -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; + } + + &.icon:hover+.description { + opacity: 1; + } + } + + .col-icon { + height: 35px; + margin-left: 5px; + width: 35px; + background-color: black; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + + .save-filter, + .reset-filter, + .all-filter { + background-color: gray; + } + + .save-filter:hover, + .reset-filter:hover, + .all-filter:hover { + background-color: $darker-alt-accent; + } } }
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 586365f6e..67af661c9 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,37 +1,51 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable, runInAction, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as rp from 'request-promise'; import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { Cast, NumCast } from '../../../new_fields/Types'; +import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; import { SearchUtil } from '../../util/SearchUtil'; -import { FilterBox } from './FilterBox'; -import "./FilterBox.scss"; import "./SearchBox.scss"; import { SearchItem } from './SearchItem'; import { IconBar } from './IconBar'; +import { FieldView } from '../nodes/FieldView'; +import { DocumentType } from "../../documents/DocumentTypes"; +import { DocumentView } from '../nodes/DocumentView'; +import { SelectionManager } from '../../util/SelectionManager'; library.add(faTimes); +export interface SearchProps { + id: string; + searchQuery?: string; + filterQquery?: string; +} + +export enum Keys { + TITLE = "title", + AUTHOR = "author", + DATA = "data" +} + @observer -export class SearchBox extends React.Component { +export class SearchBox extends React.Component<SearchProps> { @observable private _searchString: string = ""; @observable private _resultsOpen: boolean = false; @observable private _searchbarOpen: boolean = false; @observable private _results: [Doc, string[], string[]][] = []; - private _resultsSet = new Map<Doc, number>(); @observable private _openNoResults: boolean = false; @observable private _visibleElements: JSX.Element[] = []; - private resultsRef = React.createRef<HTMLDivElement>(); + private _resultsSet = new Map<Doc, number>(); + private _resultsRef = React.createRef<HTMLDivElement>(); public inputRef = React.createRef<HTMLInputElement>(); private _isSearch: ("search" | "placeholder" | undefined)[] = []; @@ -42,10 +56,18 @@ export class SearchBox extends React.Component { private _maxSearchIndex: number = 0; private _curRequest?: Promise<any> = undefined; + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + + + //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 + @observable private _basicWordStatus: boolean = false; + @observable private _nodeStatus: boolean = false; + @observable private _keyStatus: boolean = false; + constructor(props: any) { super(props); - SearchBox.Instance = this; this.resultsScrolled = this.resultsScrolled.bind(this); } @@ -53,21 +75,21 @@ export class SearchBox extends React.Component { componentDidMount = () => { if (this.inputRef.current) { this.inputRef.current.focus(); + runInAction(() => this._searchbarOpen = true); + } + if (this.props.searchQuery && this.props.filterQquery) { + console.log(this.props.searchQuery); + const sq = this.props.searchQuery; runInAction(() => { - this._searchbarOpen = true; + this._searchString = sq; + this.submitSearch(); }); } } + @action - getViews = async (doc: Doc) => { - const results = await SearchUtil.GetViewsOfDocument(doc); - let toReturn: Doc[] = []; - await runInAction(() => { - toReturn = results; - }); - return toReturn; - } + getViews = (doc: Doc) => SearchUtil.GetViewsOfDocument(doc) @action.bound onChange(e: React.ChangeEvent<HTMLInputElement>) { @@ -106,33 +128,186 @@ export class SearchBox extends React.Component { } } + public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, 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 + @observable private _filterOpen: boolean = false; + //if icons = all icons, then no icon filter is applied + @observable private _icons: string[] = this._allIcons; + //if all of these are true, no key filter is applied + @observable private _titleFieldStatus: boolean = true; + @observable private _authorFieldStatus: boolean = true; + //this also serves as an indicator if the collection status filter is applied + @observable public _deletedDocsStatus: boolean = false; + @observable private _collectionStatus = false; + + + getFinalQuery(query: string): string { + //alters the query so it looks in the correct fields + //if this is true, then not all of the field boxes are checked + //TODO: data + if (this.fieldFiltersApplied) { + query = this.applyBasicFieldFilters(query); + query = query.replace(/\s+/g, ' ').trim(); + } + + //alters the query based on if all words or any words are required + //if this._wordstatus is false, all words are required and a + is added before each + if (!this._basicWordStatus) { + query = this.basicRequireWords(query); + query = query.replace(/\s+/g, ' ').trim(); + } + + //if should be searched in a specific collection + if (this._collectionStatus) { + query = this.addCollectionFilter(query); + query = query.replace(/\s+/g, ' ').trim(); + } + return query; + } + + basicRequireWords(query: string): string { + const oldWords = query.split(" "); + const newWords: string[] = []; + oldWords.forEach(word => { + const newWrd = "+" + word; + newWords.push(newWrd); + }); + query = newWords.join(" "); + + return query; + } + + @action + filterDocsByType(docs: Doc[]) { + if (this._icons.length === this._allIcons.length) { + return docs; + } + const finalDocs: Doc[] = []; + docs.forEach(doc => { + const layoutresult = Cast(doc.type, "string"); + if (layoutresult && this._icons.includes(layoutresult)) { + finalDocs.push(doc); + } + }); + return finalDocs; + } + + addCollectionFilter(query: string): string { + const collections: Doc[] = this.getCurCollections(); + const oldWords = query.split(" "); + + const collectionString: string[] = []; + collections.forEach(doc => { + const proto = doc.proto; + const protoId = (proto || doc)[Id]; + const colString: string = "{!join from=data_l to=id}id:" + protoId + " "; + collectionString.push(colString); + }); + + let finalColString = collectionString.join(" "); + finalColString = finalColString.trim(); + return "+(" + finalColString + ")" + query; + } + + get filterTypes() { + return this._icons.length === this._allIcons.length ? undefined : this._icons; + } + + @action.bound + updateIcon(newArray: string[]) { this._icons = newArray; } + + @action.bound + getIcons(): string[] { return this._icons; } + + //TODO: basically all of this + //gets all of the collections of all the docviews that are selected + //if a collection is the only thing selected, search only in that collection (not its container) + getCurCollections(): Doc[] { + const selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments(); + const collections: Doc[] = []; + + selectedDocs.forEach(async element => { + const layout: string = StrCast(element.props.Document.layout); + //checks if selected view (element) is a collection. if it is, adds to list to search through + if (layout.indexOf("Collection") > -1) { + //makes sure collections aren't added more than once + if (!collections.includes(element.props.Document)) { + collections.push(element.props.Document); + } + } + //makes sure collections aren't added more than once + if (element.props.ContainingCollectionDoc && !collections.includes(element.props.ContainingCollectionDoc)) { + collections.push(element.props.ContainingCollectionDoc); + } + }); + + return collections; + } + + + applyBasicFieldFilters(query: string) { + let finalQuery = ""; + + if (this._titleFieldStatus) { + finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TITLE); + } + if (this._authorFieldStatus) { + finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR); + } + if (this._deletedDocsStatus) { + finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA); + } + return finalQuery; + } + + basicFieldFilters(query: string, type: string): string { + const oldWords = query.split(" "); + let mod = ""; + + if (type === Keys.AUTHOR) { + mod = " author_t:"; + } if (type === Keys.DATA) { + //TODO + } if (type === Keys.TITLE) { + mod = " title_t:"; + } + + const newWords: string[] = []; + oldWords.forEach(word => { + const newWrd = mod + word; + newWords.push(newWrd); + }); + + query = newWords.join(" "); + + return query; + } + + get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); } + + @action submitSearch = async () => { - let query = this._searchString; - query = FilterBox.Instance.getFinalQuery(query); + const query = this._searchString; + this.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 === "") { - return; - } - else { + if (query !== "") { this._endIndex = 12; this._maxSearchIndex = 0; this._numTotalResults = -1; await this.getResults(query); - } - runInAction(() => { - this._resultsOpen = true; - this._searchbarOpen = true; - this._openNoResults = true; - this.resultsScrolled(); - }); + runInAction(() => { + this._resultsOpen = true; + this._searchbarOpen = true; + this._openNoResults = true; + this.resultsScrolled(); + }); + } } getAllResults = async (query: string) => { @@ -140,11 +315,14 @@ export class SearchBox extends React.Component { } private get filterQuery() { - const types = FilterBox.Instance.filterTypes; - const includeDeleted = FilterBox.Instance.getDataStatus(); - return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}" OR type_t:"extension"`).join(" ")})` : ""); + const types = this.filterTypes; + const includeDeleted = this.getDataStatus() ? "" : " AND NOT deleted_b:true"; + const includeIcons = this.getDataStatus() ? "" : " AND NOT type_t:fonticonbox"; + return "NOT baseProto_b:true" + includeDeleted + includeIcons + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); } + getDataStatus() { return this._deletedDocsStatus; } + private NumResults = 25; private lockPromise?: Promise<void>; @@ -155,7 +333,6 @@ export class SearchBox extends React.Component { 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: this.NumResults, 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; @@ -168,9 +345,9 @@ export class SearchBox extends React.Component { 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]); - const filteredDocs = FilterBox.Instance.filterDocsByType(docs); + const filteredDocs = this.filterDocsByType(docs); runInAction(() => { - // this._results.push(...filteredDocs); + //this._results.push(...filteredDocs); filteredDocs.forEach(doc => { const index = this._resultsSet.get(doc); const highlight = highlights[doc[Id]]; @@ -200,9 +377,8 @@ export class SearchBox extends React.Component { collectionRef = React.createRef<HTMLSpanElement>(); startDragCollection = async () => { - const res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString)); - const filtered = FilterBox.Instance.filterDocsByType(res.docs); - // console.log(this._results) + const res = await this.getAllResults(this.getFinalQuery(this._searchString)); + const filtered = this.filterDocsByType(res.docs); const docs = filtered.map(doc => { const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); if (isProto) { @@ -234,22 +410,19 @@ export class SearchBox extends React.Component { y += 300; } } - return Docs.Create.TreeDocument(docs, { _width: 200, _height: 400, title: `Search Docs: "${this._searchString}"` }); + return Docs.Create.QueryDocument({ _autoHeight: true, title: this._searchString, filterQuery: this.filterQuery, searchQuery: this._searchString }); } @action.bound openSearch(e: React.SyntheticEvent) { e.stopPropagation(); this._openNoResults = false; - FilterBox.Instance.closeFilter(); this._resultsOpen = true; this._searchbarOpen = true; - FilterBox.Instance._pointerTime = e.timeStamp; } @action.bound closeSearch = () => { - FilterBox.Instance.closeFilter(); this.closeResults(); this._searchbarOpen = false; } @@ -267,11 +440,11 @@ export class SearchBox extends React.Component { @action resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { - if (!this.resultsRef.current) return; - const scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0; + if (!this._resultsRef.current) return; + const scrollY = e ? e.currentTarget.scrollTop : this._resultsRef.current ? this._resultsRef.current.scrollTop : 0; const itemHght = 53; const startIndex = Math.floor(Math.max(0, scrollY / itemHght)); - const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current.getBoundingClientRect().height / itemHght))); + const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this._resultsRef.current.getBoundingClientRect().height / itemHght))); this._endIndex = endIndex === -1 ? 12 : endIndex; @@ -337,9 +510,131 @@ export class SearchBox extends React.Component { @computed get resultHeight() { return this._numTotalResults * 70; } + //if true, any keywords can be used. if false, all keywords are required. + @action.bound + handleWordQueryChange = () => { + this._basicWordStatus = !this._basicWordStatus; + } + + @action.bound + handleNodeChange = () => { + this._nodeStatus = !this._nodeStatus; + if (this._nodeStatus) { + this.expandSection(`node${this.props.id}`); + } + else { + this.collapseSection(`node${this.props.id}`); + } + } + + @action.bound + handleKeyChange = () => { + this._keyStatus = !this._keyStatus; + if (this._keyStatus) { + this.expandSection(`key${this.props.id}`); + } + else { + this.collapseSection(`key${this.props.id}`); + } + } + + @action.bound + handleFilterChange = () => { + this._filterOpen = !this._filterOpen; + if (this._filterOpen) { + this.expandSection(`filterhead${this.props.id}`); + document.getElementById(`filterhead${this.props.id}`)!.style.padding = "5"; + } + else { + this.collapseSection(`filterhead${this.props.id}`); + + + } + } + + @computed + get menuHeight() { + return document.getElementById("hi")?.clientHeight; + } + + + collapseSection(thing: string) { + const id = this.props.id; + const element = document.getElementById(thing)!; + // get the height of the element's inner content, regardless of its actual size + const sectionHeight = element.scrollHeight; + + // temporarily disable all css transitions + const elementTransition = element.style.transition; + element.style.transition = ''; + + // on the next frame (as soon as the previous style change has taken effect), + // explicitly set the element's height to its current pixel height, so we + // aren't transitioning out of 'auto' + requestAnimationFrame(function () { + element.style.height = sectionHeight + 'px'; + element.style.transition = elementTransition; + + // on the next frame (as soon as the previous style change has taken effect), + // have the element transition to height: 0 + requestAnimationFrame(function () { + element.style.height = 0 + 'px'; + thing === `filterhead${id}` ? document.getElementById(`filterhead${id}`)!.style.padding = "0" : null; + }); + }); + + // mark the section as "currently collapsed" + element.setAttribute('data-collapsed', 'true'); + } + + expandSection(thing: string) { + console.log("expand"); + const element = document.getElementById(thing)!; + // get the height of the element's inner content, regardless of its actual size + const sectionHeight = element.scrollHeight; + + // have the element transition to the height of its inner content + element.style.height = sectionHeight + 'px'; + + // when the next css transition finishes (which should be the one we just triggered) + element.addEventListener('transitionend', function handler(e) { + // remove this event listener so it only gets triggered once + console.log("autoset"); + element.removeEventListener('transitionend', handler); + + // remove "height" from the element's inline styles, so it can return to its initial value + element.style.height = "auto"; + //element.style.height = undefined; + }); + + // mark the section as "currently not collapsed" + element.setAttribute('data-collapsed', 'false'); + + } + + autoset(thing: string) { + const element = document.getElementById(thing)!; + console.log("autoset"); + element.removeEventListener('transitionend', function (e) { }); + + // remove "height" from the element's inline styles, so it can return to its initial value + element.style.height = "auto"; + //element.style.height = undefined; + } + + @action.bound + updateTitleStatus() { this._titleFieldStatus = !this._titleFieldStatus; } + + @action.bound + updateAuthorStatus() { this._authorFieldStatus = !this._authorFieldStatus; } + + @action.bound + updateDataStatus() { this._deletedDocsStatus = !this._deletedDocsStatus; } + render() { + return ( - <div className="searchBox-container" onPointerDown={e => { e.stopPropagation(); e.preventDefault(); }}> + <div className="searchBox-container"> <div className="searchBox-bar"> <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, () => this._searchString ? this.startDragCollection() : undefined)} ref={this.collectionRef} title="Drag Results as Collection"> <FontAwesomeIcon icon="object-group" size="lg" /> @@ -347,16 +642,31 @@ export class SearchBox extends React.Component { <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} style={{ width: this._searchbarOpen ? "500px" : "100px" }} /> - <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={() => { }} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button> + <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={() => this.handleFilterChange()}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button> </div> - <div className="searchBox-quickFilter" onPointerDown={this.openSearch}> - <div className="filter-panel"><IconBar /></div> + + <div id={`filterhead${this.props.id}`} className="filter-form" > + <div id={`filterhead2${this.props.id}`} className="filter-header" style={this._filterOpen ? {} : {}}> + <button className="filter-item" style={this._basicWordStatus ? { background: "#aaaaa3", } : {}} onClick={this.handleWordQueryChange}>Keywords</button> + <button className="filter-item" style={this._keyStatus ? { background: "#aaaaa3" } : {}} onClick={this.handleKeyChange}>Keys</button> + <button className="filter-item" style={this._nodeStatus ? { background: "#aaaaa3" } : {}} onClick={this.handleNodeChange}>Nodes</button> + </div> + <div id={`node${this.props.id}`} className="filter-body" style={this._nodeStatus ? { borderTop: "grey 1px solid" } : { borderTop: "0px" }}> + <IconBar /> + </div> + <div className="filter-key" id={`key${this.props.id}`} style={this._keyStatus ? { borderTop: "grey 1px solid" } : { borderTop: "0px" }}> + <div className="filter-keybar"> + <button className="filter-item" style={this._titleFieldStatus ? { background: "#aaaaa3", } : {}} onClick={this.updateTitleStatus}>Title</button> + <button className="filter-item" style={this._deletedDocsStatus ? { background: "#aaaaa3", } : {}} onClick={this.updateDataStatus}>Deleted Docs</button> + <button className="filter-item" style={this._authorFieldStatus ? { background: "#aaaaa3", } : {}} onClick={this.updateAuthorStatus}>Author</button> + </div> + </div> </div> <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ display: this._resultsOpen ? "flex" : "none", height: this.resFull ? "auto" : this.resultHeight, overflow: "visibile" // this.resFull ? "auto" : "visible" - }} ref={this.resultsRef}> + }} ref={this._resultsRef}> {this._visibleElements} </div> </div> diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 63cef5101..0d77026ad 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; import { Doc } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, emptyPath, returnFalse, Utils } from "../../../Utils"; +import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, SetupDrag } from "../../util/DragManager"; @@ -158,6 +158,7 @@ export class SearchItem extends React.Component<SearchItemProps> { <ContentFittingDocumentView Document={this.props.doc} LibraryPath={emptyPath} + rootSelected={returnFalse} fitToBox={StrCast(this.props.doc.type).indexOf(DocumentType.COL) !== -1} addDocument={returnFalse} removeDocument={returnFalse} |
