diff options
42 files changed, 2263 insertions, 403 deletions
diff --git a/deploy/index.html b/deploy/index.html index 532b995f8..f4a019b71 100644 --- a/deploy/index.html +++ b/deploy/index.html @@ -6,6 +6,7 @@ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script> + <script src="https://rawgithub.com/juliangarnier/anime/master/anime.min.js"></script> </head> <body> diff --git a/package.json b/package.json index 804cdd262..dd8dc2149 100644 --- a/package.json +++ b/package.json @@ -50,10 +50,12 @@ "@hig/theme-context": "^2.1.3", "@hig/theme-data": "^2.3.3", "@trendmicro/react-dropdown": "^1.3.0", + "@types/animejs": "^2.0.2", "@types/async": "^2.4.1", "@types/bcrypt-nodejs": "0.0.30", "@types/bluebird": "^3.5.25", "@types/body-parser": "^1.17.0", + "@types/classnames": "^2.2.8", "@types/connect-flash": "0.0.34", "@types/cookie-parser": "^1.4.1", "@types/cookie-session": "^2.0.36", @@ -65,6 +67,7 @@ "@types/express-validator": "^3.0.0", "@types/formidable": "^1.0.31", "@types/jquery": "^3.3.29", + "@types/jquery-awesome-cursor": "^0.3.0", "@types/jsonwebtoken": "^8.3.2", "@types/lodash": "^4.14.121", "@types/mobile-detect": "^1.3.4", @@ -128,6 +131,7 @@ "image-data-uri": "^2.0.0", "image-size": "^0.7.4", "imagesloaded": "^4.1.4", + "jquery-awesome-cursor": "^0.3.1", "jsonwebtoken": "^8.5.0", "jsx-to-string": "^1.4.0", "lodash": "^4.17.11", @@ -161,6 +165,7 @@ "pug": "^2.0.3", "raw-loader": "^1.0.0", "react": "^16.8.4", + "react-anime": "^2.2.0", "react-bootstrap": "^1.0.0-beta.5", "react-bootstrap-dropdown-menu": "^1.1.15", "react-color": "^2.17.0", diff --git a/solr/conf/schema.xml b/solr/conf/schema.xml index 30e8daa65..8610786af 100644 --- a/solr/conf/schema.xml +++ b/solr/conf/schema.xml @@ -8,7 +8,7 @@ <filter class="solr.StopFilterFactory" words="stopwords.txt"/> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.PorterStemFilterFactory"/> - <filter class="solr.NGramFilterFactory" minGramSize="3" maxGramSize="12"/> + <filter class="solr.NGramFilterFactory" minGramSize="2" maxGramSize="12"/> </analyzer> <analyzer type="query"> <tokenizer class="solr.StandardTokenizerFactory"/> diff --git a/solr/conf/solrconfig.xml b/solr/conf/solrconfig.xml index 90eff5363..0d8792749 100644 --- a/solr/conf/solrconfig.xml +++ b/solr/conf/solrconfig.xml @@ -695,7 +695,7 @@ --> <lst name="defaults"> <str name="echoParams">explicit</str> - <int name="rows">10</int> + <int name="rows">10000000</int> <str name="df">text</str> <!-- Default search field <str name="df">text</str> diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index cc67c68e9..6def4d135 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -37,9 +37,25 @@ import { RouteStore } from "../../server/RouteStore"; var requestImageSize = require('../util/request-image-size'); var path = require('path'); +export enum DocTypes { + NONE = "none", + IMG = "image", + HIST = "histogram", + ICON = "icon", + TEXT = "text", + PDF = "pdf", + WEB = "web", + COL = "collection", + KVP = "kvp", + VID = "video", + AUDIO = "audio", + LINK = "link" +} + export interface DocumentOptions { x?: number; y?: number; + type?: string; ink?: InkField; width?: number; height?: number; @@ -71,7 +87,8 @@ export namespace DocUtils { let protoSrc = source.proto ? source.proto : source; let protoTarg = target.proto ? target.proto : target; UndoManager.RunInBatch(() => { - let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); + let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1}); + linkDoc.type = DocTypes.LINK; let linkDocProto = Doc.GetProto(linkDoc); linkDocProto.title = title === "" ? source.title + " to " + target.title : title; linkDocProto.linkDescription = description; @@ -92,8 +109,6 @@ export namespace DocUtils { return linkDoc; }, "make link"); } - - } export namespace Docs { @@ -148,54 +163,54 @@ export namespace Docs { function CreateImagePrototype(): Doc { let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("annotations"), - { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: ImageBox.LayoutString(), curPage: 0 }); + { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: ImageBox.LayoutString(), curPage: 0, type: DocTypes.IMG }); return imageProto; } function CreateHistogramPrototype(): Doc { let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"), - { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString() }); + { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString(), type: DocTypes.HIST }); return histoProto; } function CreateIconPrototype(): Doc { let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(), - { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) }); + { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), type: DocTypes.ICON }); return iconProto; } function CreateTextPrototype(): Doc { let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), - { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb" }); + { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb", type: DocTypes.TEXT }); return textProto; } function CreatePdfPrototype(): Doc { let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"), - { x: 0, y: 0, width: 300, height: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1 }); + { x: 0, y: 0, width: 300, height: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1, type: DocTypes.PDF }); return pdfProto; } function CreateWebPrototype(): Doc { let webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(), - { x: 0, y: 0, width: 300, height: 300 }); + { x: 0, y: 0, width: 300, height: 300, type: DocTypes.WEB }); return webProto; } function CreateCollectionPrototype(): Doc { let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"), - { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }); + { panX: 0, panY: 0, scale: 1, width: 500, height: 500, type: DocTypes.COL }); return collProto; } function CreateKVPPrototype(): Doc { let kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(), - { x: 0, y: 0, width: 300, height: 150 }); + { x: 0, y: 0, width: 300, height: 150, type: DocTypes.KVP }); return kvpProto; } function CreateVideoPrototype(): Doc { let videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("annotations"), - { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: VideoBox.LayoutString(), curPage: 0 }); + { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: VideoBox.LayoutString(), curPage: 0, type: DocTypes.VID }); return videoProto; } function CreateAudioPrototype(): Doc { let audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(), - { x: 0, y: 0, width: 300, height: 150 }); + { x: 0, y: 0, width: 300, height: 150, type: DocTypes.AUDIO }); return audioProto; } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 16b88b027..bc9724b98 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -30,6 +30,30 @@ export class DocumentManager { // this.DocumentViews = new Array<DocumentView>(); } + //gets all views + public getDocumentViewsById(id: string) { + let toReturn: DocumentView[] = []; + DocumentManager.Instance.DocumentViews.map(view => { + if (view.props.Document[Id] === id) { + toReturn.push(view); + } + }); + if (toReturn.length === 0) { + DocumentManager.Instance.DocumentViews.map(view => { + let doc = view.props.Document.proto; + if (doc && doc[Id]) { + if(doc[Id] === id) + {toReturn.push(view);} + } + }); + } + return toReturn; + } + + public getAllDocumentViews(doc: Doc){ + return this.getDocumentViewsById(doc[Id]); + } + public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null { let toReturn: DocumentView | null = null; diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 28ec8ca14..27d27a3b8 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -23,4 +23,8 @@ export namespace SearchUtil { return Search(`proto_i:"${protoId}"`, true); // return Search(`{!join from=id to=proto_i}id:${protoId}`, true); } + + export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> { + return Search(`proto_i:"${doc[Id]}"`, true); + } }
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 50dabba22..b48ece30f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -11,7 +11,7 @@ import * as request from 'request'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils'; -import { Docs } from '../documents/Documents'; +import { Docs, DocTypes } from '../documents/Documents'; import { SetupDrag, DragManager } from '../util/DragManager'; import { Transform } from '../util/Transform'; import { UndoManager } from '../util/UndoManager'; @@ -24,10 +24,10 @@ import "./Main.scss"; import { MainOverlayTextBox } from './MainOverlayTextBox'; import { DocumentView } from './nodes/DocumentView'; import { PreviewCursor } from './PreviewCursor'; -import { SearchBox } from './SearchBox'; +import { FilterBox } from './search/FilterBox'; import { SelectionManager } from '../util/SelectionManager'; import { FieldResult, Field, Doc, Opt, DocListCast } from '../../new_fields/Doc'; -import { Cast, FieldValue, StrCast } from '../../new_fields/Types'; +import { Cast, FieldValue, StrCast, PromiseValue } from '../../new_fields/Types'; import { DocServer } from '../DocServer'; import { listSpec } from '../../new_fields/Schema'; import { Id } from '../../new_fields/FieldSymbols'; @@ -36,6 +36,7 @@ import { CollectionBaseView } from './collections/CollectionBaseView'; import { List } from '../../new_fields/List'; import PDFMenu from './pdf/PDFMenu'; import { InkTool } from '../../new_fields/InkField'; +import * as _ from "lodash"; @observer export class MainView extends React.Component { @@ -46,6 +47,13 @@ export class MainView extends React.Component { @computed private get mainContainer(): Opt<Doc> { return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc)); } + @computed private get mainFreeform(): Opt<Doc> { + let docs = DocListCast(this.mainContainer!.data); + return (docs && docs.length > 1) ? docs[1] : undefined; + } + private globalDisplayFlags = observable({ + jumpToVisible: false + }); private set mainContainer(doc: Opt<Doc>) { if (doc) { if (!("presentationView" in doc)) { @@ -55,6 +63,15 @@ export class MainView extends React.Component { } } + componentWillMount() { + document.removeEventListener("keydown", this.globalKeyHandler); + document.addEventListener("keydown", this.globalKeyHandler); + } + + componentWillUnMount() { + document.removeEventListener("keydown", this.globalKeyHandler); + } + constructor(props: Readonly<{}>) { super(props); MainView.Instance = this; @@ -326,7 +343,7 @@ export class MainView extends React.Component { </div> </div> </div >, - this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div> : null, + this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null, <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}> <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div> ]; @@ -339,6 +356,39 @@ export class MainView extends React.Component { this.isSearchVisible = !this.isSearchVisible; } + @action + globalKeyHandler = (e: KeyboardEvent) => { + if (e.key === "Control" || !e.ctrlKey) return; + + if(e.key === "v") return; + if(e.key === "c") return; + + e.preventDefault(); + e.stopPropagation(); + + switch (e.key) { + case "ArrowRight": + if (this.mainFreeform) { + CollectionDockingView.Instance.AddRightSplit(this.mainFreeform!); + } + break; + case "ArrowLeft": + if (this.mainFreeform) { + CollectionDockingView.Instance.CloseRightSplit(this.mainFreeform!); + } + break; + case "o": + this.globalDisplayFlags.jumpToVisible = true; + break; + case "escape": + _.mapValues(this.globalDisplayFlags, () => false); + break; + case "f": + this.isSearchVisible = !this.isSearchVisible; + } + } + + render() { return ( <div id="main-div"> diff --git a/src/client/views/SearchBox.scss b/src/client/views/SearchBox.scss deleted file mode 100644 index b38e6091d..000000000 --- a/src/client/views/SearchBox.scss +++ /dev/null @@ -1,102 +0,0 @@ -@import "globalCssVariables"; - -.searchBox-bar { - height: 32px; - display: flex; - justify-content: flex-end; - align-items: center; - padding-left: 2px; - padding-right: 2px; - - .searchBox-input { - width: 130px; - -webkit-transition: width 0.4s; - transition: width 0.4s; - align-self: stretch; - } - - .searchBox-input:focus { - width: 500px; - outline: 3px solid lightblue; - } - - .searchBox-barChild { - flex: 0 1 auto; - margin-left: 2px; - margin-right: 2px; - } - - .searchBox-filter { - align-self: stretch; - } - - .searchBox-submit { - color: $dark-color; - } - - .searchBox-submit:hover { - color: $main-accent; - transform: scale(1.05); - cursor: pointer; - } -} - -.searchBox-results { - margin-left: 27px; //Is there a better way to do this? -} - -.filter-form { - background: $dark-color; - height: 400px; - width: 400px; - position: relative; - right: 1px; - color: $light-color; - padding: 10px; - flex-direction: column; -} - -#header { - text-transform: uppercase; - letter-spacing: 2px; - font-size: 100%; - height: 40px; -} - -#option { - height: 20px; -} - -.searchBox-results { - top: 300px; - display: flex; - flex-direction: column; - - .search-item { - width: 500px; - height: 50px; - background: $light-color-secondary; - display: flex; - justify-content: space-between; - align-items: center; - transition: all 0.1s; - border-width: 0.11px; - border-style: none; - border-color: $intermediate-color; - border-bottom-style: solid; - padding: 10px; - white-space: nowrap; - font-size: 13px; - } - - .search-item:hover { - transition: all 0.1s; - background: $lighter-alt-accent; - } - - .search-title { - text-transform: uppercase; - text-align: left; - width: 8vw; - } -}
\ No newline at end of file diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx deleted file mode 100644 index 63d2065e2..000000000 --- a/src/client/views/SearchBox.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable, action, runInAction } from 'mobx'; -import { Utils } from '../../Utils'; -import { MessageStore } from '../../server/Message'; -import "./SearchBox.scss"; -import { faSearch, faObjectGroup } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { library } from '@fortawesome/fontawesome-svg-core'; -// const app = express(); -// import * as express from 'express'; -import { Search } from '../../server/Search'; -import * as rp from 'request-promise'; -import { SearchItem } from './SearchItem'; -import { isString } from 'util'; -import { constant } from 'async'; -import { DocServer } from '../DocServer'; -import { Doc } from '../../new_fields/Doc'; -import { Id } from '../../new_fields/FieldSymbols'; -import { DocumentManager } from '../util/DocumentManager'; -import { SetupDrag } from '../util/DragManager'; -import { Docs } from '../documents/Documents'; -import { RouteStore } from '../../server/RouteStore'; -import { NumCast } from '../../new_fields/Types'; - -library.add(faSearch); -library.add(faObjectGroup); - -@observer -export class SearchBox extends React.Component { - @observable - searchString: string = ""; - - @observable private _open: boolean = false; - @observable private _resultsOpen: boolean = false; - - @observable - private _results: Doc[] = []; - - @action.bound - onChange(e: React.ChangeEvent<HTMLInputElement>) { - this.searchString = e.target.value; - } - - @action - submitSearch = async () => { - let query = this.searchString; - //gets json result into a list of documents that can be used - const results = await this.getResults(query); - - runInAction(() => { - this._resultsOpen = true; - this._results = results; - }); - } - - @action - getResults = async (query: string) => { - let response = await rp.get(DocServer.prepend('/search'), { - qs: { - query - } - }); - let res: string[] = JSON.parse(response); - const fields = await DocServer.GetRefFields(res); - const docs: Doc[] = []; - for (const id of res) { - const field = fields[id]; - if (field instanceof Doc) { - docs.push(field); - } - } - return docs; - } - public static async convertDataUri(imageUri: string, returnedFilename: string) { - try { - let posting = DocServer.prepend(RouteStore.dataUriToImage); - const returnedUri = await rp.post(posting, { - body: { - uri: imageUri, - name: returnedFilename - }, - json: true, - }); - return returnedUri; - - } catch (e) { - console.log(e); - } - } - - @action - handleClickFilter = (e: Event): void => { - var className = (e.target as any).className; - var id = (e.target as any).id; - if (className !== "filter-button" && className !== "filter-form") { - this._open = false; - } - - } - - @action - handleClickResults = (e: Event): void => { - var className = (e.target as any).className; - var id = (e.target as any).id; - if (id !== "result") { - this._resultsOpen = false; - this._results = []; - } - - } - - componentWillMount() { - document.addEventListener('mousedown', this.handleClickFilter, false); - document.addEventListener('mousedown', this.handleClickResults, false); - } - - componentWillUnmount() { - document.removeEventListener('mousedown', this.handleClickFilter, false); - document.removeEventListener('mousedown', this.handleClickResults, false); - } - - @action - toggleFilterDisplay = () => { - this._open = !this._open; - } - - enter = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter") { - this.submitSearch(); - } - } - - collectionRef = React.createRef<HTMLSpanElement>(); - startDragCollection = async () => { - const results = await this.getResults(this.searchString); - const docs = results.map(doc => { - const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); - if (isProto) { - return Doc.MakeDelegate(doc); - } else { - return Doc.MakeAlias(doc); - } - }); - let x = 0; - let y = 0; - for (const doc of docs) { - doc.x = x; - doc.y = y; - const size = 200; - const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1); - if (aspect > 1) { - doc.height = size; - doc.width = size / aspect; - } else if (aspect > 0) { - doc.width = size; - doc.height = size * aspect; - } else { - doc.width = size; - doc.height = size; - } - doc.zoomBasis = 1; - x += 250; - if (x > 1000) { - x = 0; - y += 300; - } - } - return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` }); - } - - // Useful queries: - // Delegates of a document: {!join from=id to=proto_i}id:{protoId} - // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId} - render() { - return ( - <div> - <div className="searchBox-container"> - <div className="searchBox-bar"> - <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}> - <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" /> - </span> - <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..." - className="searchBox-barChild searchBox-input" onKeyPress={this.enter} - style={{ width: this._resultsOpen ? "500px" : undefined }} /> - {/* <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button> */} - {/* <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" /> */} - </div> - {this._resultsOpen ? ( - <div className="searchBox-results"> - {this._results.map(result => <SearchItem doc={result} key={result[Id]} />)} - </div> - ) : null} - </div> - {this._open ? ( - <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}> - <div className="filter-form" id="header">Filter Search Results</div> - <div className="filter-form" id="option"> - filter by collection, key, type of node - </div> - - </div> - ) : null} - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx deleted file mode 100644 index 13e4b88f7..000000000 --- a/src/client/views/SearchItem.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React = require("react"); -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Doc } from "../../new_fields/Doc"; -import { DocumentManager } from "../util/DocumentManager"; -import { SetupDrag } from "../util/DragManager"; - - -export interface SearchProps { - doc: Doc; -} - -library.add(faCaretUp); -library.add(faObjectGroup); -library.add(faStickyNote); -library.add(faFilePdf); -library.add(faFilm); - -export class SearchItem extends React.Component<SearchProps> { - - onClick = () => { - DocumentManager.Instance.jumpToDocument(this.props.doc, false); - } - - //needs help - // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; } - - - public static DocumentIcon(layout: string) { - let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : - layout.indexOf("ImageBox") !== -1 ? faImage : - layout.indexOf("Formatted") !== -1 ? faStickyNote : - layout.indexOf("Video") !== -1 ? faFilm : - layout.indexOf("Collection") !== -1 ? faObjectGroup : - faCaretUp; - return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; - } - onPointerEnter = (e: React.PointerEvent) => { - this.props.doc.libraryBrush = true; - Doc.SetOnPrototype(this.props.doc, "protoBrush", true); - } - onPointerLeave = (e: React.PointerEvent) => { - this.props.doc.libraryBrush = false; - Doc.SetOnPrototype(this.props.doc, "protoBrush", false); - } - - collectionRef = React.createRef<HTMLDivElement>(); - startDocDrag = () => { - let doc = this.props.doc; - const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); - if (isProto) { - return Doc.MakeDelegate(doc); - } else { - return Doc.MakeAlias(doc); - } - } - render() { - return ( - <div className="search-item" ref={this.collectionRef} id="result" - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} - onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} > - <div className="search-title" id="result" >title: {this.props.doc.title}</div> - {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */} - {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */} - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index 7853544d5..c1a6ca44e 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -12,7 +12,7 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { VideoBox } from "../nodes/VideoBox"; import { NumCast, Cast, StrCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; -import { SearchBox } from "../SearchBox"; +import { SearchBox } from "../search/SearchBox"; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss index 30e158603..a80e09fa8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss @@ -1,4 +1,4 @@ -.collectionfreeformlinksview-svgCanvas{ +p.collectionfreeformlinksview-svgCanvas{ transform: translate(-10000px,-10000px); position: absolute; top: 0; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 3f7efcb66..9bfe9d7e1 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -14,7 +14,7 @@ import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { InkingCanvas } from "../../InkingCanvas"; import { PreviewCursor } from "../../PreviewCursor"; -import { SearchBox } from "../../SearchBox"; +import { SearchBox } from "../../search/FilterBox"; import { Templates } from "../../Templates"; import { CollectionViewType } from "../CollectionBaseView"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 838d4d9ac..cfbf4aab8 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -9,6 +9,7 @@ $main-accent: #aaaaa3; //$alt-accent: #59dff7; $alt-accent: #c2c2c5; $lighter-alt-accent: rgb(207, 220, 240); +$darker-alt-accent: rgb(178, 206, 248); $intermediate-color: #9c9396; $dark-color: #121721; // fonts diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 3f09d6214..c34ccdccb 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -8,6 +8,7 @@ import React = require("react"); import { Doc, DocListCast } from "../../../new_fields/Doc"; import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; +import { DocTypes } from "../../documents/Documents"; interface Props { docView: DocumentView; @@ -23,7 +24,7 @@ export class LinkMenu extends React.Component<Props> { return links.map(link => { let doc = FieldValue(Cast(link[key], Doc)); if (doc) { - return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />; + return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={DocTypes.LINK} />; } }); } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 44028ddf7..b6436d70f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -11,6 +11,8 @@ import { PdfField } from "../../../new_fields/URLField"; import { RouteStore } from "../../../server/RouteStore"; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; +import { FilterBox } from "../search/FilterBox"; +import { Annotation } from './Annotation'; import { PDFViewer } from "../pdf/PDFViewer"; import { positionSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; diff --git a/src/client/views/search/CheckBox.scss b/src/client/views/search/CheckBox.scss new file mode 100644 index 000000000..af59d5fbf --- /dev/null +++ b/src/client/views/search/CheckBox.scss @@ -0,0 +1,59 @@ +@import "../globalCssVariables"; + +.checkboxfilter { + display: flex; + margin-top: 0px; + padding: 3px; + + .outer { + display: flex; + position: relative; + justify-content: center; + align-items: center; + margin-top: 0px; + + .check-container:hover~.check-box { + background-color: $intermediate-color; + } + + .check-container { + width: 40px; + height: 40px; + position: absolute; + z-index: 1000; + + .checkmark { + z-index: 1000; + position: absolute; + fill-opacity: 0; + stroke-width: 4px; + stroke: white; + } + } + + .check-box { + z-index: 900; + position: relative; + height: 20px; + width: 20px; + overflow: visible; + background-color: transparent; + border-style: solid; + border-color: $alt-accent; + border-width: 2px; + -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; + } + } + + .checkbox-title { + display: flex; + align-items: center; + text-transform: capitalize; + margin-left: 15px; + } + +} + diff --git a/src/client/views/search/CheckBox.tsx b/src/client/views/search/CheckBox.tsx new file mode 100644 index 000000000..a9d48219a --- /dev/null +++ b/src/client/views/search/CheckBox.tsx @@ -0,0 +1,131 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx'; +import "./CheckBox.scss"; +import * as anime from 'animejs'; + +interface CheckBoxProps { + originalStatus: boolean; + updateStatus(newStatus: boolean): void; + title: string; + parent: any; + numCount: number; + default: boolean; +} + +@observer +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 checkRef: any; + @observable private _resetReaction?: IReactionDisposer; + + + constructor(props: CheckBoxProps) { + super(props); + 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", + }); + } + + 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(); + } + + 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(); + } + } + + 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(); + + } + this._status = !this._status; + this.props.updateStatus(this._status); + + } + + render() { + return ( + <div className="checkboxfilter" onClick={this.onClick}> + <div className="outer"> + <div className="check-container"> + <svg viewBox="0 12 40 40"> + <path ref={this.checkRef} className="checkmark" d="M14.1 27.2l7.1 7.2 16.7-18" /> + </svg> + </div> + <div className="check-box" /> + </div> + <div className="checkbox-title">{this.props.title}</div> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/search/CollectionFilters.scss b/src/client/views/search/CollectionFilters.scss new file mode 100644 index 000000000..b54cdcbd1 --- /dev/null +++ b/src/client/views/search/CollectionFilters.scss @@ -0,0 +1,20 @@ +@import "../globalCssVariables"; + +.collection-filters { + display: flex; + flex-direction: row; + width: 100%; + + &.main { + width: 47%; + float: left; + } + + &.optional { + width: 60%; + display: grid; + grid-template-columns: 50% 50%; + float: right; + opacity: 0; + } +}
\ No newline at end of file diff --git a/src/client/views/search/CollectionFilters.tsx b/src/client/views/search/CollectionFilters.tsx new file mode 100644 index 000000000..48d0b2ddb --- /dev/null +++ b/src/client/views/search/CollectionFilters.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { observable, action } from 'mobx'; +import { CheckBox } from './CheckBox'; +import "./CollectionFilters.scss"; +import * as anime from 'animejs'; + +interface CollectionFilterProps { + collectionStatus: boolean; + updateCollectionStatus(val: boolean): void; + collectionSelfStatus: boolean; + updateSelfCollectionStatus(val: boolean): void; + collectionParentStatus: boolean; + updateParentCollectionStatus(val: boolean): void; +} + +export class CollectionFilters extends React.Component<CollectionFilterProps> { + + static Instance: CollectionFilters; + + @observable public _resetBoolean = false; + @observable public _resetCounter: number = 0; + + @observable private _collectionsSelected = this.props.collectionStatus; + @observable private _timeline: anime.AnimeTimelineInstance; + @observable private _ref: any; + + constructor(props: CollectionFilterProps) { + super(props); + CollectionFilters.Instance = this; + this._ref = React.createRef(); + + this._timeline = anime.timeline({ + loop: false, + autoplay: false, + direction: "reverse", + }); + } + + componentDidMount = () => { + this._timeline.add({ + targets: this._ref.current, + easing: "easeInOutQuad", + duration: 500, + opacity: 1, + }); + + if (this._collectionsSelected) { + this._timeline.play(); + this._timeline.reverse(); + } + } + + @action.bound + resetCollectionFilters() { this._resetBoolean = true; } + + @action.bound + updateColStat(val: boolean) { + this.props.updateCollectionStatus(val); + + if (this._collectionsSelected !== val) { + this._timeline.play(); + this._timeline.reverse(); + } + + this._collectionsSelected = val; + } + + render() { + return ( + <div> + <div className="collection-filters"> + <div className="collection-filters main"> + <CheckBox default={false} title={"limit to current collection"} parent={this} numCount={3} updateStatus={this.updateColStat} originalStatus={this.props.collectionStatus} /> + </div> + <div className="collection-filters optional" ref={this._ref}> + <CheckBox default={true} title={"Search in self"} parent={this} numCount={3} updateStatus={this.props.updateSelfCollectionStatus} originalStatus={this.props.collectionSelfStatus} /> + <CheckBox default={true} title={"Search in parent"} parent={this} numCount={3} updateStatus={this.props.updateParentCollectionStatus} originalStatus={this.props.collectionParentStatus} /> + </div> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/search/FieldFilters.scss b/src/client/views/search/FieldFilters.scss new file mode 100644 index 000000000..ba0926140 --- /dev/null +++ b/src/client/views/search/FieldFilters.scss @@ -0,0 +1,5 @@ +.field-filters { + width: 100%; + display: grid; + grid-template-columns: 18% 20% 60%; +}
\ No newline at end of file diff --git a/src/client/views/search/FieldFilters.tsx b/src/client/views/search/FieldFilters.tsx new file mode 100644 index 000000000..648aac20a --- /dev/null +++ b/src/client/views/search/FieldFilters.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { observable } from 'mobx'; +import { CheckBox } from './CheckBox'; +import { Keys } from './FilterBox'; +import "./FieldFilters.scss"; + +export interface FieldFilterProps { + titleFieldStatus: boolean; + dataFieldStatus: boolean; + authorFieldStatus: boolean; + updateTitleStatus(stat: boolean): void; + updateAuthorStatus(stat: boolean): void; + updateDataStatus(stat: boolean): void; +} + +export class FieldFilters extends React.Component<FieldFilterProps> { + + static Instance: FieldFilters; + + @observable public _resetBoolean = false; + @observable public _resetCounter: number = 0; + + constructor(props: FieldFilterProps) { + super(props); + FieldFilters.Instance = this; + } + + resetFieldFilters() { + this._resetBoolean = true; + } + + render() { + return ( + <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} /> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/search/FilterBox.scss b/src/client/views/search/FilterBox.scss new file mode 100644 index 000000000..1eb8963d7 --- /dev/null +++ b/src/client/views/search/FilterBox.scss @@ -0,0 +1,108 @@ +@import "../globalCssVariables"; +@import "./NaviconButton.scss"; + +.filter-form { + padding: 25px; + width: 600px; + background: $dark-color; + position: relative; + right: 1px; + color: $light-color; + flex-direction: column; + display: inline-block; + transform-origin: top; + overflow: auto; + + .top-filter-header { + + #header { + text-transform: uppercase; + letter-spacing: 2px; + font-size: 25; + width: 80%; + } + + .close-icon { + width: 20%; + opacity: .6; + position: relative; + display: inline-block; + + .line { + display: block; + background: $alt-accent; + width: $width-line; + height: $height-line; + 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; + + .filter-title { + font-size: 18; + 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; + } + } + } + + .filter-buttons { + border-color: rgba(178, 206, 248, .2); // $darker-alt-accent + border-top-style: solid; + padding-top: 10px; + } +}
\ No newline at end of file diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx new file mode 100644 index 000000000..cc1feeaf7 --- /dev/null +++ b/src/client/views/search/FilterBox.tsx @@ -0,0 +1,383 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import "./SearchBox.scss"; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +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 { Cast, StrCast } from '../../../new_fields/Types'; +import * as _ from "lodash"; +import { ToggleBar } from './ToggleBar'; +import { IconBar } from './IconBar'; +import { FieldFilters } from './FieldFilters'; +import { SelectionManager } from '../../util/SelectionManager'; +import { DocumentView } from '../nodes/DocumentView'; +import { CollectionFilters } from './CollectionFilters'; +import { NaviconButton } from './NaviconButton'; +import * as $ from 'jquery'; +import "./FilterBox.scss"; +import { SearchBox } from './SearchBox'; + +library.add(faTimes); + +export enum Keys { + TITLE = "title", + AUTHOR = "author", + DATA = "data" +} + +@observer +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]; + + //if true, any keywords can be used. if false, all keywords are required. + @observable private _basicWordStatus: boolean = true; + @observable private _filterOpen: boolean = false; + @observable private _icons: string[] = this._allIcons; + @observable private _titleFieldStatus: boolean = true; + @observable private _authorFieldStatus: boolean = true; + @observable private _dataFieldStatus: boolean = true; + @observable private _collectionStatus = false; + @observable private _collectionSelfStatus = true; + @observable private _collectionParentStatus = true; + @observable private _wordStatusOpen: boolean = false; + @observable private _typeOpen: boolean = false; + @observable private _colOpen: boolean = false; + @observable private _fieldOpen: boolean = false; + public _pointerTime: number = -1; + + constructor(props: Readonly<{}>) { + super(props); + FilterBox.Instance = this; + } + + componentDidMount = () => { + document.addEventListener("pointerdown", (e) => { + if (e.timeStamp !== this._pointerTime) { + SearchBox.Instance.closeSearch(); + } + }); + } + + setupAccordion() { + $('document').ready(function () { + var acc = document.getElementsByClassName('filter-header'); + + for (var i = 0; i < acc.length; i++) { + acc[i].addEventListener("click", function (this: HTMLElement) { + this.classList.toggle("active"); + + var panel = this.nextElementSibling as HTMLElement; + if (panel.style.maxHeight) { + panel.style.overflow = "hidden"; + panel.style.maxHeight = null; + panel.style.opacity = "0"; + } else { + setTimeout(() => { + panel.style.overflow = "visible"; + }, 200); + setTimeout(() => { + panel.style.opacity = "1"; + }, 50); + panel.style.maxHeight = panel.scrollHeight + "px"; + + } + }); + } + }); + } + + @action.bound + minimizeAll() { + $('document').ready(function () { + var acc = document.getElementsByClassName('filter-header'); + + for (var i = 0; i < acc.length; i++) { + let classList = acc[i].classList; + if (classList.contains("active")) { + acc[i].classList.toggle("active"); + var panel = acc[i].nextElementSibling as HTMLElement; + panel.style.overflow = "hidden"; + panel.style.maxHeight = null; + } + } + }); + } + + @action.bound + resetFilters = () => { + ToggleBar.Instance.resetToggle(); + IconBar.Instance.selectAll(); + FieldFilters.Instance.resetFieldFilters(); + CollectionFilters.Instance.resetCollectionFilters(); + } + + basicRequireWords(query: string): string { + let oldWords = query.split(" "); + let newWords: string[] = []; + oldWords.forEach(word => { + let newWrd = "+" + word; + newWords.push(newWrd); + }); + query = newWords.join(" "); + + return query; + } + + basicFieldFilters(query: string, type: string): string { + let oldWords = query.split(" "); + let mod = ""; + + if (type === Keys.AUTHOR) { + mod = " author_t:"; + } if (type === Keys.DATA) { + //TODO + } if (type === Keys.TITLE) { + mod = " title_t:"; + } + + let newWords: string[] = []; + oldWords.forEach(word => { + let newWrd = mod + word; + newWords.push(newWrd); + }); + + query = newWords.join(" "); + + return query; + } + + 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._dataFieldStatus) { + finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA); + } + return finalQuery; + } + + get fieldFiltersApplied() { return !(this._dataFieldStatus && this._authorFieldStatus && this._titleFieldStatus); } + + //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[] { + let selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments(); + let collections: Doc[] = []; + + selectedDocs.forEach(async element => { + let layout: string = StrCast(element.props.Document.baseLayout); + //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); + } + } + //gets the selected doc's containing view + let containingView = element.props.ContainingCollectionView; + //makes sure collections aren't added more than once + if (containingView && !collections.includes(containingView.props.Document)) { + collections.push(containingView.props.Document); + } + }); + + return collections; + } + + 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; + } + + addCollectionFilter(query: string): string { + let collections: Doc[] = this.getCurCollections(); + let oldWords = query.split(" "); + + let collectionString: string[] = []; + collections.forEach(doc => { + let proto = doc.proto; + let protoId = (proto || doc)[Id]; + let colString: string = "{!join from=data_l to=id}id:" + protoId + " "; + collectionString.push(colString); + }); + + let finalColString = collectionString.join(" "); + finalColString = finalColString.trim(); + return "+(" + finalColString + ")" + query; + } + + @action + filterDocsByType(docs: Doc[]) { + let finalDocs: Doc[] = []; + docs.forEach(doc => { + let layoutresult = Cast(doc.type, "string", ""); + if (this._icons.includes(layoutresult)) { + finalDocs.push(doc); + } + }); + return finalDocs; + } + + @action.bound + openFilter = () => { + this._filterOpen = !this._filterOpen; + SearchBox.Instance.closeResults(); + this.setupAccordion(); + } + + //if true, any keywords can be used. if false, all keywords are required. + @action.bound + handleWordQueryChange = () => { this._basicWordStatus = !this._basicWordStatus; } + + @action + getBasicWordStatus() { return this._basicWordStatus; } + + @action.bound + updateIcon(newArray: string[]) { this._icons = newArray; } + + @action.bound + getIcons(): string[] { return this._icons; } + + stopProp = (e: React.PointerEvent) => { + e.stopPropagation(); + this._pointerTime = e.timeStamp; + } + + @action.bound + public closeFilter() { + this._filterOpen = false; + } + + @action.bound + toggleFieldOpen() { this._fieldOpen = !this._fieldOpen; } + + @action.bound + toggleColOpen() { this._colOpen = !this._colOpen; } + + @action.bound + toggleTypeOpen() { this._typeOpen = !this._typeOpen; } + + @action.bound + toggleWordStatusOpen() { this._wordStatusOpen = !this._wordStatusOpen; } + + @action.bound + updateTitleStatus(newStat: boolean) { this._titleFieldStatus = newStat; } + + @action.bound + updateAuthorStatus(newStat: boolean) { this._authorFieldStatus = newStat; } + + @action.bound + updateDataStatus(newStat: boolean) { this._dataFieldStatus = newStat; } + + @action.bound + updateCollectionStatus(newStat: boolean) { this._collectionStatus = newStat; } + + @action.bound + updateSelfCollectionStatus(newStat: boolean) { this._collectionSelfStatus = newStat; } + + @action.bound + updateParentCollectionStatus(newStat: boolean) { this._collectionParentStatus = newStat; } + + getCollectionStatus() { return this._collectionStatus; } + getSelfCollectionStatus() { return this._collectionSelfStatus; } + getParentCollectionStatus() { return this._collectionParentStatus; } + getTitleStatus() { return this._titleFieldStatus; } + getAuthorStatus() { return this._authorFieldStatus; } + getDataStatus() { return this._dataFieldStatus; } + + // Useful queries: + // Delegates of a document: {!join from=id to=proto_i}id:{protoId} + // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId} //id of collections prototype + render() { + return ( + <div> + <div style={{ display: "flex", flexDirection: "row-reverse" }}> + <SearchBox /> + </div> + {this._filterOpen ? ( + <div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex" } : { display: "none" }}> + <div className="top-filter-header" style={{ display: "flex", width: "100%" }}> + <div id="header">Filter Search Results</div> + <div className="close-icon" onClick={this.closeFilter}> + <span className="line line-1"></span> + <span className="line line-2"></span></div> + </div> + <div className="filter-options"> + <div className="filter-div"> + <div className="filter-header"> + <div className='filter-title words'>Required words</div> + <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleWordStatusOpen} /></div> + </div> + <div className="filter-panel" > + <ToggleBar handleChange={this.handleWordQueryChange} getStatus={this.getBasicWordStatus} + originalStatus={this._basicWordStatus} optionOne={"Include Any Keywords"} optionTwo={"Include All Keywords"} /> + </div> + </div> + <div className="filter-div"> + <div className="filter-header"> + <div className="filter-title icon">Filter by type of node</div> + <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleTypeOpen} /></div> + </div> + <div className="filter-panel"><IconBar /></div> + </div> + <div className="filter-div"> + <div className="filter-header"> + <div className='filter-title collection'>Search in current collections</div> + <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleColOpen} /></div> + </div> + <div className="filter-panel"><CollectionFilters + updateCollectionStatus={this.updateCollectionStatus} updateParentCollectionStatus={this.updateParentCollectionStatus} updateSelfCollectionStatus={this.updateSelfCollectionStatus} + collectionStatus={this._collectionStatus} collectionParentStatus={this._collectionParentStatus} collectionSelfStatus={this._collectionSelfStatus} /></div> + </div> + <div className="filter-div"> + <div className="filter-header"> + <div className="filter-title field">Filter by Basic Keys</div> + <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleFieldOpen} /></div> + </div> + <div className="filter-panel"><FieldFilters + titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._dataFieldStatus} authorFieldStatus={this._authorFieldStatus} + updateAuthorStatus={this.updateAuthorStatus} updateDataStatus={this.updateDataStatus} updateTitleStatus={this.updateTitleStatus} /> </div> + </div> + </div> + <div className="filter-buttons" style={{ display: "flex", justifyContent: "space-around" }}> + <button className="minimize-filter" onClick={this.minimizeAll}>Minimize All</button> + <button className="advanced-filter" >Advanced Filters</button> + <button className="save-filter" >Save Filters</button> + <button className="reset-filter" onClick={this.resetFilters}>Reset Filters</button> + </div> + </div> + ) : undefined} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/search/IconBar.scss b/src/client/views/search/IconBar.scss new file mode 100644 index 000000000..e384722ce --- /dev/null +++ b/src/client/views/search/IconBar.scss @@ -0,0 +1,12 @@ +@import "../globalCssVariables"; + +.icon-bar { + display: flex; + justify-content: space-evenly; + align-items: center; + height: 40px; + width: 100%; + flex-wrap: wrap; + margin-bottom: 10px; +} + diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx new file mode 100644 index 000000000..744dd898a --- /dev/null +++ b/src/client/views/search/IconBar.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +// import "./SearchBox.scss"; +import "./IconBar.scss"; +import "./IconButton.scss"; +import { DocTypes } 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'; +import * as _ from "lodash"; +import { IconButton } from './IconButton'; +import { FilterBox } from './FilterBox'; + +library.add(faSearch); +library.add(faObjectGroup); +library.add(faImage); +library.add(faStickyNote); +library.add(faFilePdf); +library.add(faFilm); +library.add(faMusic); +library.add(faLink); +library.add(faChartBar); +library.add(faGlobeAsia); +library.add(faBan); + +@observer +export class IconBar extends React.Component { + + static Instance: IconBar; + + @observable public _resetClicked: boolean = false; + @observable public _selectAllClicked: boolean = false; + @observable public _reset: number = 0; + @observable public _select: number = 0; + + constructor(props: any) { + super(props); + IconBar.Instance = this; + } + + @action.bound + getList(): string[] { return FilterBox.Instance.getIcons(); } + + @action.bound + updateList(newList: string[]) { FilterBox.Instance.updateIcon(newList); } + + @action.bound + resetSelf = () => { + this._resetClicked = true; + this.updateList([]); + } + + @action.bound + selectAll = () => { + this._selectAllClicked = true; + this.updateList(FilterBox.Instance._allIcons); + } + + render() { + return ( + <div className="icon-bar"> + <div className="type-outer"> + <div className={"type-icon all"} + onClick={this.selectAll}> + <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} /> + </div> + <div className="filter-description">Select All</div> + </div> + {FilterBox.Instance._allIcons.map((type: string) => + <IconButton type={type} /> + )} + <div className="type-outer"> + <div className={"type-icon none"} + onClick={this.resetSelf}> + <FontAwesomeIcon className="fontawesome-icon" icon={faTimesCircle} /> + </div> + <div className="filter-description">Clear</div> + </div> + </div> + ); + } +} diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss new file mode 100644 index 000000000..94b294ba5 --- /dev/null +++ b/src/client/views/search/IconButton.scss @@ -0,0 +1,52 @@ +@import "../globalCssVariables"; + +.type-outer { + display: flex; + flex-direction: column; + align-items: center; + width: 45px; + height: 60px; + + .type-icon { + height: 45px; + width: 45px; + color: $light-color; + border-radius: 50%; + 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; + font-size: 2em; + + .fontawesome-icon { + height: 24px; + width: 24px; + } + } + + .filter-description { + text-transform: capitalize; + font-size: 10; + text-align: center; + height: 15px; + margin-top: 5px; + 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; + } + + .type-icon:hover { + transform: scale(1.1); + background-color: $darker-alt-accent; + opacity: 1; + + +.filter-description { + opacity: 1; + } + } +}
\ No newline at end of file diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx new file mode 100644 index 000000000..23ab42de0 --- /dev/null +++ b/src/client/views/search/IconButton.tsx @@ -0,0 +1,192 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx'; +import "./SearchBox.scss"; +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 '../globalCssVariables.scss'; +import * as _ from "lodash"; +import { IconBar } from './IconBar'; +import { props } from 'bluebird'; +import { FilterBox } from './FilterBox'; +import { Search } from '../../../server/Search'; + +library.add(faSearch); +library.add(faObjectGroup); +library.add(faImage); +library.add(faStickyNote); +library.add(faFilePdf); +library.add(faFilm); +library.add(faMusic); +library.add(faLink); +library.add(faChartBar); +library.add(faGlobeAsia); +library.add(faBan); + +interface IconButtonProps { + type: string; +} + +@observer +export class IconButton extends React.Component<IconButtonProps>{ + + @observable private _isSelected: boolean = FilterBox.Instance.getIcons().indexOf(this.props.type) !== -1; + @observable private _hover = false; + private _resetReaction?: IReactionDisposer; + private _selectAllReaction?: IReactionDisposer; + + static Instance: IconButton; + constructor(props: IconButtonProps) { + super(props); + IconButton.Instance = this; + } + + componentDidMount = () => { + this._resetReaction = reaction( + () => IconBar.Instance._resetClicked, + () => { + if (IconBar.Instance._resetClicked) { + runInAction(() => { + this.reset(); + IconBar.Instance._reset++; + if (IconBar.Instance._reset === 9) { + IconBar.Instance._reset = 0; + IconBar.Instance._resetClicked = false; + } + }); + } + }, + ); + this._selectAllReaction = reaction( + () => IconBar.Instance._selectAllClicked, + () => { + if (IconBar.Instance._selectAllClicked) { + runInAction(() => { + this.select(); + IconBar.Instance._select++; + if (IconBar.Instance._select === 9) { + IconBar.Instance._select = 0; + IconBar.Instance._selectAllClicked = false; + } + }); + } + }, + ); + } + + @action.bound + getIcon() { + switch (this.props.type) { + case (DocTypes.NONE): + return faBan; + case (DocTypes.AUDIO): + return faMusic; + case (DocTypes.COL): + return faObjectGroup; + case (DocTypes.HIST): + return faChartBar; + case (DocTypes.IMG): + return faImage; + case (DocTypes.LINK): + return faLink; + case (DocTypes.PDF): + return faFilePdf; + case (DocTypes.TEXT): + return faStickyNote; + case (DocTypes.VID): + return faVideo; + case (DocTypes.WEB): + return faGlobeAsia; + default: + return faCaretDown; + } + } + + @action.bound + onClick = () => { + let newList: string[] = FilterBox.Instance.getIcons(); + + if (!this._isSelected) { + this._isSelected = true; + newList.push(this.props.type); + } + else { + this._isSelected = false; + _.pull(newList, this.props.type); + } + + FilterBox.Instance.updateIcon(newList); + } + + selected = { + opacity: 1, + backgroundColor: "#c2c2c5" //$alt-accent + }; + + notSelected = { + opacity: 0.6, + }; + + hoverStyle = { + opacity: 1, + backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent + }; + + @action.bound + public reset() { this._isSelected = false; } + + @action.bound + public select() { this._isSelected = true; } + + @action + onMouseLeave = () => { this._hover = false; } + + @action + onMouseEnter = () => { this._hover = true; } + + getFA = () => { + switch (this.props.type) { + case (DocTypes.NONE): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faBan} />); + case (DocTypes.AUDIO): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faMusic} />); + case (DocTypes.COL): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faObjectGroup} />); + case (DocTypes.HIST): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faChartBar} />); + case (DocTypes.IMG): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faImage} />); + case (DocTypes.LINK): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faLink} />); + case (DocTypes.PDF): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faFilePdf} />); + case (DocTypes.TEXT): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faStickyNote} />); + case (DocTypes.VID): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faVideo} />); + case (DocTypes.WEB): + return (<FontAwesomeIcon className="fontawesome-icon" icon={faGlobeAsia} />); + default: + return (<FontAwesomeIcon className="fontawesome-icon" icon={faCaretDown} />); + } + } + + render() { + return ( + <div className="type-outer" id={this.props.type + "-filter"} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + onClick={this.onClick}> + <div className="type-icon" id={this.props.type + "-icon"} + style={this._hover ? this.hoverStyle : this._isSelected ? this.selected : this.notSelected} + > + {this.getFA()} + </div> + <div className="filter-description">{this.props.type}</div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/search/NaviconButton.scss b/src/client/views/search/NaviconButton.scss new file mode 100644 index 000000000..c23bab461 --- /dev/null +++ b/src/client/views/search/NaviconButton.scss @@ -0,0 +1,69 @@ +@import "../globalCssVariables"; + +$height-icon: 15px; +$width-line: 30px; +$height-line: 4px; + +$transition-time: 0.4s; +$rotation: 45deg; +$translateY: ($height-icon / 2); +$translateX: 0; + +#hamburger-icon { + width: $width-line; + height: $height-icon; + position: relative; + display: block; + transition: all $transition-time; + -webkit-transition: all $transition-time; + -moz-transition: all $transition-time; + + .line { + display: block; + background: $alt-accent; + width: $width-line; + height: $height-line; + position: absolute; + left: 0; + border-radius: ($height-line / 2); + transition: all $transition-time; + -webkit-transition: all $transition-time; + -moz-transition: all $transition-time; + + &.line-1 { + top: 0; + } + + &.line-2 { + top: 50%; + } + + &.line-3 { + top: 100%; + } + } +} + +.filter-header.active { + .line-1 { + transform: translateY($translateY) translateX($translateX) rotate($rotation); + -webkit-transform: translateY($translateY) translateX($translateX) rotate($rotation); + -moz-transform: translateY($translateY) translateX($translateX) rotate($rotation); + } + + .line-2 { + opacity: 0; + } + + .line-3 { + transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1); + -webkit-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1); + -moz-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1); + } +} + +.filter-header:hover #hamburger-icon { + transform: scale(1.1); + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); +}
\ No newline at end of file diff --git a/src/client/views/search/NaviconButton.tsx b/src/client/views/search/NaviconButton.tsx new file mode 100644 index 000000000..3fa36b163 --- /dev/null +++ b/src/client/views/search/NaviconButton.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import "./NaviconButton.scss"; +import * as $ from 'jquery'; +import { observable } from 'mobx'; + +export interface NaviconProps{ + onClick(): void; +} + +export class NaviconButton extends React.Component<NaviconProps> { + + @observable private _ref: React.RefObject<HTMLAnchorElement> = React.createRef(); + + componentDidMount = () => { + let that = this; + if(this._ref.current){this._ref.current.addEventListener("click", function(e) { + e.preventDefault(); + if(that._ref.current){ + that._ref.current.classList.toggle('active'); + return false; + } + });} + } + + render() { + return ( + <a id="hamburger-icon" href="#" ref = {this._ref} title="Menu"> + <span className="line line-1"></span> + <span className="line line-2"></span> + <span className="line line-3"></span> + </a> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/search/Pager.scss b/src/client/views/search/Pager.scss new file mode 100644 index 000000000..2b9c81b93 --- /dev/null +++ b/src/client/views/search/Pager.scss @@ -0,0 +1,47 @@ +@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/Pager.tsx b/src/client/views/search/Pager.tsx new file mode 100644 index 000000000..258f112b9 --- /dev/null +++ b/src/client/views/search/Pager.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { faArrowCircleRight, faArrowCircleLeft } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import "./Pager.scss" +import { SearchBox } from './SearchBox'; +import { observable, action } from 'mobx'; +import { FilterBox } from './FilterBox'; + +library.add(faArrowCircleRight); +library.add(faArrowCircleLeft); + +@observer +export class Pager extends React.Component { + + @observable _leftHover: boolean = false; + @observable _rightHover: boolean = false; + + @action + onLeftClick(e: React.PointerEvent) { + FilterBox.Instance._pointerTime = e.timeStamp; + if(SearchBox.Instance._pageNum > 0){ + SearchBox.Instance._pageNum -= 1; + } + } + + @action + onRightClick(e: React.PointerEvent) { + FilterBox.Instance._pointerTime = e.timeStamp; + if(SearchBox.Instance._pageNum+1 < SearchBox.Instance._maxNum){ + SearchBox.Instance._pageNum += 1; + } + } + + @action.bound + mouseInLeft() { + this._leftHover = true; + } + + @action.bound + mouseOutLeft() { + this._leftHover = false; + } + + @action.bound + mouseInRight() { + this._rightHover = true; + } + + @action.bound + mouseOutRight() { + this._rightHover = false; + } + + render() { + return ( + <div className="search-pager"> + <div className="search-arrows"> + <div className = "arrow" + onPointerDown = {this.onLeftClick} style = {SearchBox.Instance._pageNum === 0 ? {opacity: .2} : this._leftHover ? {opacity: 1} : {opacity: .7}} + onMouseEnter = {this.mouseInLeft} onMouseOut = {this.mouseOutLeft}> + <FontAwesomeIcon className="fontawesome-icon" icon={faArrowCircleLeft} /> + </div> + <div className="pager-title"> + page {SearchBox.Instance._pageNum + 1} of {SearchBox.Instance._maxNum} + </div> + <div className = "arrow" + onPointerDown = {this.onRightClick} style = {SearchBox.Instance._pageNum === SearchBox.Instance._maxNum-1 ? {opacity: .2} : this._rightHover ? {opacity: 1} : {opacity: .7}} + onMouseEnter = {this.mouseInRight} onMouseOut = {this.mouseOutRight}> + <FontAwesomeIcon className="fontawesome-icon" icon={faArrowCircleRight} /> + </div> + </div> + </div> + ) + } + +}
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss new file mode 100644 index 000000000..2a27bbe62 --- /dev/null +++ b/src/client/views/search/SearchBox.scss @@ -0,0 +1,64 @@ +@import "../globalCssVariables"; +@import "./NaviconButton.scss"; + +.searchBox-bar { + height: 32px; + display: flex; + justify-content: flex-end; + align-items: center; + padding-left: 2px; + padding-right: 2px; + + .searchBox-barChild { + + &.searchBox-collection { + flex: 0 1 auto; + margin-left: 2px; + margin-right: 2px + } + + &.searchBox-input { + display: block; + width: 130px; + -webkit-transition: width 0.4s; + transition: width 0.4s; + align-self: stretch; + margin-left: 2px; + margin-right: 2px + } + + .searchBox-input:focus { + width: 500px; + outline: 3px solid lightblue; + } + + &.searchBox-filter { + align-self: stretch; + margin-left: 2px; + margin-right: 2px + } + } +} + +.searchBox-results { + margin-left: 27px; + top: 300px; + display: flex; + flex-direction: column; + margin-right: 72px; + height: 560px; + overflow: hidden; + overflow-y: auto; + + .no-result { + width: 500px; + background: $light-color-secondary; + border-color: $intermediate-color; + border-bottom-style: solid; + padding: 10px; + height: 50px; + text-transform: uppercase; + text-align: left; + font-weight: bold; + } +}
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx new file mode 100644 index 000000000..154e0b7b6 --- /dev/null +++ b/src/client/views/search/SearchBox.tsx @@ -0,0 +1,208 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction } 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 { 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 { Pager } from './Pager'; + +@observer +export class SearchBox extends React.Component { + + @observable private _searchString: string = ""; + @observable private _resultsOpen: boolean = false; + @observable private _results: Doc[] = []; + @observable private _openNoResults: boolean = false; + @observable public _pageNum: number = 0; + //temp + @observable public _maxNum: number = 10; + + static Instance: SearchBox; + + constructor(props: any) { + super(props); + + SearchBox.Instance = this; + } + + @action + getViews = async (doc: Doc) => { + const results = await SearchUtil.GetViewsOfDocument(doc); + let toReturn: Doc[] = []; + await runInAction(() => { + toReturn = results; + }); + return toReturn; + } + + @action.bound + onChange(e: React.ChangeEvent<HTMLInputElement>) { + this._searchString = e.target.value; + + if (this._searchString === "") { + this._results = []; + this._openNoResults = false; + } + } + + 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); + const returnedUri = await rp.post(posting, { + body: { + uri: imageUri, + name: returnedFilename + }, + json: true, + }); + return returnedUri; + + } catch (e) { + console.log(e); + } + } + + @action + submitSearch = async () => { + let query = this._searchString; // searchbox gets query + let results: Doc[]; + + query = FilterBox.Instance.getFinalQuery(query); + + //if there is no query there should be no result + if (query === "") { + results = []; + } + else { + //gets json result into a list of documents that can be used + //these are filtered by type + results = await this.getResults(query); + } + + runInAction(() => { + this._resultsOpen = true; + this._results = results; + this._openNoResults = true; + }); + } + + @action + getResults = async (query: string) => { + let response = await rp.get(DocServer.prepend('/search'), { + qs: { + query + } + }); + let res: string[] = JSON.parse(response); + const fields = await DocServer.GetRefFields(res); + const docs: Doc[] = []; + for (const id of res) { + const field = fields[id]; + if (field instanceof Doc) { + docs.push(field); + } + } + return FilterBox.Instance.filterDocsByType(docs); + } + + collectionRef = React.createRef<HTMLSpanElement>(); + startDragCollection = async () => { + const results = await this.getResults(FilterBox.Instance.getFinalQuery(this._searchString)); + const docs = results.map(doc => { + const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); + if (isProto) { + return Doc.MakeDelegate(doc); + } else { + return Doc.MakeAlias(doc); + } + }); + let x = 0; + let y = 0; + for (const doc of docs) { + doc.x = x; + doc.y = y; + const size = 200; + const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1); + if (aspect > 1) { + doc.height = size; + doc.width = size / aspect; + } else if (aspect > 0) { + doc.width = size; + doc.height = size * aspect; + } else { + doc.width = size; + doc.height = size; + } + doc.zoomBasis = 1; + x += 250; + if (x > 1000) { + x = 0; + y += 300; + } + } + return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); + } + + @action.bound + openSearch(e: React.PointerEvent) { + e.stopPropagation(); + this._openNoResults = false; + FilterBox.Instance.closeFilter(); + this._resultsOpen = true; + FilterBox.Instance._pointerTime = e.timeStamp; + } + + @action.bound + closeSearch = () => { + console.log("closing search") + FilterBox.Instance.closeFilter(); + this.closeResults(); + } + + @action.bound + closeResults() { + this._resultsOpen = false; + this._results = []; + } + + 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}> + <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" }} /> + <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} 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> */} + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss new file mode 100644 index 000000000..946680f0e --- /dev/null +++ b/src/client/views/search/SearchItem.scss @@ -0,0 +1,149 @@ +@import "../globalCssVariables"; + +.search-overview { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + height: 70px; + + .search-item { + width: 500px; + background: $light-color-secondary; + border-color: $intermediate-color; + border-bottom-style: solid; + padding: 10px; + height: 70px; + display: inline-block; + + .main-search-info { + display: flex; + flex-direction: row; + width: 100%; + + .search-title { + text-transform: uppercase; + text-align: left; + width: 80%; + font-weight: bold; + } + + .search-info { + display: flex; + justify-content: flex-end; + width: 40%; + + .link-container.item { + height: 26px; + width: 26px; + border-radius: 13px; + background: $dark-color; + color: $light-color-secondary; + display: flex; + justify-content: center; + align-items: center; + right: 15px; + -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 { + 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 .link-count { + opacity: 0; + } + + .link-container.item:hover .link-extended { + opacity: 1; + } + + .icon { + + .search-type { + width: 25PX; + height: 25PX; + display: flex; + justify-content: center; + align-items: center; + position: relative; + margin-right: 5px; + } + + .search-type:hover+.search-label { + opacity: 1; + } + + .search-label { + font-size: 10; + padding: 5px; + 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-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-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); + // height: 100% + } + +}
\ No newline at end of file diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx new file mode 100644 index 000000000..50bf1f6b8 --- /dev/null +++ b/src/client/views/search/SearchItem.tsx @@ -0,0 +1,176 @@ +import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Cast, NumCast } from "../../../new_fields/Types"; +import { observable, runInAction, computed, action } from "mobx"; +import { listSpec } from "../../../new_fields/Schema"; +import { Doc } from "../../../new_fields/Doc"; +import { DocumentManager } from "../../util/DocumentManager"; +import { SetupDrag } from "../../util/DragManager"; +import { SearchUtil } from "../../util/SearchUtil"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { observer } from "mobx-react"; +import "./SearchItem.scss"; +import { CollectionViewType } from "../collections/CollectionBaseView"; +import { DocTypes } from "../../documents/Documents"; +import { FilterBox } from "./FilterBox"; +import { DocumentView } from "../nodes/DocumentView"; +import "./SelectorContextMenu.scss"; +import { SearchBox } from "./SearchBox"; + +export interface SearchItemProps { + doc: Doc; +} + +library.add(faCaretUp); +library.add(faObjectGroup); +library.add(faStickyNote); +library.add(faFilePdf); +library.add(faFilm); +library.add(faMusic); +library.add(faLink); +library.add(faChartBar); +library.add(faGlobeAsia); + +@observer +export class SelectorContextMenu extends React.Component<SearchItemProps> { + @observable private _docs: { col: Doc, target: Doc }[] = []; + @observable private _otherDocs: { col: Doc, target: Doc }[] = []; + + constructor(props: SearchItemProps) { + super(props); + this.fetchDocuments(); + } + + 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 map: Map<Doc, Doc> = new Map; + const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true))); + allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index]))); + docs.forEach(doc => map.delete(doc)); + runInAction(() => { + this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.doc })); + this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target })); + }); + } + + getOnClick({ col, target }: { col: Doc, target: Doc }) { + return () => { + col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; + if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2; + const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2; + col.panX = newPanX; + col.panY = newPanY; + } + CollectionDockingView.Instance.AddRightSplit(col); + }; + } + + render() { + return ( + < div className="parents"> + <p className="contexts">Contexts:</p> + {this._docs.map(doc => <div className="collection"><a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a></div>)} + {this._otherDocs.map(doc => <div className="collection"><a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a></div>)} + </div> + ); + } +} + +@observer +export class SearchItem extends React.Component<SearchItemProps> { + + @observable _selected: boolean = false; + + onClick = () => { + CollectionDockingView.Instance.AddRightSplit(this.props.doc); + } + + @computed + public get DocumentIcon() { + let layoutresult = Cast(this.props.doc.type, "string", ""); + + 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 : + faCaretUp; + return <FontAwesomeIcon icon={button} size="2x" />; + } + + collectionRef = React.createRef<HTMLDivElement>(); + startDocDrag = () => { + let doc = this.props.doc; + const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); + if (isProto) { + return Doc.MakeDelegate(doc); + } else { + return Doc.MakeAlias(doc); + } + } + + @computed + get linkCount() { return Cast(this.props.doc.linkedToDocs, listSpec(Doc), []).length + Cast(this.props.doc.linkedFromDocs, listSpec(Doc), []).length; } + + @computed + get linkString(): string { + let num = this.linkCount; + if (num === 1) { + return num.toString() + " link"; + } + return num.toString() + " links"; + } + + pointerDown = (e: React.PointerEvent) => { SearchBox.Instance.openSearch(e); }; + + highlightDoc = (e: React.PointerEvent) => { + let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); + docViews.forEach(element => { + element.props.Document.libraryBrush = true; + }); + } + + unHighlightDoc = (e: React.PointerEvent) => { + let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); + docViews.forEach(element => { + element.props.Document.libraryBrush = false; + }); + } + + render() { + return ( + <div className="search-overview" onPointerDown={this.pointerDown}> + <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} ref={this.collectionRef} id="result" onClick={this.onClick} onPointerDown={() => { + this.pointerDown; + SetupDrag(this.collectionRef, this.startDocDrag); + }} > + <div className="main-search-info"> + <div className="search-title" id="result" >{this.props.doc.title}</div> + <div className="search-info"> + <div className="link-container item"> + <div className="link-count">{this.linkCount}</div> + <div className="link-extended">{this.linkString}</div> + </div> + <div className="icon"> + <div className="search-type" >{this.DocumentIcon}</div> + <div className="search-label">{this.props.doc.type}</div> + </div> + </div> + </div> + </div> + <div className="searchBox-instances"> + <SelectorContextMenu {...this.props} /> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/search/SelectorContextMenu.scss b/src/client/views/search/SelectorContextMenu.scss new file mode 100644 index 000000000..49f77b9bf --- /dev/null +++ b/src/client/views/search/SelectorContextMenu.scss @@ -0,0 +1,15 @@ +@import "../globalCssVariables"; + +.parents { + background: $lighter-alt-accent; + padding: 10px; + + .contexts { + text-transform: uppercase; + } + + .collection { + border-color: $darker-alt-accent; + border-bottom-style: solid; + } +}
\ No newline at end of file diff --git a/src/client/views/search/ToggleBar.scss b/src/client/views/search/ToggleBar.scss new file mode 100644 index 000000000..633a194fe --- /dev/null +++ b/src/client/views/search/ToggleBar.scss @@ -0,0 +1,36 @@ +@import "../globalCssVariables"; + +.toggle-title { + display: flex; + align-items: center; + color: $light-color; + text-transform: uppercase; + flex-direction: row; + justify-content: space-around; + padding: 5px; + + .toggle-option { + width: 50%; + text-align: 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; + } +} + +.toggle-bar { + height: 50px; + background-color: $alt-accent; + border-radius: 10px; + padding: 5px; + display: flex; + align-items: center; + + .toggle-button { + width: 275px; + height: 100%; + border-radius: 10px; + background-color: $light-color; + } +}
\ No newline at end of file diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx new file mode 100644 index 000000000..8a3aacf96 --- /dev/null +++ b/src/client/views/search/ToggleBar.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction, computed } from 'mobx'; +import "./SearchBox.scss"; +import "./ToggleBar.scss"; +import * as anime from 'animejs'; + +export interface ToggleBarProps { + originalStatus: boolean; + optionOne: string; + optionTwo: string; + handleChange(): void; + getStatus(): boolean; +} + +@observer +export class ToggleBar extends React.Component<ToggleBarProps>{ + static Instance: ToggleBar; + + @observable private _forwardTimeline: anime.AnimeTimelineInstance; + @observable private _toggleButton: React.RefObject<HTMLDivElement>; + @observable private _originalStatus: boolean = this.props.originalStatus; + + constructor(props: ToggleBarProps) { + super(props); + ToggleBar.Instance = this; + this._toggleButton = React.createRef(); + this._forwardTimeline = anime.timeline({ + loop: false, + autoplay: false, + direction: "reverse", + }); + } + + componentDidMount = () => { + + let totalWidth = 265; + + if (this._originalStatus) { + this._forwardTimeline.add({ + targets: this._toggleButton.current, + translateX: totalWidth, + easing: "easeInOutQuad", + duration: 500 + }); + } + else { + this._forwardTimeline.add({ + targets: this._toggleButton.current, + translateX: -totalWidth, + easing: "easeInOutQuad", + duration: 500 + }); + } + } + + @action.bound + onclick() { + this._forwardTimeline.play(); + this._forwardTimeline.reverse(); + this.props.handleChange(); + } + + @action.bound + public resetToggle = () => { + if (!this.props.getStatus()) { + this._forwardTimeline.play(); + this._forwardTimeline.reverse(); + this.props.handleChange(); + } + } + + render() { + return ( + <div> + <div className="toggle-title"> + <div className="toggle-option" style={{ opacity: (this.props.getStatus() ? 1 : .4) }}>{this.props.optionOne}</div> + <div className="toggle-option" style={{ opacity: (this.props.getStatus() ? .4 : 1) }}>{this.props.optionTwo}</div> + </div> + <div className="toggle-bar" id="toggle-bar" onClick={this.onclick} style={{ flexDirection: (this._originalStatus ? "row" : "row-reverse") }}> + <div className="toggle-button" id="toggle-button" ref={this._toggleButton} /> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/server/Search.ts b/src/server/Search.ts index d776480c6..2db2b242c 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -18,12 +18,13 @@ export class Search { } } - public async search(query: string) { + public async search(query: string, start: number = 0) { try { const searchResults = JSON.parse(await rp.get(this.url + "dash/select", { qs: { q: query, - fl: "id" + fl: "id", + start: start } })); const fields = searchResults.response.docs; diff --git a/tsconfig.json b/tsconfig.json index 68f4b058a..9ea91ec49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,10 @@ { "compilerOptions": { "target": "es5", + // "module": "system", "removeComments": true, "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, "strict": true, "jsx": "react", "allowJs": true, |