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