aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts37
-rw-r--r--src/client/util/DocumentManager.ts24
-rw-r--r--src/client/util/SearchUtil.ts4
-rw-r--r--src/client/views/MainView.tsx56
-rw-r--r--src/client/views/SearchBox.tsx207
-rw-r--r--src/client/views/SearchItem.tsx69
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx2
-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.scss58
-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.tsx90
-rw-r--r--src/client/views/search/FieldFilters.tsx43
-rw-r--r--src/client/views/search/IconBar.scss71
-rw-r--r--src/client/views/search/IconBar.tsx94
-rw-r--r--src/client/views/search/IconButton.tsx201
-rw-r--r--src/client/views/search/NaviconButton.scss95
-rw-r--r--src/client/views/search/NaviconButton.tsx28
-rw-r--r--src/client/views/search/SearchBox.scss (renamed from src/client/views/SearchBox.scss)101
-rw-r--r--src/client/views/search/SearchBox.tsx518
-rw-r--r--src/client/views/search/SearchItem.scss149
-rw-r--r--src/client/views/search/SearchItem.tsx180
-rw-r--r--src/client/views/search/ToggleBar.scss36
-rw-r--r--src/client/views/search/ToggleBar.tsx100
29 files changed, 1987 insertions, 339 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index de6c5bc6a..1d137b9ff 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -37,9 +37,25 @@ import { RouteStore } from "../../server/RouteStore";
var requestImageSize = require('request-image-size');
var path = require('path');
+export enum DocTypes {
+ NONE = "none",
+ IMG = "image",
+ HIST = "histogram",
+ ICON = "icon",
+ TEXT = "text",
+ PDF = "pdf",
+ WEB = "web",
+ COL = "collection",
+ KVP = "kvp",
+ VID = "video",
+ AUDIO = "audio",
+ LINK = "link"
+}
+
export interface DocumentOptions {
x?: number;
y?: number;
+ type?: string;
ink?: InkField;
width?: number;
height?: number;
@@ -71,7 +87,8 @@ export namespace DocUtils {
let protoSrc = source.proto ? source.proto : source;
let protoTarg = target.proto ? target.proto : target;
UndoManager.RunInBatch(() => {
- let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
+ let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1});
+ // linkDoc.type = DocTypes.LINK;
let linkDocProto = Doc.GetProto(linkDoc);
linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
linkDocProto.linkDescription = description;
@@ -92,8 +109,6 @@ export namespace DocUtils {
return linkDoc;
}, "make link");
}
-
-
}
export namespace Docs {
@@ -148,18 +163,18 @@ export namespace Docs {
function CreateImagePrototype(): Doc {
let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("annotations"),
- { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: ImageBox.LayoutString(), curPage: 0 });
+ { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: ImageBox.LayoutString(), curPage: 0, type: DocTypes.IMG });
return imageProto;
}
function CreateHistogramPrototype(): Doc {
let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"),
- { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString() });
+ { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString(), type: DocTypes.HIST });
return histoProto;
}
function CreateIconPrototype(): Doc {
let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(),
- { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) });
+ { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), type: DocTypes.ICON });
return iconProto;
}
function CreateTextPrototype(): Doc {
@@ -174,28 +189,28 @@ export namespace Docs {
}
function CreateWebPrototype(): Doc {
let webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 300 });
+ { x: 0, y: 0, width: 300, height: 300, type: DocTypes.WEB });
return webProto;
}
function CreateCollectionPrototype(): Doc {
let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"),
- { panX: 0, panY: 0, scale: 1, width: 500, height: 500 });
+ { panX: 0, panY: 0, scale: 1, width: 500, height: 500, type: DocTypes.COL });
return collProto;
}
function CreateKVPPrototype(): Doc {
let kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150 });
+ { x: 0, y: 0, width: 300, height: 150, type: DocTypes.KVP });
return kvpProto;
}
function CreateVideoPrototype(): Doc {
let videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("annotations"),
- { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: VideoBox.LayoutString(), curPage: 0 });
+ { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: VideoBox.LayoutString(), curPage: 0, type: DocTypes.VID });
return videoProto;
}
function CreateAudioPrototype(): Doc {
let audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150 });
+ { x: 0, y: 0, width: 300, height: 150, type: DocTypes.AUDIO });
return audioProto;
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 862395d74..cd4e90f2d 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -30,6 +30,30 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
+ //gets all views
+ public getDocumentViewsById(id: string) {
+ let toReturn: DocumentView[] = [];
+ DocumentManager.Instance.DocumentViews.map(view => {
+ if (view.props.Document[Id] === id) {
+ toReturn.push(view);
+ }
+ });
+ if (toReturn.length === 0) {
+ DocumentManager.Instance.DocumentViews.map(view => {
+ let doc = view.props.Document.proto;
+ if (doc && doc[Id]) {
+ if(doc[Id] === id)
+ {toReturn.push(view);}
+ }
+ });
+ }
+ return toReturn;
+ }
+
+ public getAllDocumentViews(doc: Doc){
+ return this.getDocumentViewsById(doc[Id]);
+ }
+
public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
let toReturn: DocumentView | null = null;
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 28ec8ca14..27d27a3b8 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -23,4 +23,8 @@ export namespace SearchUtil {
return Search(`proto_i:"${protoId}"`, true);
// return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
}
+
+ export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> {
+ return Search(`proto_i:"${doc[Id]}"`, true);
+ }
} \ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 51630c29b..f61b5290f 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -11,7 +11,7 @@ import * as request from 'request';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { RouteStore } from '../../server/RouteStore';
import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
-import { Docs } from '../documents/Documents';
+import { Docs, DocTypes } from '../documents/Documents';
import { SetupDrag, DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
import { UndoManager } from '../util/UndoManager';
@@ -24,10 +24,10 @@ import "./Main.scss";
import { MainOverlayTextBox } from './MainOverlayTextBox';
import { DocumentView } from './nodes/DocumentView';
import { PreviewCursor } from './PreviewCursor';
-import { SearchBox } from './SearchBox';
+import { SearchBox } from './search/SearchBox';
import { SelectionManager } from '../util/SelectionManager';
import { FieldResult, Field, Doc, Opt, DocListCast } from '../../new_fields/Doc';
-import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
+import { Cast, FieldValue, StrCast, PromiseValue } from '../../new_fields/Types';
import { DocServer } from '../DocServer';
import { listSpec } from '../../new_fields/Schema';
import { Id } from '../../new_fields/FieldSymbols';
@@ -35,6 +35,7 @@ import { HistoryUtil } from '../util/History';
import { CollectionBaseView } from './collections/CollectionBaseView';
import PDFMenu from './pdf/PDFMenu';
import { InkTool } from '../../new_fields/InkField';
+import * as _ from "lodash";
@observer
@@ -46,6 +47,13 @@ export class MainView extends React.Component {
@computed private get mainContainer(): Opt<Doc> {
return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
}
+ @computed private get mainFreeform(): Opt<Doc> {
+ let docs = DocListCast(this.mainContainer!.data);
+ return (docs && docs.length > 1) ? docs[1] : undefined;
+ }
+ private globalDisplayFlags = observable({
+ jumpToVisible: false
+ });
private set mainContainer(doc: Opt<Doc>) {
if (doc) {
if (!("presentationView" in doc)) {
@@ -55,6 +63,15 @@ export class MainView extends React.Component {
}
}
+ componentWillMount() {
+ document.removeEventListener("keydown", this.globalKeyHandler);
+ document.addEventListener("keydown", this.globalKeyHandler);
+ }
+
+ componentWillUnMount() {
+ document.removeEventListener("keydown", this.globalKeyHandler);
+ }
+
constructor(props: Readonly<{}>) {
super(props);
MainView.Instance = this;
@@ -332,6 +349,39 @@ export class MainView extends React.Component {
this.isSearchVisible = !this.isSearchVisible;
}
+ @action
+ globalKeyHandler = (e: KeyboardEvent) => {
+ if (e.key === "Control" || !e.ctrlKey) return;
+
+ if(e.key === "v") return;
+ if(e.key === "c") return;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ switch (e.key) {
+ case "ArrowRight":
+ if (this.mainFreeform) {
+ CollectionDockingView.Instance.AddRightSplit(this.mainFreeform!);
+ }
+ break;
+ case "ArrowLeft":
+ if (this.mainFreeform) {
+ CollectionDockingView.Instance.CloseRightSplit(this.mainFreeform!);
+ }
+ break;
+ case "o":
+ this.globalDisplayFlags.jumpToVisible = true;
+ break;
+ case "escape":
+ _.mapValues(this.globalDisplayFlags, () => false);
+ break;
+ case "f":
+ this.isSearchVisible = !this.isSearchVisible;
+ }
+ }
+
+
render() {
return (
<div id="main-div">
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
deleted file mode 100644
index 63d2065e2..000000000
--- a/src/client/views/SearchBox.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import * as React from 'react';
-import { observer } from 'mobx-react';
-import { observable, action, runInAction } from 'mobx';
-import { Utils } from '../../Utils';
-import { MessageStore } from '../../server/Message';
-import "./SearchBox.scss";
-import { faSearch, faObjectGroup } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { library } from '@fortawesome/fontawesome-svg-core';
-// const app = express();
-// import * as express from 'express';
-import { Search } from '../../server/Search';
-import * as rp from 'request-promise';
-import { SearchItem } from './SearchItem';
-import { isString } from 'util';
-import { constant } from 'async';
-import { DocServer } from '../DocServer';
-import { Doc } from '../../new_fields/Doc';
-import { Id } from '../../new_fields/FieldSymbols';
-import { DocumentManager } from '../util/DocumentManager';
-import { SetupDrag } from '../util/DragManager';
-import { Docs } from '../documents/Documents';
-import { RouteStore } from '../../server/RouteStore';
-import { NumCast } from '../../new_fields/Types';
-
-library.add(faSearch);
-library.add(faObjectGroup);
-
-@observer
-export class SearchBox extends React.Component {
- @observable
- searchString: string = "";
-
- @observable private _open: boolean = false;
- @observable private _resultsOpen: boolean = false;
-
- @observable
- private _results: Doc[] = [];
-
- @action.bound
- onChange(e: React.ChangeEvent<HTMLInputElement>) {
- this.searchString = e.target.value;
- }
-
- @action
- submitSearch = async () => {
- let query = this.searchString;
- //gets json result into a list of documents that can be used
- const results = await this.getResults(query);
-
- runInAction(() => {
- this._resultsOpen = true;
- this._results = results;
- });
- }
-
- @action
- getResults = async (query: string) => {
- let response = await rp.get(DocServer.prepend('/search'), {
- qs: {
- query
- }
- });
- let res: string[] = JSON.parse(response);
- const fields = await DocServer.GetRefFields(res);
- const docs: Doc[] = [];
- for (const id of res) {
- const field = fields[id];
- if (field instanceof Doc) {
- docs.push(field);
- }
- }
- return docs;
- }
- public static async convertDataUri(imageUri: string, returnedFilename: string) {
- try {
- let posting = DocServer.prepend(RouteStore.dataUriToImage);
- const returnedUri = await rp.post(posting, {
- body: {
- uri: imageUri,
- name: returnedFilename
- },
- json: true,
- });
- return returnedUri;
-
- } catch (e) {
- console.log(e);
- }
- }
-
- @action
- handleClickFilter = (e: Event): void => {
- var className = (e.target as any).className;
- var id = (e.target as any).id;
- if (className !== "filter-button" && className !== "filter-form") {
- this._open = false;
- }
-
- }
-
- @action
- handleClickResults = (e: Event): void => {
- var className = (e.target as any).className;
- var id = (e.target as any).id;
- if (id !== "result") {
- this._resultsOpen = false;
- this._results = [];
- }
-
- }
-
- componentWillMount() {
- document.addEventListener('mousedown', this.handleClickFilter, false);
- document.addEventListener('mousedown', this.handleClickResults, false);
- }
-
- componentWillUnmount() {
- document.removeEventListener('mousedown', this.handleClickFilter, false);
- document.removeEventListener('mousedown', this.handleClickResults, false);
- }
-
- @action
- toggleFilterDisplay = () => {
- this._open = !this._open;
- }
-
- enter = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
- this.submitSearch();
- }
- }
-
- collectionRef = React.createRef<HTMLSpanElement>();
- startDragCollection = async () => {
- const results = await this.getResults(this.searchString);
- const docs = results.map(doc => {
- const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
- if (isProto) {
- return Doc.MakeDelegate(doc);
- } else {
- return Doc.MakeAlias(doc);
- }
- });
- let x = 0;
- let y = 0;
- for (const doc of docs) {
- doc.x = x;
- doc.y = y;
- const size = 200;
- const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
- if (aspect > 1) {
- doc.height = size;
- doc.width = size / aspect;
- } else if (aspect > 0) {
- doc.width = size;
- doc.height = size * aspect;
- } else {
- doc.width = size;
- doc.height = size;
- }
- doc.zoomBasis = 1;
- x += 250;
- if (x > 1000) {
- x = 0;
- y += 300;
- }
- }
- return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` });
- }
-
- // Useful queries:
- // Delegates of a document: {!join from=id to=proto_i}id:{protoId}
- // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId}
- render() {
- return (
- <div>
- <div className="searchBox-container">
- <div className="searchBox-bar">
- <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
- <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" />
- </span>
- <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..."
- className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
- style={{ width: this._resultsOpen ? "500px" : undefined }} />
- {/* <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button> */}
- {/* <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" /> */}
- </div>
- {this._resultsOpen ? (
- <div className="searchBox-results">
- {this._results.map(result => <SearchItem doc={result} key={result[Id]} />)}
- </div>
- ) : null}
- </div>
- {this._open ? (
- <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}>
- <div className="filter-form" id="header">Filter Search Results</div>
- <div className="filter-form" id="option">
- filter by collection, key, type of node
- </div>
-
- </div>
- ) : null}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
deleted file mode 100644
index 6901f60da..000000000
--- a/src/client/views/SearchItem.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React = require("react");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Doc } from "../../new_fields/Doc";
-import { DocumentManager } from "../util/DocumentManager";
-import { SetupDrag } from "../util/DragManager";
-
-
-export interface SearchProps {
- doc: Doc;
-}
-
-library.add(faCaretUp);
-library.add(faObjectGroup);
-library.add(faStickyNote);
-library.add(faFilePdf);
-library.add(faFilm);
-
-export class SearchItem extends React.Component<SearchProps> {
-
- onClick = () => {
- DocumentManager.Instance.jumpToDocument(this.props.doc);
- }
-
- //needs help
- // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
-
-
- public static DocumentIcon(layout: string) {
- let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
- layout.indexOf("ImageBox") !== -1 ? faImage :
- layout.indexOf("Formatted") !== -1 ? faStickyNote :
- layout.indexOf("Video") !== -1 ? faFilm :
- layout.indexOf("Collection") !== -1 ? faObjectGroup :
- faCaretUp;
- return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
- }
- onPointerEnter = (e: React.PointerEvent) => {
- this.props.doc.libraryBrush = true;
- Doc.SetOnPrototype(this.props.doc, "protoBrush", true);
- }
- onPointerLeave = (e: React.PointerEvent) => {
- this.props.doc.libraryBrush = false;
- Doc.SetOnPrototype(this.props.doc, "protoBrush", false);
- }
-
- collectionRef = React.createRef<HTMLDivElement>();
- startDocDrag = () => {
- let doc = this.props.doc;
- const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
- if (isProto) {
- return Doc.MakeDelegate(doc);
- } else {
- return Doc.MakeAlias(doc);
- }
- }
- render() {
- return (
- <div className="search-item" ref={this.collectionRef} id="result"
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
- onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} >
- <div className="search-title" id="result" >title: {this.props.doc.title}</div>
- {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */}
- {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index de5c66c0b..30d68fffd 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -261,7 +261,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this._isPointerDown = true;
let onPointerUp = action(() => {
window.removeEventListener("pointerup", onPointerUp)
- this._isPointerDown = false
+ this._isPointerDown = false;
})
window.addEventListener("pointerup", onPointerUp);
var className = (e.target as any).className;
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 7853544d5..c1a6ca44e 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -12,7 +12,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { VideoBox } from "../nodes/VideoBox";
import { NumCast, Cast, StrCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
-import { SearchBox } from "../SearchBox";
+import { SearchBox } from "../search/SearchBox";
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
index 30e158603..a80e09fa8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -1,4 +1,4 @@
-.collectionfreeformlinksview-svgCanvas{
+p.collectionfreeformlinksview-svgCanvas{
transform: translate(-10000px,-10000px);
position: absolute;
top: 0;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3f7efcb66..ee7a9f64f 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -14,7 +14,7 @@ import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
-import { SearchBox } from "../../SearchBox";
+import { SearchBox } from "../../search/SearchBox";
import { Templates } from "../../Templates";
import { CollectionViewType } from "../CollectionBaseView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss
index 838d4d9ac..cfbf4aab8 100644
--- a/src/client/views/globalCssVariables.scss
+++ b/src/client/views/globalCssVariables.scss
@@ -9,6 +9,7 @@ $main-accent: #aaaaa3;
//$alt-accent: #59dff7;
$alt-accent: #c2c2c5;
$lighter-alt-accent: rgb(207, 220, 240);
+$darker-alt-accent: rgb(178, 206, 248);
$intermediate-color: #9c9396;
$dark-color: #121721;
// fonts
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 3f09d6214..c34ccdccb 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -8,6 +8,7 @@ import React = require("react");
import { Doc, DocListCast } from "../../../new_fields/Doc";
import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { Id } from "../../../new_fields/FieldSymbols";
+import { DocTypes } from "../../documents/Documents";
interface Props {
docView: DocumentView;
@@ -23,7 +24,7 @@ export class LinkMenu extends React.Component<Props> {
return links.map(link => {
let doc = FieldValue(Cast(link[key], Doc));
if (doc) {
- return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
+ return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={DocTypes.LINK} />;
}
});
}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index d2de1cb1c..07f4733ba 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -11,6 +11,8 @@ import { PdfField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
+import { SearchBox } from "../search/SearchBox";
+import { Annotation } from './Annotation';
import { PDFViewer } from "../pdf/PDFViewer";
import { positionSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
diff --git a/src/client/views/search/CheckBox.scss b/src/client/views/search/CheckBox.scss
new file mode 100644
index 000000000..9b0af68d5
--- /dev/null
+++ b/src/client/views/search/CheckBox.scss
@@ -0,0 +1,58 @@
+@import "../globalCssVariables";
+
+.checkboxfilter {
+ display: flex;
+ margin-top: 0px;
+ padding: 3px;
+
+ .outer {
+ display: flex;
+ position: relative;
+ justify-content: center;
+ align-items: center;
+ margin-top: 0px;
+ }
+
+ .check-box {
+ z-index: 900;
+ position: relative;
+ height: 20px;
+ width: 20px;
+ overflow: visible;
+ background-color: transparent;
+ border-style: solid;
+ border-color: $alt-accent;
+ border-width: 2px;
+ -webkit-transition: all 0.2s ease-in-out;
+ -moz-transition: all 0.2s ease-in-out;
+ -o-transition: all 0.2s ease-in-out;
+ transition: all 0.2s ease-in-out;
+ }
+
+ .check-container:hover ~ .check-box {
+ background-color: $intermediate-color;
+ }
+
+ .check-container {
+ width: 40px;
+ height: 40px;
+ position: absolute;
+ z-index: 1000;
+ }
+
+ .checkmark {
+ z-index: 1000;
+ position: absolute;
+ fill-opacity: 0;
+ stroke-width: 4px;
+ stroke: white;
+ }
+
+}
+
+.checkbox-title {
+ display: flex;
+ align-items: center;
+ text-transform: capitalize;
+ margin-left: 15px;
+} \ No newline at end of file
diff --git a/src/client/views/search/CheckBox.tsx b/src/client/views/search/CheckBox.tsx
new file mode 100644
index 000000000..c75980e7c
--- /dev/null
+++ b/src/client/views/search/CheckBox.tsx
@@ -0,0 +1,131 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx';
+import "./CheckBox.scss";
+import * as anime from 'animejs';
+
+interface CheckBoxProps {
+ originalStatus: boolean;
+ updateStatus(newStatus: boolean): void;
+ title: string;
+ parent: any;
+ numCount: number;
+ default: boolean;
+}
+
+@observer
+export class CheckBox extends React.Component<CheckBoxProps>{
+ // true = checked, false = unchecked
+ @observable _status: boolean;
+ @observable uncheckTimeline: anime.AnimeTimelineInstance;
+ @observable checkTimeline: anime.AnimeTimelineInstance;
+ @observable checkRef: any;
+ @observable _resetReaction?: IReactionDisposer;
+
+
+ constructor(props: CheckBoxProps) {
+ super(props);
+ this._status = this.props.originalStatus;
+ this.checkRef = React.createRef();
+
+ this.checkTimeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "normal",
+ }); this.uncheckTimeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "normal",
+ });
+ }
+
+ componentDidMount = () => {
+ this.uncheckTimeline.add({
+ targets: this.checkRef.current,
+ easing: "easeInOutQuad",
+ duration: 500,
+ opacity: 0,
+ });
+ this.checkTimeline.add({
+ targets: this.checkRef.current,
+ easing: "easeInOutQuad",
+ duration: 500,
+ strokeDashoffset: [anime.setDashoffset, 0],
+ opacity: 1
+ });
+
+ if (this.props.originalStatus) {
+ this.checkTimeline.play();
+ this.checkTimeline.restart();
+ }
+
+ this._resetReaction = reaction(
+ () => this.props.parent.resetBoolean,
+ () => {
+ if (this.props.parent.resetBoolean) {
+ runInAction(() => {
+ this.reset();
+ this.props.parent.resetCounter++;
+ if (this.props.parent.resetCounter === this.props.numCount) {
+ this.props.parent.resetCounter = 0;
+ this.props.parent.resetBoolean = false;
+ }
+ });
+ }
+ },
+ );
+ }
+
+ @action.bound
+ reset() {
+ if (this.props.default) {
+ if (!this._status) {
+ this._status = true;
+ this.checkTimeline.play();
+ this.checkTimeline.restart();
+ }
+ }
+ else {
+ if (this._status) {
+ this._status = false;
+ this.uncheckTimeline.play();
+ this.uncheckTimeline.restart();
+ }
+ }
+
+ this.props.updateStatus(this.props.default);
+ }
+
+ @action.bound
+ onClick = () => {
+ if (this._status) {
+ this.uncheckTimeline.play();
+ this.uncheckTimeline.restart();
+ }
+ else {
+ this.checkTimeline.play();
+ this.checkTimeline.restart();
+
+ }
+ this._status = !this._status;
+ this.props.updateStatus(this._status);
+
+ }
+
+ render() {
+ return (
+ <div className="checkboxfilter" onClick={this.onClick}>
+ <div className="outer">
+ <div className="check-container">
+ <svg viewBox="0 12 40 40">
+ <path ref={this.checkRef} className="checkmark" d="M14.1 27.2l7.1 7.2 16.7-18" />
+ </svg>
+ </div>
+ <div className="check-box" />
+ </div>
+ <div className="checkbox-title">{this.props.title}</div>
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/search/CollectionFilters.scss b/src/client/views/search/CollectionFilters.scss
new file mode 100644
index 000000000..8d5ab96e7
--- /dev/null
+++ b/src/client/views/search/CollectionFilters.scss
@@ -0,0 +1,20 @@
+@import "../globalCssVariables";
+
+.collection-filters{
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+}
+
+.collection-filters.main{
+ width: 47%;
+ float: left;
+}
+
+.collection-filters.optional{
+ width: 60%;
+ display: grid;
+ grid-template-columns: 50% 50%;
+ float: right;
+ opacity: 0;
+} \ No newline at end of file
diff --git a/src/client/views/search/CollectionFilters.tsx b/src/client/views/search/CollectionFilters.tsx
new file mode 100644
index 000000000..5bfe37efb
--- /dev/null
+++ b/src/client/views/search/CollectionFilters.tsx
@@ -0,0 +1,90 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction } from 'mobx';
+import "./SearchBox.scss";
+import { CheckBox } from './CheckBox';
+import { Keys } from './SearchBox';
+import "./SearchBox.scss";
+import "./CollectionFilters.scss";
+import { FieldFilters } from './FieldFilters';
+import * as anime from 'animejs';
+
+interface CollectionFilterProps {
+ collectionStatus: boolean;
+ updateCollectionStatus(val: boolean): void;
+ collectionSelfStatus: boolean;
+ updateSelfCollectionStatus(val: boolean): void;
+ collectionParentStatus: boolean;
+ updateParentCollectionStatus(val: boolean): void;
+}
+
+export class CollectionFilters extends React.Component<CollectionFilterProps> {
+
+ static Instance: CollectionFilters;
+
+ @observable public resetBoolean = false;
+ @observable public resetCounter: number = 0;
+ @observable collectionsSelected = this.props.collectionStatus;
+ @observable timeline: anime.AnimeTimelineInstance;
+ @observable ref: any;
+
+ constructor(props: CollectionFilterProps) {
+ super(props);
+ CollectionFilters.Instance = this;
+ this.ref = React.createRef();
+
+ this.timeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "reverse",
+ });
+ }
+
+ componentDidMount = () => {
+ this.timeline.add({
+ targets: this.ref.current,
+ easing: "easeInOutQuad",
+ duration: 500,
+ opacity: 1,
+ });
+
+ if(this.collectionsSelected){
+ this.timeline.play();
+ this.timeline.reverse();
+ }
+ }
+
+ @action.bound
+ resetCollectionFilters() {
+ this.resetBoolean = true;
+ }
+
+ @action.bound
+ updateColStat(val: boolean) {
+ this.props.updateCollectionStatus(val);
+
+ if (this.collectionsSelected !== val) {
+ this.timeline.play();
+ this.timeline.reverse();
+ }
+
+ this.collectionsSelected = val;
+ }
+
+ render() {
+ return (
+ <div>
+ <div className='collection-title'>Search in current collections</div>
+ <div className="collection-filters">
+ <div className="collection-filters main">
+ <CheckBox default={false} title={"limit to current collection"} parent={this} numCount={3} updateStatus={this.updateColStat} originalStatus={this.props.collectionStatus} />
+ </div>
+ <div className="collection-filters optional" ref={this.ref}>
+ <CheckBox default={true} title={"Search in self"} parent={this} numCount={3} updateStatus={this.props.updateSelfCollectionStatus} originalStatus={this.props.collectionSelfStatus} />
+ <CheckBox default={true} title={"Search in parent"} parent={this} numCount={3} updateStatus={this.props.updateParentCollectionStatus} originalStatus={this.props.collectionParentStatus} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/FieldFilters.tsx b/src/client/views/search/FieldFilters.tsx
new file mode 100644
index 000000000..c7928dcd6
--- /dev/null
+++ b/src/client/views/search/FieldFilters.tsx
@@ -0,0 +1,43 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, } from 'mobx';
+import "./SearchBox.scss";
+import { CheckBox } from './CheckBox';
+import { Keys } from './SearchBox';
+import "./SearchBox.scss";
+
+export interface FieldFilterProps {
+ titleFieldStatus: boolean;
+ dataFieldStatus: boolean;
+ authorFieldStatus: boolean;
+ updateTitleStatus(stat: boolean): void;
+ updateAuthorStatus(stat: boolean): void;
+ updateDataStatus(stat: boolean): void;
+}
+
+export class FieldFilters extends React.Component<FieldFilterProps> {
+
+ static Instance: FieldFilters;
+ @observable public resetBoolean = false;
+ @observable public resetCounter: number = 0;
+
+ constructor(props: FieldFilterProps) {
+ super(props);
+ FieldFilters.Instance = this;
+ }
+
+ resetFieldFilters() {
+ this.resetBoolean = true;
+ }
+
+ render() {
+ return (
+ <div>
+ <div className="filter field-title">Filter by Basic Keys</div>
+ <CheckBox default = {true} numCount={3} parent={this} originalStatus={this.props.titleFieldStatus} updateStatus={this.props.updateTitleStatus} title={Keys.TITLE} />
+ <CheckBox default = {true} numCount={3} parent={this} originalStatus={this.props.authorFieldStatus} updateStatus={this.props.updateAuthorStatus} title={Keys.AUTHOR} />
+ <CheckBox default = {true} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={Keys.DATA} />
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/IconBar.scss b/src/client/views/search/IconBar.scss
new file mode 100644
index 000000000..db4e49fe9
--- /dev/null
+++ b/src/client/views/search/IconBar.scss
@@ -0,0 +1,71 @@
+@import "../globalCssVariables";
+
+
+.icon-bar {
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 40px;
+ width: 100%;
+ flex-wrap: wrap;
+}
+
+.icon-title {
+ color: $light-color;
+ text-transform: uppercase;
+ padding: 5px;
+}
+
+.type-icon {
+ height: 45px;
+ width: 45px;
+ color: $light-color;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ -webkit-transition: all 0.2s ease-in-out;
+ -moz-transition: all 0.2s ease-in-out;
+ -o-transition: all 0.2s ease-in-out;
+ transition: all 0.2s ease-in-out;
+ font-size: 2em;
+}
+
+.fontawesome-icon {
+ height: 24px;
+ width: 24px;
+}
+
+.filter-description{
+ text-transform: capitalize;
+}
+
+.type-icon:hover {
+ transform: scale(1.1);
+ background-color: $darker-alt-accent;
+ opacity: 1;
+
+ +.filter-description {
+ opacity: 1;
+ }
+}
+
+.type-outer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 45px;
+ height: 60px;
+}
+
+.filter-description {
+ font-size: 10;
+ text-align: center;
+ height: 15px;
+ margin-top: 5px;
+ opacity: 0;
+ -webkit-transition: all 0.2s ease-in-out;
+ -moz-transition: all 0.2s ease-in-out;
+ -o-transition: all 0.2s ease-in-out;
+ transition: all 0.2s ease-in-out;
+} \ No newline at end of file
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
new file mode 100644
index 000000000..2ae4af642
--- /dev/null
+++ b/src/client/views/search/IconBar.tsx
@@ -0,0 +1,94 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction } from 'mobx';
+import "./SearchBox.scss";
+import "./IconBar.scss";
+import * as anime from 'animejs';
+import { DocTypes } from '../../documents/Documents';
+import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faTimesCircle, faCheckCircle } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library, icon } from '@fortawesome/fontawesome-svg-core';
+import * as _ from "lodash";
+import $ from 'jquery';
+import { array } from 'prop-types';
+import { IconButton } from './IconButton';
+import { list } from 'serializr';
+import { SearchBox } from './SearchBox';
+
+library.add(faSearch);
+library.add(faObjectGroup);
+library.add(faImage);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+library.add(faMusic);
+library.add(faLink);
+library.add(faChartBar);
+library.add(faGlobeAsia);
+library.add(faBan);
+
+@observer
+export class IconBar extends React.Component {
+
+ static Instance: IconBar;
+
+ allIcons: string[] = [DocTypes.AUDIO, DocTypes.COL, DocTypes.HIST, DocTypes.IMG, DocTypes.LINK, DocTypes.PDF, DocTypes.TEXT, DocTypes.VID, DocTypes.WEB];
+ @observable public ResetClicked: boolean = false;
+ @observable public SelectAllClicked: boolean = false;
+ public Reset: number = 0;
+ public Select: number = 0;
+
+ constructor(props: any) {
+ super(props);
+ IconBar.Instance = this;
+ }
+
+ @action.bound
+ getList = (): string[] => {
+ return SearchBox.Instance.getIcons();
+ }
+
+ @action.bound
+ updateList(newList: string[]) {
+ SearchBox.Instance.updateIcon(newList);
+ }
+
+ @action.bound
+ resetSelf = () => {
+ this.ResetClicked = true;
+ this.updateList([]);
+ }
+
+ @action.bound
+ selectAll = () => {
+ this.SelectAllClicked = true;
+ this.updateList(this.allIcons);
+ }
+
+ render() {
+ return (
+ <div>
+ <div className="filter icon-title">Filter by type of node</div>
+ <div className="filter icon-bar">
+ <div className="filter type-outer">
+ <div className={"type-icon none not-selected"}
+ onClick={this.selectAll}>
+ <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} />
+ </div>
+ <div className="filter-description">Select All</div>
+ </div>
+ {this.allIcons.map((type: string) =>
+ <IconButton type={type}/>
+ )}
+ <div className="filter type-outer">
+ <div className={"type-icon none not-selected"}
+ onClick={this.resetSelf}>
+ <FontAwesomeIcon className="fontawesome-icon" icon={faTimesCircle} />
+ </div>
+ <div className="filter-description">Clear</div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx
new file mode 100644
index 000000000..9df285b70
--- /dev/null
+++ b/src/client/views/search/IconButton.tsx
@@ -0,0 +1,201 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx';
+import "./SearchBox.scss";
+import "./IconBar.scss";
+import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faVideo, faCaretDown } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library, icon } from '@fortawesome/fontawesome-svg-core';
+import { DocTypes } from '../../documents/Documents';
+import '../globalCssVariables.scss';
+import * as _ from "lodash";
+import { IconBar } from './IconBar';
+import { props } from 'bluebird';
+import { SearchBox } from './SearchBox';
+import { Search } from '../../../server/Search';
+
+library.add(faSearch);
+library.add(faObjectGroup);
+library.add(faImage);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+library.add(faMusic);
+library.add(faLink);
+library.add(faChartBar);
+library.add(faGlobeAsia);
+library.add(faBan);
+
+interface IconButtonProps {
+ type: string;
+}
+
+@observer
+export class IconButton extends React.Component<IconButtonProps>{
+
+ @observable isSelected: boolean = SearchBox.Instance.getIcons().indexOf(this.props.type) !== -1;
+ @observable hover = false;
+
+ private _resetReaction?: IReactionDisposer;
+ private _selectAllReaction?: IReactionDisposer;
+
+ static Instance: IconButton;
+ constructor(props: IconButtonProps) {
+ super(props);
+ IconButton.Instance = this;
+ }
+
+ componentDidMount = () => {
+ this._resetReaction = reaction(
+ () => IconBar.Instance.ResetClicked,
+ () => {
+ if (IconBar.Instance.ResetClicked) {
+ runInAction(() => {
+ this.reset();
+ IconBar.Instance.Reset++;
+ if (IconBar.Instance.Reset === 9) {
+ IconBar.Instance.Reset = 0;
+ IconBar.Instance.ResetClicked = false;
+ }
+ });
+ }
+ },
+ );
+ this._selectAllReaction = reaction(
+ () => IconBar.Instance.SelectAllClicked,
+ () => {
+ if (IconBar.Instance.SelectAllClicked) {
+ runInAction(() => {
+ this.select();
+ IconBar.Instance.Select++;
+ if (IconBar.Instance.Select === 9) {
+ IconBar.Instance.Select = 0;
+ IconBar.Instance.SelectAllClicked = false;
+ }
+ });
+ }
+ },
+ );
+ }
+
+ @action.bound
+ getIcon() {
+ switch (this.props.type) {
+ case (DocTypes.NONE):
+ return faBan;
+ case (DocTypes.AUDIO):
+ return faMusic;
+ case (DocTypes.COL):
+ return faObjectGroup;
+ case (DocTypes.HIST):
+ return faChartBar;
+ case (DocTypes.IMG):
+ return faImage;
+ case (DocTypes.LINK):
+ return faLink;
+ case (DocTypes.PDF):
+ return faFilePdf;
+ case (DocTypes.TEXT):
+ return faStickyNote;
+ case (DocTypes.VID):
+ return faVideo;
+ case (DocTypes.WEB):
+ return faGlobeAsia;
+ default:
+ return faCaretDown;
+ }
+ }
+
+ @action.bound
+ onClick = () => {
+ let newList: string[] = SearchBox.Instance.getIcons();
+
+ if (!this.isSelected) {
+ this.isSelected = true;
+ newList.push(this.props.type);
+ }
+ else {
+ this.isSelected = false;
+ _.pull(newList, this.props.type);
+ }
+
+ SearchBox.Instance.updateIcon(newList);
+ }
+
+ selected = {
+ opacity: 1,
+ backgroundColor: "#c2c2c5" //$alt-accent
+ };
+
+ notSelected = {
+ opacity: 0.6,
+ };
+
+ hoverStyle = {
+ opacity: 1,
+ backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent
+ };
+
+ @action.bound
+ public reset() {
+ this.isSelected = false;
+ }
+
+ @action.bound
+ public select() {
+ this.isSelected = true;
+ }
+
+ @action
+ onMouseLeave = () => {
+ this.hover = false;
+ }
+
+ @action
+ onMouseEnter = () => {
+ this.hover = true;
+ }
+
+ getFA = () => {
+ switch (this.props.type) {
+ case (DocTypes.NONE):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faBan} />);
+ case (DocTypes.AUDIO):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faMusic} />);
+ case (DocTypes.COL):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faObjectGroup} />);
+ case (DocTypes.HIST):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faChartBar} />);
+ case (DocTypes.IMG):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faImage} />);
+ case (DocTypes.LINK):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faLink} />);
+ case (DocTypes.PDF):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faFilePdf} />);
+ case (DocTypes.TEXT):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faStickyNote} />);
+ case (DocTypes.VID):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faVideo} />);
+ case (DocTypes.WEB):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faGlobeAsia} />);
+ default:
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faCaretDown} />);
+ }
+ }
+
+ render() {
+ return (
+ <div className="type-outer" id={this.props.type + "-filter"}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}
+ onClick={this.onClick}>
+ <div className="type-icon" id={this.props.type + "-icon"}
+ style={this.hover ? this.hoverStyle : this.isSelected ? this.selected : this.notSelected}
+ >
+ {this.getFA()}
+ </div>
+ <div className="filter-description">{this.props.type}</div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/NaviconButton.scss b/src/client/views/search/NaviconButton.scss
new file mode 100644
index 000000000..529f11a57
--- /dev/null
+++ b/src/client/views/search/NaviconButton.scss
@@ -0,0 +1,95 @@
+@import "../globalCssVariables";
+
+
+@import url(https://fonts.googleapis.com/css?family=Montserrat:400,700);
+
+// $background: #3d566e;
+$color: #ecf0f1;
+
+$height-icon: 20px;
+$width-line: 30px;
+$height-line: 4px;
+
+$transition-time: 0.4s;
+$rotation: 45deg;
+$translateY: ($height-icon / 2);
+$translateX: 0;
+
+// body {
+// background: $background;
+// color: $color;
+// font-family: 'Montserrat', sans-serif;
+// -webkit-font-smoothing: antialiased;
+// text-align:center;
+// }
+
+#hamburger-icon {
+ width:$width-line;
+ height:$height-icon;
+ position:relative;
+ display:block;
+// margin: ($height-icon * 2) auto $height-icon auto;
+
+ .line {
+ display:block;
+ background:$color;
+ width:$width-line;
+ height:$height-line;
+ position:absolute;
+ left:0;
+ border-radius:($height-line / 2);
+ transition: all $transition-time;
+ -webkit-transition: all $transition-time;
+ -moz-transition: all $transition-time;
+
+ &.line-1 {
+ top:0;
+ }
+ &.line-2 {
+ top:50%;
+ }
+ &.line-3 {
+ top:100%;
+ }
+ }
+ &:hover, &:focus {
+ .line-1 {
+ transform: translateY($height-line / 2 * -1);
+ -webkit-transform: translateY($height-line / 2 * -1);
+ -moz-transform: translateY($height-line / 2 * -1);
+ }
+ .line-3 {
+ transform: translateY($height-line / 2);
+ -webkit-transform: translateY($height-line / 2);
+ -moz-transform: translateY($height-line / 2);
+ }
+ }
+ &.active {
+ .line-1 {
+ transform: translateY($translateY) translateX($translateX) rotate($rotation);
+ -webkit-transform: translateY($translateY) translateX($translateX) rotate($rotation);
+ -moz-transform: translateY($translateY) translateX($translateX) rotate($rotation);
+ }
+ .line-2 {
+ opacity:0;
+ }
+ .line-3 {
+ transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1);
+ -webkit-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1);
+ -moz-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1);
+ }
+ }
+}
+
+// h1 {
+// text-transform:uppercase;
+// }
+// a {
+// text-decoration:none;
+// color:#95a5a6;
+// margin: 0.5em 1.5em;
+// display:inline-block;
+// &:hover, &:focus {
+// color:$color;
+// }
+// } \ No newline at end of file
diff --git a/src/client/views/search/NaviconButton.tsx b/src/client/views/search/NaviconButton.tsx
new file mode 100644
index 000000000..aa9e627c0
--- /dev/null
+++ b/src/client/views/search/NaviconButton.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import "./NaviconButton.scss";
+import * as $ from 'jquery';
+
+
+export class NaviconButton extends React.Component {
+
+ componentDidMount = () => {
+ $(document).ready(function () {
+ var hamburger = $('#hamburger-icon');
+ hamburger.click(function () {
+ hamburger.toggleClass('active');
+ return false;
+ });
+ });
+ }
+
+ render() {
+ return (
+ <a id="hamburger-icon" href="#" title="Menu">
+ <span className="line line-1"></span>
+ <span className="line line-2"></span>
+ <span className="line line-3"></span>
+ </a>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchBox.scss b/src/client/views/search/SearchBox.scss
index b38e6091d..3b96e3922 100644
--- a/src/client/views/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -1,4 +1,4 @@
-@import "globalCssVariables";
+@import "../globalCssVariables";
.searchBox-bar {
height: 32px;
@@ -30,15 +30,6 @@
align-self: stretch;
}
- .searchBox-submit {
- color: $dark-color;
- }
-
- .searchBox-submit:hover {
- color: $main-accent;
- transform: scale(1.05);
- cursor: pointer;
- }
}
.searchBox-results {
@@ -47,56 +38,78 @@
.filter-form {
background: $dark-color;
- height: 400px;
- width: 400px;
+ // height: 400px;
+ height: auto;
+ overflow: auto;
position: relative;
right: 1px;
color: $light-color;
- padding: 10px;
flex-direction: column;
+ display: inline-block;
+}
+
+#filter{
+ padding: 25px;
+ width: 600px;
}
#header {
text-transform: uppercase;
letter-spacing: 2px;
- font-size: 100%;
+ font-size: 25;
height: 40px;
}
-#option {
- height: 20px;
-}
-
.searchBox-results {
top: 300px;
display: flex;
flex-direction: column;
+ margin-right: 72px;
+}
- .search-item {
- width: 500px;
- height: 50px;
- background: $light-color-secondary;
- display: flex;
- justify-content: space-between;
- align-items: center;
- transition: all 0.1s;
- border-width: 0.11px;
- border-style: none;
- border-color: $intermediate-color;
- border-bottom-style: solid;
- padding: 10px;
- white-space: nowrap;
- font-size: 13px;
- }
+.filter-collection{
+ display: inline-block;
+ width: 100%;
+}
- .search-item:hover {
- transition: all 0.1s;
- background: $lighter-alt-accent;
- }
+.field-title {
+ color: $light-color;
+ text-transform: uppercase;
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+.type-of-node{
+ height: 90px;
+}
+
+.required-words{
+ display: inline-block;
+ width: 100%;
+}
+
+.filter-div{
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+.collection-title{
+ color: $light-color;
+ text-transform: uppercase;
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+.no-result {
+ width: 500px;
+ background: $light-color-secondary;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ height: 50px;
+ text-transform: uppercase;
+ text-align: left;
+ // width: 80%;
+ font-weight: bold;
+}
- .search-title {
- text-transform: uppercase;
- text-align: left;
- width: 8vw;
- }
-} \ No newline at end of file
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
new file mode 100644
index 000000000..71472886f
--- /dev/null
+++ b/src/client/views/search/SearchBox.tsx
@@ -0,0 +1,518 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction } from 'mobx';
+import "./SearchBox.scss";
+import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faThList, faWineGlassAlt } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import * as rp from 'request-promise';
+import { SearchItem } from './SearchItem';
+import { DocServer } from '../../DocServer';
+import { Doc, Opt } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { SetupDrag } from '../../util/DragManager';
+import { Docs, DocTypes } from '../../documents/Documents';
+import { RouteStore } from '../../../server/RouteStore';
+import { NumCast, Cast, StrCast } from '../../../new_fields/Types';
+import { SearchUtil } from '../../util/SearchUtil';
+import * as anime from 'animejs';
+import { updateFunction } from '../../../new_fields/util';
+import * as _ from "lodash";
+import { findDOMNode } from 'react-dom';
+import { ToggleBar } from './ToggleBar';
+import { IconBar } from './IconBar';
+import { type } from 'os';
+import { CheckBox } from './CheckBox';
+import { FieldFilters } from './FieldFilters';
+import { SelectionManager } from '../../util/SelectionManager';
+import { DocumentView } from '../nodes/DocumentView';
+import { CollectionView } from '../collections/CollectionView';
+import { CollectionPDFView } from '../collections/CollectionPDFView';
+import { CollectionVideoView } from '../collections/CollectionVideoView';
+import { CollectionFilters } from './CollectionFilters';
+import { NaviconButton } from './NaviconButton';
+
+export enum Keys {
+ TITLE = "title",
+ AUTHOR = "author",
+ DATA = "data"
+}
+
+@observer
+export class SearchBox extends React.Component {
+
+ static Instance: SearchBox;
+
+ @observable _searchString: string = "";
+ //if true, any keywords can be used. if false, all keywords are required.
+ @observable _basicWordStatus: boolean = true;
+ @observable private _filterOpen: boolean = false;
+ @observable private _resultsOpen: boolean = false;
+ @observable private _results: Doc[] = [];
+ @observable private _openNoResults: boolean = false;
+ allIcons: string[] = [DocTypes.AUDIO, DocTypes.COL, DocTypes.HIST, DocTypes.IMG, DocTypes.LINK, DocTypes.PDF, DocTypes.TEXT, DocTypes.VID, DocTypes.WEB];
+ @observable _icons: string[] = this.allIcons;
+ @observable _selectedTypes: any[] = [];
+ @observable titleFieldStatus: boolean = true;
+ @observable authorFieldStatus: boolean = true;
+ @observable dataFieldStatus: boolean = true;
+ @observable collectionStatus = false;
+ @observable collectionSelfStatus = true;
+ @observable collectionParentStatus = true;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ SearchBox.Instance = this;
+ }
+
+ componentDidMount = () => {
+ document.addEventListener("pointerdown", (e) => {
+ if (e.timeStamp !== this._pointerTime) {
+ this.closeSearch();
+ }
+ });
+
+ //empties search query after 30 seconds of the search bar/filter box not being open
+ // if (!this._resultsOpen && !this._filterOpen) {
+ // setTimeout(this.clearSearchQuery, 30000);
+ // }
+ }
+
+ @action.bound
+ resetFilters = () => {
+ ToggleBar.Instance.resetToggle();
+ IconBar.Instance.selectAll();
+ FieldFilters.Instance.resetFieldFilters();
+ CollectionFilters.Instance.resetCollectionFilters();
+ }
+
+ @action.bound
+ onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ this._searchString = e.target.value;
+
+ if (this._searchString === "") {
+ this._results = [];
+ this._openNoResults = false;
+ }
+ }
+
+ @action.bound
+ clearSearchQuery() {
+ this._searchString = "";
+ this._results = [];
+ }
+
+ basicRequireWords(query: string): string {
+ let oldWords = query.split(" ");
+ let newWords: string[] = [];
+ oldWords.forEach(word => {
+ let newWrd = "+" + word;
+ newWords.push(newWrd);
+ });
+ query = newWords.join(" ");
+
+ return query;
+ }
+
+ basicFieldFilters(query: string, type: string): string {
+ let oldWords = query.split(" ");
+ let mod = "";
+
+ if (type === Keys.AUTHOR) {
+ mod = " author_t:";
+ } if (type === Keys.DATA) {
+ //TODO
+ } if (type === Keys.TITLE) {
+ mod = " title_t:";
+ }
+
+ let newWords: string[] = [];
+ oldWords.forEach(word => {
+ let newWrd = mod + word;
+ newWords.push(newWrd);
+ });
+
+ query = newWords.join(" ");
+
+ return query;
+ }
+
+ applyBasicFieldFilters(query: string) {
+ let finalQuery = "";
+
+ if (this.titleFieldStatus) {
+ finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TITLE);
+ }
+ if (this.authorFieldStatus) {
+ finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR);
+ }
+ if (this.dataFieldStatus) {
+ finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA);
+ }
+ return finalQuery;
+ }
+
+ get fieldFiltersApplied() { return !(this.dataFieldStatus && this.authorFieldStatus && this.titleFieldStatus); }
+
+ //gets all of the collections of all the docviews that are selected
+ //if a collection is the only thing selected, search only in that collection (not its container)
+ getCurCollections(): Doc[] {
+ let selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments();
+ let collections: Doc[] = [];
+
+ //TODO: make this some sort of error
+ if (selectedDocs.length === 0) {
+ console.log("there is nothing selected!")
+ }
+ //also searches in a collection if it is selected
+ else {
+ selectedDocs.forEach(async element => {
+ let layout: string = StrCast(element.props.Document.baseLayout);
+ // console.log("doc title:", element.props.Document.title)
+ //checks if selected view (element) is a collection. if it is, adds to list to search through
+ if (layout.indexOf("Collection") > -1) {
+ console.log("current doc is a collection")
+ // console.log(element.props.Document)
+ //makes sure collections aren't added more than once
+ if (!collections.includes(element.props.Document)) {
+ collections.push(element.props.Document);
+ }
+ }
+ //gets the selected doc's containing view
+ let containingView = element.props.ContainingCollectionView;
+ //makes sure collections aren't added more than once
+ if (containingView && !collections.includes(containingView.props.Document)) {
+ collections.push(containingView.props.Document);
+ }
+ });
+ }
+
+ return collections;
+ }
+
+ getFinalQuery(query: string): string {
+ //alters the query so it looks in the correct fields
+ //if this is true, then not all of the field boxes are checked
+ //TODO: data
+ if (this.fieldFiltersApplied) {
+ query = this.applyBasicFieldFilters(query);
+ query = query.replace(/\s+/g, ' ').trim();
+ }
+
+ //alters the query based on if all words or any words are required
+ //if this._wordstatus is false, all words are required and a + is added before each
+ if (!this._basicWordStatus) {
+ query = this.basicRequireWords(query);
+ console.log(query)
+ query = query.replace(/\s+/g, ' ').trim();
+ }
+
+ //if should be searched in a specific collection
+ if (this.collectionStatus) {
+ query = this.addCollectionFilter(query);
+ query = query.replace(/\s+/g, ' ').trim();
+ }
+ return query;
+ }
+
+ addCollectionFilter(query: string): string {
+ let collections: Doc[] = this.getCurCollections();
+ let oldWords = query.split(" ");
+
+ let collectionString: string[] = [];
+ collections.forEach(doc => {
+ let proto = doc.proto;
+ let protoId = (proto || doc)[Id];
+ let colString: string = "{!join from=data_l to=id}id:" + protoId + " ";
+ collectionString.push(colString);
+ });
+
+ let finalColString = collectionString.join(" ");
+ finalColString = finalColString.trim();
+ return "+(" + finalColString + ")" + query;
+ }
+
+ @action
+ submitSearch = async () => {
+ let query = this._searchString;
+ let results: Doc[];
+
+ query = this.getFinalQuery(query);
+
+ //if there is no query there should be no result
+ if (query === "") {
+ results = [];
+ }
+ else {
+ //gets json result into a list of documents that can be used
+ results = await this.getResults(query);
+ }
+
+ runInAction(() => {
+ this._resultsOpen = true;
+ this._results = results;
+ this._openNoResults = true;
+ });
+ }
+
+ @action
+ getResults = async (query: string) => {
+ let response = await rp.get(DocServer.prepend('/search'), {
+ qs: {
+ query
+ }
+ });
+ let res: string[] = JSON.parse(response);
+ const fields = await DocServer.GetRefFields(res);
+ const docs: Doc[] = [];
+ for (const id of res) {
+ const field = fields[id];
+ if (field instanceof Doc) {
+ docs.push(field);
+ }
+ }
+ return this.filterDocsByType(docs);
+ }
+
+ //this.icons will now include all the icons that need to be included
+ @action filterDocsByType(docs: Doc[]) {
+ let finalDocs: Doc[] = [];
+ docs.forEach(doc => {
+ let layoutresult = Cast(doc.type, "string", "");
+ if (this._icons.includes(layoutresult)) {
+ finalDocs.push(doc);
+ }
+ });
+ return finalDocs;
+ }
+
+ public static async convertDataUri(imageUri: string, returnedFilename: string) {
+ try {
+ let posting = DocServer.prepend(RouteStore.dataUriToImage);
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename
+ },
+ json: true,
+ });
+ return returnedUri;
+
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ enter = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ this.submitSearch();
+ }
+ }
+
+ @action.bound
+ closeSearch = () => {
+ this._filterOpen = false;
+ this._resultsOpen = false;
+ this._results = [];
+ }
+
+ @action
+ openFilter = () => {
+ this._filterOpen = !this._filterOpen;
+ this._resultsOpen = false;
+ this._results = [];
+ }
+
+ collectionRef = React.createRef<HTMLSpanElement>();
+ startDragCollection = async () => {
+ const results = await this.getResults(this._searchString);
+ const docs = results.map(doc => {
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ });
+ let x = 0;
+ let y = 0;
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ const size = 200;
+ const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
+ if (aspect > 1) {
+ doc.height = size;
+ doc.width = size / aspect;
+ } else if (aspect > 0) {
+ doc.width = size;
+ doc.height = size * aspect;
+ } else {
+ doc.width = size;
+ doc.height = size;
+ }
+ doc.zoomBasis = 1;
+ x += 250;
+ if (x > 1000) {
+ x = 0;
+ y += 300;
+ }
+ }
+ return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
+ }
+
+ @action
+ getViews = async (doc: Doc) => {
+ const results = await SearchUtil.GetViewsOfDocument(doc);
+ let toReturn: Doc[] = [];
+ await runInAction(() => {
+ toReturn = results;
+ });
+ return toReturn;
+ }
+
+ //if true, any keywords can be used. if false, all keywords are required.
+ @action.bound
+ handleWordQueryChange = () => {
+ this._basicWordStatus = !this._basicWordStatus;
+ }
+
+ @action
+ getBasicWordStatus() {
+ return this._basicWordStatus;
+ }
+
+ @action.bound
+ updateIcon(newArray: string[]) {
+ this._icons = newArray;
+ }
+
+ @action.bound
+ getIcons(): string[] {
+ return this._icons;
+ }
+
+ private _pointerTime: number = -1;
+
+ stopProp = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._pointerTime = e.timeStamp;
+ }
+
+ @action.bound
+ openSearch(e: React.PointerEvent) {
+ e.stopPropagation();
+ this._openNoResults = false;
+ this._filterOpen = false;
+ this._resultsOpen = true;
+ this._pointerTime = e.timeStamp;
+ }
+
+ @action.bound
+ updateTitleStatus(newStat: boolean) {
+ this.titleFieldStatus = newStat;
+ }
+
+ @action.bound
+ updateAuthorStatus(newStat: boolean) {
+ this.authorFieldStatus = newStat;
+ }
+
+ @action.bound
+ updateDataStatus(newStat: boolean) {
+ this.dataFieldStatus = newStat;
+ }
+
+ @action.bound
+ updateCollectionStatus(newStat: boolean) {
+ this.collectionStatus = newStat;
+ }
+
+ @action.bound
+ updateSelfCollectionStatus(newStat: boolean) {
+ this.collectionSelfStatus = newStat;
+ }
+
+ @action.bound
+ updateParentCollectionStatus(newStat: boolean) {
+ this.collectionParentStatus = newStat;
+ }
+
+ getCollectionStatus() {
+ return this.collectionStatus;
+ }
+
+ getSelfCollectionStatus() {
+ return this.collectionSelfStatus;
+ }
+
+ getParentCollectionStatus() {
+ return this.collectionParentStatus;
+ }
+
+ getTitleStatus() {
+ return this.titleFieldStatus;
+ }
+
+ getAuthorStatus() {
+ return this.authorFieldStatus;
+ }
+
+ getDataStatus() {
+ return this.dataFieldStatus;
+ }
+
+ // Useful queries:
+ // Delegates of a document: {!join from=id to=proto_i}id:{protoId}
+ // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId} //id of collections prototype
+ render() {
+ return (
+ <div>
+ <div className="searchBox-container">
+ <div className="searchBox-bar">
+ <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
+ <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" />
+ </span>
+ <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..."
+ className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter}
+ style={{ width: this._resultsOpen ? "500px" : "100px" }} />
+ <button className="searchBox-barChild searchBox-filter" onClick={this.openFilter} onPointerDown={this.stopProp}>Filter</button>
+ </div>
+ {this._resultsOpen ? (
+ <div className="searchBox-results">
+ {(this._results.length !== 0) ? (
+ this._results.map(result => <SearchItem doc={result} key={result[Id]} />)
+ ) :
+ this._openNoResults ? (<div className="no-result">No Search Results</div>) : null}
+
+ </div>
+ ) : undefined}
+ </div>
+ {this._filterOpen ? (
+ <div className="filter-form" onPointerDown={this.stopProp} id="filter" style={this._filterOpen ? { display: "flex" } : { display: "none" }}>
+ <div className="filter-form filter-div" id="header">Filter Search Results</div>
+ <div className="filter-form " id="option">
+ <div className="required-words filter-div">
+ <ToggleBar originalStatus={this._basicWordStatus} optionOne={"Include Any Keywords"} optionTwo={"Include All Keywords"} />
+ </div>
+ <div className="type-of-node filter-div">
+ <IconBar />
+ </div>
+ <div className="filter-collection filter-div">
+ <CollectionFilters
+ updateCollectionStatus = {this.updateCollectionStatus} updateParentCollectionStatus = {this.updateParentCollectionStatus} updateSelfCollectionStatus = {this.updateSelfCollectionStatus}
+ collectionStatus = {this.collectionStatus} collectionParentStatus = {this.collectionParentStatus} collectionSelfStatus = {this.collectionSelfStatus}/>
+ </div>
+ <div className="where-in-doc filter-div">
+ <FieldFilters
+ titleFieldStatus={this.titleFieldStatus} dataFieldStatus={this.dataFieldStatus} authorFieldStatus={this.authorFieldStatus}
+ updateAuthorStatus={this.updateAuthorStatus} updateDataStatus={this.updateDataStatus} updateTitleStatus={this.updateTitleStatus} />
+ </div>
+ </div>
+ <button className="reset-filter" onClick={this.resetFilters}>Reset Filters</button>
+ </div>
+ ) : undefined}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss
new file mode 100644
index 000000000..6e88d1f44
--- /dev/null
+++ b/src/client/views/search/SearchItem.scss
@@ -0,0 +1,149 @@
+@import "../globalCssVariables";
+
+.search-item {
+ width: 500px;
+ background: $light-color-secondary;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ height: 70px;
+}
+
+.search-info {
+ display: flex;
+ justify-content: flex-end;
+ width: 40%;
+}
+
+.main-search-info {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+}
+
+.search-item:hover {
+ transition: all 0.2s;
+ background: $lighter-alt-accent;
+}
+
+.search-title {
+ text-transform: uppercase;
+ text-align: left;
+ width: 80%;
+ font-weight: bold;
+}
+
+.link-container.item {
+ height: 26px;
+ width: 26px;
+ border-radius: 13px;
+ background: $dark-color;
+ color: $light-color-secondary;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-right: 10px;
+ -webkit-transition: all 0.2s ease-in-out;
+ -moz-transition: all 0.2s ease-in-out;
+ -o-transition: all 0.2s ease-in-out;
+ transition: all 0.2s ease-in-out;
+ transform-origin: top right;
+ overflow: hidden;
+
+ .link-count {
+ opacity: 1;
+ position: absolute;
+ z-index: 1000;
+ -webkit-transition: opacity 0.2s ease-in-out;
+ -moz-transition: opacity 0.2s ease-in-out;
+ -o-transition: opacity 0.2s ease-in-out;
+ transition: opacity 0.2s ease-in-out;
+ }
+
+ .link-extended {
+ opacity: 0;
+ position: absolute;
+ z-index: 500;
+ overflow: hidden;
+ -webkit-transition: opacity 0.2s ease-in-out;
+ -moz-transition: opacity 0.2s ease-in-out;
+ -o-transition: opacity 0.2s ease-in-out;
+ transition: opacity 0.2s ease-in-out;
+ }
+}
+
+.link-container.item:hover{
+ width: 70px;
+}
+
+.link-container.item:hover .link-count{
+ opacity: 0;
+}
+
+.link-container.item:hover .link-extended{
+ opacity: 1;
+}
+
+.search-type {
+ width: 25PX;
+ height: 25PX;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+
+.searchBox-instances {
+ float: left;
+ opacity: 1;
+ width: 150px;
+ transition: all 0.2s ease;
+ color: black;
+ transform-origin: top right;
+ -webkit-transform: scale(0);
+ -ms-transform: scale(0);
+ transform: scale(0);
+ height: 100%
+}
+
+.collection {
+ border-color: $darker-alt-accent;
+ border-bottom-style: solid;
+}
+
+.search-overview {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ height: 70px;
+}
+
+.parents {
+ background: $lighter-alt-accent;
+ padding: 10px;
+}
+
+.search-item:hover~.searchBox-instances,
+.searchBox-instances:hover,
+.searchBox-instances:active {
+ opacity: 1;
+ background: $lighter-alt-accent;
+ -webkit-transform: scale(1);
+ -ms-transform: scale(1);
+ transform: scale(1);
+}
+
+.search-label{
+ text-align: right;
+ float: right;
+ text-transform: capitalize;
+ opacity: 0;
+ -webkit-transition: opacity 0.2s ease-in-out;
+ -moz-transition: opacity 0.2s ease-in-out;
+ -o-transition: opacity 0.2s ease-in-out;
+ transition: opacity 0.2s ease-in-out;
+}
+
+.search-type:hover +.search-label{
+ opacity: 1;
+} \ No newline at end of file
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
new file mode 100644
index 000000000..c19057819
--- /dev/null
+++ b/src/client/views/search/SearchItem.tsx
@@ -0,0 +1,180 @@
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { observable, runInAction, computed, action } from "mobx";
+import { listSpec } from "../../../new_fields/Schema";
+import { Doc } from "../../../new_fields/Doc";
+import { DocumentManager } from "../../util/DocumentManager";
+import { SetupDrag } from "../../util/DragManager";
+import { SearchUtil } from "../../util/SearchUtil";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { observer } from "mobx-react";
+import "./SearchItem.scss";
+import { CollectionViewType } from "../collections/CollectionBaseView";
+import { DocTypes } from "../../documents/Documents";
+import { SearchBox } from "./SearchBox";
+import { DocumentView } from "../nodes/DocumentView";
+
+export interface SearchItemProps {
+ doc: Doc;
+}
+
+library.add(faCaretUp);
+library.add(faObjectGroup);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+library.add(faMusic);
+library.add(faLink);
+library.add(faChartBar);
+library.add(faGlobeAsia);
+
+@observer
+export class SelectorContextMenu extends React.Component<SearchItemProps> {
+ @observable private _docs: { col: Doc, target: Doc }[] = [];
+ @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
+
+ constructor(props: SearchItemProps) {
+ super(props);
+
+ this.fetchDocuments();
+ }
+
+ async fetchDocuments() {
+ let aliases = (await SearchUtil.GetViewsOfDocument(this.props.doc)).filter(doc => doc !== this.props.doc);
+ const docs = await SearchUtil.Search(`data_l:"${this.props.doc[Id]}"`, true);
+ const map: Map<Doc, Doc> = new Map;
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true)));
+ allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
+ docs.forEach(doc => map.delete(doc));
+ runInAction(() => {
+ this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.doc }));
+ this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+ });
+ }
+
+ getOnClick({ col, target }: { col: Doc, target: Doc }) {
+ return () => {
+ col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
+ if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2;
+ col.panX = newPanX;
+ col.panY = newPanY;
+ }
+ CollectionDockingView.Instance.AddRightSplit(col);
+ };
+ }
+
+ //these all need class names in order to find ancestor - please do not delete
+ render() {
+ return (
+ < div className="parents">
+ <p className="contexts">Contexts:</p>
+ {this._docs.map(doc => <div className="collection"><a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a></div>)}
+ {this._otherDocs.map(doc => <div className="collection"><a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a></div>)}
+ </div>
+ );
+ }
+}
+
+@observer
+export class SearchItem extends React.Component<SearchItemProps> {
+
+ @observable _selected: boolean = false;
+
+ onClick = () => {
+ CollectionDockingView.Instance.AddRightSplit(this.props.doc);
+ }
+
+ @computed
+ public get DocumentIcon() {
+ let layoutresult = Cast(this.props.doc.type, "string", "");
+
+ let button = layoutresult.indexOf(DocTypes.PDF) !== -1 ? faFilePdf :
+ layoutresult.indexOf(DocTypes.IMG) !== -1 ? faImage :
+ layoutresult.indexOf(DocTypes.TEXT) !== -1 ? faStickyNote :
+ layoutresult.indexOf(DocTypes.VID) !== -1 ? faFilm :
+ layoutresult.indexOf(DocTypes.COL) !== -1 ? faObjectGroup :
+ layoutresult.indexOf(DocTypes.AUDIO) !== -1 ? faMusic :
+ layoutresult.indexOf(DocTypes.LINK) !== -1 ? faLink :
+ layoutresult.indexOf(DocTypes.HIST) !== -1 ? faChartBar :
+ layoutresult.indexOf(DocTypes.WEB) !== -1 ? faGlobeAsia :
+ faCaretUp;
+ return <FontAwesomeIcon icon={button} size="2x" />;
+ }
+
+ collectionRef = React.createRef<HTMLDivElement>();
+ startDocDrag = () => {
+ let doc = this.props.doc;
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ }
+
+ @computed
+ get linkCount() {
+ return Cast(this.props.doc.linkedToDocs, listSpec(Doc), []).length + Cast(this.props.doc.linkedFromDocs, listSpec(Doc), []).length;
+ }
+
+ @computed
+ get linkString(): string {
+ let num = this.linkCount;
+ if (num === 1) {
+ return num.toString() + " link";
+ }
+ return num.toString() + " links";
+ }
+
+ pointerDown = (e: React.PointerEvent) => {
+ SearchBox.Instance.openSearch(e);
+ }
+
+ highlightDoc = (e: React.PointerEvent) => {
+ let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc);
+ docViews.forEach(element => {
+ element.props.Document.libraryBrush = true;
+ });
+ }
+
+ unHighlightDoc = (e: React.PointerEvent) => {
+ let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc);
+ docViews.forEach(element => {
+ element.props.Document.libraryBrush = false;
+ });
+ }
+
+ render() {
+ return (
+ <div className="search-overview" onPointerDown={this.pointerDown}>
+ <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} ref={this.collectionRef} id="result" onClick={this.onClick} onPointerDown={() => {
+ this.pointerDown;
+ SetupDrag(this.collectionRef, this.startDocDrag);
+ }} >
+ <div className="main-search-info">
+ <div className="search-title" id="result" >{this.props.doc.title}</div>
+ <div className="search-info">
+ <div className="link-container item">
+ <div className="link-count">{this.linkCount}</div>
+ <div className="link-extended">{this.linkString}</div>
+ </div>
+ <div className="icon">
+ <div className="search-type" >{this.DocumentIcon}</div>
+ <div className="search-label">{this.props.doc.type}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="searchBox-instances">
+ <SelectorContextMenu {...this.props} />
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/ToggleBar.scss b/src/client/views/search/ToggleBar.scss
new file mode 100644
index 000000000..633a194fe
--- /dev/null
+++ b/src/client/views/search/ToggleBar.scss
@@ -0,0 +1,36 @@
+@import "../globalCssVariables";
+
+.toggle-title {
+ display: flex;
+ align-items: center;
+ color: $light-color;
+ text-transform: uppercase;
+ flex-direction: row;
+ justify-content: space-around;
+ padding: 5px;
+
+ .toggle-option {
+ width: 50%;
+ text-align: center;
+ -webkit-transition: all 0.2s ease-in-out;
+ -moz-transition: all 0.2s ease-in-out;
+ -o-transition: all 0.2s ease-in-out;
+ transition: all 0.2s ease-in-out;
+ }
+}
+
+.toggle-bar {
+ height: 50px;
+ background-color: $alt-accent;
+ border-radius: 10px;
+ padding: 5px;
+ display: flex;
+ align-items: center;
+
+ .toggle-button {
+ width: 275px;
+ height: 100%;
+ border-radius: 10px;
+ background-color: $light-color;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx
new file mode 100644
index 000000000..6f141d42a
--- /dev/null
+++ b/src/client/views/search/ToggleBar.tsx
@@ -0,0 +1,100 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, computed } from 'mobx';
+import "./SearchBox.scss";
+import "./ToggleBar.scss";
+import * as anime from 'animejs';
+import { SearchBox } from './SearchBox';
+
+export interface ToggleBarProps {
+ originalStatus: boolean;
+ optionOne: string;
+ optionTwo: string;
+}
+
+@observer
+export class ToggleBar extends React.Component<ToggleBarProps>{
+ static Instance: ToggleBar;
+
+ @observable forwardTimeline: anime.AnimeTimelineInstance;
+ @observable _toggleButton: React.RefObject<HTMLDivElement>;
+ @observable _originalStatus: boolean = this.props.originalStatus;
+
+ constructor(props: ToggleBarProps) {
+ super(props);
+ ToggleBar.Instance = this;
+ this._toggleButton = React.createRef();
+ this.forwardTimeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "reverse",
+ });
+ }
+
+ @computed get totalWidth() { return this.getTotalWidth(); }
+
+ getTotalWidth() {
+ let bar = document.getElementById("toggle-bar");
+ let tog = document.getElementById("toggle-button");
+ let barwidth = 0;
+ let togwidth = 0;
+ if (bar && tog) {
+ barwidth = bar.clientWidth;
+ togwidth = tog.clientWidth;
+ }
+ let totalWidth = (barwidth - togwidth - 10);
+ return totalWidth;
+ }
+
+ componentDidMount = () => {
+
+ let totalWidth = this.totalWidth;
+
+ if (this._originalStatus) {
+ this.forwardTimeline.add({
+ targets: this._toggleButton.current,
+ translateX: totalWidth,
+ easing: "easeInOutQuad",
+ duration: 500
+ });
+ }
+ else {
+ this.forwardTimeline.add({
+ targets: this._toggleButton.current,
+ translateX: -totalWidth,
+ easing: "easeInOutQuad",
+ duration: 500
+ });
+ }
+ }
+
+ @action.bound
+ onclick() {
+ this.forwardTimeline.play();
+ this.forwardTimeline.reverse();
+ SearchBox.Instance.handleWordQueryChange();
+ }
+
+ @action.bound
+ public resetToggle = () => {
+ if (!SearchBox.Instance.getBasicWordStatus()) {
+ this.forwardTimeline.play();
+ this.forwardTimeline.reverse();
+ SearchBox.Instance.handleWordQueryChange();
+ }
+ }
+
+ render() {
+ return (
+ <div>
+ <div className="toggle-title">
+ <div className="toggle-option" style={{ opacity: (SearchBox.Instance.getBasicWordStatus() ? 1 : .4) }}>{this.props.optionOne}</div>
+ <div className="toggle-option" style={{ opacity: (SearchBox.Instance.getBasicWordStatus() ? .4 : 1) }}>{this.props.optionTwo}</div>
+ </div>
+ <div className="toggle-bar" id="toggle-bar" style={{ flexDirection: (this._originalStatus ? "row" : "row-reverse") }}>
+ <div className="toggle-button" id="toggle-button" ref={this._toggleButton} onClick={this.onclick} />
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file