diff options
Diffstat (limited to 'src')
29 files changed, 1987 insertions, 339 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index de6c5bc6a..1d137b9ff 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('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,18 +163,18 @@ 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 { @@ -174,28 +189,28 @@ export namespace Docs { } 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 862395d74..cd4e90f2d 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 51630c29b..f61b5290f 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 { SearchBox } from './search/SearchBox'; 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'; @@ -35,6 +35,7 @@ import { HistoryUtil } from '../util/History'; import { CollectionBaseView } from './collections/CollectionBaseView'; import PDFMenu from './pdf/PDFMenu'; import { InkTool } from '../../new_fields/InkField'; +import * as _ from "lodash"; @observer @@ -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; @@ -332,6 +349,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.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 6901f60da..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); - } - - //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/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index de5c66c0b..30d68fffd 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -261,7 +261,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._isPointerDown = true; let onPointerUp = action(() => { window.removeEventListener("pointerup", onPointerUp) - this._isPointerDown = false + this._isPointerDown = false; }) window.addEventListener("pointerup", onPointerUp); var className = (e.target as any).className; 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..ee7a9f64f 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/SearchBox"; 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 d2de1cb1c..07f4733ba 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 { SearchBox } from "../search/SearchBox"; +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..9b0af68d5 --- /dev/null +++ b/src/client/views/search/CheckBox.scss @@ -0,0 +1,58 @@ +@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-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; + } + + .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; + } + +} + +.checkbox-title { + display: flex; + align-items: center; + text-transform: capitalize; + margin-left: 15px; +}
\ No newline at end of file diff --git a/src/client/views/search/CheckBox.tsx b/src/client/views/search/CheckBox.tsx new file mode 100644 index 000000000..c75980e7c --- /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 _status: boolean; + @observable uncheckTimeline: anime.AnimeTimelineInstance; + @observable checkTimeline: anime.AnimeTimelineInstance; + @observable checkRef: any; + @observable _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..8d5ab96e7 --- /dev/null +++ b/src/client/views/search/CollectionFilters.scss @@ -0,0 +1,20 @@ +@import "../globalCssVariables"; + +.collection-filters{ + display: flex; + flex-direction: row; + width: 100%; +} + +.collection-filters.main{ + width: 47%; + float: left; +} + +.collection-filters.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..5bfe37efb --- /dev/null +++ b/src/client/views/search/CollectionFilters.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction } from 'mobx'; +import "./SearchBox.scss"; +import { CheckBox } from './CheckBox'; +import { Keys } from './SearchBox'; +import "./SearchBox.scss"; +import "./CollectionFilters.scss"; +import { FieldFilters } from './FieldFilters'; +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 collectionsSelected = this.props.collectionStatus; + @observable timeline: anime.AnimeTimelineInstance; + @observable 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-title'>Search in current collections</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.tsx b/src/client/views/search/FieldFilters.tsx new file mode 100644 index 000000000..c7928dcd6 --- /dev/null +++ b/src/client/views/search/FieldFilters.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction, } from 'mobx'; +import "./SearchBox.scss"; +import { CheckBox } from './CheckBox'; +import { Keys } from './SearchBox'; +import "./SearchBox.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> + <div className="filter field-title">Filter by Basic Keys</div> + <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/IconBar.scss b/src/client/views/search/IconBar.scss new file mode 100644 index 000000000..db4e49fe9 --- /dev/null +++ b/src/client/views/search/IconBar.scss @@ -0,0 +1,71 @@ +@import "../globalCssVariables"; + + +.icon-bar { + display: flex; + justify-content: space-evenly; + align-items: center; + height: 40px; + width: 100%; + flex-wrap: wrap; +} + +.icon-title { + color: $light-color; + text-transform: uppercase; + padding: 5px; +} + +.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; +} + +.type-icon:hover { + transform: scale(1.1); + background-color: $darker-alt-accent; + opacity: 1; + + +.filter-description { + opacity: 1; + } +} + +.type-outer { + display: flex; + flex-direction: column; + align-items: center; + width: 45px; + height: 60px; +} + +.filter-description { + 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; +}
\ No newline at end of file diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx new file mode 100644 index 000000000..2ae4af642 --- /dev/null +++ b/src/client/views/search/IconBar.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction } from 'mobx'; +import "./SearchBox.scss"; +import "./IconBar.scss"; +import * as anime from 'animejs'; +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, icon } from '@fortawesome/fontawesome-svg-core'; +import * as _ from "lodash"; +import $ from 'jquery'; +import { array } from 'prop-types'; +import { IconButton } from './IconButton'; +import { list } from 'serializr'; +import { SearchBox } from './SearchBox'; + +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; + + allIcons: string[] = [DocTypes.AUDIO, DocTypes.COL, DocTypes.HIST, DocTypes.IMG, DocTypes.LINK, DocTypes.PDF, DocTypes.TEXT, DocTypes.VID, DocTypes.WEB]; + @observable public ResetClicked: boolean = false; + @observable public SelectAllClicked: boolean = false; + public Reset: number = 0; + public Select: number = 0; + + constructor(props: any) { + super(props); + IconBar.Instance = this; + } + + @action.bound + getList = (): string[] => { + return SearchBox.Instance.getIcons(); + } + + @action.bound + updateList(newList: string[]) { + SearchBox.Instance.updateIcon(newList); + } + + @action.bound + resetSelf = () => { + this.ResetClicked = true; + this.updateList([]); + } + + @action.bound + selectAll = () => { + this.SelectAllClicked = true; + this.updateList(this.allIcons); + } + + render() { + return ( + <div> + <div className="filter icon-title">Filter by type of node</div> + <div className="filter icon-bar"> + <div className="filter type-outer"> + <div className={"type-icon none not-selected"} + onClick={this.selectAll}> + <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} /> + </div> + <div className="filter-description">Select All</div> + </div> + {this.allIcons.map((type: string) => + <IconButton type={type}/> + )} + <div className="filter type-outer"> + <div className={"type-icon none not-selected"} + onClick={this.resetSelf}> + <FontAwesomeIcon className="fontawesome-icon" icon={faTimesCircle} /> + </div> + <div className="filter-description">Clear</div> + </div> + </div> + </div> + ); + } +} diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx new file mode 100644 index 000000000..9df285b70 --- /dev/null +++ b/src/client/views/search/IconButton.tsx @@ -0,0 +1,201 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx'; +import "./SearchBox.scss"; +import "./IconBar.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 { SearchBox } from './SearchBox'; +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 isSelected: boolean = SearchBox.Instance.getIcons().indexOf(this.props.type) !== -1; + @observable 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[] = SearchBox.Instance.getIcons(); + + if (!this.isSelected) { + this.isSelected = true; + newList.push(this.props.type); + } + else { + this.isSelected = false; + _.pull(newList, this.props.type); + } + + SearchBox.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..529f11a57 --- /dev/null +++ b/src/client/views/search/NaviconButton.scss @@ -0,0 +1,95 @@ +@import "../globalCssVariables"; + + +@import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); + +// $background: #3d566e; +$color: #ecf0f1; + +$height-icon: 20px; +$width-line: 30px; +$height-line: 4px; + +$transition-time: 0.4s; +$rotation: 45deg; +$translateY: ($height-icon / 2); +$translateX: 0; + +// body { +// background: $background; +// color: $color; +// font-family: 'Montserrat', sans-serif; +// -webkit-font-smoothing: antialiased; +// text-align:center; +// } + +#hamburger-icon { + width:$width-line; + height:$height-icon; + position:relative; + display:block; +// margin: ($height-icon * 2) auto $height-icon auto; + + .line { + display:block; + background:$color; + 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%; + } + } + &:hover, &:focus { + .line-1 { + transform: translateY($height-line / 2 * -1); + -webkit-transform: translateY($height-line / 2 * -1); + -moz-transform: translateY($height-line / 2 * -1); + } + .line-3 { + transform: translateY($height-line / 2); + -webkit-transform: translateY($height-line / 2); + -moz-transform: translateY($height-line / 2); + } + } + &.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); + } + } +} + +// h1 { +// text-transform:uppercase; +// } +// a { +// text-decoration:none; +// color:#95a5a6; +// margin: 0.5em 1.5em; +// display:inline-block; +// &:hover, &:focus { +// color:$color; +// } +// }
\ 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..aa9e627c0 --- /dev/null +++ b/src/client/views/search/NaviconButton.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import "./NaviconButton.scss"; +import * as $ from 'jquery'; + + +export class NaviconButton extends React.Component { + + componentDidMount = () => { + $(document).ready(function () { + var hamburger = $('#hamburger-icon'); + hamburger.click(function () { + hamburger.toggleClass('active'); + return false; + }); + }); + } + + render() { + return ( + <a id="hamburger-icon" href="#" 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/SearchBox.scss b/src/client/views/search/SearchBox.scss index b38e6091d..3b96e3922 100644 --- a/src/client/views/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -1,4 +1,4 @@ -@import "globalCssVariables"; +@import "../globalCssVariables"; .searchBox-bar { height: 32px; @@ -30,15 +30,6 @@ align-self: stretch; } - .searchBox-submit { - color: $dark-color; - } - - .searchBox-submit:hover { - color: $main-accent; - transform: scale(1.05); - cursor: pointer; - } } .searchBox-results { @@ -47,56 +38,78 @@ .filter-form { background: $dark-color; - height: 400px; - width: 400px; + // height: 400px; + height: auto; + overflow: auto; position: relative; right: 1px; color: $light-color; - padding: 10px; flex-direction: column; + display: inline-block; +} + +#filter{ + padding: 25px; + width: 600px; } #header { text-transform: uppercase; letter-spacing: 2px; - font-size: 100%; + font-size: 25; height: 40px; } -#option { - height: 20px; -} - .searchBox-results { top: 300px; display: flex; flex-direction: column; + margin-right: 72px; +} - .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; - } +.filter-collection{ + display: inline-block; + width: 100%; +} - .search-item:hover { - transition: all 0.1s; - background: $lighter-alt-accent; - } +.field-title { + color: $light-color; + text-transform: uppercase; + margin-top: 5px; + margin-bottom: 5px; +} + +.type-of-node{ + height: 90px; +} + +.required-words{ + display: inline-block; + width: 100%; +} + +.filter-div{ + margin-top: 5px; + margin-bottom: 5px; +} + +.collection-title{ + color: $light-color; + text-transform: uppercase; + margin-top: 5px; + margin-bottom: 5px; +} + +.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; + // width: 80%; + font-weight: bold; +} - .search-title { - text-transform: uppercase; - text-align: left; - width: 8vw; - } -}
\ 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..71472886f --- /dev/null +++ b/src/client/views/search/SearchBox.tsx @@ -0,0 +1,518 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction } from 'mobx'; +import "./SearchBox.scss"; +import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faThList, faWineGlassAlt } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import * as rp from 'request-promise'; +import { SearchItem } from './SearchItem'; +import { DocServer } from '../../DocServer'; +import { Doc, Opt } from '../../../new_fields/Doc'; +import { Id } from '../../../new_fields/FieldSymbols'; +import { SetupDrag } from '../../util/DragManager'; +import { Docs, DocTypes } from '../../documents/Documents'; +import { RouteStore } from '../../../server/RouteStore'; +import { NumCast, Cast, StrCast } from '../../../new_fields/Types'; +import { SearchUtil } from '../../util/SearchUtil'; +import * as anime from 'animejs'; +import { updateFunction } from '../../../new_fields/util'; +import * as _ from "lodash"; +import { findDOMNode } from 'react-dom'; +import { ToggleBar } from './ToggleBar'; +import { IconBar } from './IconBar'; +import { type } from 'os'; +import { CheckBox } from './CheckBox'; +import { FieldFilters } from './FieldFilters'; +import { SelectionManager } from '../../util/SelectionManager'; +import { DocumentView } from '../nodes/DocumentView'; +import { CollectionView } from '../collections/CollectionView'; +import { CollectionPDFView } from '../collections/CollectionPDFView'; +import { CollectionVideoView } from '../collections/CollectionVideoView'; +import { CollectionFilters } from './CollectionFilters'; +import { NaviconButton } from './NaviconButton'; + +export enum Keys { + TITLE = "title", + AUTHOR = "author", + DATA = "data" +} + +@observer +export class SearchBox extends React.Component { + + static Instance: SearchBox; + + @observable _searchString: string = ""; + //if true, any keywords can be used. if false, all keywords are required. + @observable _basicWordStatus: boolean = true; + @observable private _filterOpen: boolean = false; + @observable private _resultsOpen: boolean = false; + @observable private _results: Doc[] = []; + @observable private _openNoResults: boolean = false; + allIcons: string[] = [DocTypes.AUDIO, DocTypes.COL, DocTypes.HIST, DocTypes.IMG, DocTypes.LINK, DocTypes.PDF, DocTypes.TEXT, DocTypes.VID, DocTypes.WEB]; + @observable _icons: string[] = this.allIcons; + @observable _selectedTypes: any[] = []; + @observable titleFieldStatus: boolean = true; + @observable authorFieldStatus: boolean = true; + @observable dataFieldStatus: boolean = true; + @observable collectionStatus = false; + @observable collectionSelfStatus = true; + @observable collectionParentStatus = true; + + constructor(props: Readonly<{}>) { + super(props); + SearchBox.Instance = this; + } + + componentDidMount = () => { + document.addEventListener("pointerdown", (e) => { + if (e.timeStamp !== this._pointerTime) { + this.closeSearch(); + } + }); + + //empties search query after 30 seconds of the search bar/filter box not being open + // if (!this._resultsOpen && !this._filterOpen) { + // setTimeout(this.clearSearchQuery, 30000); + // } + } + + @action.bound + resetFilters = () => { + ToggleBar.Instance.resetToggle(); + IconBar.Instance.selectAll(); + FieldFilters.Instance.resetFieldFilters(); + CollectionFilters.Instance.resetCollectionFilters(); + } + + @action.bound + onChange(e: React.ChangeEvent<HTMLInputElement>) { + this._searchString = e.target.value; + + if (this._searchString === "") { + this._results = []; + this._openNoResults = false; + } + } + + @action.bound + clearSearchQuery() { + this._searchString = ""; + this._results = []; + } + + 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); } + + //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[] = []; + + //TODO: make this some sort of error + if (selectedDocs.length === 0) { + console.log("there is nothing selected!") + } + //also searches in a collection if it is selected + else { + selectedDocs.forEach(async element => { + let layout: string = StrCast(element.props.Document.baseLayout); + // console.log("doc title:", element.props.Document.title) + //checks if selected view (element) is a collection. if it is, adds to list to search through + if (layout.indexOf("Collection") > -1) { + console.log("current doc is a collection") + // console.log(element.props.Document) + //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); + console.log(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 + submitSearch = async () => { + let query = this._searchString; + let results: Doc[]; + + query = this.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 + 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 this.filterDocsByType(docs); + } + + //this.icons will now include all the icons that need to be included + @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; + } + + 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); + } + } + + enter = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + this.submitSearch(); + } + } + + @action.bound + closeSearch = () => { + this._filterOpen = false; + this._resultsOpen = false; + this._results = []; + } + + @action + openFilter = () => { + this._filterOpen = !this._filterOpen; + this._resultsOpen = false; + this._results = []; + } + + 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}"` }); + } + + @action + getViews = async (doc: Doc) => { + const results = await SearchUtil.GetViewsOfDocument(doc); + let toReturn: Doc[] = []; + await runInAction(() => { + toReturn = results; + }); + return toReturn; + } + + //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; + } + + private _pointerTime: number = -1; + + stopProp = (e: React.PointerEvent) => { + e.stopPropagation(); + this._pointerTime = e.timeStamp; + } + + @action.bound + openSearch(e: React.PointerEvent) { + e.stopPropagation(); + this._openNoResults = false; + this._filterOpen = false; + this._resultsOpen = true; + this._pointerTime = e.timeStamp; + } + + @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 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" onPointerDown={this.openSearch} onKeyPress={this.enter} + style={{ width: this._resultsOpen ? "500px" : "100px" }} /> + <button className="searchBox-barChild searchBox-filter" onClick={this.openFilter} onPointerDown={this.stopProp}>Filter</button> + </div> + {this._resultsOpen ? ( + <div className="searchBox-results"> + {(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> + ) : undefined} + </div> + {this._filterOpen ? ( + <div className="filter-form" onPointerDown={this.stopProp} id="filter" style={this._filterOpen ? { display: "flex" } : { display: "none" }}> + <div className="filter-form filter-div" id="header">Filter Search Results</div> + <div className="filter-form " id="option"> + <div className="required-words filter-div"> + <ToggleBar originalStatus={this._basicWordStatus} optionOne={"Include Any Keywords"} optionTwo={"Include All Keywords"} /> + </div> + <div className="type-of-node filter-div"> + <IconBar /> + </div> + <div className="filter-collection filter-div"> + <CollectionFilters + updateCollectionStatus = {this.updateCollectionStatus} updateParentCollectionStatus = {this.updateParentCollectionStatus} updateSelfCollectionStatus = {this.updateSelfCollectionStatus} + collectionStatus = {this.collectionStatus} collectionParentStatus = {this.collectionParentStatus} collectionSelfStatus = {this.collectionSelfStatus}/> + </div> + <div className="where-in-doc filter-div"> + <FieldFilters + titleFieldStatus={this.titleFieldStatus} dataFieldStatus={this.dataFieldStatus} authorFieldStatus={this.authorFieldStatus} + updateAuthorStatus={this.updateAuthorStatus} updateDataStatus={this.updateDataStatus} updateTitleStatus={this.updateTitleStatus} /> + </div> + </div> + <button className="reset-filter" onClick={this.resetFilters}>Reset Filters</button> + </div> + ) : undefined} + </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..6e88d1f44 --- /dev/null +++ b/src/client/views/search/SearchItem.scss @@ -0,0 +1,149 @@ +@import "../globalCssVariables"; + +.search-item { + width: 500px; + background: $light-color-secondary; + border-color: $intermediate-color; + border-bottom-style: solid; + padding: 10px; + height: 70px; +} + +.search-info { + display: flex; + justify-content: flex-end; + width: 40%; +} + +.main-search-info { + display: flex; + flex-direction: row; + width: 100%; +} + +.search-item:hover { + transition: all 0.2s; + background: $lighter-alt-accent; +} + +.search-title { + text-transform: uppercase; + text-align: left; + width: 80%; + font-weight: bold; +} + +.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; + margin-right: 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; + transform-origin: top right; + overflow: hidden; + + .link-count { + opacity: 1; + position: absolute; + z-index: 1000; + -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: absolute; + 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; +} + +.search-type { + width: 25PX; + height: 25PX; + display: flex; + justify-content: center; + align-items: center; +} + + +.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% +} + +.collection { + border-color: $darker-alt-accent; + border-bottom-style: solid; +} + +.search-overview { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + height: 70px; +} + +.parents { + background: $lighter-alt-accent; + padding: 10px; +} + +.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-label{ + text-align: right; + float: right; + text-transform: capitalize; + opacity: 0; + -webkit-transition: opacity 0.2s ease-in-out; + -moz-transition: opacity 0.2s ease-in-out; + -o-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; +} + +.search-type:hover +.search-label{ + opacity: 1; +}
\ 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..c19057819 --- /dev/null +++ b/src/client/views/search/SearchItem.tsx @@ -0,0 +1,180 @@ +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 { SearchBox } from "./SearchBox"; +import { DocumentView } from "../nodes/DocumentView"; + +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); + }; + } + + //these all need class names in order to find ancestor - please do not delete + 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/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..6f141d42a --- /dev/null +++ b/src/client/views/search/ToggleBar.tsx @@ -0,0 +1,100 @@ +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'; +import { SearchBox } from './SearchBox'; + +export interface ToggleBarProps { + originalStatus: boolean; + optionOne: string; + optionTwo: string; +} + +@observer +export class ToggleBar extends React.Component<ToggleBarProps>{ + static Instance: ToggleBar; + + @observable forwardTimeline: anime.AnimeTimelineInstance; + @observable _toggleButton: React.RefObject<HTMLDivElement>; + @observable _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", + }); + } + + @computed get totalWidth() { return this.getTotalWidth(); } + + getTotalWidth() { + let bar = document.getElementById("toggle-bar"); + let tog = document.getElementById("toggle-button"); + let barwidth = 0; + let togwidth = 0; + if (bar && tog) { + barwidth = bar.clientWidth; + togwidth = tog.clientWidth; + } + let totalWidth = (barwidth - togwidth - 10); + return totalWidth; + } + + componentDidMount = () => { + + let totalWidth = this.totalWidth; + + 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(); + SearchBox.Instance.handleWordQueryChange(); + } + + @action.bound + public resetToggle = () => { + if (!SearchBox.Instance.getBasicWordStatus()) { + this.forwardTimeline.play(); + this.forwardTimeline.reverse(); + SearchBox.Instance.handleWordQueryChange(); + } + } + + render() { + return ( + <div> + <div className="toggle-title"> + <div className="toggle-option" style={{ opacity: (SearchBox.Instance.getBasicWordStatus() ? 1 : .4) }}>{this.props.optionOne}</div> + <div className="toggle-option" style={{ opacity: (SearchBox.Instance.getBasicWordStatus() ? .4 : 1) }}>{this.props.optionTwo}</div> + </div> + <div className="toggle-bar" id="toggle-bar" style={{ flexDirection: (this._originalStatus ? "row" : "row-reverse") }}> + <div className="toggle-button" id="toggle-button" ref={this._toggleButton} onClick={this.onclick} /> + </div> + </div> + ); + } +}
\ No newline at end of file |