diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 36 | ||||
-rw-r--r-- | src/client/util/SearchUtil.ts | 4 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 53 | ||||
-rw-r--r-- | src/client/views/SearchItem.tsx | 69 | ||||
-rw-r--r-- | src/client/views/collections/CollectionVideoView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss | 2 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/globalCssVariables.scss | 1 | ||||
-rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 2 | ||||
-rw-r--r-- | src/client/views/search/IconBar.scss | 40 | ||||
-rw-r--r-- | src/client/views/search/IconBar.tsx | 224 | ||||
-rw-r--r-- | src/client/views/search/SearchBox.scss (renamed from src/client/views/SearchBox.scss) | 65 | ||||
-rw-r--r-- | src/client/views/search/SearchBox.tsx (renamed from src/client/views/SearchBox.tsx) | 166 | ||||
-rw-r--r-- | src/client/views/search/SearchItem.scss | 94 | ||||
-rw-r--r-- | src/client/views/search/SearchItem.tsx | 143 | ||||
-rw-r--r-- | src/client/views/search/ToggleBar.scss | 36 | ||||
-rw-r--r-- | src/client/views/search/ToggleBar.tsx | 75 |
17 files changed, 823 insertions, 191 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 2ae127e21..9d83f0e36 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -38,9 +38,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; @@ -150,54 +166,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, height: 150, backgroundColor: "#f1efeb" }); + { x: 0, y: 0, width: 300, height: 150, backgroundColor: "#f1efeb", type: DocTypes.TEXT }); return textProto; } function CreatePdfPrototype(): Doc { let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"), - { x: 0, y: 0, nativeWidth: 1200, width: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1 }); + { x: 0, y: 0, nativeWidth: 1200, width: 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/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 42d5929bf..307f23df1 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -23,16 +23,17 @@ 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'; import { HistoryUtil } from '../util/History'; import { CollectionBaseView } from './collections/CollectionBaseView'; - +import { timingSafeEqual } from 'crypto'; +import * as _ from "lodash"; @observer export class MainView extends React.Component { @@ -43,6 +44,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)) { @@ -52,6 +60,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; @@ -305,6 +322,36 @@ export class MainView extends React.Component { this.isSearchVisible = !this.isSearchVisible; } + @action + globalKeyHandler = (e: KeyboardEvent) => { + if (e.key === "Control" || !e.ctrlKey) 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/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/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 c699b3437..b6c566964 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/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 855c744e6..05088f688 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -15,7 +15,7 @@ import { Utils } from '../../../Utils'; import { DocServer } from "../../DocServer"; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; -import { SearchBox } from "../SearchBox"; +import { SearchBox } from "../search/SearchBox"; import { Annotation } from './Annotation'; import { positionSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; diff --git a/src/client/views/search/IconBar.scss b/src/client/views/search/IconBar.scss new file mode 100644 index 000000000..eb7b45754 --- /dev/null +++ b/src/client/views/search/IconBar.scss @@ -0,0 +1,40 @@ +@import "../globalCssVariables"; + + +.icon-bar{ + display: flex; + justify-content: space-evenly; + align-items: center; + height: 40px; + width: 100%; + flex-wrap: wrap; + font-size: 2em; +} + +.type-icon{ + height: 50px; + width: 50px; + color: $light-color; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + // background-color: "#121721"; +} + +.type-icon.selected{ + background-color: $alt-accent; +} + +.type-icon.not-selected{ + background-color: transparent; +} + +.fontawesome-icon{ + height: 28px; + width: 28px; +} + +.type-icon:hover{ + background-color: $alt-accent; +}
\ 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..bf98e1ef3 --- /dev/null +++ b/src/client/views/search/IconBar.tsx @@ -0,0 +1,224 @@ +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 } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import * as _ from "lodash"; +var classNames = require('classnames'); + +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); + +export interface IconBarProps { + updateIcon(icons: string[]): void; + getIcons(): string[]; +} + +@observer +export class IconBar extends React.Component<IconBarProps> { + + @observable noneRef = React.createRef<HTMLDivElement>(); + @observable colRef = React.createRef<HTMLDivElement>(); + @observable imgRef = React.createRef<HTMLDivElement>(); + @observable textRef = React.createRef<HTMLDivElement>(); + @observable pdfRef = React.createRef<HTMLDivElement>(); + @observable vidRef = React.createRef<HTMLDivElement>(); + @observable audioRef = React.createRef<HTMLDivElement>(); + @observable linkRef = React.createRef<HTMLDivElement>(); + @observable histRef = React.createRef<HTMLDivElement>(); + @observable webRef = React.createRef<HTMLDivElement>(); + @observable allRefs: React.RefObject<HTMLDivElement>[] = [this.colRef, this.imgRef, this.textRef, this.pdfRef, this.vidRef, this.audioRef, this.linkRef, this.histRef, this.webRef]; + + //gets ref associated with given string + @action.bound + getRef = (value: string) => { + let toReturn; + switch (value) { + case (DocTypes.NONE): + toReturn = this.noneRef.current; + break; + case (DocTypes.AUDIO): + toReturn = this.audioRef.current; + break; + case (DocTypes.COL): + toReturn = this.colRef.current; + break; + case (DocTypes.HIST): + toReturn = this.histRef.current; + break; + case (DocTypes.IMG): + toReturn = this.imgRef.current; + break; + case (DocTypes.LINK): + toReturn = this.linkRef.current; + break; + case (DocTypes.PDF): + toReturn = this.pdfRef.current; + break; + case (DocTypes.TEXT): + toReturn = this.textRef.current; + break; + case (DocTypes.VID): + toReturn = this.vidRef.current; + break; + case (DocTypes.WEB): + toReturn = this.webRef.current; + break; + default: + toReturn = null; + break; + } + + return toReturn; + } + + @action.bound + unselectAllRefs() { + this.allRefs.forEach(element => { + if (element.current) { + element.current.setAttribute("data-selected", "false"); + } + }); + } + + @action.bound + alternateRef(ref: any) { + if (ref.getAttribute("data-selected") === "true") { + ref.setAttribute("data-selected", "false") + } + else { + ref.setAttribute("data-selected", "true") + } + } + + @action.bound + onClick = (value: string) => { + let icons: string[] = this.props.getIcons() + let ref = this.getRef(value); + this.alternateRef(ref); + if (value === DocTypes.NONE) { + icons = []; + // if its none, change the color of all the other circles + this.unselectAllRefs(); + } + else { + //if it's already selected, unselect it + if (icons.includes(value)) { + icons = _.without(icons, value); + } + //if it's not yet selected, select it + else { + icons.push(value); + } + } + this.props.updateIcon(icons); + //ok i know that this is bad but i dont know how else to get it to rerender and change the classname, + //any help here is greatly appreciated thanks frens + this.forceUpdate(); + } + + //checks if option is selected based on the attribute data-selected + @action.bound + isRefSelected(ref: React.RefObject<HTMLDivElement>) { + if (ref.current) { + if (ref.current.getAttribute("data-selected") === "true") { + return true; + } + return false; + } + } + + render() { + + return ( + <div> + <div className="icon-bar"> + <div + className={"type-icon none"} + ref={this.noneRef} + data-selected={"true"} + onClick={() => { this.onClick(DocTypes.NONE) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: -2 }} icon={faBan} /> + </div> + <div + className={"type-icon " + (this.isRefSelected(this.pdfRef) ? "selected" : "not-selected")} + ref={this.pdfRef} + data-selected={"false"} + onClick={() => { this.onClick(DocTypes.PDF) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: 0 }} icon={faFilePdf} /> + </div> + <div + className={"type-icon " + (this.isRefSelected(this.histRef) ? "selected" : "not-selected")} + ref={this.histRef} + data-selected={"false"} + onClick={() => { this.onClick(DocTypes.HIST) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: 1 }} icon={faChartBar} /> + </div> + <div + className={"type-icon " + (this.isRefSelected(this.colRef) ? "selected" : "not-selected")} + ref={this.colRef} + data-selected={"false"} + onClick={() => { this.onClick(DocTypes.COL) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: 2 }} icon={faObjectGroup} /> + </div> + <div + className={"type-icon " + (this.isRefSelected(this.imgRef) ? "selected" : "not-selected")} + ref={this.imgRef} + data-selected={"false"} + onClick={() => { this.onClick(DocTypes.IMG) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: 3 }} icon={faImage} /> + </div> + <div + className={"type-icon " + (this.isRefSelected(this.vidRef) ? "selected" : "not-selected")} + ref={this.vidRef} + data-selected={"false"} + onClick={() => { this.onClick(DocTypes.VID) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: 4 }} icon={faFilm} /> + </div> + <div + className={"type-icon " + (this.isRefSelected(this.webRef) ?"selected" : "not-selected")} + ref={this.webRef} + data-selected={"false"} + onClick={() => { this.onClick(DocTypes.WEB) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: 5 }} icon={faGlobeAsia} /> + </div> + <div + className={"type-icon " + (this.isRefSelected(this.linkRef) ?"selected" : "not-selected")} + ref={this.linkRef} + data-selected={"false"} + onClick={() => { this.onClick(DocTypes.LINK) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: 6 }} icon={faLink} /> + </div> + <div + className={"type-icon " + (this.isRefSelected(this.audioRef) ? "selected" : "not-selected")} + ref={this.audioRef} + data-selected={"false"} + onClick={() => { this.onClick(DocTypes.AUDIO) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: 7 }} icon={faMusic} /> + </div> + <div + className={"type-icon " + (this.isRefSelected(this.textRef) ?"selected" : "not-selected")} + ref={this.textRef} + data-selected={"false"} + onClick={() => { this.onClick(DocTypes.TEXT) }}> + <FontAwesomeIcon className="fontawesome-icon" style={{ order: 8 }} icon={faStickyNote} /> + </div> + </div> + </div> + ) + } +} diff --git a/src/client/views/SearchBox.scss b/src/client/views/search/SearchBox.scss index b38e6091d..4ce40614f 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 { @@ -48,55 +39,45 @@ .filter-form { background: $dark-color; height: 400px; - width: 400px; position: relative; right: 1px; color: $light-color; - padding: 10px; flex-direction: column; } +#filter{ + padding: 25px; + width: 600px; +} + #header { text-transform: uppercase; letter-spacing: 2px; - font-size: 100%; + font-size: 25; height: 40px; } -#option { - height: 20px; -} +// #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; - } +.type-of-node{ + height: 60px; +} - .search-item:hover { - transition: all 0.1s; - background: $lighter-alt-accent; - } +.required-words{ + height: 110px; +} + +.filter-div{ + margin-top: 5px; + margin-bottom: 5px; +} - .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/search/SearchBox.tsx index 63d2065e2..329643464 100644 --- a/src/client/views/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,50 +1,47 @@ 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 { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faThList } 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); +import { DocServer } from '../../DocServer'; +import { Doc } 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 } 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 "./globalCssVariables.scss"; +import { findDOMNode } from 'react-dom'; +import { ToggleBar } from './ToggleBar'; +import { IconBar } from './IconBar'; + @observer export class SearchBox extends React.Component { - @observable - searchString: string = ""; - + @observable _searchString: string = ""; + @observable _wordStatus: boolean = true; + @observable _icons: string[] = []; @observable private _open: boolean = false; @observable private _resultsOpen: boolean = false; - - @observable - private _results: Doc[] = []; + @observable private _results: Doc[] = []; + @observable forceReRender: boolean = false; @action.bound onChange(e: React.ChangeEvent<HTMLInputElement>) { - this.searchString = e.target.value; + this._searchString = e.target.value; } @action submitSearch = async () => { - let query = this.searchString; + let query = this._searchString; //gets json result into a list of documents that can be used const results = await this.getResults(query); @@ -52,6 +49,7 @@ export class SearchBox extends React.Component { this._resultsOpen = true; this._results = results; }); + } @action @@ -72,6 +70,7 @@ export class SearchBox extends React.Component { } return docs; } + public static async convertDataUri(imageUri: string, returnedFilename: string) { try { let posting = DocServer.prepend(RouteStore.dataUriToImage); @@ -90,42 +89,49 @@ export class SearchBox extends React.Component { } @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") { + handleSearchClick = (e: Event): void => { + let element = document.getElementsByClassName((e.target as any).className)[0]; + let name: string = (e.target as any).className; + //handles case with filter button + if (String(name).indexOf("filter") !== -1 || String(name).indexOf("SVG") !== -1) { + this._resultsOpen = false; + this._open = true; + } + else if (element && element.parentElement) { + //if the filter element is found, show the form and hide the results + if (this.findAncestor(element, "filter-form")) { + this._resultsOpen = false; + this._open = true; + } + //if in main search div, keep results open and close filter + else if (this.findAncestor(element, "main-searchDiv")) { + this._resultsOpen = true; + this._open = false; + } + } + //not in either, close both + else { + this._resultsOpen = false; 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 = []; - } - + //finds ancestor div that matches class name passed in, if not found false returned + findAncestor(curElement: any, cls: string) { + while ((curElement = curElement.parentElement) && !curElement.classList.contains(cls)); + return curElement; } componentWillMount() { - document.addEventListener('mousedown', this.handleClickFilter, false); - document.addEventListener('mousedown', this.handleClickResults, false); + document.addEventListener('mousedown', this.handleSearchClick, false); } componentWillUnmount() { - document.removeEventListener('mousedown', this.handleClickFilter, false); - document.removeEventListener('mousedown', this.handleClickResults, false); + document.removeEventListener('mousedown', this.handleSearchClick, false); } - @action - toggleFilterDisplay = () => { - this._open = !this._open; - } - - enter = (e: React.KeyboardEvent<HTMLInputElement>) => { + enter = (e: React.KeyboardEvent) => { if (e.key === "Enter") { this.submitSearch(); } @@ -133,7 +139,7 @@ export class SearchBox extends React.Component { collectionRef = React.createRef<HTMLSpanElement>(); startDragCollection = async () => { - const results = await this.getResults(this.searchString); + const results = await this.getResults(this._searchString); const docs = results.map(doc => { const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); if (isProto) { @@ -166,7 +172,31 @@ export class SearchBox extends React.Component { y += 300; } } - return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` }); + return Docs.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; + } + + handleWordQueryChange = (value: boolean) => { + this._wordStatus = value; + } + + @action.bound + updateIcon(newArray: string[]) { + this._icons = newArray; + } + + @action.bound + getIcons(): string[] { + return this._icons; } // Useful queries: @@ -180,27 +210,37 @@ export class SearchBox extends React.Component { <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..." + <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" /> */} + style={{ width: this._resultsOpen ? "500px" : "100px" }} /> + <button className="searchBox-barChild searchBox-filter">Filter</button> </div> {this._resultsOpen ? ( <div className="searchBox-results"> {this._results.map(result => <SearchItem doc={result} key={result[Id]} />)} </div> - ) : null} + ) : undefined} </div> + {/* these all need class names in order to find ancestor - please do not delete */} {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 className="filter-form filter-div" id="header">Filter Search Results</div> + <div className="filter-form " id="option"> + <div className="required-words filter-div"> + <ToggleBar optionOne={"Include Any Keywords"} optionTwo={"Include All Keywords"} changeStatus={this.handleWordQueryChange} /> + </div> + <div className="type-of-node filter-div"> + <IconBar updateIcon={this.updateIcon} getIcons={this.getIcons}/> + </div> + <div className="filter-collection filter-div"> + temp for filtering by collection + </div> + <div className="where-in-doc filter-div"> + temp for filtering where in doc the keywords are found + </div> + </div> </div> - ) : null} + ) : undefined} </div> ); } diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss new file mode 100644 index 000000000..2d4c06a5c --- /dev/null +++ b/src/client/views/search/SearchItem.scss @@ -0,0 +1,94 @@ +@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: 100%; +} + +.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: 8vw; + font-weight: bold; +} + +.link-count { + height: 25px; + width: 25px; + border-radius: 50%; + background: $dark-color; + color: $light-color-secondary; + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; +} + +.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); +}
\ 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..9b4170f4c --- /dev/null +++ b/src/client/views/search/SearchItem.tsx @@ -0,0 +1,143 @@ +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 } 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"; + +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; + @observable hover = false; + + onClick = () => { + // DocumentManager.Instance.jumpToDocument(this.props.doc); + CollectionDockingView.Instance.AddRightSplit(this.props.doc); + } + + public 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); + } + } + + linkCount = () => { + return Cast(this.props.doc.linkedToDocs, listSpec(Doc), []).length + Cast(this.props.doc.linkedFromDocs, listSpec(Doc), []).length; + } + + render() { + return ( + <div className="search-overview"> + <div className="search-item" ref={this.collectionRef} id="result" onClick={this.onClick} onPointerDown={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-count">{this.linkCount()}</div> + <div className="search-type" >{this.DocumentIcon()}</div> + </div> + </div> + <div className="found">Where Found: (i.e. title, body, etc)</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..601b9a1dc --- /dev/null +++ b/src/client/views/search/ToggleBar.scss @@ -0,0 +1,36 @@ +@import "../globalCssVariables"; + +.toggle{ + +} + +.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: 100px; + text-align: center; + } +} + +.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..2fc9c0040 --- /dev/null +++ b/src/client/views/search/ToggleBar.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction } from 'mobx'; +import "./SearchBox.scss"; +import "./ToggleBar.scss"; +import * as anime from 'animejs'; + +export interface ToggleBarProps { + //false = right, true = left + // status: boolean; + changeStatus(value: boolean): void; + optionOne: string; + optionTwo: string; +} + +//TODO: justify content will align to specific side. Maybe do status passed in and out? +@observer +export class ToggleBar extends React.Component<ToggleBarProps>{ + + @observable _status: boolean = false; + @observable timeline: anime.AnimeTimelineInstance; + @observable _toggleButton: React.RefObject<HTMLDivElement>; + + constructor(props: ToggleBarProps) { + super(props); + this._toggleButton = React.createRef(); + this.timeline = anime.timeline({ + autoplay: false, + direction: "reverse" + }); + } + + componentDidMount = () => { + + 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); + + this.timeline.add({ + targets: this._toggleButton.current, + loop: false, + translateX: totalWidth, + easing: "easeInOutQuad", + duration: 500 + }); + } + + @action.bound + onclick() { + this._status = !this._status; + this.props.changeStatus(this._status); + this.timeline.play(); + this.timeline.reverse(); + } + + render() { + return ( + <div> + <div className="toggle-title"> + <div className="toggle-option">{this.props.optionOne}</div> + <div className="toggle-option">{this.props.optionTwo}</div> + </div> + <div className="toggle-bar" id="toggle-bar"> + <div className="toggle-button" id="toggle-button" ref={this._toggleButton} onClick={this.onclick} /> + </div> + </div> + ); + }; +}
\ No newline at end of file |