diff options
author | Abdullah Ahmed <abdullah_ahmed@brown.edu> | 2019-06-29 19:25:38 -0400 |
---|---|---|
committer | Abdullah Ahmed <abdullah_ahmed@brown.edu> | 2019-06-29 19:25:38 -0400 |
commit | 82f61a9ff5406326a8bae736a63ddae7a386a181 (patch) | |
tree | 92fbbc7a35866c62618f31ec688fcd387116d9c2 | |
parent | 881df5e1255681a306af2d9f78b092b3688ad38c (diff) | |
parent | 8e6caaf2a4f9f5c9777719a85dcacf4922830c04 (diff) |
Merge branch 'text_box_ab' of https://github.com/browngraphicslab/Dash-Web into text_box_ab
142 files changed, 10507 insertions, 2503 deletions
diff --git a/.gitignore b/.gitignore index a499c39a3..5d3100dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules package-lock.json dist/ .DS_Store +.env +ClientUtils.ts
\ No newline at end of file diff --git a/deploy/index.html b/deploy/index.html index 532b995f8..f4a019b71 100644 --- a/deploy/index.html +++ b/deploy/index.html @@ -6,6 +6,7 @@ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script> + <script src="https://rawgithub.com/juliangarnier/anime/master/anime.min.js"></script> </head> <body> diff --git a/package.json b/package.json index 438572b57..51d1bab5d 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "", "main": "index.js", "scripts": { - "start": "ts-node-dev -- src/server/index.ts", - "debug": "ts-node-dev --inspect -- src/server/index.ts", - "build": "webpack --env production", + "start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts", + "debug": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --inspect -- src/server/index.ts", + "build": "cross-env NODE_OPTIONS=--max_old_space_size=4096 webpack --env production", "test": "mocha -r ts-node/register test/**/*.ts", "tsc": "tsc" }, @@ -19,6 +19,7 @@ "awesome-typescript-loader": "^5.2.1", "chai": "^4.2.0", "copy-webpack-plugin": "^4.6.0", + "cross-env": "^5.2.0", "css-loader": "^2.1.1", "file-loader": "^3.0.1", "fork-ts-checker-webpack-plugin": "^1.0.2", @@ -42,26 +43,32 @@ "dependencies": { "@fortawesome/fontawesome-free-solid": "^5.0.13", "@fortawesome/fontawesome-svg-core": "^1.2.15", + "@fortawesome/free-brands-svg-icons": "^5.9.0", + "@fortawesome/free-regular-svg-icons": "^5.9.0", "@fortawesome/free-solid-svg-icons": "^5.7.2", "@fortawesome/react-fontawesome": "^0.1.4", "@hig/flyout": "^1.0.3", "@hig/theme-context": "^2.1.3", "@hig/theme-data": "^2.3.3", "@trendmicro/react-dropdown": "^1.3.0", + "@types/animejs": "^2.0.2", "@types/async": "^2.4.1", "@types/bcrypt-nodejs": "0.0.30", "@types/bluebird": "^3.5.25", "@types/body-parser": "^1.17.0", + "@types/classnames": "^2.2.8", "@types/connect-flash": "0.0.34", "@types/cookie-parser": "^1.4.1", "@types/cookie-session": "^2.0.36", "@types/d3-format": "^1.3.1", + "@types/dotenv": "^6.1.1", "@types/express": "^4.16.1", "@types/express-flash": "0.0.0", "@types/express-session": "^1.15.12", "@types/express-validator": "^3.0.0", "@types/formidable": "^1.0.31", "@types/jquery": "^3.3.29", + "@types/jquery-awesome-cursor": "^0.3.0", "@types/jsonwebtoken": "^8.3.2", "@types/lodash": "^4.14.121", "@types/mobile-detect": "^1.3.4", @@ -71,6 +78,7 @@ "@types/nodemailer": "^4.6.6", "@types/passport": "^1.0.0", "@types/passport-local": "^1.0.33", + "@types/pdfjs-dist": "^2.0.0", "@types/prosemirror-commands": "^1.0.1", "@types/prosemirror-history": "^1.0.1", "@types/prosemirror-inputrules": "^1.0.2", @@ -101,6 +109,7 @@ "bluebird": "^3.5.3", "body-parser": "^1.18.3", "bootstrap": "^4.3.1", + "canvas": "^2.5.0", "class-transformer": "^0.2.0", "connect-flash": "^0.1.1", "connect-mongo": "^2.0.3", @@ -108,6 +117,7 @@ "cookie-session": "^2.0.0-beta.3", "crypto-browserify": "^3.11.0", "d3-format": "^1.3.2", + "dotenv": "^8.0.0", "express": "^4.16.4", "express-flash": "0.0.2", "express-session": "^1.15.6", @@ -120,7 +130,9 @@ "html-to-image": "^0.1.0", "i": "^0.3.6", "image-data-uri": "^2.0.0", + "image-size": "^0.7.4", "imagesloaded": "^4.1.4", + "jquery-awesome-cursor": "^0.3.1", "jsonwebtoken": "^8.5.0", "jsx-to-string": "^1.4.0", "lodash": "^4.17.11", @@ -139,6 +151,8 @@ "p-limit": "^2.2.0", "passport": "^0.4.0", "passport-local": "^1.0.0", + "pdfjs-dist": "^2.0.943", + "probe-image-size": "^4.0.0", "prosemirror-commands": "^1.0.7", "prosemirror-example-setup": "^1.0.1", "prosemirror-history": "^1.0.4", @@ -152,6 +166,7 @@ "pug": "^2.0.3", "raw-loader": "^1.0.0", "react": "^16.8.4", + "react-anime": "^2.2.0", "react-bootstrap": "^1.0.0-beta.5", "react-bootstrap-dropdown-menu": "^1.1.15", "react-color": "^2.17.0", @@ -169,13 +184,13 @@ "react-split-pane": "^0.1.85", "react-table": "^6.9.2", "request": "^2.88.0", - "request-image-size": "^2.1.0", "request-promise": "^4.2.4", "serializr": "^1.5.1", "sharp": "^0.22.1", "socket.io": "^2.2.0", "socket.io-client": "^2.2.0", "solr-node": "^1.1.3", + "standard-http-error": "^2.0.1", "typescript-collections": "^1.3.2", "url-loader": "^1.1.2", "uuid": "^3.3.2", diff --git a/solr/conf/schema.xml b/solr/conf/schema.xml index 30e8daa65..8610786af 100644 --- a/solr/conf/schema.xml +++ b/solr/conf/schema.xml @@ -8,7 +8,7 @@ <filter class="solr.StopFilterFactory" words="stopwords.txt"/> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.PorterStemFilterFactory"/> - <filter class="solr.NGramFilterFactory" minGramSize="3" maxGramSize="12"/> + <filter class="solr.NGramFilterFactory" minGramSize="2" maxGramSize="12"/> </analyzer> <analyzer type="query"> <tokenizer class="solr.StandardTokenizerFactory"/> diff --git a/solr/conf/solrconfig.xml b/solr/conf/solrconfig.xml index 90eff5363..0d8792749 100644 --- a/solr/conf/solrconfig.xml +++ b/solr/conf/solrconfig.xml @@ -695,7 +695,7 @@ --> <lst name="defaults"> <str name="echoParams">explicit</str> - <int name="rows">10</int> + <int name="rows">10000000</int> <str name="df">text</str> <!-- Default search field <str name="df">text</str> diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index b1df89dbd..6dc98dbbc 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -18,7 +18,6 @@ import { action } from "mobx"; import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel"; import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel"; import { AggregateFunction } from "../northstar/model/idea/idea"; -import { Template } from "../views/Templates"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; import { Field, Doc, Opt } from "../../new_fields/Doc"; @@ -26,7 +25,7 @@ import { OmitKeys } from "../../Utils"; import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField"; import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; -import { Cast, NumCast } from "../../new_fields/Types"; +import { Cast, NumCast, StrCast } from "../../new_fields/Types"; import { IconField } from "../../new_fields/IconField"; import { listSpec } from "../../new_fields/Schema"; import { DocServer } from "../DocServer"; @@ -35,12 +34,30 @@ import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; import { UndoManager } from "../util/UndoManager"; import { RouteStore } from "../../server/RouteStore"; -var requestImageSize = require('request-image-size'); +import { LinkManager } from "../util/LinkManager"; +import { DocumentManager } from "../util/DocumentManager"; +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; @@ -51,7 +68,6 @@ export interface DocumentOptions { panY?: number; page?: number; scale?: number; - baseLayout?: string; layout?: string; templates?: List<string>; viewType?: number; @@ -69,33 +85,34 @@ export interface DocumentOptions { const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; export namespace DocUtils { - export function MakeLink(source: Doc, target: Doc, targetContext?: Doc) { - let protoSrc = source.proto ? source.proto : source; - let protoTarg = target.proto ? target.proto : target; + export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") { + if (LinkManager.Instance.doesLinkExist(source, target)) return; + let sv = DocumentManager.Instance.getDocumentView(source); + if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return; + if (target === CurrentUserUtils.UserDocument) return; + UndoManager.RunInBatch(() => { let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); + linkDoc.type = DocTypes.LINK; let linkDocProto = Doc.GetProto(linkDoc); - linkDocProto.title = source.title + " to " + target.title; - linkDocProto.linkDescription = ""; - linkDocProto.linkTags = "Default"; - linkDocProto.linkedTo = target; - linkDocProto.linkedFrom = source; - linkDocProto.linkedToPage = target.curPage; - linkDocProto.linkedFromPage = source.curPage; - linkDocProto.linkedToContext = targetContext; + linkDocProto.context = targetContext; + linkDocProto.title = title; //=== "" ? source.title + " to " + target.title : title; + linkDocProto.linkDescription = description; + linkDocProto.linkTags = tags; + + linkDocProto.anchor1 = source; + linkDocProto.anchor1Page = source.curPage; + linkDocProto.anchor1Groups = new List<Doc>([]); + linkDocProto.anchor2 = target; + linkDocProto.anchor2Page = target.curPage; + linkDocProto.anchor2Groups = new List<Doc>([]); + + LinkManager.Instance.addLink(linkDoc); - let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc)); - let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc)); - !linkedFrom && (protoTarg.linkedFromDocs = linkedFrom = new List<Doc>()); - !linkedTo && (protoSrc.linkedToDocs = linkedTo = new List<Doc>()); - linkedFrom.push(linkDoc); - linkedTo.push(linkDoc); return linkDoc; }, "make link"); } - - } export namespace Docs { @@ -109,6 +126,7 @@ export namespace Docs { let audioProto: Doc; let pdfProto: Doc; let iconProto: Doc; + // let linkProto: Doc; const textProtoId = "textProto"; const histoProtoId = "histoProto"; const pdfProtoId = "pdfProto"; @@ -119,6 +137,7 @@ export namespace Docs { const videoProtoId = "videoProto"; const audioProtoId = "audioProto"; const iconProtoId = "iconProto"; + // const linkProtoId = "linkProto"; export function initProtos(): Promise<void> { return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => { @@ -136,7 +155,7 @@ export namespace Docs { } function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Doc { - return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout, baseLayout: layout }); + return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout }); } function SetInstanceOptions<U extends Field>(doc: Doc, options: DocumentOptions, value: U) { const deleg = Doc.MakeDelegate(doc); @@ -150,58 +169,58 @@ 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, nativeWidth: 1200, width: 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; } - function CreateInstance(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) { + export function CreateInstance(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) { const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys); if (!("author" in protoProps)) { protoProps.author = CurrentUserUtils.email; @@ -250,6 +269,7 @@ export namespace Docs { export function IconDocument(icon: string, options: DocumentOptions = {}) { return CreateInstance(iconProto, new IconField(icon), options); } + export function PdfDocument(url: string, options: DocumentOptions = {}) { return CreateInstance(pdfProto, new PdfField(new URL(url)), options); } @@ -346,7 +366,7 @@ export namespace Docs { {layout} </div> <div style="height:(100% + 25px); width:100%; position:absolute"> - <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/> + <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/> </div> </div> `); @@ -358,14 +378,14 @@ export namespace Docs { {layout} </div> <div style="height:25px; width:100%; position:absolute"> - <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/> + <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/> </div> </div> `); } /* - + this template requires an additional style setting on the collectionView-cont to make the layout relative .collectionView-cont { @@ -381,7 +401,7 @@ export namespace Docs { {layout} </div> <div style="height:15%; width:100%; position:absolute"> - <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/> + <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/> </div> </div> `); diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx index d7732ee86..a60eaea85 100644 --- a/src/client/northstar/dash-nodes/HistogramBox.tsx +++ b/src/client/northstar/dash-nodes/HistogramBox.tsx @@ -125,8 +125,7 @@ export class HistogramBox extends React.Component<FieldViewProps> { let mapped = brushingDocs.map((brush, i) => { brush.backgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length]; let brushed = DocListCast(brush.brushingDocs); - if (!brushed.length) - return null; + if (!brushed.length) return null; return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] }; }); runInAction(() => this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...mapped.filter(m => m) as { l: Doc, b: Doc }[])); diff --git a/src/client/util/ClientUtils.ts.temp b/src/client/util/ClientUtils.ts.temp new file mode 100644 index 000000000..f9fad5ed9 --- /dev/null +++ b/src/client/util/ClientUtils.ts.temp @@ -0,0 +1,3 @@ +export namespace ClientUtils { + export const RELEASE = "mode"; +}
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index ff0c1560b..d0014dbf3 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,7 +1,7 @@ -import { computed, observable } from 'mobx'; +import { computed, observable, action } from 'mobx'; import { DocumentView } from '../views/nodes/DocumentView'; import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; -import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types'; +import { FieldValue, Cast, NumCast, BoolCast, StrCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; import { undoBatch } from './UndoManager'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; @@ -9,6 +9,8 @@ import { CollectionView } from '../views/collections/CollectionView'; import { CollectionPDFView } from '../views/collections/CollectionPDFView'; import { CollectionVideoView } from '../views/collections/CollectionVideoView'; import { Id } from '../../new_fields/FieldSymbols'; +import { LinkManager } from './LinkManager'; +import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; export class DocumentManager { @@ -30,14 +32,38 @@ 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; let passes = preferredCollection ? [preferredCollection, undefined] : [undefined]; - for (let i = 0; i < passes.length; i++) { + for (let pass of passes) { DocumentManager.Instance.DocumentViews.map(view => { - if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { + if (view.props.Document[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; return; } @@ -45,7 +71,7 @@ export class DocumentManager { if (!toReturn) { DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document.proto; - if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { + if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; } }); @@ -66,13 +92,11 @@ export class DocumentManager { //gets document view that is in a freeform canvas collection DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document; - // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) { if (doc === toFind) { toReturn.push(view); } else { - let docSrc = FieldValue(doc.proto); - if (docSrc && Object.is(docSrc, toFind)) { + if (Doc.AreProtosEqual(doc, toFind)) { toReturn.push(view); } } @@ -83,39 +107,27 @@ export class DocumentManager { @computed public get LinkedDocumentViews() { - return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => { - let linksList = DocListCast(dv.props.Document.linkedToDocs); - if (linksList && linksList.length) { - pairs.push(...linksList.reduce((pairs, link) => { - if (link) { - let linkToDoc = FieldValue(Cast(link.linkedTo, Doc)); - if (linkToDoc) { - DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => - pairs.push({ a: dv, b: docView1, l: link })); - } - } - return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); - } - linksList = DocListCast(dv.props.Document.linkedFromDocs); - if (linksList && linksList.length) { - pairs.push(...linksList.reduce((pairs, link) => { - if (link) { - let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc)); - if (linkFromDoc) { - DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 => - pairs.push({ a: dv, b: docView1, l: link })); - } - } - return pairs; - }, pairs)); - } + let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => { + let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document); + pairs.push(...linksList.reduce((pairs, link) => { + if (link) { + let linkToDoc = LinkManager.Instance.getOppositeAnchor(link, dv.props.Document); + DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { + pairs.push({ a: dv, b: docView1, l: link }); + }); + } + return pairs; + }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); + // } return pairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); + + return pairs; } + @undoBatch - public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => { + public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => { let doc = Doc.GetProto(docDelegate); const contextDoc = await Cast(doc.annotationOn, Doc); if (contextDoc) { @@ -129,39 +141,62 @@ export class DocumentManager { if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) { docView.props.Document.libraryBrush = true; if (linkPage !== undefined) docView.props.Document.curPage = linkPage; - docView.props.focus(docView.props.Document); + docView.props.focus(docView.props.Document, willZoom); } else { if (!contextDoc) { if (docContext) { let targetContextView: DocumentView | null; if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) { docContext.panTransformType = "Ease"; - targetContextView.props.focus(docDelegate); + targetContextView.props.focus(docDelegate, willZoom); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, docContext); setTimeout(() => { - this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage); + this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); }, 10); } } else { const actualDoc = Doc.MakeAlias(docDelegate); actualDoc.libraryBrush = true; if (linkPage !== undefined) actualDoc.curPage = linkPage; - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, actualDoc); } } else { let contextView: DocumentView | null; docDelegate.libraryBrush = true; if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) { contextDoc.panTransformType = "Ease"; - contextView.props.focus(docDelegate); + contextView.props.focus(docDelegate, willZoom); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, contextDoc); setTimeout(() => { - this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage); + this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); }, 10); } } } } + + @action + zoomIntoScale = (docDelegate: Doc, scale: number) => { + let doc = Doc.GetProto(docDelegate); + + let docView: DocumentView | null; + docView = DocumentManager.Instance.getDocumentView(doc); + if (docView) { + docView.props.zoomToScale(scale); + } + } + + getScaleOfDocView = (docDelegate: Doc) => { + let doc = Doc.GetProto(docDelegate); + + let docView: DocumentView | null; + docView = DocumentManager.Instance.getDocumentView(doc); + if (docView) { + return docView.props.getScale(); + } else { + return 1; + } + } }
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index ab00fabe1..a6bba3656 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,11 +1,15 @@ import { action, runInAction, observable } from "mobx"; import { Doc, DocListCastAsync } from "../../new_fields/Doc"; -import { Cast } from "../../new_fields/Types"; +import { Cast, StrCast } from "../../new_fields/Types"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import * as globalCssVariables from "../views/globalCssVariables.scss"; +import { LinkManager } from "./LinkManager"; import { URLField } from "../../new_fields/URLField"; import { SelectionManager } from "./SelectionManager"; +import { Docs, DocUtils } from "../documents/Documents"; +import { DocumentManager } from "./DocumentManager"; +import { Id } from "../../new_fields/FieldSymbols"; export type dropActionType = "alias" | "copy" | undefined; export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, options?: any, dontHideOnDrop?: boolean) { @@ -15,7 +19,8 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); - var dragData = new DragManager.DocumentDragData([await docFunc()]); + let doc = await docFunc(); + var dragData = new DragManager.DocumentDragData([doc], [doc]); dragData.dropAction = dropAction; dragData.moveDocument = moveFunc; dragData.options = options; @@ -27,40 +32,55 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () document.removeEventListener('pointerup', onRowUp); }; let onItemDown = async (e: React.PointerEvent) => { - // if (this.props.isSelected() || this.props.isTopMost) { if (e.button === 0) { e.stopPropagation(); if (e.shiftKey && CollectionDockingView.Instance) { - CollectionDockingView.Instance.StartOtherDrag([await docFunc()], e); + CollectionDockingView.Instance.StartOtherDrag(e, [await docFunc()]); } else { document.addEventListener("pointermove", onRowMove); document.addEventListener("pointerup", onRowUp); } } - //} }; return onItemDown; } +export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) { + let draggeddoc = LinkManager.Instance.getOppositeAnchor(linkDoc, sourceDoc); + + let moddrag = await Cast(draggeddoc.annotationOn, Doc); + let dragdocs = moddrag ? [moddrag] : [draggeddoc]; + let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs); + dragData.dropAction = "alias" as dropActionType; + DragManager.StartLinkedDocumentDrag([dragEle], sourceDoc, dragData, x, y, { + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); +} + export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) { let srcTarg = sourceDoc.proto; let draggedDocs: Doc[] = []; - let draggedFromDocs: Doc[] = []; + if (srcTarg) { - let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs); - let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs); - if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc); - if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc); + let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg); + if (linkDocs) { + draggedDocs = linkDocs.map(link => { + return LinkManager.Instance.getOppositeAnchor(link, sourceDoc); + }); + } } - draggedDocs.push(...draggedFromDocs); if (draggedDocs.length) { let moddrag: Doc[] = []; for (const draggedDoc of draggedDocs) { let doc = await Cast(draggedDoc.annotationOn, Doc); if (doc) moddrag.push(doc); } - let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); - DragManager.StartDocumentDrag([dragEle], dragData, x, y, { + let dragdocs = moddrag.length ? moddrag : draggedDocs; + let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs); + DragManager.StartLinkedDocumentDrag([dragEle], sourceDoc, dragData, x, y, { handlers: { dragComplete: action(emptyFunction), }, @@ -90,6 +110,8 @@ export namespace DragManager { handlers: DragHandlers; hideSource: boolean | (() => boolean); + + withoutShiftDrag?: boolean; } export interface DragDropDisposer { @@ -141,13 +163,15 @@ export namespace DragManager { export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; export class DocumentDragData { - constructor(dragDoc: Doc[]) { + constructor(dragDoc: Doc[], dragDataDocs: (Doc | undefined)[]) { this.draggedDocuments = dragDoc; + this.draggedDataDocs = dragDataDocs; this.droppedDocuments = dragDoc; this.xOffset = 0; this.yOffset = 0; } draggedDocuments: Doc[]; + draggedDataDocs: (Doc | undefined)[]; droppedDocuments: Doc[]; xOffset: number; yOffset: number; @@ -157,17 +181,64 @@ export namespace DragManager { [id: string]: any; } + export class AnnotationDragData { + constructor(dragDoc: Doc, annotationDoc: Doc, dropDoc: Doc) { + this.dragDocument = dragDoc; + this.dropDocument = dropDoc; + this.annotationDocument = annotationDoc; + this.xOffset = this.yOffset = 0; + } + dragDocument: Doc; + annotationDocument: Doc; + dropDocument: Doc; + xOffset: number; + yOffset: number; + dropAction: dropActionType; + userDropAction: dropActionType; + } + export let StartDragFunctions: (() => void)[] = []; export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { runInAction(() => StartDragFunctions.map(func => func())); StartDrag(eles, dragData, downX, downY, options, - (dropData: { [id: string]: any }) => + (dropData: { [id: string]: any }) => { (dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) : dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) : - dragData.draggedDocuments)); + dragData.draggedDocuments + ); + }); + } + + export function StartLinkedDocumentDrag(eles: HTMLElement[], sourceDoc: Doc, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { + + runInAction(() => StartDragFunctions.map(func => func())); + StartDrag(eles, dragData, downX, downY, options, + (dropData: { [id: string]: any }) => { + // dropData.droppedDocuments = + let droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => { + let dvs = DocumentManager.Instance.getDocumentViews(d); + + if (dvs.length) { + let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView); + if (inContext.length) { + inContext.forEach(dv => droppedDocs.push(dv.props.Document)); + } else { + droppedDocs.push(Doc.MakeAlias(d)); + } + } else { + droppedDocs.push(Doc.MakeAlias(d)); + } + return droppedDocs; + }, []); + dropData.droppedDocuments = droppedDocuments; + }); + } + + export function StartAnnotationDrag(eles: HTMLElement[], dragData: AnnotationDragData, downX: number, downY: number, options?: DragOptions) { + StartDrag(eles, dragData, downX, downY, options); } export class LinkDragData { @@ -217,7 +288,9 @@ export namespace DragManager { let ys: number[] = []; const docs: Doc[] = - dragData instanceof DocumentDragData ? dragData.draggedDocuments : []; + dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; + const datadocs: (Doc | undefined)[] = + dragData instanceof DocumentDragData ? dragData.draggedDataDocs : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; let dragElements = eles.map(ele => { const w = ele.offsetWidth, h = ele.offsetHeight; @@ -261,11 +334,14 @@ export namespace DragManager { // } // } let set = dragElement.getElementsByTagName('*'); - for (let i = 0; i < set.length; i++) + if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none"; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < set.length; i++) { if (set[i].hasAttribute("style")) { let s = set[i]; (s as any).style.pointerEvents = "none"; } + } dragDiv.appendChild(dragElement); @@ -289,14 +365,14 @@ export namespace DragManager { if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined; } - if (e.shiftKey && CollectionDockingView.Instance) { + if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) { AbortDrag(); - CollectionDockingView.Instance.StartOtherDrag(docs, { + CollectionDockingView.Instance.StartOtherDrag({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 - }); + }, docs, datadocs); } //TODO: Why can't we use e.movementX and e.movementY? let moveX = e.pageX - lastX; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts new file mode 100644 index 000000000..f2f3e51dd --- /dev/null +++ b/src/client/util/LinkManager.ts @@ -0,0 +1,238 @@ +import { observable, action } from "mobx"; +import { StrCast, Cast, FieldValue } from "../../new_fields/Types"; +import { Doc, DocListCast } from "../../new_fields/Doc"; +import { listSpec } from "../../new_fields/Schema"; +import { List } from "../../new_fields/List"; +import { Id } from "../../new_fields/FieldSymbols"; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; + + +/* + * link doc: + * - anchor1: doc + * - anchor1page: number + * - anchor1groups: list of group docs representing the groups anchor1 categorizes this link/anchor2 in + * - anchor2: doc + * - anchor2page: number + * - anchor2groups: list of group docs representing the groups anchor2 categorizes this link/anchor1 in + * + * group doc: + * - type: string representing the group type/name/category + * - metadata: doc representing the metadata kvps + * + * metadata doc: + * - user defined kvps + */ +export class LinkManager { + + private static _instance: LinkManager; + public static get Instance(): LinkManager { + return this._instance || (this._instance = new this()); + } + private constructor() { + } + + // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes' + // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type + public get LinkManagerDoc(): Doc | undefined { + return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc)); + } + + public getAllLinks(): Doc[] { + return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : []; + } + + public addLink(linkDoc: Doc): boolean { + let linkList = LinkManager.Instance.getAllLinks(); + linkList.push(linkDoc); + if (LinkManager.Instance.LinkManagerDoc) { + LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList); + return true; + } + return false; + } + + public deleteLink(linkDoc: Doc): boolean { + let linkList = LinkManager.Instance.getAllLinks(); + let index = LinkManager.Instance.getAllLinks().indexOf(linkDoc); + if (index > -1) { + linkList.splice(index, 1); + if (LinkManager.Instance.LinkManagerDoc) { + LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList); + return true; + } + } + return false; + } + + // finds all links that contain the given anchor + public getAllRelatedLinks(anchor: Doc): Doc[] {//List<Doc> { + let related = LinkManager.Instance.getAllLinks().filter(link => { + let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)); + let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc)); + return protomatch1 || protomatch2; + }); + return related; + } + + public deleteAllLinksOnAnchor(anchor: Doc) { + let related = LinkManager.Instance.getAllRelatedLinks(anchor); + related.forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); + } + + public addGroupType(groupType: string): boolean { + if (LinkManager.Instance.LinkManagerDoc) { + LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]); + let groupTypes = LinkManager.Instance.getAllGroupTypes(); + groupTypes.push(groupType); + LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes); + return true; + } + return false; + } + + // removes all group docs from all links with the given group type + public deleteGroupType(groupType: string): boolean { + if (LinkManager.Instance.LinkManagerDoc) { + if (LinkManager.Instance.LinkManagerDoc[groupType]) { + let groupTypes = LinkManager.Instance.getAllGroupTypes(); + let index = groupTypes.findIndex(type => type.toUpperCase() === groupType.toUpperCase()); + if (index > -1) groupTypes.splice(index, 1); + LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes); + LinkManager.Instance.LinkManagerDoc[groupType] = undefined; + LinkManager.Instance.getAllLinks().forEach(linkDoc => { + LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType); + LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType); + }); + } + return true; + } else return false; + } + + public getAllGroupTypes(): string[] { + if (LinkManager.Instance.LinkManagerDoc) { + if (LinkManager.Instance.LinkManagerDoc.allGroupTypes) { + return Cast(LinkManager.Instance.LinkManagerDoc.allGroupTypes, listSpec("string"), []); + } else { + LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>([]); + return []; + } + } + return []; + } + + // gets the groups associates with an anchor in a link + public getAnchorGroups(linkDoc: Doc, anchor: Doc): Array<Doc> { + if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) { + return DocListCast(linkDoc.anchor1Groups); + } else { + return DocListCast(linkDoc.anchor2Groups); + } + } + + // sets the groups of the given anchor in the given link + public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) { + if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) { + linkDoc.anchor1Groups = new List<Doc>(groups); + } else { + linkDoc.anchor2Groups = new List<Doc>(groups); + } + } + + public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) { + let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor); + let index = groups.findIndex(gDoc => { + return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase(); + }); + if (index > -1 && replace) { + groups[index] = groupDoc; + } + if (index === -1) { + groups.push(groupDoc); + } + LinkManager.Instance.setAnchorGroups(linkDoc, anchor, groups); + } + + // removes group doc of given group type only from given anchor on given link + public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) { + let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor); + let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase()); + LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups); + } + + // returns map of group type to anchor's links in that group type + public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> { + let related = this.getAllRelatedLinks(anchor); + let anchorGroups = new Map<string, Array<Doc>>(); + related.forEach(link => { + let groups = LinkManager.Instance.getAnchorGroups(link, anchor); + + if (groups.length > 0) { + groups.forEach(groupDoc => { + let groupType = StrCast(groupDoc.type); + if (groupType === "") { + let group = anchorGroups.get("*"); + anchorGroups.set("*", group ? [...group, link] : [link]); + } else { + let group = anchorGroups.get(groupType); + anchorGroups.set(groupType, group ? [...group, link] : [link]); + } + }); + } else { + // if link is in no groups then put it in default group + let group = anchorGroups.get("*"); + anchorGroups.set("*", group ? [...group, link] : [link]); + } + + }); + return anchorGroups; + } + + // gets a list of strings representing the keys of the metadata associated with the given group type + public getMetadataKeysInGroup(groupType: string): string[] { + if (LinkManager.Instance.LinkManagerDoc) { + return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : []; + } + return []; + } + + public setMetadataKeysForGroup(groupType: string, keys: string[]): boolean { + if (LinkManager.Instance.LinkManagerDoc) { + LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keys); + return true; + } + return false; + } + + // returns a list of all metadata docs associated with the given group type + public getAllMetadataDocsInGroup(groupType: string): Array<Doc> { + let md: Doc[] = []; + let allLinks = LinkManager.Instance.getAllLinks(); + allLinks.forEach(linkDoc => { + let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc)); + let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc)); + anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) md.push(Cast(groupDoc.metadata, Doc, new Doc)); }); + anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) md.push(Cast(groupDoc.metadata, Doc, new Doc)); }); + }); + return md; + } + + // checks if a link with the given anchors exists + public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean { + let allLinks = LinkManager.Instance.getAllLinks(); + let index = allLinks.findIndex(linkDoc => { + return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor2)) || + (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor1)); + }); + return index !== -1; + } + + // finds the opposite anchor of a given anchor in a link + public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc { + if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) { + return Cast(linkDoc.anchor2, Doc, new Doc); + } else { + return Cast(linkDoc.anchor1, Doc, new Doc); + } + } +}
\ No newline at end of file diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts index 091926d0a..fa9e2e5af 100644 --- a/src/client/util/ProsemirrorExampleTransfer.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -7,6 +7,8 @@ import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirr import { undo, redo } from "prosemirror-history"; import { undoInputRule } from "prosemirror-inputrules"; import { Transaction, EditorState } from "prosemirror-state"; +import { TooltipTextMenu } from "./TooltipTextMenu"; +import { Statement } from "../northstar/model/idea/idea"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -96,5 +98,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: }); } + bind("Mod-s", TooltipTextMenu.insertStar); + return keys; } diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 3078e0bbc..64fdc6f30 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType, Slice, Mark } from "prosemirror-model"; +import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType, Slice, Mark, Node } from "prosemirror-model"; import { joinUp, lift, setBlockType, toggleMark, wrapIn, selectNodeForward, deleteSelection } from 'prosemirror-commands'; import { redo, undo } from 'prosemirror-history'; import { orderedList, bulletList, listItem, } from 'prosemirror-schema-list'; @@ -91,8 +91,8 @@ export const nodes: { [index: string]: NodeSpec } = { attrs: { visibility: { default: false }, text: { default: undefined }, - oldtextslice: { default: undefined }, - oldtextlen: { default: 0 } + textslice: { default: undefined }, + textlen: { default: 0 } }, group: "inline", @@ -157,12 +157,12 @@ export const nodes: { [index: string]: NodeSpec } = { title: dom.getAttribute("title"), alt: dom.getAttribute("alt"), width: Math.min(100, Number(dom.getAttribute("width"))), - } + }; } }], toDOM(node) { - const attrs = { style: `width: ${node.attrs.width}` } - return ["video", { ...node.attrs, ...attrs }] + const attrs = { style: `width: ${node.attrs.width}` }; + return ["video", { ...node.attrs, ...attrs }]; } }, @@ -285,7 +285,7 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [{ style: 'background: #d9dbdd' }], toDOM() { return ['span', { - style: 'background: #d9dbdd' + style: 'color: blue' }]; } }, @@ -350,6 +350,16 @@ export const marks: { [index: string]: MarkSpec } = { /** FONT SIZES */ + pFontSize: { + attrs: { + fontSize: { default: 10 } + }, + inclusive: false, + parseDOM: [{ style: 'font-size: 10px;' }], + toDOM: (node) => ['span', { + style: `font-size: ${node.attrs.fontSize}px;` + }] + }, p10: { parseDOM: [{ style: 'font-size: 10px;' }], @@ -483,39 +493,44 @@ export class SummarizedView { _view: any; constructor(node: any, view: any, getPos: any) { this._collapsed = document.createElement("span"); - this._collapsed.textContent = "㊉"; + this._collapsed.textContent = node.attrs.visibility ? "㊀" : "㊉"; this._collapsed.style.opacity = "0.5"; - // this._collapsed.style.background = "yellow"; this._collapsed.style.position = "relative"; this._collapsed.style.width = "40px"; this._collapsed.style.height = "20px"; let self = this; this._view = view; + const js = node.toJSON; + node.toJSON = function () { + + return js.apply(this, arguments); + }; this._collapsed.onpointerdown = function (e: any) { - console.log("star pressed!"); if (node.attrs.visibility) { - node.attrs.visibility = !node.attrs.visibility; - console.log("content is visible"); + // node.attrs.visibility = !node.attrs.visibility; let y = getPos(); + const attrs = { ...node.attrs }; + attrs.visibility = !attrs.visibility; let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight); let length = to - from; let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length); - node.attrs.text = newSelection.content(); - view.dispatch(view.state.tr.setSelection(newSelection)); - view.dispatch(view.state.tr.deleteSelection(view.state, () => { })); + // update attrs of node + attrs.text = newSelection.content(); + attrs.textslice = newSelection.content().toJSON(); + view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); + view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { })); self._collapsed.textContent = "㊉"; } else { - node.attrs.visibility = !node.attrs.visibility; - console.log("content is invisible"); + // node.attrs.visibility = !node.attrs.visibility; let y = getPos(); - console.log(y); + const attrs = { ...node.attrs }; + attrs.visibility = !attrs.visibility; + view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); let mark = view.state.schema.mark(view.state.schema.marks.highlight); - console.log("PASTING " + node.attrs.text.toString()); view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1))); const from = view.state.selection.from; - view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + node.attrs.oldtextlen, mark)); - //view.dispatch(view.state.tr.setSelection(view.state.doc, from + node.attrs.oldtextlen + 1, from + node.attrs.oldtextlen + 1)); - view.dispatch(view.state.tr.removeStoredMark(mark)); + let size = node.attrs.text.size; + view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + size, mark).removeStoredMark(mark)); self._collapsed.textContent = "㊀"; } e.preventDefault(); @@ -529,21 +544,23 @@ export class SummarizedView { updateSummarizedText(start?: any, mark?: any) { let $start = this._view.state.doc.resolve(start); - let first_startPos = $start.start(), endPos = first_startPos; - while ($start.nodeAfter !== null) { - let startIndex = $start.index(), endIndex = $start.indexAfter(); - while (startIndex > 0 && mark.isInSet($start.parent.child(startIndex - 1).marks)) startIndex--; - while (endIndex < $start.parent.childCount && mark.isInSet($start.parent.child(endIndex).marks)) endIndex++; - let startPos = $start.start(), endPos = startPos; - for (let i = 0; i < endIndex; i++) { - let size = $start.parent.child(i).nodeSize; - console.log($start.parent.child(i).childCount); - if (i < startIndex) startPos += size; - endPos += size; - } - $start = this._view.state.doc.resolve(start + (endPos - startPos) + 1); + let endPos = start; + + let _mark = this._view.state.schema.mark(this._view.state.schema.marks.highlight); + let visited = new Set(); + for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) { + let skip = false; + this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { + if (node.isLeaf && !visited.has(node) && !skip) { + if (node.marks.includes(_mark)) { + visited.add(node); + endPos = i + node.nodeSize - 1; + } + else skip = true; + } + }); } - return { from: first_startPos, to: endPos }; + return { from: start, to: endPos }; } deselectNode() { @@ -564,7 +581,7 @@ const fromJson = schema.nodeFromJSON; schema.nodeFromJSON = (json: any) => { let node = fromJson(json); if (json.type === "star") { - node.attrs.oldtext = Slice.fromJSON(schema, node.attrs.oldtextslice); + node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice); } return node; };
\ No newline at end of file diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 40e2ad6bb..30a05154a 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -12,7 +12,7 @@ import { Doc, Field } from '../../new_fields/Doc'; import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField'; import { List } from '../../new_fields/List'; import { RichTextField } from '../../new_fields/RichTextField'; -import { ScriptField, ComputedField } from '../../fields/ScriptField'; +import { ScriptField, ComputedField } from '../../new_fields/ScriptField'; export interface ScriptSucccess { success: true; @@ -39,7 +39,6 @@ export interface CompileError { } export type CompileResult = CompiledScript | CompileError; - function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); if ((options.typecheck !== false && errors) || !script) { @@ -64,10 +63,20 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an } } let thisParam = args.this || capturedVariables.this; + let batch: { end(): void } | undefined = undefined; try { + if (!options.editable) { + batch = Doc.MakeReadOnly(); + } const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray); + if (batch) { + batch.end(); + } return { success: true, result }; } catch (error) { + if (batch) { + batch.end(); + } return { success: false, error }; } }; @@ -133,6 +142,7 @@ export interface ScriptOptions { params?: { [name: string]: string }; capturedVariables?: { [name: string]: Field }; typecheck?: boolean; + editable?: boolean; } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { 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/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 09bccb1a0..7dbb81e76 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -66,7 +66,7 @@ export namespace SelectionManager { export function GetIsDragging() { return manager.IsDragging; } export function SelectedDocuments(): Array<DocumentView> { - return manager.SelectedDocuments; + return manager.SelectedDocuments.slice(); } export function ViewsSortedHorizontally(): DocumentView[] { let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => { diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index c10a61558..b10573b3e 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -18,6 +18,7 @@ .ProseMirror-menuitem { margin-right: 3px; display: inline-block; + z-index: 100000; } .ProseMirror-menuseparator { @@ -59,7 +60,6 @@ } .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { - position: absolute; background: $dark-color; color:white; border: 1px solid rgb(223, 223, 223); @@ -67,9 +67,10 @@ } .ProseMirror-menu-dropdown-menu { - z-index: 15; + z-index: 100000; min-width: 6em; background: white; + position: absolute; } .linking { @@ -80,6 +81,7 @@ cursor: pointer; padding: 2px 8px 2px 4px; width: auto; + z-index: 100000; } .ProseMirror-menu-dropdown-item:hover { @@ -233,19 +235,20 @@ } .tooltipMenu { - position: absolute; - z-index: 50; + position: relative; + z-index: 2000; background: #121721; border: 1px solid silver; border-radius: 15px; - padding: 2px 10px; - //margin-bottom: 100px; + //height: 60px; + //padding: 2px 10px; + //margin-top: 100px; //-webkit-transform: translateX(-50%); //transform: translateX(-50%); - transform: translateY(-50%); + transform: translateY(-85px); pointer-events: all; - height: 100; - width:250; + height: 30px; + width:500px; .ProseMirror-example-setup-style hr { padding: 2px 10px; border: none; @@ -307,7 +310,7 @@ padding-right: 0px; } .summarize{ - margin-left: 15px; + //margin-left: 15px; color: white; height: 20px; // background-color: white; diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index a19b6c1d8..a96615488 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -6,7 +6,8 @@ import { keymap } from "prosemirror-keymap"; import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "./RichTextSchema"; -import { Schema, NodeType, MarkType, Mark } from "prosemirror-model"; +import { Schema, NodeType, MarkType, Mark, ResolvedPos } from "prosemirror-model"; +import { Node as ProsNode } from "prosemirror-model" import React = require("react"); import "./TooltipTextMenu.scss"; const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); @@ -29,6 +30,9 @@ import { CollectionDockingView } from "../views/collections/CollectionDockingVie import { DocumentManager } from "./DocumentManager"; import { Id } from "../../new_fields/FieldSymbols"; import { Utils } from "../../Utils"; +import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; +import { text } from "body-parser"; +import { type } from "os"; // import { wrap } from "module"; const SVG = "http://www.w3.org/2000/svg"; @@ -42,7 +46,7 @@ export class TooltipTextMenu { private fontStyles: MarkType[]; private fontSizes: MarkType[]; private listTypes: NodeType[]; - private editorProps: FieldViewProps; + private editorProps: FieldViewProps & FormattedTextBoxProps; private state: EditorState; private fontSizeToNum: Map<MarkType, number>; private fontStylesToName: Map<MarkType, string>; @@ -58,15 +62,23 @@ export class TooltipTextMenu { private fontStyleDom?: Node; private listTypeBtnDom?: Node; - constructor(view: EditorView, editorProps: FieldViewProps) { + private _activeMarks: Mark[] = []; + + private _collapseBtn?: MenuItem; + + constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) { this.view = view; this.state = view.state; this.editorProps = editorProps; this.tooltip = document.createElement("div"); this.tooltip.className = "tooltipMenu"; + // this.createCollapse(); + // if (this._collapseBtn) { + // this.tooltip.appendChild(this._collapseBtn.render(this.view).dom); + // } //add the div which is the tooltip - view.dom.parentNode!.parentNode!.appendChild(this.tooltip); + //view.dom.parentNode!.parentNode!.appendChild(this.tooltip); //add additional icons library.add(faListUl); @@ -78,7 +90,7 @@ export class TooltipTextMenu { { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough", "Strikethrough") }, { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript", "Superscript") }, { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript", "Subscript") }, - { command: deleteSelection, dom: this.icon("C", 'collapse', 'Collapse') } + { command: toggleMark(schema.marks.highlight), dom: this.icon("H", 'blue', 'Blue') } // { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, // { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") }, // { command: lift, dom: this.icon("<", "lift") }, @@ -120,6 +132,7 @@ export class TooltipTextMenu { this.fontSizeToNum.set(schema.marks.p32, 32); this.fontSizeToNum.set(schema.marks.p48, 48); this.fontSizeToNum.set(schema.marks.p72, 72); + //this.fontSizeToNum.set(schema.marks.pFontSize,schema.marks.pFontSize.) this.fontSizes = Array.from(this.fontSizeToNum.keys()); //list types @@ -131,11 +144,23 @@ export class TooltipTextMenu { this.link = document.createElement("a"); this.link.target = "_blank"; this.link.style.color = "white"; - this.tooltip.appendChild(this.link); + //this.tooltip.appendChild(this.link); this.tooltip.appendChild(this.createLink().render(this.view).dom); + this.tooltip.appendChild(this.createStar().render(this.view).dom); + + + + this.updateListItemDropdown(":", this.listTypeBtnDom); + this.update(view, undefined); + + //view.dom.parentNode!.parentNode!.insertBefore(this.tooltip, view.dom.parentNode); + + // quick and dirty null check + const outer_div = this.editorProps.outer_div; + outer_div && outer_div(this.tooltip); } //label of dropdown will change to given label @@ -149,12 +174,15 @@ export class TooltipTextMenu { fontSizeBtns.push(this.dropdownMarkBtn(String(number), "color: black; width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); }); - if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); } - this.fontSizeDom = (new Dropdown(cut(fontSizeBtns), { + let newfontSizeDom = (new Dropdown(cut(fontSizeBtns), { label: label, css: "color:black; min-width: 60px; padding-left: 5px; margin-right: 0;" }) as MenuItem).render(this.view).dom; - this.tooltip.appendChild(this.fontSizeDom); + if (this.fontSizeDom) { this.tooltip.replaceChild(newfontSizeDom, this.fontSizeDom); } + else { + this.tooltip.appendChild(newfontSizeDom); + } + this.fontSizeDom = newfontSizeDom; } //label of dropdown will change to given label @@ -168,13 +196,16 @@ export class TooltipTextMenu { fontBtns.push(this.dropdownMarkBtn(name, "color: black; font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); }); - if (this.fontStyleDom) { this.tooltip.removeChild(this.fontStyleDom); } - this.fontStyleDom = (new Dropdown(cut(fontBtns), { + let newfontStyleDom = (new Dropdown(cut(fontBtns), { label: label, css: "color:black; width: 125px; margin-left: -3px; padding-left: 2px;" }) as MenuItem).render(this.view).dom; + if (this.fontStyleDom) { this.tooltip.replaceChild(newfontStyleDom, this.fontStyleDom); } + else { + this.tooltip.appendChild(newfontStyleDom); + } + this.fontStyleDom = newfontStyleDom; - this.tooltip.appendChild(this.fontStyleDom); } updateLinkMenu() { @@ -207,9 +238,9 @@ export class TooltipTextMenu { DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { if (f instanceof Doc) { if (DocumentManager.Instance.getDocumentView(f)) { - DocumentManager.Instance.getDocumentView(f)!.props.focus(f); + DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false); } - else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f); + else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f, f); } })); } @@ -239,23 +270,10 @@ export class TooltipTextMenu { hideSource: false }); }; - this.linkEditor.appendChild(this.linkDrag); - this.linkEditor.appendChild(this.linkText); - this.linkEditor.appendChild(linkBtn); - this.tooltip.appendChild(this.linkEditor); - - // SUMMARIZE BUTTON - - let starButton = document.createElement("span"); - starButton.className = "summarize"; - starButton.textContent = "★"; - starButton.title = 'Summarize'; - starButton.onclick = () => { - let state = this.view.state; - this.insertStar(state, this.view.dispatch); - }; - - this.tooltip.appendChild(starButton); + // this.linkEditor.appendChild(this.linkDrag); + // this.linkEditor.appendChild(this.linkText); + // this.linkEditor.appendChild(linkBtn); + //this.tooltip.appendChild(this.linkEditor); } let node = this.view.state.selection.$from.nodeAfter; @@ -281,18 +299,17 @@ export class TooltipTextMenu { link = node && node.marks.find(m => m.type.name === "link"); } - insertStar(state: EditorState<any>, dispatch: any) { - console.log("creating star..."); - let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), oldtextslice: state.selection.content().toJSON(), oldtextlen: state.selection.to - state.selection.from }); + public static insertStar(state: EditorState<any>, dispatch: any) { + let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from }); if (dispatch) { - console.log(newNode.attrs.text.toString()); + //console.log(newNode.attrs.text.toString()); dispatch(state.tr.replaceSelectionWith(newNode)); } return true; } //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown - updateListItemDropdown(label: string, listTypeBtn: Node) { + updateListItemDropdown(label: string, listTypeBtn: any) { //remove old btn if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); } @@ -315,7 +332,7 @@ export class TooltipTextMenu { } //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text - changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) { + changeToMarkInGroup = (markType: MarkType, view: EditorView, fontMarks: MarkType[]) => { let { empty, $cursor, ranges } = view.state.selection as TextSelection; let state = view.state; let dispatch = view.dispatch; @@ -341,7 +358,17 @@ export class TooltipTextMenu { } } } - }); //actually apply font + }); + // fontsize + if (markType.name[0] === 'p') { + let size = this.fontSizeToNum.get(markType); + if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + } + else { + let fontName = this.fontStylesToName.get(markType); + if (fontName) { this.updateFontStyleDropdown(fontName); } + } + //actually apply font return toggleMark(markType)(view.state, view.dispatch, view); } @@ -370,6 +397,62 @@ export class TooltipTextMenu { }); } + createStar() { + return new MenuItem({ + title: "Summarize", + label: "Summarize", + icon: icons.join, + css: "color:white;", + class: "summarize", + execEvent: "", + run: (state, dispatch, view) => { + TooltipTextMenu.insertStar(state, dispatch); + } + + }); + } + + createCollapse() { + this._collapseBtn = new MenuItem({ + title: "Collapse", + //label: "Collapse", + icon: icons.join, + execEvent: "", + css: "color:white;", + class: "summarize", + run: (state, dispatch, view) => { + this.collapseToolTip(); + } + }); + } + + collapseToolTip() { + if (this._collapseBtn) { + if (this._collapseBtn.spec.title === "Collapse") { + // const newcollapseBtn = new MenuItem({ + // title: "Expand", + // icon: icons.join, + // execEvent: "", + // css: "color:white;", + // class: "summarize", + // run: (state, dispatch, view) => { + // this.collapseToolTip(); + // } + // }); + // this.tooltip.replaceChild(newcollapseBtn.render(this.view).dom, this._collapseBtn.render(this.view).dom); + // this._collapseBtn = newcollapseBtn; + this.tooltip.style.width = "30px"; + this._collapseBtn.spec.title = "Expand"; + this._collapseBtn.render(this.view); + } + else { + this._collapseBtn.spec.title = "Collapse"; + this.tooltip.style.width = "550px"; + this._collapseBtn.render(this.view); + } + } + } + createLink() { let markType = schema.marks.link; return new MenuItem({ @@ -499,68 +582,59 @@ export class TooltipTextMenu { // Hide the tooltip if the selection is empty if (state.selection.empty) { - this.tooltip.style.display = "none"; - return; + //this.tooltip.style.display = "none"; + //return; } - let linksInSelection = this.activeMarksOnSelection([schema.marks.link]); - if (linksInSelection.length > 0) { - let attributes = this.getMarksInSelection(this.view.state, [schema.marks.link])[0].attrs; - this.link.href = attributes.href; - this.link.textContent = attributes.title; - this.link.style.visibility = "visible"; - } else this.link.style.visibility = "hidden"; + //let linksInSelection = this.activeMarksOnSelection([schema.marks.link]); + // if (linksInSelection.length > 0) { + // let attributes = this.getMarksInSelection(this.view.state, [schema.marks.link])[0].attrs; + // this.link.href = attributes.href; + // this.link.textContent = attributes.title; + // this.link.style.visibility = "visible"; + // } else this.link.style.visibility = "hidden"; // Otherwise, reposition it and update its content - this.tooltip.style.display = ""; + //this.tooltip.style.display = ""; let { from, to } = state.selection; - let start = view.coordsAtPos(from), end = view.coordsAtPos(to); - // The box in which the tooltip is positioned, to use as base - let box = this.tooltip.offsetParent!.getBoundingClientRect(); - // Find a center-ish x position from the selection endpoints (when - // crossing lines, end may be more to the left) - let left = Math.max((start.left + end.left) / 2, start.left + 3); - this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px"; - let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale; - let mid = Math.min(start.left, end.left) + width; - - //this.tooltip.style.width = 225 + "px"; - this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; - this.tooltip.style.top = "-100px"; - //this.tooltip.style.height = "100px"; - - // let transform = this.editorProps.ScreenToLocalTransform(); - // this.tooltip.style.width = `${225 / transform.Scale}px`; - // Utils //UPDATE LIST ITEM DROPDOWN - this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom!); //UPDATE FONT STYLE DROPDOWN let activeStyles = this.activeMarksOnSelection(this.fontStyles); - if (activeStyles.length === 1) { - // if we want to update something somewhere with active font name - let fontName = this.fontStylesToName.get(activeStyles[0]); - if (fontName) { this.updateFontStyleDropdown(fontName); } - } else if (activeStyles.length === 0) { - //crimson on default - this.updateFontStyleDropdown("Crimson Text"); - } else { - this.updateFontStyleDropdown("Various"); + if (activeStyles !== undefined) { + // activeStyles.forEach((markType) => { + // this._activeMarks.push(this.view.state.schema.mark(markType)); + // }); + if (activeStyles.length === 1) { + // if we want to update something somewhere with active font name + let fontName = this.fontStylesToName.get(activeStyles[0]); + if (fontName) { this.updateFontStyleDropdown(fontName); } + } else if (activeStyles.length === 0) { + //crimson on default + this.updateFontStyleDropdown("Crimson Text"); + } else { + this.updateFontStyleDropdown("Various"); + } } //UPDATE FONT SIZE DROPDOWN let activeSizes = this.activeMarksOnSelection(this.fontSizes); - if (activeSizes.length === 1) { //if there's only one active font size - let size = this.fontSizeToNum.get(activeSizes[0]); - if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } - } else if (activeSizes.length === 0) { - //should be 14 on default - this.updateFontSizeDropdown("14 pt"); - } else { //multiple font sizes selected - this.updateFontSizeDropdown("Various"); + if (activeSizes !== undefined) { + if (activeSizes.length === 1) { //if there's only one active font size + // activeSizes.forEach((markType) => { + // this._activeMarks.push(this.view.state.schema.mark(markType)); + // }); + let size = this.fontSizeToNum.get(activeSizes[0]); + if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + } else if (activeSizes.length === 0) { + //should be 14 on default + this.updateFontSizeDropdown("14 pt"); + } else { //multiple font sizes selected + this.updateFontSizeDropdown("Various"); + } } - + this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); this.updateLinkMenu(); } @@ -570,19 +644,74 @@ export class TooltipTextMenu { let { empty, $cursor, ranges } = this.view.state.selection as TextSelection; let state = this.view.state; let dispatch = this.view.dispatch; - - let activeMarks = markGroup.filter(mark => { - if (dispatch) { - let has = false, tr = state.tr; - for (let i = 0; !has && i < ranges.length; i++) { - let { $from, $to } = ranges[i]; - return state.doc.rangeHasMark($from.pos, $to.pos, mark); + let activeMarks: MarkType[]; + if (!empty) { + activeMarks = markGroup.filter(mark => { + if (dispatch) { + let has = false, tr = state.tr; + for (let i = 0; !has && i < ranges.length; i++) { + let { $from, $to } = ranges[i]; + let hasmark: boolean = state.doc.rangeHasMark($from.pos, $to.pos, mark); + return state.doc.rangeHasMark($from.pos, $to.pos, mark); + } } + return false; + }); + } + else { + const pos = this.view.state.selection.$from; + const ref_node: ProsNode = this.reference_node(pos); + if (ref_node !== null && ref_node !== this.view.state.doc) { + let text_node_type: NodeType; + if (ref_node.isText) { + text_node_type = ref_node.type; + } + else { + return []; + } + + this._activeMarks = ref_node.marks; + + activeMarks = markGroup.filter(mark_type => { + if (dispatch) { + let mark = state.schema.mark(mark_type); + return ref_node.marks.includes(mark); + } + return false; + }); } - return false; - }); + else { + return []; + } + + } return activeMarks; } - destroy() { this.tooltip.remove(); } + reference_node(pos: ResolvedPos<any>): ProsNode { + let ref_node: ProsNode = this.view.state.doc; + if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { + ref_node = pos.nodeAfter; + } + else if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { + ref_node = pos.nodeBefore; + } + else if (pos.pos > 0) { + let skip = false; + for (let i: number = pos.pos - 1; i > 0; i--) { + this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { + if (node.isLeaf && !skip) { + ref_node = node; + skip = true; + } + + }); + } + } + return ref_node; + } + + destroy() { + this.tooltip.remove(); + } } diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.js new file mode 100644 index 000000000..0f9328872 --- /dev/null +++ b/src/client/util/request-image-size.js @@ -0,0 +1,73 @@ +/** + * request-image-size: Detect image dimensions via request. + * Licensed under the MIT license. + * + * https://github.com/FdezRomero/request-image-size + * © 2017 Rodrigo Fernández Romero + * + * Based on the work of Johannes J. Schmidt + * https://github.com/jo/http-image-size + */ + +const request = require('request'); +const imageSize = require('image-size'); +const HttpError = require('standard-http-error'); + +module.exports = function requestImageSize(options) { + let opts = { + encoding: null + }; + + if (options && typeof options === 'object') { + opts = Object.assign(options, opts); + } else if (options && typeof options === 'string') { + opts = Object.assign({ uri: options }, opts); + } else { + return Promise.reject(new Error('You should provide an URI string or a "request" options object.')); + } + + opts.encoding = null; + + return new Promise((resolve, reject) => { + const req = request(opts); + + req.on('response', res => { + if (res.statusCode >= 400) { + return reject(new HttpError(res.statusCode, res.statusMessage)); + } + + let buffer = new Buffer([]); + let size; + let imageSizeError; + + res.on('data', chunk => { + buffer = Buffer.concat([buffer, chunk]); + + try { + size = imageSize(buffer); + } catch (err) { + imageSizeError = err; + return; + } + + if (size) { + resolve(size); + return req.abort(); + } + }); + + res.on('error', err => reject(err)); + + res.on('end', () => { + if (!size) { + return reject(imageSizeError); + } + + size.downloaded = buffer.length; + return resolve(size); + }); + }); + + req.on('error', err => reject(err)); + }); +}; diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 7e066d53a..254163b53 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -8,19 +8,19 @@ flex-direction: column; } -.contextMenu-item:first-child { - background: $intermediate-color; - color: $light-color; -} +// .contextMenu-item:first-child { +// background: $intermediate-color; +// color: $light-color; +// } -.contextMenu-item:first-child::placeholder { - color: $light-color; -} +// .contextMenu-item:first-child::placeholder { +// color: $light-color; +// } -.contextMenu-item:first-child:hover { - background: $intermediate-color; - color: $light-color; -} +// .contextMenu-item:first-child:hover { +// background: $intermediate-color; +// color: $light-color; +// } .contextMenu-subMenu-cont { position: absolute; @@ -53,6 +53,33 @@ font-size: 20px; } +.contextMenu-itemSelected { + background: rgb(136, 136, 136) +} + +.contextMenu-group { + // width: 11vw; //10vw + height: 30px; //2vh + background: rgb(200, 200, 200); + display: flex; //comment out to allow search icon to be inline with search text + justify-content: left; + align-items: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all .1s; + border-width: .11px; + border-style: none; + border-color: $intermediate-color; // rgb(187, 186, 186); + border-bottom-style: solid; + // padding: 10px 0px 10px 0px; + white-space: nowrap; + font-size: 20px; +} + .contextMenu-item:hover { transition: all 0.1s; background: $lighter-alt-accent; diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index da374455e..69692dbb8 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -1,11 +1,12 @@ import React = require("react"); -import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem"; -import { observable, action } from "mobx"; -import { observer } from "mobx-react" -import "./ContextMenu.scss" +import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextMenuItem"; +import { observable, action, computed } from "mobx"; +import { observer } from "mobx-react"; +import "./ContextMenu.scss"; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons'; +import Measure from "react-measure"; library.add(faSearch); library.add(faCircle); @@ -14,29 +15,27 @@ library.add(faCircle); export class ContextMenu extends React.Component { static Instance: ContextMenu; - @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }]; + @observable private _items: Array<ContextMenuProps> = []; @observable private _pageX: number = 0; @observable private _pageY: number = 0; - @observable private _display: string = "none"; + @observable private _display: boolean = false; @observable private _searchString: string = ""; // afaik displaymenu can be called before all the items are added to the menu, so can't determine in displayMenu what the height of the menu will be @observable private _yRelativeToTop: boolean = true; + @observable selectedIndex = -1; - - private ref: React.RefObject<HTMLDivElement>; + @observable private _width: number = 0; + @observable private _height: number = 0; constructor(props: Readonly<{}>) { super(props); - this.ref = React.createRef(); - ContextMenu.Instance = this; } @action clearItems() { this._items = []; - this._display = "none"; } @action @@ -50,63 +49,178 @@ export class ContextMenu extends React.Component { return this._items; } + static readonly buffer = 20; + get pageX() { + const x = this._pageX; + if (x < 0) { + return 0; + } + const width = this._width; + if (x + width > window.innerWidth - ContextMenu.buffer) { + return window.innerWidth - ContextMenu.buffer - width; + } + return x; + } + + get pageY() { + const y = this._pageY; + if (y < 0) { + return 0; + } + const height = this._height; + if (y + height > window.innerHeight - ContextMenu.buffer) { + return window.innerHeight - ContextMenu.buffer - height; + } + return y; + } + @action displayMenu(x: number, y: number) { //maxX and maxY will change if the UI/font size changes, but will work for any amount //of items added to the menu - let maxX = window.innerWidth - 150; - let maxY = window.innerHeight - ((this._items.length + 1/*for search box*/) * 34 + 30); - this._pageX = x > maxX ? maxX : x; - this._pageY = y > maxY ? maxY : y; + this._pageX = x; + this._pageY = y; this._searchString = ""; - this._display = "flex"; + this._display = true; } - intersects = (x: number, y: number): boolean => { - if (this.ref.current && this._display !== "none") { - let menuSize = { width: this.ref.current.getBoundingClientRect().width, height: this.ref.current.getBoundingClientRect().height }; - - let upperLeft = { x: this._pageX, y: this._yRelativeToTop ? this._pageY : window.innerHeight - (this._pageY + menuSize.height) }; - let bottomRight = { x: this._pageX + menuSize.width, y: this._yRelativeToTop ? this._pageY + menuSize.height : window.innerHeight - this._pageY }; + @action + closeMenu = () => { + this.clearItems(); + this._display = false; + } - if (x >= upperLeft.x && x <= bottomRight.x) { - if (y >= upperLeft.y && y <= bottomRight.y) { - return true; + @computed get filteredItems(): (OriginalMenuProps | string[])[] { + const searchString = this._searchString.toLowerCase().split(" "); + const matches = (descriptions: string[]): boolean => { + return searchString.every(s => descriptions.some(desc => desc.toLowerCase().includes(s))); + }; + const flattenItems = (items: ContextMenuProps[], groupFunc: (groupName: any) => string[]) => { + let eles: (OriginalMenuProps | string[])[] = []; + + const leaves: OriginalMenuProps[] = []; + for (const item of items) { + const description = item.description; + const path = groupFunc(description); + if ("subitems" in item) { + const children = flattenItems(item.subitems, name => [...groupFunc(description), name]); + if (children.length || matches(path)) { + eles.push(path); + eles = eles.concat(children); + } + } else { + if (!matches(path)) { + continue; + } + leaves.push(item); } } - } - return false; + + eles = [...leaves, ...eles]; + + return eles; + }; + return flattenItems(this._items, name => [name]); } - @action - closeMenu = () => { - this.clearItems(); + @computed get flatItems(): OriginalMenuProps[] { + return this.filteredItems.filter(item => !Array.isArray(item)) as OriginalMenuProps[]; + } + + @computed get filteredViews() { + const createGroupHeader = (contents: any) => { + return ( + <div className="contextMenu-group"> + <div className="contextMenu-description">{contents}</div> + </div> + ); + }; + const createItem = (item: ContextMenuProps, selected: boolean) => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} selected={selected} />; + let itemIndex = 0; + return this.filteredItems.map(value => { + if (Array.isArray(value)) { + return createGroupHeader(value.join(" -> ")); + } else { + return createItem(value, itemIndex++ === this.selectedIndex); + } + }); + } + + @computed get menuItems() { + if (!this._searchString) { + return this._items.map(item => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />); + } + return this.filteredViews; } render() { - let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } : - { left: this._pageX, bottom: this._pageY, display: this._display }; + if (!this._display) { + return null; + } + let style = this._yRelativeToTop ? { left: this.pageX, top: this.pageY } : + { left: this.pageX, bottom: this.pageY }; + console.log(this._pageX); + console.log(this.pageX); + console.log(); - return ( - <div className="contextMenu-cont" style={style} ref={this.ref}> + const contents = ( + <> <span> <span className="icon-background"> <FontAwesomeIcon icon="search" size="lg" /> </span> - <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} /> + <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> </span> - {this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1). - map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.closeMenu} />)} - </div> + {this.menuItems} + </> + ); + return ( + <Measure offset onResize={action((r: any) => { this._width = r.offset.width; this._height = r.offset.height; })}> + {({ measureRef }) => ( + <div className="contextMenu-cont" style={style} ref={measureRef}> + {contents} + </div> + ) + } + </Measure> ); } @action + onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "ArrowDown") { + if (this.selectedIndex < this.flatItems.length - 1) { + this.selectedIndex++; + } + e.preventDefault(); + } else if (e.key === "ArrowUp") { + if (this.selectedIndex > 0) { + this.selectedIndex--; + } + e.preventDefault(); + } else if (e.key === "Enter") { + const item = this.flatItems[this.selectedIndex]; + item.event(); + this.closeMenu(); + } + } + + @action onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this._searchString = e.target.value; + if (!this._searchString) { + this.selectedIndex = -1; + } + else { + if (this.selectedIndex === -1) { + this.selectedIndex = 0; + } else { + this.selectedIndex = Math.min(this.flatItems.length - 1, this.selectedIndex); + } + } } }
\ No newline at end of file diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index fcda0db89..9bbb97d7e 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,12 +1,15 @@ import React = require("react"); import { observable, action } from "mobx"; import { observer } from "mobx-react"; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +library.add(faAngleRight); + export interface OriginalMenuProps { description: string; - event: (e: React.MouseEvent<HTMLDivElement>) => void; + event: () => void; icon?: IconProp; //maybe should be optional (icon?) closeMenu?: () => void; } @@ -14,16 +17,14 @@ export interface OriginalMenuProps { export interface SubmenuProps { description: string; subitems: ContextMenuProps[]; + icon?: IconProp; //maybe should be optional (icon?) closeMenu?: () => void; } -export interface ContextMenuItemProps { - type: ContextMenuProps | SubmenuProps; -} export type ContextMenuProps = OriginalMenuProps | SubmenuProps; @observer -export class ContextMenuItem extends React.Component<ContextMenuProps> { +export class ContextMenuItem extends React.Component<ContextMenuProps & { selected?: boolean }> { @observable private _items: Array<ContextMenuProps> = []; @observable private overItem = false; @@ -36,33 +37,64 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> { handleEvent = (e: React.MouseEvent<HTMLDivElement>) => { if ("event" in this.props) { - this.props.event(e); + this.props.event(); this.props.closeMenu && this.props.closeMenu(); } } + currentTimeout?: any; + static readonly timeout = 300; + onPointerEnter = () => { + if (this.currentTimeout) { + clearTimeout(this.currentTimeout); + this.currentTimeout = undefined; + } + if (this.overItem) { + return; + } + this.currentTimeout = setTimeout(action(() => this.overItem = true), ContextMenuItem.timeout); + } + + onPointerLeave = () => { + if (this.currentTimeout) { + clearTimeout(this.currentTimeout); + this.currentTimeout = undefined; + } + if (!this.overItem) { + return; + } + this.currentTimeout = setTimeout(action(() => this.overItem = false), ContextMenuItem.timeout); + } + render() { if ("event" in this.props) { return ( - <div className="contextMenu-item" onClick={this.handleEvent}> - <span className="icon-background"> - {this.props.icon ? <FontAwesomeIcon icon={this.props.icon} size="sm" /> : <FontAwesomeIcon icon="circle" size="sm" />} - </span> + <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onClick={this.handleEvent}> + {this.props.icon ? ( + <span className="icon-background"> + <FontAwesomeIcon icon={this.props.icon} size="sm" /> + </span> + ) : null} <div className="contextMenu-description"> {this.props.description} </div> </div> ); - } - else { + } else if ("subitems" in this.props) { let submenu = !this.overItem ? (null) : <div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; return ( - <div className="contextMenu-item" onMouseEnter={action(() => { this.overItem = true; })} onMouseLeave={action(() => this.overItem = false)}> + <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseEnter={this.onPointerEnter} onMouseLeave={this.onPointerLeave}> + {this.props.icon ? ( + <span className="icon-background"> + <FontAwesomeIcon icon={this.props.icon} size="sm" /> + </span> + ) : null} <div className="contextMenu-description"> {this.props.description} + <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "5px" }} /> </div> {submenu} </div> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index ceca940b6..db10007f4 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -5,8 +5,8 @@ import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; -import { listSpec } from "../../new_fields/Schema"; -import { Cast, NumCast, StrCast, BoolCast } from "../../new_fields/Types"; +import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types"; +import { URLField } from '../../new_fields/URLField'; import { emptyFunction, Utils } from "../../Utils"; import { Docs } from "../documents/Documents"; import { DocumentManager } from "../util/DocumentManager"; @@ -24,7 +24,8 @@ import { LinkMenu } from "./nodes/LinkMenu"; import { TemplateMenu } from "./TemplateMenu"; import { Template, Templates } from "./Templates"; import React = require("react"); -import { URLField } from '../../new_fields/URLField'; +import { RichTextField } from '../../new_fields/RichTextField'; +import { LinkManager } from '../util/LinkManager'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -43,6 +44,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> private _linkButton = React.createRef<HTMLDivElement>(); private _linkerButton = React.createRef<HTMLDivElement>(); private _embedButton = React.createRef<HTMLDivElement>(); + private _tooltipoff = React.createRef<HTMLDivElement>(); + private _textDoc?: Doc; private _downX = 0; private _downY = 0; private _iconDoc?: Doc = undefined; @@ -73,6 +76,33 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (text[0] === '#') { this._fieldKey = text.slice(1, text.length); this._title = this.selectionTitle; + } else if (text.startsWith(">")) { + let field = SelectionManager.SelectedDocuments()[0]; + let collection = field.props.ContainingCollectionView!.props.Document; + + let collectionKey = field.props.ContainingCollectionView!.props.fieldKey; + let collectionKeyProp = `fieldKey={"${collectionKey}"}`; + let metaKey = text.slice(1, text.length); + let metaKeyProp = `fieldKey={"${metaKey}"}`; + + let template = Doc.MakeAlias(field.props.Document); + template.proto = collection; + template.title = metaKey; + template.nativeWidth = Cast(field.nativeWidth, "number"); + template.nativeHeight = Cast(field.nativeHeight, "number"); + template.embed = true; + template.isTemplate = true; + template.templates = new List<string>([Templates.TitleBar(metaKey)]); + if (field.props.Document.backgroundLayout) { + let metaAnoKeyProp = `fieldKey={"${metaKey}"} fieldExt={"annotations"}`; + let collectionAnoKeyProp = `fieldKey={"annotations"}`; + template.layout = StrCast(field.props.Document.layout).replace(collectionAnoKeyProp, metaAnoKeyProp); + template.backgroundLayout = StrCast(field.props.Document.backgroundLayout).replace(collectionKeyProp, metaKeyProp); + } else { + template.layout = StrCast(field.props.Document.layout).replace(collectionKeyProp, metaKeyProp); + } + Doc.AddDocToList(collection, collectionKey, template); + SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument && dv.props.removeDocument(dv.props.Document)); } else { if (SelectionManager.SelectedDocuments().length > 0) { @@ -122,7 +152,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @computed get Bounds(): { x: number, y: number, b: number, r: number } { return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { - if (documentView.props.isTopMost) { + if (documentView.props.renderDepth === 0) { return bounds; } let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse(); @@ -149,7 +179,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let dragDocView = SelectionManager.SelectedDocuments()[0]; const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0); const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top); - let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); + let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document), SelectionManager.SelectedDocuments().map(dv => dv.props.DataDoc ? dv.props.DataDoc : dv.props.Document)); dragData.xOffset = xoff; dragData.yOffset = yoff; dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument; @@ -464,16 +494,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> runInAction(() => FormattedTextBox.InputBoxOverlay = undefined); SelectionManager.SelectedDocuments().forEach(element => { - const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect(); - - if (rect.width !== 0 && (dX != 0 || dY != 0 || dW != 0 || dH != 0)) { + if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { let doc = PositionDocument(element.props.Document); let nwidth = doc.nativeWidth || 0; let nheight = doc.nativeHeight || 0; - let zoomBasis = NumCast(doc.zoomBasis, 1); - let width = (doc.width || 0) / zoomBasis; - let height = (doc.height || (nheight / nwidth * width)) / zoomBasis; - let scale = width / rect.width; + let width = (doc.width || 0); + let height = (doc.height || (nheight / nwidth * width)); + let scale = element.props.ScreenToLocalTransform().Scale; let actualdW = Math.max(width + (dW * scale), 20); let actualdH = Math.max(height + (dH * scale), 20); doc.x = (doc.x || 0) + dX * (actualdW - width); @@ -487,25 +514,27 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } if (nwidth > 0 && nheight > 0) { if (Math.abs(dW) > Math.abs(dH)) { - if (!fixedAspect) proto.nativeWidth = zoomBasis * actualdW / (doc.width || 1) * NumCast(proto.nativeWidth); - doc.width = zoomBasis * actualdW; - // doc.zoomBasis = zoomBasis * width / actualdW; + if (!fixedAspect) { + Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true); + } + doc.width = actualdW; if (fixedAspect) doc.height = nheight / nwidth * doc.width; - else doc.height = zoomBasis * actualdH; - proto.nativeHeight = (doc.height || 0) / doc.width * NumCast(proto.nativeWidth); + else doc.height = actualdH; + Doc.SetInPlace(element.props.Document, "nativeHeight", (doc.height || 0) / doc.width * (doc.nativeWidth || 0), true); } else { - if (!fixedAspect) proto.nativeHeight = zoomBasis * actualdH / (doc.height || 1) * NumCast(proto.nativeHeight); - doc.height = zoomBasis * actualdH; - //doc.zoomBasis = zoomBasis * height / actualdH; + if (!fixedAspect) { + Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true); + } + doc.height = actualdH; if (fixedAspect) doc.width = nwidth / nheight * doc.height; - else doc.width = zoomBasis * actualdW; - proto.nativeWidth = (doc.width || 0) / doc.height * NumCast(proto.nativeHeight); + else doc.width = actualdW; + Doc.SetInPlace(element.props.Document, "nativeWidth", (doc.width || 0) / doc.height * (doc.nativeHeight || 0), true); } } else { - doc.width = zoomBasis * actualdW; - doc.height = zoomBasis * actualdH; - proto.autoHeight = undefined; + dW && (doc.width = actualdW); + dH && (doc.height = actualdH); + Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true); } } }); @@ -560,6 +589,37 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> ); } + considerTooltip = () => { + let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; + let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; + if (!isTextDoc) return null; + this._textDoc = thisDoc; + return ( + <div className="tooltipwrapper"> + <div style={{ paddingTop: 3, marginLeft: 30 }} title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}> + {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */} + T + </div> + </div> + + ); + } + + onTooltipOff = (e: React.PointerEvent): void => { + e.stopPropagation(); + if (this._textDoc) { + if (this._tooltipoff.current) { + if (this._tooltipoff.current.title === "Hide Tooltip") { + this._tooltipoff.current.title = "Show Tooltip"; + this._textDoc.tooltip = "hi"; + } + else { + this._tooltipoff.current.title = "Hide Tooltip"; + } + } + } + } + render() { var bounds = this.Bounds; let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; @@ -574,9 +634,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let linkButton = null; if (SelectionManager.SelectedDocuments().length > 0) { let selFirst = SelectionManager.SelectedDocuments()[0]; - let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length; - let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length; - let linkCount = linkToSize + linkFromSize; + let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length; linkButton = (<Flyout anchorPoint={anchorPoints.RIGHT_TOP} content={<LinkMenu docView={selFirst} @@ -619,7 +677,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2, pointerEvents: this.Interacting ? "none" : "all", - zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0, + zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0, }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} > </div> <div className="documentDecorations-container" style={{ @@ -655,6 +713,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> </div> <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} /> {this.considerEmbed()} + {/* {this.considerTooltip()} */} </div> </div > </div> diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index c946d68e1..97a2d19dd 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -18,13 +18,18 @@ export interface EditableProps { OnFillDown?(value: string): void; + OnTab?(): void; + /** * The contents to render when not editing */ contents: any; + fontStyle?: string; height?: number; display?: string; oneLine?: boolean; + editing?: boolean; + onClick?: (e: React.MouseEvent) => boolean; } /** @@ -34,40 +39,54 @@ export interface EditableProps { */ @observer export class EditableView extends React.Component<EditableProps> { - @observable - editing: boolean = false; + @observable _editing: boolean = false; + + constructor(props: EditableProps) { + super(props); + this._editing = this.props.editing ? true : false; + } @action onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter") { + if (e.key === "Tab") { + this.props.OnTab && this.props.OnTab(); + } else if (e.key === "Enter") { if (!e.ctrlKey) { if (this.props.SetValue(e.currentTarget.value)) { - this.editing = false; + this._editing = false; } } else if (this.props.OnFillDown) { this.props.OnFillDown(e.currentTarget.value); - this.editing = false; + this._editing = false; } } else if (e.key === "Escape") { - this.editing = false; + this._editing = false; } } @action onClick = (e: React.MouseEvent) => { - this.editing = true; + if (!this.props.onClick || !this.props.onClick(e)) { + this._editing = true; + } + e.stopPropagation(); + } + + stopPropagation(e: React.SyntheticEvent) { e.stopPropagation(); } render() { - if (this.editing) { - return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)} + if (this._editing) { + return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus + onBlur={action(() => this._editing = false)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} style={{ display: this.props.display }} />; } else { return ( - <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }} + <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} + style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }} onClick={this.onClick} > - <span>{this.props.contents}</span> + <span style={{ fontStyle: this.props.fontStyle }}>{this.props.contents}</span> </div> ); } diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 5d4ea76cd..fd7e5b07d 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -14,6 +14,7 @@ import { Cast, PromiseValue, NumCast } from "../../new_fields/Types"; interface InkCanvasProps { getScreenTransform: () => Transform; Document: Doc; + inkFieldKey: string; children: () => JSX.Element[]; } @@ -40,7 +41,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { } componentDidMount() { - PromiseValue(Cast(this.props.Document.ink, InkField)).then(ink => runInAction(() => { + PromiseValue(Cast(this.props.Document[this.props.inkFieldKey], InkField)).then(ink => runInAction(() => { if (ink) { let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) => strokeData.pathData.reduce(([mix, max, miy, may], p) => @@ -55,12 +56,12 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { @computed get inkData(): Map<string, StrokeData> { - let map = Cast(this.props.Document.ink, InkField); + let map = Cast(this.props.Document[this.props.inkFieldKey], InkField); return !map ? new Map : new Map(map.inkData); } set inkData(value: Map<string, StrokeData>) { - Doc.GetProto(this.props.Document).ink = new InkField(value); + Doc.GetProto(this.props.Document)[this.props.inkFieldKey] = new InkField(value); } @action diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index b98132c23..ec228ce98 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,5 +1,5 @@ import { observable, action, computed } from "mobx"; -import { CirclePicker, ColorResult } from 'react-color'; +import { ColorResult } from 'react-color'; import React = require("react"); import { observer } from "mobx-react"; import "./InkingControl.scss"; @@ -17,7 +17,7 @@ export class InkingControl extends React.Component { @observable private _selectedTool: InkTool = InkTool.None; @observable private _selectedColor: string = "rgb(244, 67, 54)"; @observable private _selectedWidth: string = "25"; - @observable private _open: boolean = false; + @observable public _open: boolean = false; constructor(props: Readonly<{}>) { super(props); @@ -28,11 +28,18 @@ export class InkingControl extends React.Component { switchTool = (tool: InkTool): void => { this._selectedTool = tool; } + decimalToHexString(number: number) { + if (number < 0) { + number = 0xFFFFFFFF + number + 1; + } + + return number.toString(16).toUpperCase(); + } @action switchColor = (color: ColorResult): void => { - this._selectedColor = color.hex; - SelectionManager.SelectedDocuments().forEach(doc => Doc.GetProto(doc.props.Document).backgroundColor = color.hex); + this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); + if (InkingControl.Instance.selectedTool === InkTool.None) SelectionManager.SelectedDocuments().forEach(doc => (doc.props.Document.isTemplate ? doc.props.Document : Doc.GetProto(doc.props.Document)).backgroundColor = this._selectedColor); } @action diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 690139341..0271edcd2 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -20,15 +20,7 @@ div { -ms-user-select: none; } -#dash-title { - position: absolute; - right: 46.5%; - letter-spacing: 3px; - top: 9px; - font-size: 12px; - color: $alt-accent; - z-index: 9999; -} + .jsx-parser { width: 100%; @@ -43,8 +35,8 @@ p { ::-webkit-scrollbar { -webkit-appearance: none; - height: 10px; - width: 10px; + height: 8px; + width: 8px; } ::-webkit-scrollbar-thumb { @@ -200,7 +192,7 @@ button:hover { position: absolute; top: 0; left: 0; - overflow: scroll; + overflow: auto; z-index: 1; } @@ -210,6 +202,7 @@ button:hover { position: absolute; top: 0; left: 0; + overflow: hidden; } #add-options-content { diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss index f6a746e63..f636ca070 100644 --- a/src/client/views/MainOverlayTextBox.scss +++ b/src/client/views/MainOverlayTextBox.scss @@ -1,7 +1,7 @@ @import "globalCssVariables"; .mainOverlayTextBox-textInput { background-color: rgba(248, 6, 6, 0.001); - width: 200px; + width: 400px; height: 200px; position:absolute; overflow: visible; @@ -17,4 +17,9 @@ top: 0; left: 0; } +} +.mainOverlayTextBox-.unscaled_div{ + z-index: 10000; + position: absolute; + pointer-events: none; }
\ No newline at end of file diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index 23e90ece5..1933ab94b 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -25,19 +25,28 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> private _textProxyDiv: React.RefObject<HTMLDivElement>; private _textBottom: boolean | undefined; private _textAutoHeight: boolean | undefined; + private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); }; + private _outerdiv: HTMLElement | null = null; + private _textBox: FormattedTextBox | undefined; + private _tooltip?: HTMLElement; @observable public TextDoc?: Doc; + updateTooltip = () => { + this._outerdiv && this._tooltip && !this._outerdiv.contains(this._tooltip) && this._outerdiv.appendChild(this._tooltip); + } + constructor(props: MainOverlayTextBoxProps) { super(props); this._textProxyDiv = React.createRef(); MainOverlayTextBox.Instance = this; reaction(() => FormattedTextBox.InputBoxOverlay, (box?: FormattedTextBox) => { + this._textBox = box; if (box) { - this.TextDoc = box.props.Document; + this.TextDoc = box.props.DataDoc; let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined); let xf = () => { box.props.ScreenToLocalTransform(); return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); }; - this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content") + this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content"); } else { this.TextDoc = undefined; @@ -79,10 +88,10 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> } @action textBoxMove = (e: PointerEvent) => { - if (e.movementX > 1 || e.movementY > 1) { + if ((e.movementX > 1 || e.movementY > 1) && FormattedTextBox.InputBoxOverlay) { document.removeEventListener("pointermove", this.textBoxMove); document.removeEventListener('pointerup', this.textBoxUp); - let dragData = new DragManager.DocumentDragData(FormattedTextBox.InputBoxOverlay ? [FormattedTextBox.InputBoxOverlay.props.Document] : []); + let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document], [FormattedTextBox.InputBoxOverlay.props.DataDoc]); const [left, top] = this._textXf().inverse().transformPoint(0, 0); dragData.xOffset = e.clientX - left; dragData.yOffset = e.clientY - top; @@ -99,9 +108,9 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> document.removeEventListener('pointerup', this.textBoxUp); } - addDocTab = (doc: Doc, location: string) => { + addDocTab = (doc: Doc, dataDoc: Doc, location: string) => { if (true) { // location === "onRight") { need to figure out stack to add "inTab" - CollectionDockingView.Instance.AddRightSplit(doc); + CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); } } render() { @@ -111,14 +120,18 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> let s = this._textXf().Scale; let location = this._textBottom ? textRect.bottom : textRect.top; let hgt = this._textAutoHeight || this._textBottom ? "auto" : this._textTargetDiv.clientHeight; - return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${location}px) scale(${1 / s},${1 / s})`, width: "auto", height: "0px" }} > - <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll} - style={{ width: `${textRect.width * s}px`, height: "0px" }}> - <div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}> - <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} hideOnLeave={this._textHideOnLeave} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document} - isSelected={returnTrue} select={emptyFunction} isTopMost={true} selectOnLoad={true} - ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} - ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} /> + return <div ref={this._setouterdiv} className="mainOverlayTextBox-unscaled_div" style={{ transform: `translate(${textRect.left}px, ${location}px)` }} > + <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s},${1 / s})`, width: "auto", height: "0px" }} > + <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll} + style={{ width: `${textRect.width * s}px`, height: "0px" }}> + <div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}> + <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} fieldExt="" hideOnLeave={this._textHideOnLeave} isOverlay={true} + Document={FormattedTextBox.InputBoxOverlay.props.Document} + DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc} + isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true} + ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} + ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} /> + </div> </div> </div> </ div>; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 879c2aca0..6290d8985 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,9 +1,9 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt, faBell } from '@fortawesome/free-solid-svg-icons'; +import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faArrowDown, faArrowUp, faCheck, faPenNib, faThumbtack, faRedoAlt, faTable, faTree, faUndoAlt, faBell, faCommentAlt, faCut } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; -import { CirclePicker } from 'react-color'; +import { CirclePicker, SliderPicker, BlockPicker, TwitterPicker, SketchPicker } from 'react-color'; import "normalize.css"; import * as React from 'react'; import Measure from 'react-measure'; @@ -11,11 +11,11 @@ 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'; -import { PresentationView } from './PresentationView'; +import { PresentationView } from './presentationview/PresentationView'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; @@ -24,17 +24,19 @@ 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'; import { HistoryUtil } from '../util/History'; 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 { @@ -45,15 +47,31 @@ 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)) { - doc.presentationView = Docs.TreeDocument([], { title: "Presentation" }); + doc.presentationView = new List<Doc>([Docs.TreeDocument([], { title: "Presentation" })]); } CurrentUserUtils.UserDocument.activeWorkspace = doc; } } + 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; @@ -90,6 +108,12 @@ export class MainView extends React.Component { library.add(faFilm); library.add(faMusic); library.add(faTree); + library.add(faCut); + library.add(faCommentAlt); + library.add(faThumbtack); + library.add(faCheck); + library.add(faArrowDown); + library.add(faArrowUp); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -102,6 +126,12 @@ export class MainView extends React.Component { if (e.key === "Escape") { DragManager.AbortDrag(); SelectionManager.DeselectAll(); + } else if (e.key === "z" && e.ctrlKey) { + e.preventDefault(); + UndoManager.Undo(); + } else if ((e.key === "y" && e.ctrlKey) || (e.key === "z" && e.ctrlKey && e.shiftKey)) { + e.preventDefault(); + UndoManager.Redo(); } }, false); // drag event handler // click interactions for the context menu @@ -109,7 +139,7 @@ export class MainView extends React.Component { const targets = document.elementsFromPoint(e.x, e.y); if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) { - ContextMenu.Instance.clearItems(); + ContextMenu.Instance.closeMenu(); } }), true); } @@ -135,8 +165,13 @@ export class MainView extends React.Component { const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc)); if (list) { let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` }); - var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] }; + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] }; let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id); + if (!CurrentUserUtils.UserDocument.linkManagerDoc) { + let linkManagerDoc = new Doc(); + linkManagerDoc.allLinks = new List<Doc>([]); + CurrentUserUtils.UserDocument.linkManagerDoc = linkManagerDoc; + } list.push(mainDoc); // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) setTimeout(() => { @@ -168,7 +203,7 @@ export class MainView extends React.Component { openNotifsCol = () => { if (this._notifsCol && CollectionDockingView.Instance) { - CollectionDockingView.Instance.AddRightSplit(this._notifsCol); + CollectionDockingView.Instance.AddRightSplit(this._notifsCol, this._notifsCol); } } @@ -194,6 +229,7 @@ export class MainView extends React.Component { let mainCont = this.mainContainer; let content = !mainCont ? (null) : <DocumentView Document={mainCont} + DataDoc={undefined} addDocument={undefined} addDocTab={emptyFunction} removeDocument={undefined} @@ -201,19 +237,22 @@ export class MainView extends React.Component { ContentScaling={returnOne} PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} - isTopMost={true} + renderDepth={0} selectOnLoad={false} focus={emptyFunction} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} - ContainingCollectionView={undefined} />; - const pres = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined; + ContainingCollectionView={undefined} + zoomToScale={emptyFunction} + getScale={returnOne} + />; + let castRes = mainCont ? FieldValue(Cast(mainCont.presentationView, listSpec(Doc))) : undefined; return <Measure offset onResize={this.onResize}> {({ measureRef }) => <div ref={measureRef} id="mainContent-div" onDrop={this.onDrop}> {content} - {pres ? <PresentationView Document={pres} key="presentation" /> : null} + {castRes ? <PresentationView Documents={castRes} key="presentation" /> : null} </div> } </Measure>; @@ -227,6 +266,15 @@ export class MainView extends React.Component { return { fontSize: "50%" }; } + onColorClick = (e: React.MouseEvent) => { + let target = (e.nativeEvent as any).path[0]; + let parent = (e.nativeEvent as any).path[1]; + if (target.localName === "input" || parent.localName === "span") { + e.stopPropagation(); + } + } + + @observable private _colorPickerDisplay = false; /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */ nodesMenu() { @@ -254,8 +302,8 @@ export class MainView extends React.Component { <li key="redo"><button className="add-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li> <li key="color"><button className="add-button round-button" title="Redo" onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} > - <div className="toolbar-color-picker" style={this._colorPickerDisplay ? { display: "block" } : { display: "none" }}> - <CirclePicker onChange={InkingControl.Instance.switchColor} circleSize={22} width={"220"} /> + <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}> + <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} /> </div> </div></button></li> {btns.map(btn => @@ -301,7 +349,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> ]; @@ -314,6 +362,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, undefined); + } + 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"> @@ -323,6 +404,7 @@ export class MainView extends React.Component { <ContextMenu /> {this.nodesMenu()} {this.miscButtons} + <PDFMenu /> <MainOverlayTextBox /> </div> ); diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx deleted file mode 100644 index d2d41a4ba..000000000 --- a/src/client/views/PresentationView.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../new_fields/Doc"; -import { Id } from "../../new_fields/FieldSymbols"; -import { List } from "../../new_fields/List"; -import { listSpec } from "../../new_fields/Schema"; -import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types"; -import { DocumentManager } from "../util/DocumentManager"; -import "./PresentationView.scss"; -import React = require("react"); - -export interface PresViewProps { - Document: Doc; -} - -interface PresListProps extends PresViewProps { - deleteDocument(index: number): void; - gotoDocument(index: number): void; -} - -@observer -/** - * Component that takes in a document prop and a boolean whether it's collapsed or not. - */ -class PresentationViewList extends React.Component<PresListProps> { - - - /** - * Renders a single child document. It will just append a list element. - * @param document The document to render. - */ - renderChild = (document: Doc, index: number) => { - let title = document.title; - - //to get currently selected presentation doc - let selected = NumCast(this.props.Document.selectedDoc, 0); - - let className = "presentationView-item"; - if (selected === index) { - //this doc is selected - className += " presentationView-selected"; - } - let onEnter = (e: React.PointerEvent) => { document.libraryBrush = true; } - let onLeave = (e: React.PointerEvent) => { document.libraryBrush = false; } - return ( - <div className={className} key={document[Id] + index} - onPointerEnter={onEnter} onPointerLeave={onLeave} - style={{ - outlineColor: "maroon", - outlineStyle: "dashed", - outlineWidth: BoolCast(document.libraryBrush, false) || BoolCast(document.protoBrush, false) ? `1px` : "0px", - }} - onClick={e => { this.props.gotoDocument(index); e.stopPropagation(); }}> - <strong className="presentationView-name"> - {`${index + 1}. ${title}`} - </strong> - <button className="presentation-icon" onClick={e => { this.props.deleteDocument(index); e.stopPropagation(); }}>X</button> - </div> - ); - - } - - render() { - const children = DocListCast(this.props.Document.data); - - return ( - <div className="presentationView-listCont"> - {children.map(this.renderChild)} - </div> - ); - } -} - - -@observer -export class PresentationView extends React.Component<PresViewProps> { - public static Instance: PresentationView; - - //observable means render is re-called every time variable is changed - @observable - collapsed: boolean = false; - closePresentation = action(() => this.props.Document.width = 0); - next = () => { - const current = NumCast(this.props.Document.selectedDoc); - this.gotoDocument(current + 1); - - } - back = () => { - const current = NumCast(this.props.Document.selectedDoc); - this.gotoDocument(current - 1); - } - - @action - public RemoveDoc = (index: number) => { - const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc))); - if (value) { - value.splice(index, 1); - } - } - - public gotoDocument = async (index: number) => { - const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc))); - if (!list) { - return; - } - if (index < 0 || index >= list.length) { - return; - } - - this.props.Document.selectedDoc = index; - const doc = await list[index]; - DocumentManager.Instance.jumpToDocument(doc); - } - - //initilize class variables - constructor(props: PresViewProps) { - super(props); - PresentationView.Instance = this; - } - - /** - * Adds a document to the presentation view - **/ - @action - public PinDoc(doc: Doc) { - //add this new doc to props.Document - const data = Cast(this.props.Document.data, listSpec(Doc)); - if (data) { - data.push(doc); - } else { - this.props.Document.data = new List([doc]); - } - - this.props.Document.width = 300; - } - - render() { - let titleStr = StrCast(this.props.Document.title); - let width = NumCast(this.props.Document.width); - - //TODO: next and back should be icons - return ( - <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}> - <div className="presentationView-heading"> - <div className="presentationView-title">{titleStr}</div> - <button className='presentation-icon' onClick={this.closePresentation}>X</button> - </div> - <div className="presentation-buttons"> - <button className="presentation-button" onClick={this.back}>back</button> - <button className="presentation-button" onClick={this.next}>next</button> - </div> - <PresentationViewList Document={this.props.Document} deleteDocument={this.RemoveDoc} gotoDocument={this.gotoDocument} /> - </div> - ); - } -}
\ No newline at end of file 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 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/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 3288abd90..a9bc4d3d2 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -45,7 +45,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { if (template.Name === "Bullet") { let topDocView = this.props.docs[0]; topDocView.addTemplate(template); - topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document.proto!)); + topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document)); } else { this.props.docs.map(d => d.addTemplate(template)); } diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index 3cd525afa..5bb8d454a 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -48,12 +48,12 @@ export namespace Templates { </div>` ); export const Title = new Template("Title", TemplatePosition.InnerTop, - `<div style="height:100%"> - <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; "> - <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span> + `<div> + <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100"> + <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.DataDoc.title}</span> </div> - <div style="display:flex; flex-direction:column; height:calc(100% - 25px);"> - <div style="height:min-content; width:100%;overflow:auto">{layout}</div> + <div style="height:calc(100% - 25px);"> + <div style="width:100%;overflow:auto">{layout}</div> </div> </div>` ); @@ -62,7 +62,7 @@ export namespace Templates { <div style="width:100%; background-color: rgba(0, 0, 0, .4); color: white; "> <FormattedTextBox {...props} height={"min-content"} color={"white"} fieldKey={"header"} /> </div> - <div style="width:100%;height:min-content;overflow:auto;">{layout}</div> + <div style="width:100%;height:100%;overflow:auto;">{layout}</div> </div > ` ); export const Bullet = new Template("Bullet", TemplatePosition.InnerTop, @@ -84,6 +84,16 @@ export namespace Templates { </div > `); } + export function TitleBar(datastring: string) { + return (`<div> + <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100"> + <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${datastring}</span> + </div> + <div style="height:calc(100% - 25px);"> + <div style="width:100%;overflow:auto">{layout}</div> + </div> + </div>` ); + } export const TemplateList: Template[] = [Title, Header, Caption, Bullet]; export function sortTemplates(a: Template, b: Template) { diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss index 6f97e60f8..3594ac9f4 100644 --- a/src/client/views/_nodeModuleOverrides.scss +++ b/src/client/views/_nodeModuleOverrides.scss @@ -3,7 +3,6 @@ // goldenlayout stuff div .lm_header { background: $dark-color; - min-height: 2em; } .lm_tab { diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 5238ad114..879898018 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -1,16 +1,16 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../new_fields/Doc'; +import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { listSpec } from '../../../new_fields/Schema'; -import { Cast, FieldValue, NumCast, PromiseValue } from '../../../new_fields/Types'; +import { BoolCast, Cast, NumCast, PromiseValue } from '../../../new_fields/Types'; +import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; import './CollectionBaseView.scss'; -import { DocumentManager } from '../../util/DocumentManager'; export enum CollectionViewType { Invalid, @@ -36,7 +36,6 @@ export interface CollectionViewProps extends FieldViewProps { contentRef?: React.Ref<HTMLDivElement>; } - @observer export class CollectionBaseView extends React.Component<CollectionViewProps> { @observable private static _safeMode = false; @@ -60,10 +59,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { } } + @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc ? this.props.DataDoc : this.props.Document : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; } + active = (): boolean => { var isSelected = this.props.isSelected(); - var topMost = this.props.isTopMost; - return isSelected || this._isChildActive || topMost; + return isSelected || this._isChildActive || this.props.renderDepth === 0; } //TODO should this be observable? @@ -73,81 +74,35 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { this.props.whenActiveChanged(isActive); } - createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean { - if (!(documentToAdd instanceof Doc)) { - return false; - } - let data = DocListCast(documentToAdd.data); - for (const doc of data) { - if (this.createsCycle(doc, containerDocument)) { - return true; - } - } - let annots = DocListCast(documentToAdd.annotations); - for (const annot of annots) { - if (this.createsCycle(annot, containerDocument)) { - return true; - } - } - for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) { - if (containerProto[Id] === documentToAdd[Id]) { - return true; - } - } - return false; - } - @computed get isAnnotationOverlay() { return this.props.fieldKey === "annotations"; } - @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { - let props = this.props; - var curPage = NumCast(props.Document.curPage, -1); + var curPage = NumCast(this.props.Document.curPage, -1); Doc.GetProto(doc).page = curPage; if (curPage >= 0) { - Doc.GetProto(doc).annotationOn = props.Document; + Doc.GetProto(doc).annotationOn = this.props.Document; } - if (!this.createsCycle(doc, props.Document)) { - //TODO This won't create the field if it doesn't already exist - const value = Cast(props.Document[props.fieldKey], listSpec(Doc)); - let alreadyAdded = true; - if (value !== undefined) { - if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) { - alreadyAdded = false; - value.push(doc); - } - } else { - alreadyAdded = false; - Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc])); - } - // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument? - if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) { - let zoom = NumCast(this.props.Document.scale, 1); - // Doc.GetProto(doc).zoomBasis = zoom; + allowDuplicates = true; + const value = Cast(this.dataDoc[this.dataField], listSpec(Doc)); + if (value !== undefined) { + if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) { + value.push(doc); } + } else { + Doc.GetProto(this.dataDoc)[this.dataField] = new List([doc]); } return true; } @action.bound removeDocument(doc: Doc): boolean { - let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView) + let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); docView && SelectionManager.DeselectDoc(docView); - const props = this.props; //TODO This won't create the field if it doesn't already exist - const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []); - let index = -1; - for (let i = 0; i < value.length; i++) { - let v = value[i]; - if (v instanceof Doc && v[Id] === doc[Id]) { - index = i; - break; - } - } - PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => { - if (annotationOn === props.Document) { - doc.annotationOn = undefined; - } - }); + const value = Cast(this.dataDoc[this.dataField], listSpec(Doc), []); + let index = value.reduce((p, v, i) => (v instanceof Doc && v[Id] === doc[Id]) ? i : p, -1); + PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => + annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined) + ); if (index !== -1) { value.splice(index, 1); @@ -161,7 +116,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { @action.bound moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { - if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { + if (Doc.AreProtosEqual(this.dataDoc, targetCollection)) { return true; } if (this.removeDocument(doc)) { @@ -187,4 +142,4 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 51e29cb54..e675e82fc 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -9,8 +9,8 @@ import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; import { Id } from '../../../new_fields/FieldSymbols'; import { FieldId } from "../../../new_fields/RefField"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, returnTrue, Utils } from "../../../Utils"; +import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { emptyFunction, returnTrue, Utils, returnOne } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { DocumentManager } from '../../util/DocumentManager'; import { DragLinksAsDocuments, DragManager } from "../../util/DragManager"; @@ -24,11 +24,16 @@ import { SubCollectionViewProps } from "./CollectionSubView"; import { ParentDocSelector } from './ParentDocumentSelector'; import React = require("react"); import { MainView } from '../MainView'; +import { LinkManager } from '../../util/LinkManager'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faFile } from '@fortawesome/free-solid-svg-icons'; +library.add(faFile); @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { public static Instance: CollectionDockingView; - public static makeDocumentConfig(document: Doc, width?: number) { + public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -36,6 +41,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], + dataDocumentId: dataDoc ? dataDoc[Id] : "" //collectionDockingView: CollectionDockingView.Instance } }; @@ -45,28 +51,30 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp private _containerRef = React.createRef<HTMLDivElement>(); private _flush: boolean = false; private _ignoreStateChange = ""; + private _isPointerDown = false; constructor(props: SubCollectionViewProps) { super(props); - CollectionDockingView.Instance = this; + if (props.addDocTab === emptyFunction) CollectionDockingView.Instance = this; + //Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; } hack: boolean = false; undohack: any = null; - public StartOtherDrag(dragDocs: Doc[], e: any) { + public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs?: (Doc | undefined)[]) { this.hack = true; this.undohack = UndoManager.StartBatch("goldenDrag"); - dragDocs.map(dragDoc => - this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener. + dragDocs.map((dragDoc, i) => + this.AddRightSplit(dragDoc, dragDataDocs ? dragDataDocs[i] : dragDoc, true).contentItems[0].tab._dragListener. onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } @action - public OpenFullScreen(document: Doc) { + public OpenFullScreen(document: Doc, dataDoc: Doc) { let newItemStackConfig = { type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document)] + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] }; var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); this._goldenLayout.root.contentItems[0].addChild(docconfig); @@ -121,22 +129,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // @action - public AddRightSplit = (document: Doc, minimize: boolean = false) => { + public AddRightSplit = (document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); } let newItemStackConfig = { type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document)] + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] }; var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); - if (this._goldenLayout.root.contentItems[0].isRow) { + if (this._goldenLayout.root.contentItems.length === 0) { + this._goldenLayout.root.addChild(newContentItem); + } else if (this._goldenLayout.root.contentItems[0].isRow) { this._goldenLayout.root.contentItems[0].addChild(newContentItem); - } - else { + } else { var collayout = this._goldenLayout.root.contentItems[0]; var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout); collayout.parent.replaceChild(collayout, newRow); @@ -158,12 +167,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return newContentItem; } @action - public AddTab = (stack: any, document: Doc) => { + public AddTab = (stack: any, document: Doc, dataDocument: Doc) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); } - let docContentConfig = CollectionDockingView.makeDocumentConfig(document); + let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument); var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout); stack.addChild(newContentItem.contentItems[0], undefined); this.layoutChanged(); @@ -248,6 +257,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @action onPointerUp = (e: React.PointerEvent): void => { + this._isPointerDown = false; if (this._flush) { this._flush = false; setTimeout(() => this.stateChanged(), 10); @@ -255,6 +265,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } @action onPointerDown = (e: React.PointerEvent): void => { + this._isPointerDown = true; + let onPointerUp = action(() => { + window.removeEventListener("pointerup", onPointerUp); + this._isPointerDown = false; + }); + window.addEventListener("pointerup", onPointerUp); var className = (e.target as any).className; if (className === "messageCounter") { e.stopPropagation(); @@ -266,25 +282,28 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) => (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc))); } else - if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) { + if ((className === "lm_title" || className === "lm_tab lm_active") && e.shiftKey) { e.stopPropagation(); e.preventDefault(); let x = e.clientX; let y = e.clientY; let docid = (e.target as any).DashDocId; + let datadocid = (e.target as any).DashDataDocId; let tab = (e.target as any).parentElement as HTMLElement; let glTab = (e.target as any).Tab; if (glTab && glTab.contentItem && glTab.contentItem.parent) { glTab.contentItem.parent.setActiveContentItem(glTab.contentItem); } - DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { + DocServer.GetRefField(docid).then(action(async (f: Opt<Field>) => { if (f instanceof Doc) { - DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y, + let dataDoc = (datadocid !== docid) ? await DocServer.GetRefField(datadocid) : f; + DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f], [dataDoc instanceof Doc ? dataDoc : f]), x, y, { handlers: { dragComplete: emptyFunction, }, - hideSource: false + hideSource: false, + withoutShiftDrag: true }); } })); @@ -329,24 +348,45 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (tab.contentItem.config.fixed) { tab.contentItem.parent.config.fixed = true; } - DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => { - if (doc instanceof Doc) { - let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`); - tab.element.append(counter); - let upDiv = document.createElement("span"); - const stack = tab.contentItem.parent; - ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv); - tab.reactComponents = [upDiv]; - tab.element.append(upDiv); - counter.DashDocId = tab.contentItem.config.props.documentId; - tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title], - () => { - counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length; - tab.titleElement[0].textContent = doc.title; - }, { fireImmediately: true }); - tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; - } - }); + let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; + let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc; + if (doc instanceof Doc && dataDoc instanceof Doc) { + let dragSpan = document.createElement("span"); + dragSpan.style.position = "relative"; + dragSpan.style.bottom = "6px"; + dragSpan.style.paddingLeft = "4px"; + dragSpan.style.paddingRight = "2px"; + let upDiv = document.createElement("span"); + const stack = tab.contentItem.parent; + // shifts the focus to this tab when another tab is dragged over it + tab.element[0].onmouseenter = (e: any) => { + if (!this._isPointerDown) return; + var activeContentItem = tab.header.parent.getActiveContentItem(); + if (tab.contentItem !== activeContentItem) { + tab.header.parent.setActiveContentItem(tab.contentItem); + } + tab.setActive(true); + }; + ReactDOM.render(<span onPointerDown={ + e => { + e.preventDefault(); + e.stopPropagation(); + DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc], [dataDoc]), e.clientX, e.clientY, { + handlers: { dragComplete: emptyFunction }, + hideSource: false + }); + }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan); + ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={doc => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv); + tab.reactComponents = [dragSpan, upDiv]; + tab.element.append(dragSpan); + tab.element.append(upDiv); + tab.reactionDisposer = reaction(() => [doc.title], + () => { + tab.titleElement[0].textContent = doc.title; + }, { fireImmediately: true }); + //TODO why can't this just be doc instead of the id? + tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; + } } tab.titleElement[0].Tab = tab; tab.closeElement.off('click') //unbind the current click handler @@ -409,6 +449,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp interface DockedFrameProps { documentId: FieldId; + dataDocumentId: FieldId; //collectionDockingView: CollectionDockingView } @observer @@ -417,19 +458,30 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; + @observable private _dataDoc: Opt<Doc>; get _stack(): any { let parent = (this.props as any).glContainer.parent.parent; - if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) + if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) { return parent.parent.contentItems[1]; + } return parent; } constructor(props: any) { super(props); - DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); + DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => { + this._dataDoc = this._document = f as Doc; + if (this.props.dataDocumentId && this.props.documentId !== this.props.dataDocumentId) { + DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc)); + } + })); } nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth); - nativeHeight = () => NumCast(this._document!.nativeHeight, this._panelHeight); + nativeHeight = () => { + let nh = NumCast(this._document!.nativeHeight, this._panelHeight); + let res = BoolCast(this._document!.ignoreAspect) ? this._panelHeight : nh; + return res; + } contentScaling = () => { const nativeH = this.nativeHeight(); const nativeW = this.nativeWidth(); @@ -450,6 +502,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { let docHeight = NumCast(this._document!.height); if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1; if (StrCast(this._document!.layout).indexOf("Collection") === -1 || + !BoolCast(this._document!.fitToContents, false) || NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1; let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ? this._panelHeight / docHeight : this._panelWidth / docWidth); @@ -457,23 +510,25 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } - addDocTab = (doc: Doc, location: string) => { + addDocTab = (doc: Doc, dataDoc: Doc, location: string) => { if (doc.dockingConfig) { MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { - CollectionDockingView.Instance.AddRightSplit(doc); + CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); } else { - CollectionDockingView.Instance.AddTab(this._stack, doc); + CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); } } get content() { - if (!this._document) { + if (!this._document || !this._dataDoc) { return (null); } return ( <div className="collectionDockingView-content" ref={this._mainCont} - style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}> - <DocumentView key={this._document[Id]} Document={this._document} + style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier})` }}> + <DocumentView key={this._document[Id]} + Document={this._document} + DataDoc={this._dataDoc} bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} @@ -481,13 +536,15 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { PanelWidth={this.nativeWidth} PanelHeight={this.nativeHeight} ScreenToLocalTransform={this.ScreenToLocalTransform} - isTopMost={true} + renderDepth={0} selectOnLoad={false} parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} addDocTab={this.addDocTab} - ContainingCollectionView={undefined} /> + ContainingCollectionView={undefined} + zoomToScale={emptyFunction} + getScale={returnOne} /> </div >); } @@ -498,4 +555,4 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { {({ measureRef }) => <div ref={measureRef}> {theContent} </div>} </Measure>; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index bf887ce7c..31a73ab36 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,60 +1,74 @@ -import { action, observable } from "mobx"; +import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; +import { WidthSym } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { NumCast } from "../../../new_fields/Types"; +import { emptyFunction } from "../../../Utils"; import { ContextMenu } from "../ContextMenu"; +import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView"; +import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import "./CollectionPDFView.scss"; import React = require("react"); -import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView"; -import { emptyFunction } from "../../../Utils"; -import { NumCast } from "../../../new_fields/Types"; -import { Id } from "../../../new_fields/FieldSymbols"; +import { PDFBox } from "../nodes/PDFBox"; @observer export class CollectionPDFView extends React.Component<FieldViewProps> { + private _pdfBox?: PDFBox; + private _reactionDisposer?: IReactionDisposer; + private _buttonTray: React.RefObject<HTMLDivElement>; + + constructor(props: FieldViewProps) { + super(props); + + this._buttonTray = React.createRef(); + } + + componentDidMount() { + this._reactionDisposer = reaction( + () => NumCast(this.props.Document.scrollY), + () => { + // let transform = this.props.ScreenToLocalTransform(); + if (this._buttonTray.current) { + // console.log(this._buttonTray.current.offsetHeight); + // console.log(NumCast(this.props.Document.scrollY)); + let scale = this.nativeWidth() / this.props.Document[WidthSym](); + this.props.Document.panY = NumCast(this.props.Document.scrollY); + // console.log(scale); + } + // console.log(this.props.Document[HeightSym]()); + }, + { fireImmediately: true } + ); + } + + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); + } public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(CollectionPDFView, fieldKey); } @observable _inThumb = false; - private set curPage(value: number) { this.props.Document.curPage = value; } + private set curPage(value: number) { this._pdfBox && this._pdfBox.GotoPage(value); } private get curPage() { return NumCast(this.props.Document.curPage, -1); } private get numPages() { return NumCast(this.props.Document.numPages); } - @action onPageBack = () => this.curPage > 1 ? (this.props.Document.curPage = this.curPage - 1) : -1; - @action onPageForward = () => this.curPage < this.numPages ? (this.props.Document.curPage = this.curPage + 1) : -1; + @action onPageBack = () => this._pdfBox && this._pdfBox.BackPage(); + @action onPageForward = () => this._pdfBox && this._pdfBox.ForwardPage(); - @action - onThumbDown = (e: React.PointerEvent) => { - document.addEventListener("pointermove", this.onThumbMove, false); - document.addEventListener("pointerup", this.onThumbUp, false); - e.stopPropagation(); - this._inThumb = true; - } - @action - onThumbMove = (e: PointerEvent) => { - let pso = (e.clientY - (e as any).target.parentElement.getBoundingClientRect().top) / (e as any).target.parentElement.getBoundingClientRect().height; - this.curPage = Math.trunc(Math.min(this.numPages, pso * this.numPages + 1)); - e.stopPropagation(); - } - @action - onThumbUp = (e: PointerEvent) => { - this._inThumb = false; - document.removeEventListener("pointermove", this.onThumbMove); - document.removeEventListener("pointerup", this.onThumbUp); - } nativeWidth = () => NumCast(this.props.Document.nativeWidth); nativeHeight = () => NumCast(this.props.Document.nativeHeight); private get uIButtons() { let ratio = (this.curPage - 1) / this.numPages * 100; return ( - <div className="collectionPdfView-buttonTray" key="tray" style={{ height: "100%" }}> + <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}> <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button> <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button> - <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} > + {/* <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} > <div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} /> - </div> + </div> */} </div> ); } @@ -65,11 +79,14 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { } } + setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; }; + + private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; return ( <> - <CollectionFreeFormView {...props} CollectionView={this} /> + <CollectionFreeFormView {...props} setPdfBox={this.setPdfBox} CollectionView={this} /> {renderProps.active() ? this.uIButtons : (null)} </> ); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index b9e5a5b65..98bf513bb 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -2,36 +2,35 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, untracked, runInAction, trace } from "mobx"; +import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; -import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; import "react-table/react-table.css"; -import { emptyFunction, returnFalse, returnZero } from "../../../Utils"; -import { SetupDrag } from "../../util/DragManager"; +import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; +import { Doc, DocListCast, DocListCastAsync, Field } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; +import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { Docs } from "../../documents/Documents"; +import { Gateway } from "../../northstar/manager/Gateway"; +import { SetupDrag, DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; -import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss"; +import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; +import { ContextMenu } from "../ContextMenu"; import { anchorPoints, Flyout } from "../DocumentDecorations"; import '../DocumentDecorations.scss'; import { EditableView } from "../EditableView"; import { DocumentView } from "../nodes/DocumentView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { CollectionPDFView } from "./CollectionPDFView"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc"; -import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { listSpec } from "../../../new_fields/Schema"; -import { List } from "../../../new_fields/List"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { Gateway } from "../../northstar/manager/Gateway"; -import { Docs } from "../../documents/Documents"; -import { ContextMenu } from "../ContextMenu"; -import { CollectionView } from "./CollectionView"; -import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; -import { SelectionManager } from "../../util/SelectionManager"; +import { CollectionView } from "./CollectionView"; import { undoBatch } from "../../util/UndoManager"; +import { timesSeries } from "async"; library.add(faCog); @@ -90,8 +89,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { let columnDocs = DocListCast(schemaDoc.data); if (columnDocs) { let ddoc = columnDocs.find(doc => doc.title === columnName); - if (ddoc) + if (ddoc) { return ddoc; + } } } return this.props.Document; @@ -100,11 +100,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { Document: rowProps.original, + DataDoc: rowProps.original, fieldKey: rowProps.column.id as string, + fieldExt: "", ContainingCollectionView: this.props.CollectionView, isSelected: returnFalse, select: emptyFunction, - isTopMost: false, + renderDepth: this.props.renderDepth + 1, selectOnLoad: false, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, @@ -116,9 +118,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { }; let fieldContentView = <FieldView {...props} />; let reference = React.createRef<HTMLDivElement>(); - let onItemDown = (e: React.PointerEvent) => + let onItemDown = (e: React.PointerEvent) => { (this.props.CollectionView.props.isSelected() ? SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e) : undefined); + }; let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); if (!res.success) return false; @@ -282,13 +285,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get previewDocument(): Doc | undefined { - const children = DocListCast(this.props.Document[this.props.fieldKey]); - const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined; - return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; + const selected = this.childDocs.length > this._selectedIndex ? this.childDocs[this._selectedIndex] : undefined; + let pdc = selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; + return pdc; } getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate( - - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth); + - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth) get documentKeysCheckList() { @@ -337,7 +340,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { columns={this.tableColumns} column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} getTrProps={this.getTrProps} - /> + />; } @computed @@ -348,21 +351,26 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get previewPanel() { - trace(); - return <CollectionSchemaPreview - Document={this.previewDocument} - width={this.previewWidth} - height={this.previewHeight} - getTransform={this.getPreviewTransform} - CollectionView={this.props.CollectionView} - addDocument={this.props.addDocument} - removeDocument={this.props.removeDocument} - active={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - addDocTab={this.props.addDocTab} - setPreviewScript={this.setPreviewScript} - previewScript={this.previewScript} - /> + return <div ref={this.createTarget}> + <CollectionSchemaPreview + Document={this.previewDocument} + DataDocument={BoolCast(this.props.Document.isTemplate) ? this.previewDocument : this.props.DataDoc} + childDocs={this.childDocs} + renderDepth={this.props.renderDepth} + width={this.previewWidth} + height={this.previewHeight} + getTransform={this.getPreviewTransform} + CollectionView={this.props.CollectionView} + moveDocument={this.props.moveDocument} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + active={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + setPreviewScript={this.setPreviewScript} + previewScript={this.previewScript} + /> + </div>; } @action setPreviewScript = (script: string) => { @@ -384,21 +392,27 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } interface CollectionSchemaPreviewProps { Document?: Doc; + DataDocument?: Doc; + childDocs?: Doc[]; + renderDepth: number; width: () => number; height: () => number; - CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView; getTransform: () => Transform; addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean; removeDocument: (document: Doc) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; - addDocTab: (document: Doc, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc, where: string) => void; setPreviewScript: (script: string) => void; previewScript?: string; } @observer export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{ + private dropDisposer?: DragManager.DragDropDisposer; + _mainCont?: HTMLDivElement; private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.width()); } private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.height()); } private contentScaling = () => { @@ -408,47 +422,63 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre } return wscale; } - private PanelWidth = () => this.nativeWidth * this.contentScaling(); - private PanelHeight = () => this.nativeHeight * this.contentScaling(); - private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling()) - get centeringOffset() { return (this.props.width() - this.nativeWidth * this.contentScaling()) / 2; } - @action - onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.props.setPreviewScript(e.currentTarget.value); + protected createDropTarget = (ele: HTMLDivElement) => { } + private createTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; + this.dropDisposer && this.dropDisposer(); + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } + } + @undoBatch @action - public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { - SelectionManager.DeselectAll(); - if (expandedDocs) { - let isMinimized: boolean | undefined; - expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { - if (isMinimized === undefined) { - isMinimized = BoolCast(maximizedDoc.isMinimized, false); - } - maximizedDoc.isMinimized = !isMinimized; + drop = (e: Event, de: DragManager.DropEvent) => { + if (de.data instanceof DragManager.DocumentDragData) { + let docDrag = de.data; + this.props.childDocs && this.props.childDocs.map(otherdoc => { + Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(docDrag.draggedDocuments[0]); }); + e.stopPropagation(); } + return true; + } + private PanelWidth = () => this.nativeWidth ? this.nativeWidth * this.contentScaling() : this.props.width(); + private PanelHeight = () => this.nativeHeight ? this.nativeHeight * this.contentScaling() : this.props.height(); + private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling()); + get centeringOffset() { return this.nativeWidth ? (this.props.width() - this.nativeWidth * this.contentScaling()) / 2 : 0; } + @action + onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.props.setPreviewScript(e.currentTarget.value); } render() { let input = this.props.previewScript === undefined ? (null) : - <input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange} - style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} />; + <div ref={this.createTarget}><input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange} + style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} /></div>; return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width(), height: "100%" }}> {!this.props.Document || !this.props.width ? (null) : ( <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)`, height: "100%" }}> - <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false} - addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} + <DocumentView + DataDoc={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDocument} + Document={this.props.Document} + renderDepth={this.props.renderDepth + 1} + selectOnLoad={false} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + moveDocument={this.props.moveDocument} ScreenToLocalTransform={this.getTransform} ContentScaling={this.contentScaling} - PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} ContainingCollectionView={this.props.CollectionView} focus={emptyFunction} parentActive={this.props.active} whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} addDocTab={this.props.addDocTab} - collapseToPoint={this.collapseToPoint} + zoomToScale={emptyFunction} + getScale={returnOne} /> </div>)} {input} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index af194aec9..034a09eaa 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -38,4 +38,24 @@ background: $dark-color; color: $light-color; } + + .collectionStackingView-columnDragger { + width: 15; + height: 15; + position: absolute; + margin-left: -5; + } + + + .collectionStackingView-columnDoc, + .collectionStackingView-masonryDoc { + margin-left: auto; + margin-right: auto; + } + + .collectionStackingView-masonryDoc { + transform-origin: top left; + grid-column-end: span 1; + height: 100%; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index cad7cd50c..6c4ea18a1 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,28 +1,27 @@ import React = require("react"); -import { action, computed, IReactionDisposer, reaction, trace } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { BoolCast, NumCast } from "../../../new_fields/Types"; -import { emptyFunction, returnOne, Utils } from "../../../Utils"; -import { SelectionManager } from "../../util/SelectionManager"; -import { undoBatch } from "../../util/UndoManager"; -import { DocumentView } from "../nodes/DocumentView"; +import { emptyFunction, Utils } from "../../../Utils"; +import { ContextMenu } from "../ContextMenu"; import { CollectionSchemaPreview } from "./CollectionSchemaView"; import "./CollectionStackingView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { ContextMenu } from "../ContextMenu"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { _masonryGridRef: HTMLDivElement | null = null; + _draggerRef = React.createRef<HTMLDivElement>(); _heightDisposer?: IReactionDisposer; _gridSize = 1; @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); } @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); } - @computed get columnWidth() { return this.singleColumn ? this.props.PanelWidth() - 2 * this.xMargin : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); } + @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); } singleColDocHeight(d: Doc) { let nw = NumCast(d.nativeWidth); @@ -62,47 +61,35 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { this._masonryGridRef = ele; this.createDropTarget(ele!); } - @undoBatch - @action - public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { - SelectionManager.DeselectAll(); - if (expandedDocs) { - let isMinimized: boolean | undefined; - expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { - if (isMinimized === undefined) { - isMinimized = BoolCast(maximizedDoc.isMinimized, false); - } - maximizedDoc.isMinimized = !isMinimized; - }); - } - } @computed get singleColumnChildren() { let children = this.childDocs.filter(d => !d.isMinimized); return children.map((d, i) => { let dref = React.createRef<HTMLDivElement>(); - let script = undefined; - let colWidth = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; - let rowHeight = () => this.singleColDocHeight(d); let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]()); - return <div className="collectionStackingView-masonryDoc" + let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => this.singleColDocHeight(d); + return <div className="collectionStackingView-columnDoc" key={d[Id]} ref={dref} - style={{ width: colWidth(), height: rowHeight(), marginLeft: "auto", marginRight: "auto" }} > + style={{ width: width(), height: height() }} > <CollectionSchemaPreview Document={d} - width={colWidth} - height={rowHeight} + DataDocument={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDoc} + renderDepth={this.props.renderDepth} + width={width} + height={height} getTransform={dxf} CollectionView={this.props.CollectionView} addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} active={this.props.active} whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} setPreviewScript={emptyFunction} - previewScript={script}> + previewScript={undefined}> </CollectionSchemaPreview> </div>; }); @@ -110,44 +97,64 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get children() { return this.childDocs.filter(d => !d.isMinimized).map((d, i) => { + let aspect = d.nativeHeight ? NumCast(d.nativeWidth) / NumCast(d.nativeHeight) : undefined; let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(d, dref.current!); - let rowSpan = Math.ceil((this.columnWidth / d[WidthSym]() * d[HeightSym]() + this.gridGap) / (this._gridSize + this.gridGap)); - let childFocus = (doc: Doc) => { - doc.libraryBrush = true; - this.props.focus(this.props.Document); // just focus on this collection, not the underlying document because the API doesn't support adding an offset to focus on and we can't pan zoom our contents to be centered. - } + let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]()); + let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => aspect ? width() / aspect : d[HeightSym](); + let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); return (<div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} - style={{ - width: NumCast(d.nativeWidth, d[WidthSym]()), - height: NumCast(d.nativeHeight, d[HeightSym]()), - transformOrigin: "top left", - gridRowEnd: `span ${rowSpan}`, - gridColumnEnd: `span 1`, - transform: `scale(${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())}, ${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())})` - }} > - <DocumentView key={d[Id]} Document={d} + style={{ gridRowEnd: `span ${rowSpan}` }} > + <CollectionSchemaPreview + Document={d} + DataDocument={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDoc} + renderDepth={this.props.renderDepth} + CollectionView={this.props.CollectionView} addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} - moveDocument={this.moveDocument} - ContainingCollectionView={this.props.CollectionView} - isTopMost={false} - ScreenToLocalTransform={dxf} - focus={childFocus} - ContentScaling={returnOne} - PanelWidth={d[WidthSym]} - PanelHeight={d[HeightSym]} - selectOnLoad={false} - parentActive={this.props.active} + getTransform={dxf} + width={width} + height={height} + active={this.props.active} addDocTab={this.props.addDocTab} - bringToFront={emptyFunction} whenActiveChanged={this.props.whenActiveChanged} - collapseToPoint={this.collapseToPoint} - /> + setPreviewScript={emptyFunction} + previewScript={undefined}> + </CollectionSchemaPreview> </div>); - }) + }); + } + + _columnStart: number = 0; + columnDividerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + } + @action + onDividerMove = (e: PointerEvent): void => { + let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + let delta = dragPos - this._columnStart; + this._columnStart = dragPos; + + this.props.Document.columnWidth = this.columnWidth + delta; + } + + @action + onDividerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onDividerMove); + document.removeEventListener('pointerup', this.onDividerUp); + } + + @computed get columnDragger() { + return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ left: `${this.columnWidth + this.xMargin}px` }} > + <FontAwesomeIcon icon={"caret-down"} /> + </div>; } onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 @@ -163,10 +170,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { let templatecols = ""; for (let i = 0; i < cols; i++) templatecols += `${this.columnWidth}px `; return ( - <div className="collectionStackingView"> + <div className="collectionStackingView" ref={this.createRef} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > <div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`} - onContextMenu={this.onContextMenu} - ref={this.createRef} onWheel={(e: React.WheelEvent) => e.stopPropagation()} style={{ padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`, margin: "auto", @@ -179,7 +184,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }} > {this.singleColumn ? this.singleColumnChildren : this.children} - </div></div> + {this.singleColumn ? (null) : this.columnDragger} + </div> + </div> ); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 93a1a8cda..3b3bbdbd9 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,10 +1,11 @@ -import { action } from "mobx"; +import { action, computed } from "mobx"; import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, PromiseValue } from "../../../new_fields/Types"; +import { BoolCast, Cast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { RouteStore } from "../../../server/RouteStore"; import { DocServer } from "../../DocServer"; @@ -13,12 +14,11 @@ import { DragManager } from "../../util/DragManager"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; import React = require("react"); -import { FormattedTextBox } from "../nodes/FormattedTextBox"; -import { Id } from "../../../new_fields/FieldSymbols"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -36,9 +36,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { - if (this.dropDisposer) { - this.dropDisposer(); - } + this.dropDisposer && this.dropDisposer(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } @@ -47,10 +45,13 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.createDropTarget(ele); } + @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + get childDocs() { + let self = this; //TODO tfs: This might not be what we want? //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) - return DocListCast(this.props.Document[this.props.fieldKey]); + return DocListCast((BoolCast(this.props.Document.isTemplate) ? this.extensionDoc : this.props.Document)[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]); } @action @@ -95,6 +96,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { e.stopPropagation(); return added; } + else if (de.data instanceof DragManager.AnnotationDragData) { + return this.props.addDocument(de.data.dropDocument); + } return false; } @@ -174,10 +178,31 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } return; } - if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) { - let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); - this.props.addDocument(htmlDoc, false); - return; + if (html && !html.startsWith("<a")) { + let tags = html.split("<"); + if (tags[0] === "") tags.splice(0, 1); + let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; + if (img) { + let split = img.split("src=\"")[1].split("\"")[0]; + let doc = Docs.ImageDocument(split, { ...options, width: 300 }); + this.props.addDocument(doc, false); + return; + } else { + let path = window.location.origin + "/doc/"; + if (text.startsWith(path)) { + let docid = text.replace(DocServer.prepend("/doc/"), "").split("?")[0]; + DocServer.GetRefField(docid).then(f => { + if (f instanceof Doc) { + if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView + (f instanceof Doc) && this.props.addDocument(f, false); + } + }); + } else { + let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); + this.props.addDocument(htmlDoc, false); + } + return; + } } if (text && text.indexOf("www.youtube.com/watch") !== -1) { const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 2dc4b2e80..5205f4313 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -4,15 +4,15 @@ border-width: $COLLECTION_BORDER_WIDTH; border-color: transparent; border-style: solid; - border-radius: $border-radius; + border-radius: inherit; box-sizing: border-box; height: 100%; - padding: 20px; + padding-top: 20px; padding-left: 10px; padding-right: 0px; background: $light-color-secondary; font-size: 13px; - overflow: scroll; + overflow: auto; ul { list-style: none; @@ -25,12 +25,10 @@ } .bullet { - float: left; position: relative; width: 15px; - display: block; color: $intermediate-color; - margin-top: 8px; + margin-top: 4px; transform: scale(1.3, 1.3); } .editableView-container { @@ -45,44 +43,53 @@ display: inline; } - - .docContainer:hover { - .delete-button { - display: inline; - // width: auto; - } - } - - .coll-title { - width: max-content; + .editableView-input, .editableView-container-editing { display: block; + text-overflow: ellipsis; font-size: 24px; + white-space: nowrap; } +} +.collectionTreeView-keyHeader { + font-style: italic; + font-size: 8pt; + margin-left: 3px; + display:none; + background: lightgray; +} - .collectionTreeView-keyHeader { - font-style: italic; - font-size: 8pt; - } +.collectionTreeView-subtitle { + font-style: italic; + font-size: 8pt; + color: $intermediate-color; } .docContainer { - margin-left: 10px; - display: block; + position: relative; + text-overflow: ellipsis; + white-space: pre-wrap; + overflow: hidden; // width:100%;//width: max-content; - .treeViewItem-openRight { - margin-left: 5px; - display: none; - } } -#docContainer-data { - margin-top: 5px; +.treeViewItem-openRight { + display: none; } -.docContainer:hover { +.treeViewItem-border { + display:inherit; + border-left: dashed 1px #00000042; +} + +.treeViewItem-header:hover { + .collectionTreeView-keyHeader { + display:inherit; + } .treeViewItem-openRight { display: inline-block; height:13px; + margin-top:2px; + margin-left: 5px; // display: inline; svg { display:block; @@ -94,16 +101,14 @@ .treeViewItem-header { border: transparent 1px solid; + display:flex; } .treeViewItem-header-above { - border: transparent 1px solid; border-top: black 1px solid; } .treeViewItem-header-below { - border: transparent 1px solid; border-bottom: black 1px solid; } .treeViewItem-header-inside { - border: transparent 1px solid; border: black 1px solid; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 8a6764c58..5c80fbd38 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,51 +1,59 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight, faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faAngleRight, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, trace } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from '../../../new_fields/Doc'; +import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; +import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; -import { BoolCast, Cast, NumCast, StrCast, PromiseValue } from '../../../new_fields/Types'; -import { Docs } from '../../documents/Documents'; +import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; +import { emptyFunction, Utils } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { EditableView } from "../EditableView"; import { MainView } from '../MainView'; +import { Templates } from '../Templates'; import { CollectionViewType } from './CollectionBaseView'; import { CollectionDockingView } from './CollectionDockingView'; +import { CollectionSchemaPreview } from './CollectionSchemaView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); -import { Transform } from '../../util/Transform'; -import { SelectionManager } from '../../util/SelectionManager'; +import { LinkManager } from '../../util/LinkManager'; export interface TreeViewProps { document: Doc; - deleteDoc: (doc: Doc) => void; + dataDoc?: Doc; + containingCollection: Doc; + renderDepth: number; + deleteDoc: (doc: Doc) => boolean; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void; + panelWidth: () => number; + panelHeight: () => number; addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; + indentDocument?: () => void; ScreenToLocalTransform: () => Transform; + outerXf: () => { translateX: number, translateY: number }; treeViewId: string; parentKey: string; active: () => boolean; } -export enum BulletType { - Collapsed, - Collapsible, - List -} - library.add(faTrashAlt); library.add(faAngleRight); library.add(faCaretDown); library.add(faCaretRight); +library.add(faCaretSquareDown); +library.add(faCaretSquareRight); @observer /** @@ -53,26 +61,48 @@ library.add(faCaretRight); */ class TreeView extends React.Component<TreeViewProps> { private _header?: React.RefObject<HTMLDivElement> = React.createRef(); - private treedropDisposer?: DragManager.DragDropDisposer; - protected createTreeDropTarget = (ele: HTMLDivElement) => { - this.treedropDisposer && this.treedropDisposer(); - if (ele) { - this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } }); + private _treedropDisposer?: DragManager.DragDropDisposer; + private _dref = React.createRef<HTMLDivElement>(); + @observable __chosenKey: string = ""; + @computed get _chosenKey() { return this.__chosenKey ? this.__chosenKey : this.fieldKey; } + @observable _collapsed: boolean = true; + + @computed get fieldKey() { + let keys = Array.from(Object.keys(this.resolvedDataDoc)); + if (this.resolvedDataDoc.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); + while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); } + let keyList: string[] = []; + keys.map(key => { + let docList = Cast(this.resolvedDataDoc[key], listSpec(Doc)); + if (docList && docList.length > 0) { + keyList.push(key); + } + }); + let layout = StrCast(this.props.document.layout); + if (layout.indexOf("fieldKey={\"") !== -1) { + return layout.split("fieldKey={\"")[1].split("\"")[0]; + } + return keyList.length ? keyList[0] : "data"; } - @observable _isOver: boolean = false; - @observable _collapsed: boolean = true; + @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; } - @undoBatch delete = () => this.props.deleteDoc(this.props.document); - @undoBatch openRight = async () => this.props.addDocTab(this.props.document, "openRight"); + protected createTreeDropTarget = (ele: HTMLDivElement) => { + this._treedropDisposer && this._treedropDisposer(); + if (ele) { + this._treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } }); + } + } - @action onMouseEnter = () => { this._isOver = true; } - @action onMouseLeave = () => { this._isOver = false; } + @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc); + @undoBatch openRight = async () => this.props.addDocTab(this.props.document, this.props.document, "onRight"); + onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); onPointerEnter = (e: React.PointerEvent): void => { this.props.active() && (this.props.document.libraryBrush = true); - if (e.buttons === 1) { + if (e.buttons === 1 && SelectionManager.GetIsDragging()) { this._header!.current!.className = "treeViewItem-header"; document.addEventListener("pointermove", this.onDragMove, true); } @@ -88,95 +118,133 @@ class TreeView extends React.Component<TreeViewProps> { let rect = this._header!.current!.getBoundingClientRect(); let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); let before = x[1] < bounds[1]; - let inside = x[0] > bounds[0] + 75 || (!before && this._bulletType === BulletType.Collapsible); - this._header!.current!.className = "treeViewItem-header" - if (inside && this._bulletType != BulletType.List) this._header!.current!.className = "treeViewItem-header-inside"; - else if (before) this._header!.current!.className = "treeViewItem-header-above"; - else if (!before) this._header!.current!.className = "treeViewItem-header-below"; - e.stopPropagation(); - } - onPointerDown = (e: React.PointerEvent) => { + let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed); + this._header!.current!.className = "treeViewItem-header"; + if (inside) this._header!.current!.className += " treeViewItem-header-inside"; + else if (before) this._header!.current!.className += " treeViewItem-header-above"; + else if (!before) this._header!.current!.className += " treeViewItem-header-below"; e.stopPropagation(); } @action - remove = (document: Document, key: string) => { - let children = Cast(this.props.document[key], listSpec(Doc), []); - children.indexOf(document) !== -1 && children.splice(children.indexOf(document), 1); + remove = (document: Document, key: string): boolean => { + let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []); + if (children.indexOf(document) !== -1) { + children.splice(children.indexOf(document), 1); + return true; + } + return false; } @action - move: DragManager.MoveFunction = (document: Doc, target: Doc, addDoc) => { - if (this.props.document !== target) { - //TODO This should check if it was removed - this.props.deleteDoc(document); - return addDoc(document); + move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { + return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); + } + @action + indent = () => this.props.addDocument(this.props.document) && this.delete() + + renderBullet() { + let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc)); + let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc); + let isDoc = doc instanceof Doc || docList; + return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)}> + {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />} + </div>; + } + + titleClicked = (e: React.MouseEvent) => { + if (this._collapsed) return false; + else { + this.props.document.embed = !BoolCast(this.props.document.embed); + return true; } - return true; } + static loadId = ""; + editableView = (key: string, style?: string) => (<EditableView + oneLine={true} + display={"inline"} + editing={this.resolvedDataDoc[Id] === TreeView.loadId} + contents={StrCast(this.props.document[key])} + onClick={this.titleClicked} + height={36} + fontStyle={style} + GetValue={() => StrCast(this.props.document[key])} + SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true} + OnFillDown={(value: string) => { + Doc.GetProto(this.resolvedDataDoc)[key] = value; + let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + TreeView.loadId = doc[Id]; + return this.props.addDocument(doc); + }} + OnTab={() => this.props.indentDocument && this.props.indentDocument()} + />) - renderBullet(type: BulletType) { - let onClicked = action(() => this._collapsed = !this._collapsed); - let bullet: IconProp | undefined = undefined; - switch (type) { - case BulletType.Collapsed: bullet = "caret-right"; break; - case BulletType.Collapsible: bullet = "caret-down"; break; + @computed get keyList() { + let keys = Array.from(Object.keys(this.resolvedDataDoc)); + if (this.resolvedDataDoc.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); + while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); } - return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>; + let keyList: string[] = keys.reduce((l, key) => Cast(this.resolvedDataDoc[key], listSpec(Doc)) ? [...l, key] : l, [] as string[]); + keys.map(key => Cast(this.resolvedDataDoc[key], Doc) instanceof Doc && keyList.push(key)); + if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links"); + if (keyList.indexOf(this.fieldKey) !== -1) { + keyList.splice(keyList.indexOf(this.fieldKey), 1); + } + keyList.splice(0, 0, this.fieldKey); + return keyList; } /** * Renders the EditableView title element for placement into the tree. */ renderTitle() { let reference = React.createRef<HTMLDivElement>(); - let onItemDown = SetupDrag(reference, () => this.props.document, this.move, this.props.dropAction, this.props.treeViewId, true); - let editableView = (titleString: string) => - (<EditableView - oneLine={!this._isOver ? true : false} - display={"inline"} - contents={titleString} - height={36} - GetValue={() => StrCast(this.props.document.title)} - SetValue={(value: string) => { - let target = this.props.document.proto ? this.props.document.proto : this.props.document; - target.title = value; - return true; - }} - />); - let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : []; - let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : ( + let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); + + let headerElements = ( + <span className="collectionTreeView-keyHeader" key={this._chosenKey} + onPointerDown={action(() => { + let ind = this.keyList.indexOf(this._chosenKey); + ind = (ind + 1) % this.keyList.length; + this.__chosenKey = this.keyList[ind]; + })} > + {this._chosenKey} + </span>); + let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : []; + let openRight = dataDocs && dataDocs.indexOf(this.resolvedDataDoc) !== -1 ? (null) : ( <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> <FontAwesomeIcon icon="angle-right" size="lg" /> - {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */} </div>); - return ( - <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} + return <> + <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0", pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" }} > - {editableView(StrCast(this.props.document.title))} - {openRight} + {this.editableView("title")} {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */} - </div >); + </div > + {headerElements} + {openRight} + </>; } onWorkspaceContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) }); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)) }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }, icon: "layer-group" }); if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) { - ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" }); - ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" }); - if (DocumentManager.Instance.getDocumentViews(this.props.document).length) { - ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" }); + ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" }); + if (DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).length) { + ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document, true)) }); } ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); } else { ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); } - ContextMenu.Instance.displayMenu(e.pageX - 156, e.pageY - 15); + ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); } } @@ -185,118 +253,190 @@ class TreeView extends React.Component<TreeViewProps> { let rect = this._header!.current!.getBoundingClientRect(); let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); let before = x[1] < bounds[1]; - let inside = x[0] > bounds[0] + 75 || (!before && this._bulletType === BulletType.Collapsible); + let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed); + if (de.data instanceof DragManager.LinkDragData) { + let sourceDoc = de.data.linkSourceDocument; + let destDoc = this.props.document; + DocUtils.MakeLink(sourceDoc, destDoc); + e.stopPropagation(); + } if (de.data instanceof DragManager.DocumentDragData) { - let addDoc = (doc: Doc) => this.props.addDocument(doc, this.props.document, before); + let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before); if (inside) { - let docList = Cast(this.props.document.data, listSpec(Doc)); + let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc)); if (docList !== undefined) { - addDoc = (doc: Doc) => { docList && docList.push(doc); return true; } + addDoc = (doc: Doc) => { docList && docList.push(doc); return true; }; } } - let added = false; - if (de.data.dropAction || de.data.userDropAction) { - added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before) || added, false); - } else if (de.data.moveDocument) { - let movedDocs = de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments; - added = movedDocs.reduce((added: boolean, d) => - de.data.moveDocument(d, this.props.document, addDoc) || added, false); - } else { - added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before), false); - } e.stopPropagation(); - return added; + let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments); + return (de.data.dropAction || de.data.userDropAction) ? + de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false) + : (de.data.moveDocument) ? + movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false) + : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false); } return false; } - public static AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean) { - let list = Cast(target[key], listSpec(Doc)); - if (list) { - let ind = relativeTo ? list.indexOf(relativeTo) : -1; - if (ind === -1) list.push(doc); - else list.splice(before ? ind : ind + 1, 0, doc); - } - return true; + docTransform = () => { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!); + let outerXf = this.props.outerXf(); + let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); + return finalXf; + } + + renderLinks = () => { + let ele: JSX.Element[] = []; + let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey); + let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before); + let groups = LinkManager.Instance.getRelatedGroupedLinks(this.props.document); + groups.forEach((groupLinkDocs, groupType) => { + let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document)); + ele.push( + <div key={"treeviewlink-" + groupType + "subtitle"}> + <div className="collectionTreeView-subtitle">{groupType}:</div> + { + TreeView.GetChildElements(destLinks, this.props.treeViewId, this.props.document, this.props.dataDoc, "treeviewlink-" + groupType, addDoc, remDoc, this.move, + this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth) + } + </div> + ); + }); + return ele; } - _bulletType: BulletType = BulletType.List; render() { - let bulletType = BulletType.List; - let contentElement: (JSX.Element | null)[] = []; - let keys = Array.from(Object.keys(this.props.document)); - if (this.props.document.proto instanceof Doc) { - keys.push(...Array.from(Object.keys(this.props.document.proto))); - while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); - } - keys.map(key => { - let docList = Cast(this.props.document[key], listSpec(Doc)); - let remDoc = (doc: Doc) => this.remove(doc, key); - let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.document, key, doc, addBefore, before); - let doc = Cast(this.props.document[key], Doc); - if (doc instanceof Doc || docList) { - if (!this._collapsed) { - bulletType = BulletType.Collapsible; - contentElement.push(<ul key={key + "more"}> - {(key === "data") ? (null) : - <span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>} - <div style={{ display: "block" }}> - {TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, key, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.active)} - </div> - </ul >); - } else { - bulletType = BulletType.Collapsed; - } + let contentElement: (JSX.Element | null) = null; + let docList = Cast(this.resolvedDataDoc[this._chosenKey], listSpec(Doc)); + let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey); + let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.resolvedDataDoc, this._chosenKey, doc, addBefore, before); + let doc = Cast(this.resolvedDataDoc[this._chosenKey], Doc); + let docWidth = () => NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5; + if (!this._collapsed) { + if (!this.props.document.embed) { + contentElement = <ul key={this._chosenKey + "more"}> + {this._chosenKey === "links" ? this.renderLinks() : + TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move, + this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)} + </ul >; + } else { + console.log("PW = " + this.props.panelWidth()); + contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.props.panelHeight() }} key={this.props.document[Id]}> + <CollectionSchemaPreview + Document={this.props.document} + DataDocument={this.resolvedDataDoc} + renderDepth={this.props.renderDepth} + width={docWidth} + height={this.props.panelHeight} + getTransform={this.docTransform} + CollectionView={undefined} + addDocument={emptyFunction as any} + moveDocument={this.props.moveDocument} + removeDocument={emptyFunction as any} + active={this.props.active} + whenActiveChanged={emptyFunction as any} + addDocTab={this.props.addDocTab} + setPreviewScript={emptyFunction}> + </CollectionSchemaPreview> + </div>; } - }); - this._bulletType = bulletType; - return <div className="treeViewItem-container" - ref={this.createTreeDropTarget} - onContextMenu={this.onWorkspaceContextMenu}> + } + return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}> <li className="collection-child"> <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - {this.renderBullet(bulletType)} + {this.renderBullet()} {this.renderTitle()} </div> - {contentElement} + <div className="treeViewItem-border"> + {contentElement} + </div> </li> </div>; } public static GetChildElements( docs: Doc[], treeViewId: string, + containingCollection: Doc, + dataDoc: Doc | undefined, key: string, add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean, - remove: ((doc: Doc) => void), + remove: ((doc: Doc) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, - addDocTab: (doc: Doc, where: string) => void, + addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void, screenToLocalXf: () => Transform, - active: () => boolean + outerXf: () => { translateX: number, translateY: number }, + active: () => boolean, + panelWidth: () => number, + renderDepth: number ) { - return docs.filter(child => !child.excludeFromLibrary && (key !== "data" || !child.isMinimized)).map(child => - <TreeView document={child} treeViewId={treeViewId} key={child[Id]} deleteDoc={remove} addDocument={add} moveDocument={move} - dropAction={dropAction} addDocTab={addDocTab} ScreenToLocalTransform={screenToLocalXf} parentKey={key} active={active} />); + let docList = docs.filter(child => !child.excludeFromLibrary); + let rowWidth = () => panelWidth() - 20; + return docList.map((child, i) => { + let indent = i === 0 ? undefined : () => { + if (StrCast(docList[i - 1].layout).indexOf("CollectionView") !== -1) { + let fieldKeysub = StrCast(docList[i - 1].layout).split("fieldKey")[1]; + let fieldKey = fieldKeysub.split("\"")[1]; + Doc.AddDocToList(docList[i - 1], fieldKey, child); + remove(child); + } + }; + let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false); + }; + let rowHeight = () => { + let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0); + return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym](); + }; + return <TreeView + document={child} + dataDoc={dataDoc} + containingCollection={containingCollection} + treeViewId={treeViewId} + key={child[Id]} + indentDocument={indent} + renderDepth={renderDepth} + deleteDoc={remove} + addDocument={addDocument} + panelWidth={rowWidth} + panelHeight={rowHeight} + moveDocument={move} + dropAction={dropAction} + addDocTab={addDocTab} + ScreenToLocalTransform={screenToLocalXf} + outerXf={outerXf} + parentKey={key} + active={active} />; + }); } } @observer export class CollectionTreeView extends CollectionSubView(Document) { private treedropDisposer?: DragManager.DragDropDisposer; + private _mainEle?: HTMLDivElement; + protected createTreeDropTarget = (ele: HTMLDivElement) => { - if (this.treedropDisposer) { - this.treedropDisposer(); - } - if (ele) { + this.treedropDisposer && this.treedropDisposer(); + if (this._mainEle = ele) { this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } } + componentWillUnmount() { + this.treedropDisposer && this.treedropDisposer(); + } + @action - remove = (document: Document) => { + remove = (document: Document): boolean => { let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); - children.indexOf(document) !== -1 && children.splice(children.indexOf(document), 1); + if (children.indexOf(document) !== -1) { + children.splice(children.indexOf(document), 1); + return true; + } + return false; } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout @@ -306,40 +446,40 @@ export class CollectionTreeView extends CollectionSubView(Document) { } } - onTreeDrop = (e: React.DragEvent) => { - this.onDrop(e, {}); - } + @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + + outerXf = () => Utils.GetScreenTransform(this._mainEle!); + onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); + render() { - let dropAction = Cast(this.props.Document.dropAction, "string") as dropActionType; - if (!this.childDocs) { - return (null); - } - let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + let dropAction = StrCast(this.props.Document.dropAction) as dropActionType; + let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); - let childElements = TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.fieldKey, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.active); - return ( + return !this.childDocs ? (null) : ( <div id="body" className="collectionTreeView-dropTarget" - style={{ borderRadius: "inherit" }} + style={{ overflow: "auto" }} onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()} + onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> - <div className="coll-title"> - <EditableView - contents={this.props.Document.title} - display={"inline"} - height={72} - GetValue={() => StrCast(this.props.Document.title)} - SetValue={(value: string) => { - let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; - target.title = value; - return true; - }} /> - </div> - <ul className="no-indent"> - {childElements} + <EditableView + contents={this.resolvedDataDoc.title} + display={"block"} + height={72} + GetValue={() => StrCast(this.resolvedDataDoc.title)} + SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true} + OnFillDown={(value: string) => { + Doc.GetProto(this.props.Document).title = value; + let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + TreeView.loadId = doc[Id]; + Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true); + }} /> + <ul className="no-indent" style={{ width: "max-content" }} > + { + TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove, + moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth) + } </ul> </div > ); 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/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 68eefab4c..0517c17bf 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -2,8 +2,10 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; import { observer } from "mobx-react"; import * as React from 'react'; +import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { Docs } from '../../documents/Documents'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; @@ -29,7 +31,7 @@ export class CollectionView extends React.Component<FieldViewProps> { private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; - switch (type) { + switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) { case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />); case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />); case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />); @@ -41,7 +43,7 @@ export class CollectionView extends React.Component<FieldViewProps> { return (null); } - get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } // bcz: ? Why do we need to compare Id's? + get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; } onContextMenu = (e: React.MouseEvent): void => { if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 @@ -54,6 +56,16 @@ export class CollectionView extends React.Component<FieldViewProps> { subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" }); subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" }); ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems }); + ContextMenu.Instance.addItem({ + description: "Apply Template", event: undoBatch(() => { + let otherdoc = new Doc(); + otherdoc.width = 100; + otherdoc.height = 50; + Doc.GetProto(otherdoc).title = "applied(" + this.props.Document.title + ")"; + Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document); + this.props.addDocTab && this.props.addDocTab(otherdoc, otherdoc, "onRight"); + }), icon: "project-diagram" + }); } } diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index f11af04a3..fa7058d5a 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -9,7 +9,7 @@ import { CollectionDockingView } from "./CollectionDockingView"; import { NumCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionBaseView"; -type SelectorProps = { Document: Doc, addDocTab(doc: Doc, location: string): void }; +type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc, location: string): void }; @observer export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -43,7 +43,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { col.panX = newPanX; col.panY = newPanY; } - this.props.addDocTab(col, "inTab"); + this.props.addDocTab(col, col, "inTab"); // bcz: dataDoc? }; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 7a0fd2b31..fc5212edd 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -9,6 +9,7 @@ opacity: 0.5; transform: translate(10000px,10000px); pointer-events: all; + cursor: pointer; } .collectionfreeformlinkview-linkText { stroke: rgb(0,0,0); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index ba7e6cf9e..b546d1b78 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -25,18 +25,18 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2); let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2); let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2); - this.props.LinkDocs.map(l => { - let width = l[WidthSym](); - l.x = (x1 + x2) / 2 - width / 2; - l.y = (y1 + y2) / 2 + 10; - if (!this.props.removeDocument(l)) this.props.addDocument(l, false); - }); + // this.props.LinkDocs.map(l => { + // let width = l[WidthSym](); + // l.x = (x1 + x2) / 2 - width / 2; + // l.y = (y1 + y2) / 2 + 10; + // if (!this.props.removeDocument(l)) this.props.addDocument(l, false); + // }); e.stopPropagation(); e.preventDefault(); } } render() { - let l = this.props.LinkDocs; + // let l = this.props.LinkDocs; let a = this.props.A; let b = this.props.B; let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2); @@ -44,13 +44,13 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2); let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2); let text = ""; - let first = this.props.LinkDocs[0]; - if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : ""); - else text = "-multiple-"; + // let first = this.props.LinkDocs[0]; + // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : ""); + // else text = "-multiple-"; return ( <> <line key="linkLine" className="collectionfreeformlinkview-linkLine" - style={{ strokeWidth: `${2 * l.length / 2}` }} + style={{ strokeWidth: `${2 * 1 / 2}` }} x1={`${x1}`} y1={`${y1}`} x2={`${x2}`} y2={`${y2}`} /> {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle" diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index c4dd534ed..ebeb1fcee 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -97,6 +97,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { let srcViews = this.documentAnchors(connection.a); let targetViews = this.documentAnchors(connection.b); + let possiblePairs: { a: Doc, b: Doc, }[] = []; srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document }))); possiblePairs.map(possiblePair => { @@ -109,7 +110,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP } return match || found; }, false)) { - drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }) + drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }); } }); return drawnPairs; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 63f24ff53..996032b1d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,6 +1,6 @@ import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc"; +import { Doc, DocListCastAsync, HeightSym, WidthSym } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; @@ -13,11 +13,13 @@ import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; +import { ContextMenu } from "../../ContextMenu"; import { InkingCanvas } from "../../InkingCanvas"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; import { pageSchema } from "../../nodes/ImageBox"; +import PDFMenu from "../../pdf/PDFMenu"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; @@ -45,7 +47,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed get nativeWidth() { return this.Document.nativeWidth || 0; } @computed get nativeHeight() { return this.Document.nativeHeight || 0; } - public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } + public get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } private panX = () => this.Document.panX || 0; private panY = () => this.Document.panY || 0; @@ -80,30 +82,45 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) { - if (de.data.droppedDocuments.length) { - let dragDoc = de.data.droppedDocuments[0]; - let zoom = NumCast(dragDoc.zoomBasis, 1); - let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); - let x = xp - de.data.xOffset / zoom; - let y = yp - de.data.yOffset / zoom; - let dropX = NumCast(de.data.droppedDocuments[0].x); - let dropY = NumCast(de.data.droppedDocuments[0].y); - de.data.droppedDocuments.forEach(d => { - d.x = x + NumCast(d.x) - dropX; - d.y = y + NumCast(d.y) - dropY; - if (!NumCast(d.width)) { - d.width = 300; - } - if (!NumCast(d.height)) { - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); - d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; - } - this.bringToFront(d); - }); + if (super.drop(e, de)) { + if (de.data instanceof DragManager.DocumentDragData) { + if (de.data.droppedDocuments.length) { + let dragDoc = de.data.droppedDocuments[0]; + let zoom = NumCast(dragDoc.zoomBasis, 1); + let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); + let x = xp - de.data.xOffset / zoom; + let y = yp - de.data.yOffset / zoom; + let dropX = NumCast(de.data.droppedDocuments[0].x); + let dropY = NumCast(de.data.droppedDocuments[0].y); + de.data.droppedDocuments.forEach(d => { + d.x = x + NumCast(d.x) - dropX; + d.y = y + NumCast(d.y) - dropY; + if (!NumCast(d.width)) { + d.width = 300; + } + if (!NumCast(d.height)) { + let nw = NumCast(d.nativeWidth); + let nh = NumCast(d.nativeHeight); + d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; + } + this.bringToFront(d); + }); + } + } + else if (de.data instanceof DragManager.AnnotationDragData) { + if (de.data.dropDocument) { + let dragDoc = de.data.dropDocument; + let zoom = NumCast(dragDoc.zoomBasis, 1); + let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); + let x = xp - de.data.xOffset / zoom; + let y = yp - de.data.yOffset / zoom; + let dropX = NumCast(de.data.dropDocument.x); + let dropY = NumCast(de.data.dropDocument.y); + dragDoc.x = x + NumCast(dragDoc.x) - dropX; + dragDoc.y = y + NumCast(dragDoc.y) - dropY; + this.bringToFront(dragDoc); + } } - return true; } return false; } @@ -133,6 +150,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let docs = this.childDocs || []; let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); if (!this.isAnnotationOverlay) { + PDFMenu.Instance.fadeOut(true); let minx = docs.length ? NumCast(docs[0].x) : 0; let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis, 1) + minx : minx; let miny = docs.length ? NumCast(docs[0].y) : 0; @@ -145,7 +163,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; }, [[minx, maxx], [miny, maxy]]); - let ink = Cast(this.props.Document.ink, InkField); + let ink = Cast(this.extensionDoc.ink, InkField); if (ink && ink.inkData) { ink.inkData.forEach((value: StrokeData, key: string) => { let bounds = InkingCanvas.StrokeRect(value); @@ -180,7 +198,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { var dv = DocumentManager.Instance.getDocumentView(doc); return dv && SelectionManager.IsSelected(dv) ? true : false; }); - if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) { + if (!this.props.isSelected() && !childSelected && this.props.renderDepth > 0) { return; } e.stopPropagation(); @@ -199,7 +217,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } else { // if (modes[e.deltaMode] === 'pixels') coefficient = 50; // else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height?? - let deltaScale = (1 - (e.deltaY / coefficient)); + let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1; if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { deltaScale = 1 / this.zoomScaling(); } @@ -244,7 +262,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { doc.zIndex = docs.length + 1; } - focusDocument = (doc: Doc) => { + focusDocument = (doc: Doc, willZoom: boolean) => { const panX = this.Document.panX; const panY = this.Document.panY; const id = this.Document[Id]; @@ -271,22 +289,59 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { newState.initializers[id] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); this.setPan(newPanX, newPanY); + this.props.Document.panTransformType = "Ease"; this.props.focus(this.props.Document); + if (willZoom) { + this.setScaleToZoom(doc); + } + + } + + setScaleToZoom = (doc: Doc) => { + let p = this.props; + let PanelHeight = p.PanelHeight(); + let panelWidth = p.PanelWidth(); + + let docHeight = NumCast(doc.height); + let docWidth = NumCast(doc.width); + let targetHeight = 0.5 * PanelHeight; + let targetWidth = 0.5 * panelWidth; + + let maxScaleX: number = targetWidth / docWidth; + let maxScaleY: number = targetHeight / docHeight; + let maxApplicableScale = Math.min(maxScaleX, maxScaleY); + this.Document.scale = maxApplicableScale; } + zoomToScale = (scale: number) => { + this.Document.scale = scale; + } - getDocumentViewProps(document: Doc): DocumentViewProps { + getScale = () => { + if (this.Document.scale) { + return this.Document.scale; + } + return 1; + } + + + getDocumentViewProps(layoutDoc: Doc): DocumentViewProps { + let datadoc = BoolCast(this.props.Document.isTemplate) || this.props.DataDoc === this.props.Document ? undefined : this.props.DataDoc; + if (Cast(layoutDoc.layout, Doc) instanceof Doc) { // if this document is using a template to render, then set the dataDoc for the template to be this document + datadoc = layoutDoc; + } return { - Document: document, + DataDoc: datadoc, + Document: layoutDoc, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, ScreenToLocalTransform: this.getTransform, - isTopMost: false, - selectOnLoad: document[Id] === this._selectOnLoaded, - PanelWidth: document[WidthSym], - PanelHeight: document[HeightSym], + renderDepth: this.props.renderDepth + 1, + selectOnLoad: layoutDoc[Id] === this._selectOnLoaded, + PanelWidth: layoutDoc[WidthSym], + PanelHeight: layoutDoc[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, focus: this.focusDocument, @@ -294,6 +349,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, addDocTab: this.props.addDocTab, + zoomToScale: this.zoomToScale, + getScale: this.getScale }; } @@ -322,17 +379,45 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } + onContextMenu = () => { + ContextMenu.Instance.addItem({ + description: "Arrange contents in grid", + event: async () => { + const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); + if (docs) { + let startX = this.Document.panX || 0; + let x = startX; + let y = this.Document.panY || 0; + let i = 0; + const width = Math.max(...docs.map(doc => NumCast(doc.width))); + const height = Math.max(...docs.map(doc => NumCast(doc.height))); + for (const doc of docs) { + doc.x = x; + doc.y = y; + x += width + 20; + if (++i === 6) { + i = 0; + x = startX; + y += height + 20; + } + } + } + } + }); + } + private childViews = () => [ - <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, + <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} DataDoc={this.props.DataDoc} />, ...this.views - ]; + ] render() { const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; const easing = () => this.props.Document.panTransformType === "Ease"; + if (this.props.fieldExt) Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey); return ( <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} style={{ borderRadius: "inherit" }} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} > + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onContextMenu={this.onContextMenu}> <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected} addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> @@ -340,7 +425,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> - <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} > + <InkingCanvas getScreenTransform={this.getTransform} Document={this.extensionDoc} inkFieldKey={this.props.fieldExt ? "ink" : this.props.fieldKey + "_ink"} > {this.childViews} </InkingCanvas> </CollectionFreeFormLinksView> @@ -357,7 +442,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { @computed get overlayView() { return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"} - isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); + renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); } render() { return this.overlayView; @@ -367,8 +452,9 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & @observer class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { @computed get backgroundView() { + let props = this.props; return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} - isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); + renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); } render() { return this.props.Document.backgroundLayout ? this.backgroundView : (null); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 5f2a732b9..2a78cda2f 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -14,7 +14,6 @@ import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { InkingCanvas } from "../../InkingCanvas"; import { PreviewCursor } from "../../PreviewCursor"; -import { SearchBox } from "../../SearchBox"; import { Templates } from "../../Templates"; import { CollectionViewType } from "../CollectionBaseView"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; @@ -95,7 +94,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } }); } else if (!e.ctrlKey) { - let newBox = Docs.TextDocument({ width: 200, height: 30, x: x, y: y, title: "-typed text-" }); + let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); newBox.proto!.autoHeight = true; this.props.addLiveTextDocument(newBox); } @@ -184,9 +183,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @action onPointerUp = (e: PointerEvent): void => { - console.log("pointer up!"); + // console.log("pointer up!"); if (this._visible) { - console.log("visible"); + // console.log("visible"); let mselect = this.marqueeSelect(); if (!e.shiftKey) { SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); @@ -195,7 +194,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> mselect.length ? this.cleanupInteractions(true, false) : this.cleanupInteractions(true); } else { - console.log("invisible"); + //console.log("invisible"); this.cleanupInteractions(true); } @@ -275,13 +274,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps> panY: 0, borderRounding: e.key === "e" ? -1 : undefined, backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white", - scale: zoomBasis, - width: bounds.width * zoomBasis, - height: bounds.height * zoomBasis, + width: bounds.width, + height: bounds.height, ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined, title: e.key === "s" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection", }); - newCollection.zoomBasis = zoomBasis; this.marqueeInkDelete(inkData); if (e.key === "s") { @@ -304,35 +301,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.props.addLiveTextDocument(container); // }); } else if (e.key === "S") { - await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 0.2 }).then((dataUrl) => { - selected.map(d => { - this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.page = -1; - return d; - }); - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); - SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => { - if (returnedFilename) { - let url = DocServer.prepend(returnedFilename); - let imageSummary = Docs.ImageDocument(url, { - x: bounds.left, y: bounds.top + 100 / zoomBasis, - width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-" - }); - summary.imageSummary = imageSummary; - this.props.addDocument(imageSummary, false); - } - }) - newCollection.proto!.summaryDoc = summary; - selected = [newCollection]; - newCollection.x = bounds.left + bounds.width; - //this.props.addDocument(newCollection, false); - summary.proto!.summarizedDocs = new List<Doc>(selected); - summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" - - this.props.addLiveTextDocument(summary); + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.page = -1; + return d; }); + let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + newCollection.proto!.summaryDoc = summary; + selected = [newCollection]; + newCollection.x = bounds.left + bounds.width; + //this.props.addDocument(newCollection, false); + summary.proto!.summarizedDocs = new List<Doc>(selected); + summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" + + this.props.addLiveTextDocument(summary); } else { this.props.addDocument(newCollection, false); 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/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 499b83c0f..858959d81 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,17 +1,12 @@ -import { computed, IReactionDisposer, reaction, action } from "mobx"; +import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; -import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types"; -import { OmitKeys } from "../../../Utils"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { BoolCast, FieldValue, NumCast } from "../../../new_fields/Types"; import { Transform } from "../../util/Transform"; import { DocComponent } from "../DocComponent"; import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); -import { UndoManager } from "../../util/UndoManager"; -import { SelectionManager } from "../../util/SelectionManager"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { } @@ -27,13 +22,7 @@ const FreeformDocument = makeInterface(schema, positionSchema); @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) { - private _mainCont = React.createRef<HTMLDivElement>(); - _bringToFrontDisposer?: IReactionDisposer; - - @computed get transform() { - return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `; - } - + @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}) `; } @computed get X() { return FieldValue(this.Document.x, 0); } @computed get Y() { return FieldValue(this.Document.y, 0); } @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); } @@ -61,89 +50,18 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF .translate(-this.X, -this.Y) .scale(1 / this.contentScaling()).scale(1 / this.zoom) - @computed - get docView() { - return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit} - ContentScaling={this.contentScaling} - ScreenToLocalTransform={this.getTransform} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - collapseToPoint={this.collapseToPoint} - />; - } - - componentDidMount() { - this._bringToFrontDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => { - this.props.bringToFront(this.props.Document); - if (values instanceof List) { - let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]); - this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0], - this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false); - } - }, { fireImmediately: true }); - } - - componentWillUnmount() { - if (this._bringToFrontDisposer) this._bringToFrontDisposer(); - } - - static _undoBatch?: UndoManager.Batch = undefined; - @action - public collapseToPoint = async (scrpt: number[], expandedDocs: Doc[] | undefined): Promise<void> => { - SelectionManager.DeselectAll(); - if (expandedDocs) { - if (!CollectionFreeFormDocumentView._undoBatch) { - CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); - } - let isMinimized: boolean | undefined; - expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { - let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); - if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { - if (isMinimized === undefined) { - isMinimized = BoolCast(maximizedDoc.isMinimized, false); - } - maximizedDoc.willMaximize = isMinimized; - maximizedDoc.isMinimized = false; - maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); - } + animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => { + this.props.bringToFront(this.props.Document); + let targetPos = [this.Document.x || 0, this.Document.y || 0]; + let iconPos = this.props.ScreenToLocalTransform().transformPoint(icon[0], icon[1]); + DocumentView.animateBetweenIconFunc(this.props.Document, + this.Document.width || 0, this.Document.height || 0, stime, maximizing, (progress: number) => { + let pval = maximizing ? + [iconPos[0] + (targetPos[0] - iconPos[0]) * progress, iconPos[1] + (targetPos[1] - iconPos[1]) * progress] : + [targetPos[0] + (iconPos[0] - targetPos[0]) * progress, targetPos[1] + (iconPos[1] - targetPos[1]) * progress]; + this.Document.x = progress === 1 ? targetPos[0] : pval[0]; + this.Document.y = progress === 1 ? targetPos[1] : pval[1]; }); - setTimeout(() => { - CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end(); - CollectionFreeFormDocumentView._undoBatch = undefined; - }, 500); - } - } - - animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) { - - setTimeout(() => { - let now = Date.now(); - let progress = Math.min(1, (now - stime) / 200); - let pval = maximizing ? - [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] : - [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress]; - this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; - this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; - this.props.Document.x = pval[0]; - this.props.Document.y = pval[1]; - if (first) { - this.props.Document.proto!.willMaximize = false; - } - if (now < stime + 200) { - this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing); - } - else { - if (!maximizing) { - this.props.Document.proto!.isMinimized = true; - this.props.Document.x = targ[0]; - this.props.Document.y = targ[1]; - this.props.Document.width = width; - this.props.Document.height = height; - } - this.props.Document.proto!.isIconAnimating = undefined; - } - }, - 2); } borderRounding = () => { @@ -155,34 +73,25 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } render() { - let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDocs, listSpec(Doc))); - let zoomFade = 1; - //var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1); - // let transform = this.getTransform().scale(this.contentScaling()).inverse(); - // var [sptX, sptY] = transform.transformPoint(0, 0); - // let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight()); - // let w = bptX - sptX; - //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1; - const screenWidth = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800); - let fadeUp = .75 * screenWidth; - let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth; - // zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1; - return ( - <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} + <div className="collectionFreeFormDocumentView-container" style={{ - opacity: zoomFade, - borderRadius: `${this.borderRounding()}px`, transformOrigin: "left top", + position: "absolute", + backgroundColor: "transparent", + borderRadius: `${this.borderRounding()}px`, transform: this.transform, - pointerEvents: (zoomFade < 0.09 ? "none" : "all"), width: this.width, height: this.height, - position: "absolute", zIndex: this.Document.zIndex || 0, - backgroundColor: "transparent" }} > - {this.docView} + <DocumentView {...this.props} + ContentScaling={this.contentScaling} + ScreenToLocalTransform={this.getTransform} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + animateBetweenIcon={this.animateBetweenIcon} + /> </div> ); } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 02396c3af..0da4888a1 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -23,6 +23,7 @@ import { FieldViewProps } from "./FieldView"; import { Without, OmitKeys } from "../../../Utils"; import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; +import { Doc } from "../../../new_fields/Doc"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -47,11 +48,12 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { hideOnLeave?: boolean }> { @computed get layout(): string { - const layout = Cast(this.props.Document[this.props.layoutKey], "string"); + let layoutDoc = this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; + const layout = Cast(layoutDoc[this.props.layoutKey], "string"); if (layout === undefined) { return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : - KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : ""); + KeyValueBox.LayoutString(layoutDoc.proto ? "proto" : ""); } else if (typeof layout === "string") { return layout; } else { @@ -59,8 +61,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { } } - CreateBindings(): JsxBindings { - return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit }; + CreateBindings(layoutDoc?: Doc): JsxBindings { + return { props: { ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, Document: layoutDoc } }; } @computed get templates(): List<string> { @@ -101,10 +103,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { } render() { + if (this.props.renderDepth > 7) return (null); if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); return <ObserverJsxParser components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} - bindings={this.CreateBindings()} + bindings={this.CreateBindings(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document)} jsx={this.finalLayout} showWarnings={true} onError={(test: any) => { console.log(test); }} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 7c72fb6e6..3a4b46b7e 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -4,6 +4,7 @@ position: inherit; top: 0; left:0; + pointer-events: all; // background: $light-color; //overflow: hidden; transform-origin: left top; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 6fe01963a..4ac43ef4d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,11 +1,11 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash, faConciergeBell, faFolder, faMapPin, faLink, faFingerprint, faCrosshairs, faDesktop } from '@fortawesome/free-solid-svg-icons'; -import { action, computed, IReactionDisposer, reaction, trace, observable } from "mobx"; +import * as fa from '@fortawesome/free-solid-svg-icons'; +import { action, computed, IReactionDisposer, reaction, trace, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { ObjectField } from "../../../new_fields/ObjectField"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; import { BoolCast, Cast, FieldValue, StrCast, NumCast, PromiseValue } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { emptyFunction, Utils } from "../../../Utils"; @@ -23,59 +23,72 @@ import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { DocComponent } from "../DocComponent"; -import { PresentationView } from "../PresentationView"; +import { PresentationView } from "../presentationview/PresentationView"; import { Template } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; +import * as rp from "request-promise"; import "./DocumentView.scss"; import React = require("react"); import { Id, Copy } from '../../../new_fields/FieldSymbols'; import { ContextMenuProps } from '../ContextMenuItem'; +import { list, object, createSimpleSchema } from 'serializr'; +import { LinkManager } from '../../util/LinkManager'; +import { RouteStore } from '../../../server/RouteStore'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? -library.add(faTrash); -library.add(faExpandArrowsAlt); -library.add(faCompressArrowsAlt); -library.add(faLayerGroup); -library.add(faAlignCenter); -library.add(faCaretSquareRight); -library.add(faSquare); -library.add(faConciergeBell); -library.add(faFolder); -library.add(faMapPin); -library.add(faLink); -library.add(faFingerprint); -library.add(faCrosshairs); -library.add(faDesktop); - -const linkSchema = createSchema({ - title: "string", - linkDescription: "string", - linkTags: "string", - linkedTo: Doc, - linkedFrom: Doc -}); - -type LinkDoc = makeInterface<[typeof linkSchema]>; -const LinkDoc = makeInterface(linkSchema); +library.add(fa.faTrash); +library.add(fa.faShare); +library.add(fa.faExpandArrowsAlt); +library.add(fa.faCompressArrowsAlt); +library.add(fa.faLayerGroup); +library.add(fa.faExternalLinkAlt); +library.add(fa.faAlignCenter); +library.add(fa.faCaretSquareRight); +library.add(fa.faSquare); +library.add(fa.faConciergeBell); +library.add(fa.faFolder); +library.add(fa.faMapPin); +library.add(fa.faLink); +library.add(fa.faFingerprint); +library.add(fa.faCrosshairs); +library.add(fa.faDesktop); +library.add(fa.faUnlock); +library.add(fa.faLock); + + +// const linkSchema = createSchema({ +// title: "string", +// linkDescription: "string", +// linkTags: "string", +// linkedTo: Doc, +// linkedFrom: Doc +// }); + +// type LinkDoc = makeInterface<[typeof linkSchema]>; +// const LinkDoc = makeInterface(linkSchema); export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Doc; + DataDoc?: Doc; addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean; removeDocument?: (doc: Doc) => boolean; moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; - isTopMost: boolean; + renderDepth: number; ContentScaling: () => number; PanelWidth: () => number; PanelHeight: () => number; - focus: (doc: Doc) => void; + focus: (doc: Doc, willZoom: boolean) => void; selectOnLoad: boolean; parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc) => void; - addDocTab: (doc: Doc, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void; collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void; + zoomToScale: (scale: number) => void; + getScale: () => number; + animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void; } const schema = createSchema({ @@ -83,6 +96,8 @@ const schema = createSchema({ nativeWidth: "number", nativeHeight: "number", backgroundColor: "string", + opacity: "number", + hidden: "boolean" }); export const positionSchema = createSchema({ @@ -112,7 +127,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu public get ContentDiv() { return this._mainCont.current; } @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); } - @computed get topMost(): boolean { return this.props.isTopMost; } + @computed get topMost(): boolean { return this.props.renderDepth === 0; } @computed get templates(): List<string> { let field = this.props.Document.templates; if (field && field instanceof List) { @@ -127,6 +142,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu super(props); } + _animateToIconDisposer?: IReactionDisposer; _reactionDisposer?: IReactionDisposer; @action componentDidMount() { @@ -148,8 +164,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded"; } }, { fireImmediately: true }); + this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => + (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false) + , { fireImmediately: true }); DocumentManager.Instance.DocumentViews.push(this); } + + animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => { + this.props.animateBetweenIcon ? this.props.animateBetweenIcon(iconPos, startTime, maximizing) : + DocumentView.animateBetweenIconFunc(this.props.Document, this.Document[WidthSym](), this.Document[HeightSym](), startTime, maximizing); + } + + public static animateBetweenIconFunc = (doc: Doc, width: number, height: number, stime: number, maximizing: boolean, cb?: (progress: number) => void) => { + setTimeout(() => { + let now = Date.now(); + let progress = now < stime + 200 ? Math.min(1, (now - stime) / 200) : 1; + doc.width = progress === 1 ? width : maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; + doc.height = progress === 1 ? height : maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; + cb && cb(progress); + if (now < stime + 200) { + DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb); + } + else { + Doc.GetProto(doc).isMinimized = !maximizing; + Doc.GetProto(doc).isIconAnimating = undefined; + } + Doc.GetProto(doc).willMaximize = false; + }, + 2); + } @action componentDidUpdate() { if (this._dropDisposer) { @@ -164,6 +207,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @action componentWillUnmount() { if (this._reactionDisposer) this._reactionDisposer(); + if (this._animateToIconDisposer) this._animateToIconDisposer(); if (this._dropDisposer) this._dropDisposer(); DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); } @@ -172,11 +216,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); } + get dataDoc() { return this.props.DataDoc ? this.props.DataDoc : this.props.Document; } startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) { if (this._mainCont.current) { let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; + let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); - let dragData = new DragManager.DocumentDragData(allConnected); + let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected); const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; dragData.xOffset = xoff; @@ -195,7 +241,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (minimizedDoc) { let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint( NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y)); - this.props.collapseToPoint && this.props.collapseToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs)); + this.collapseTargetsToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs)); + } + } + + static _undoBatch?: UndoManager.Batch = undefined; + @action + public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { + SelectionManager.DeselectAll(); + if (expandedDocs) { + if (!DocumentView._undoBatch) { + DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); + } + let isMinimized: boolean | undefined; + expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { + let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); + if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { + if (isMinimized === undefined) { + isMinimized = BoolCast(maximizedDoc.isMinimized, false); + } + maximizedDoc.willMaximize = isMinimized; + maximizedDoc.isMinimized = false; + maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); + } + }); + setTimeout(() => { + DocumentView._undoBatch && DocumentView._undoBatch.end(); + DocumentView._undoBatch = undefined; + }, 500); } } @@ -203,8 +276,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); let altKey = e.altKey; let ctrlKey = e.ctrlKey; - if (this._doubleTap && !this.props.isTopMost) { - this.props.addDocTab(this.props.Document, "inTab"); + if (this._doubleTap && this.props.renderDepth) { + let fullScreenAlias = Doc.MakeAlias(this.props.Document); + fullScreenAlias.templates = new List<string>(); + this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab"); SelectionManager.DeselectAll(); this.props.Document.libraryBrush = false; } @@ -218,8 +293,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs); let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); - let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []); - let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []); + let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); let expandedDocs: Doc[] = []; expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs; expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; @@ -229,8 +303,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let expandedProtoDocs = expandedDocs.map(doc => Doc.GetProto(doc)); let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace"); let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target); - if (altKey) { - maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace"); + if (altKey || ctrlKey) { + maxLocation = this.props.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace")); if (!maxLocation || maxLocation === "inPlace") { let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView); let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized, false), false); @@ -247,31 +321,38 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (dataDocs) { expandedDocs.forEach(maxDoc => (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) && - this.props.addDocTab(getDispDoc(maxDoc), maxLocation))); + this.props.addDocTab(getDispDoc(maxDoc), getDispDoc(maxDoc), maxLocation))); } } else { let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); - this.props.collapseToPoint && this.props.collapseToPoint(scrpt, expandedProtoDocs); + this.collapseTargetsToPoint(scrpt, expandedProtoDocs); } } - else if (linkedToDocs.length || linkedFromDocs.length) { - let linkedFwdDocs = [ - linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], - linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; - - let linkedFwdContextDocs = [ - linkedToDocs.length ? await (linkedToDocs[0].linkedToContext) as Doc : linkedFromDocs.length ? await PromiseValue(linkedFromDocs[0].linkedFromContext) as Doc : undefined, - linkedFromDocs.length ? await (linkedFromDocs[0].linkedFromContext) as Doc : linkedToDocs.length ? await PromiseValue(linkedToDocs[0].linkedToContext) as Doc : undefined]; - - let linkedFwdPage = [ - linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined, - linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined]; - - if (!linkedFwdDocs.some(l => l instanceof Promise)) { - let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab"); - let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); - } + else if (linkedDocs.length) { + let linkedDoc = linkedDocs.length ? linkedDocs[0] : expandedDocs[0]; + let linkedPages = [linkedDocs.length ? NumCast(linkedDocs[0].anchor1Page, undefined) : NumCast(linkedDocs[0].anchor2Page, undefined), + linkedDocs.length ? NumCast(linkedDocs[0].anchor2Page, undefined) : NumCast(linkedDocs[0].anchor1Page, undefined)]; + let maxLocation = StrCast(linkedDoc.maximizeLocation, "inTab"); + DocumentManager.Instance.jumpToDocument(linkedDoc, ctrlKey, false, document => this.props.addDocTab(document, document, maxLocation), linkedPages[altKey ? 1 : 0]); + + // else if (linkedToDocs.length || linkedFromDocs.length) { + // let linkedFwdDocs = [ + // linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], + // linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; + + // let linkedFwdContextDocs = [ + // linkedToDocs.length ? await (linkedToDocs[0].linkedToContext) as Doc : linkedFromDocs.length ? await PromiseValue(linkedFromDocs[0].linkedFromContext) as Doc : undefined, + // linkedFromDocs.length ? await (linkedFromDocs[0].linkedFromContext) as Doc : linkedToDocs.length ? await PromiseValue(linkedToDocs[0].linkedToContext) as Doc : undefined]; + + // let linkedFwdPage = [ + // linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined, + // linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined]; + + // if (!linkedFwdDocs.some(l => l instanceof Promise)) { + // let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab"); + // let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; + // DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, document => this.props.addDocTab(document, document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); + // } } } } @@ -281,7 +362,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._downY = e.clientY; this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) { - CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); + CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.dataDoc]); e.stopPropagation(); } else { if (this.active) e.stopPropagation(); // events stop at the lowest document that is active. @@ -296,7 +377,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - if (!e.altKey && !this.topMost && e.buttons === 1) { + if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) { this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander); } } @@ -311,8 +392,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._lastTap = Date.now(); } - deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); } - fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") }; + deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }; + fieldsClicked = (): void => { let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }; makeBtnClicked = (): void => { let doc = Doc.GetProto(this.props.Document); doc.isButton = !BoolCast(doc.isButton, false); @@ -326,7 +407,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } fullScreenClicked = (): void => { - CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeCopy(this.props.Document, false)); + CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeAlias(this.props.Document), this.dataDoc); SelectionManager.DeselectAll(); } @@ -385,7 +466,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.templates = this.templates; } - freezeNativeDimensions = (e: React.MouseEvent): void => { + freezeNativeDimensions = (): void => { let proto = Doc.GetProto(this.props.Document); if (proto.ignoreAspect === undefined && !proto.nativeWidth) { proto.nativeWidth = this.props.PanelWidth(); @@ -396,7 +477,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @action - onContextMenu = (e: React.MouseEvent): void => { + onContextMenu = async (e: React.MouseEvent): Promise<void> => { + e.persist(); e.stopPropagation(); if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || e.isDefaultPrevented()) { @@ -408,40 +490,64 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const cm = ContextMenu.Instance; let subitems: ContextMenuProps[] = []; subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" }); - subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" }); - subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "onRight"), icon: "caret-square-right" }); - subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" }); subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); - cm.addItem({ description: "Open...", subitems: subitems }); + cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "edit" }); cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" }); + cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Pos" : "Lock Pos", event: () => this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" }); cm.addItem({ description: "Find aliases", event: async () => { const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); - this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight"); + this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), Docs.SchemaDocument(["title"], aliases, {}), "onRight"); // bcz: dataDoc? }, icon: "search" }); - cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" }); + cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); - if (!this.topMost) { - // DocumentViews should stop propagation of this event - e.stopPropagation(); - } - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - if (!SelectionManager.IsSelected(this)) { - SelectionManager.SelectDoc(this, false); - } + type User = { email: string, userDocumentId: string }; + const users: User[] = JSON.parse(await rp.get(DocServer.prepend(RouteStore.getUsers))); + let usersMenu: ContextMenuProps[] = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({ + description: email, event: async () => { + const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc); + if (!userDocument) { + throw new Error(`Couldn't get user document of user ${email}`); + } + const notifDoc = await Cast(userDocument.optionalRightCollection, Doc); + if (notifDoc instanceof Doc) { + const data = await Cast(notifDoc.data, listSpec(Doc)); + const sharedDoc = Doc.MakeAlias(this.props.Document); + if (data) { + data.push(sharedDoc); + } else { + notifDoc.data = new List([sharedDoc]); + } + } + } + })); + runInAction(() => { + cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" }); + if (!this.topMost) { + // DocumentViews should stop propagation of this event + e.stopPropagation(); + } + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + if (!SelectionManager.IsSelected(this)) { + SelectionManager.SelectDoc(this, false); + } + }); } onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }; onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }; isSelected = () => SelectionManager.IsSelected(this); - @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); } + @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; @computed get nativeWidth() { return this.Document.nativeWidth || 0; } @computed get nativeHeight() { return this.Document.nativeHeight || 0; } @@ -451,27 +557,28 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } render() { - var scaling = this.props.ContentScaling(); - var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; + if (this.Document.hidden) { + return null; + } + let backgroundColor = this.props.Document.layout instanceof Doc ? StrCast(this.props.Document.layout.backgroundColor) : this.Document.backgroundColor; var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%"; + var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; return ( - <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`} + <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} style={{ outlineColor: "maroon", outlineStyle: "dashed", - outlineWidth: BoolCast(this.props.Document.libraryBrush, false) || - BoolCast(this.props.Document.protoBrush, false) ? - `${1 * this.props.ScreenToLocalTransform().Scale}px` - : "0px", + outlineWidth: BoolCast(this.props.Document.libraryBrush) || BoolCast(this.props.Document.protoBrush) ? + `${this.props.ScreenToLocalTransform().Scale}px` : "0px", borderRadius: "inherit", - background: this.Document.backgroundColor || "", + background: backgroundColor, width: nativeWidth, height: nativeHeight, - transform: `scale(${scaling}, ${scaling})` + transform: `scale(${this.props.ContentScaling()})`, + opacity: this.Document.opacity }} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} > {this.contents} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 5a83de8e3..55f61ddff 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -7,17 +7,17 @@ import { IconField } from "../../../new_fields/IconField"; import { List } from "../../../new_fields/List"; import { RichTextField } from "../../../new_fields/RichTextField"; import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField"; -import { emptyFunction, returnFalse, returnOne } from "../../../Utils"; import { Transform } from "../../util/Transform"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { AudioBox } from "./AudioBox"; -import { DocumentContentsView } from "./DocumentContentsView"; import { FormattedTextBox } from "./FormattedTextBox"; import { IconBox } from "./IconBox"; import { ImageBox } from "./ImageBox"; +import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; +import { Id } from "../../../new_fields/FieldSymbols"; // @@ -27,14 +27,16 @@ import { VideoBox } from "./VideoBox"; // export interface FieldViewProps { fieldKey: string; + fieldExt: string; ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Doc; + DataDoc?: Doc; isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; - isTopMost: boolean; + renderDepth: number; selectOnLoad: boolean; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; - addDocTab: (document: Doc, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc, where: string) => void; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; @@ -44,6 +46,7 @@ export interface FieldViewProps { PanelWidth: () => number; PanelHeight: () => number; setVideoBox?: (player: VideoBox) => void; + setPdfBox?: (player: PDFBox) => void; } @observer @@ -83,31 +86,32 @@ export class FieldView extends React.Component<FieldViewProps> { return <p>{field.date.toLocaleString()}</p>; } else if (field instanceof Doc) { - let returnHundred = () => 100; - return ( - <DocumentContentsView Document={field} - addDocument={undefined} - addDocTab={this.props.addDocTab} - removeDocument={undefined} - ScreenToLocalTransform={Transform.Identity} - ContentScaling={returnOne} - PanelWidth={returnHundred} - PanelHeight={returnHundred} - isTopMost={true} //TODO Why is this top most? - selectOnLoad={false} - focus={emptyFunction} - isSelected={this.props.isSelected} - select={returnFalse} - layoutKey={"layout"} - ContainingCollectionView={this.props.ContainingCollectionView} - parentActive={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - bringToFront={emptyFunction} /> - ); + return <p><b>{field.title + " + " + field[Id]}</b></p>; + // let returnHundred = () => 100; + // return ( + // <DocumentContentsView Document={field} + // addDocument={undefined} + // addDocTab={this.props.addDocTab} + // removeDocument={undefined} + // ScreenToLocalTransform={Transform.Identity} + // ContentScaling={returnOne} + // PanelWidth={returnHundred} + // PanelHeight={returnHundred} + // renderDepth={0} //TODO Why is renderDepth reset? + // selectOnLoad={false} + // focus={emptyFunction} + // isSelected={this.props.isSelected} + // select={returnFalse} + // layoutKey={"layout"} + // ContainingCollectionView={this.props.ContainingCollectionView} + // parentActive={this.props.active} + // whenActiveChanged={this.props.whenActiveChanged} + // bringToFront={emptyFunction} /> + // ); } else if (field instanceof List) { return (<div> - {field.map(f => f instanceof Doc ? f.title : f.toString()).join(", ")} + {field.map(f => f instanceof Doc ? f.title : (f && f.toString && f.toString())).join(", ")} </div>); } // bcz: this belongs here, but it doesn't render well so taking it out for now diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index e3e40860c..d3045ae2f 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -1,7 +1,7 @@ @import "../globalCssVariables"; .ProseMirror { width: 100%; - height: 100; + height: 100%; min-height: 100%; font-family: $serif; } diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index e5a43c60a..2a45aeb43 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction, computed } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -10,11 +10,13 @@ import { EditorState, Plugin, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc, Opt } from "../../../new_fields/Doc"; import { Id } from '../../../new_fields/FieldSymbols'; +import { List } from '../../../new_fields/List'; import { RichTextField } from "../../../new_fields/RichTextField"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; +import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from "../../util/DragManager"; import buildKeymap from "../../util/ProsemirrorExampleTransfer"; import { inpRules } from "../../util/RichTextRules"; @@ -27,6 +29,7 @@ import { ContextMenu } from "../../views/ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; +import { Templates } from '../Templates'; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); @@ -42,6 +45,7 @@ export interface FormattedTextBoxProps { hideOnLeave?: boolean; height?: string; color?: string; + outer_div?: (domminus: HTMLElement) => void; } const richTextSchema = createSchema({ @@ -57,6 +61,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return FieldView.LayoutString(FormattedTextBox, fieldStr); } private _ref: React.RefObject<HTMLDivElement>; + private _outerdiv?: (dominus: HTMLElement) => void; private _proseRef?: HTMLDivElement; private _editorView: Opt<EditorView>; private _toolTipTextMenu: TooltipTextMenu | undefined = undefined; @@ -94,6 +99,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe constructor(props: FieldViewProps) { super(props); + if (this.props.outer_div) { + this._outerdiv = this.props.outer_div; + } this._ref = React.createRef(); if (this.props.isOverlay) { @@ -101,19 +109,21 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + dispatchTransaction = (tx: Transaction) => { if (this._editorView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); this._applyingChange = true; - Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON()))); - Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n")); + Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); + Doc.GetProto(this.dataDoc)[this.props.fieldKey + "_text"] = state.doc.textBetween(0, state.doc.content.size, "\n\n"); this._applyingChange = false; - let title = StrCast(this.props.Document.title); + let title = StrCast(this.dataDoc.title); if (title && title.startsWith("-") && this._editorView) { let str = this._editorView.state.doc.textContent; let titlestr = str.substr(0, Math.min(40, str.length)); - let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc; target.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } } @@ -139,6 +149,28 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image; this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url }))); e.stopPropagation(); + } else { + if (de.data instanceof DragManager.DocumentDragData) { + let ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc)); + if (!ldocs) { + this.dataDoc.subBulletDocs = new List<Doc>([]); + } + ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc)); + if (!ldocs) return; + if (!ldocs || !ldocs[0] || ldocs[0] instanceof Promise || StrCast((ldocs[0] as Doc).layout).indexOf("CollectionView") === -1) { + ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.dataDoc.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 })); + this.props.addDocument && this.props.addDocument(ldocs[0] as Doc); + this.props.Document.templates = new List<string>([Templates.Bullet.Layout]); + this.props.Document.isBullet = true; + } + let stackDoc = (ldocs[0] as Doc); + if (de.data.moveDocument) { + de.data.moveDocument(de.data.draggedDocuments[0], stackDoc, (doc) => { + Cast(stackDoc.data, listSpec(Doc))!.push(doc); + return true; + }); + } + } } } @@ -171,26 +203,31 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe FormattedTextBox.InputBoxOverlay = this; FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop; } - }); + }, { fireImmediately: true }); } this._reactionDisposer = reaction( () => { - const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined; + const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; }, field => this._editorView && !this._applyingChange && this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))) ); - this.setupEditor(config, this.props.Document, this.props.fieldKey); + this.setupEditor(config, this.dataDoc, this.props.fieldKey); } private setupEditor(config: any, doc: Doc, fieldKey: string) { let field = doc ? Cast(doc[fieldKey], RichTextField) : undefined; let startup = StrCast(doc.documentText); startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : ""; - if (!startup && !field && doc) { - startup = StrCast(doc[fieldKey]); + if (!field && doc) { + let text = StrCast(doc[fieldKey]); + if (text) { + startup = text; + } else if (Cast(doc[fieldKey], "number")) { + startup = NumCast(doc[fieldKey], 99).toString(); + } } if (this._proseRef) { this._editorView = new EditorView(this._proseRef, { @@ -229,7 +266,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) { - this._toolTipTextMenu.tooltip.style.opacity = "0"; + //this._toolTipTextMenu.tooltip.style.opacity = "0"; } } let ctrlKey = e.ctrlKey; @@ -241,6 +278,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (href) { if (href.indexOf(DocServer.prepend("/doc/")) === 0) { this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; + if (this._linkClicked) { + DocServer.GetRefField(this._linkClicked).then(f => { + (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, false, document => this.props.addDocTab(document, document, "inTab")); + }); + e.stopPropagation(); + e.preventDefault(); + } } else { let webDoc = Docs.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) }); this.props.addDocument && this.props.addDocument(webDoc); @@ -257,7 +301,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } onPointerUp = (e: React.PointerEvent): void => { if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) { - this._toolTipTextMenu.tooltip.style.opacity = "1"; + //this._toolTipTextMenu.tooltip.style.opacity = "1"; } if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); @@ -283,6 +327,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe onClick = (e: React.MouseEvent): void => { this._proseRef!.focus(); if (this._linkClicked) { + this._linkClicked = ""; e.preventDefault(); e.stopPropagation(); } @@ -301,6 +346,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); } }); + //this.props.Document.tooltip = self._toolTipTextMenu; } tooltipLinkingMenuPlugin() { @@ -327,10 +373,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe // stop propagation doesn't seem to stop propagation of native keyboard events. // so we set a flag on the native event that marks that the event's been handled. (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; - if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) { + if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView) { let str = this._editorView.state.doc.textContent; let titlestr = str.substr(0, Math.min(40, str.length)); - let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc; target.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } if (!this._undoTyping) { @@ -339,11 +385,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this.props.isOverlay && this.props.Document.autoHeight) { let xf = this._ref.current!.getBoundingClientRect(); let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height); - let nh = NumCast(this.props.Document.nativeHeight, 0); + let nh = NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); let sh = scrBounds.height; this.props.Document.height = nh ? dh / nh * sh : sh; - this.props.Document.proto!.nativeHeight = nh ? sh : undefined; + this.dataDoc.proto!.nativeHeight = nh ? sh : undefined; } } @@ -376,6 +422,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "initial", pointerEvents: interactive ? "all" : "none", + fontSize: "13px" }} onKeyDown={this.onKeyPress} onFocus={this.onFocused} diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 00021bc78..d6ab2a34a 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -37,14 +37,14 @@ export class IconBox extends React.Component<FieldViewProps> { return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; } - setLabelField = (e: React.MouseEvent): void => { + setLabelField = (): void => { this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel); } - setUseOwnTitleField = (e: React.MouseEvent): void => { + setUseOwnTitleField = (): void => { this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle); } - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { ContextMenu.Instance.addItem({ description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon", event: this.setLabelField diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0d19508fa..5c00d9d44 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,13 +1,13 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faImage } from '@fortawesome/free-solid-svg-icons'; -import { action, observable } from 'mobx'; +import { action, observable, computed } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { Doc, HeightSym, WidthSym, DocListCast } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; -import { Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types'; +import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types'; import { ImageField } from '../../../new_fields/URLField'; import { Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; @@ -36,7 +36,7 @@ const ImageDocument = makeInterface(pageSchema, positionSchema); @observer export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) { - public static LayoutString() { return FieldView.LayoutString(ImageBox); } + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _downX: number = 0; private _downY: number = 0; @@ -46,6 +46,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD private dropDisposer?: DragManager.DragDropDisposer; + @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + protected createDropTarget = (ele: HTMLDivElement) => { if (this.dropDisposer) { @@ -61,24 +63,28 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD console.log("IMPLEMENT ME PLEASE"); } + @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "Alternates"); } @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.DocumentDragData) { de.data.droppedDocuments.forEach(action((drop: Doc) => { - let layout = StrCast(drop.backgroundLayout); - if (layout.indexOf(ImageBox.name) !== -1) { - let imgData = this.props.Document[this.props.fieldKey]; - if (imgData instanceof ImageField) { - Doc.SetOnPrototype(this.props.Document, "data", new List([imgData])); - } - let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]); - if (imgList) { - let field = drop.data; - if (field instanceof ImageField) imgList.push(field); - else if (field instanceof List) imgList.concat(field); - } + if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) { + Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url); e.stopPropagation(); + } else { + if (this.extensionDoc !== this.dataDoc) { + let layout = StrCast(drop.backgroundLayout); + if (layout.indexOf(ImageBox.name) !== -1) { + let imgData = this.extensionDoc.Alternates; + if (!imgData) { + Doc.GetProto(this.extensionDoc).Alternates = new List([]); + } + let imgList = Cast(this.extensionDoc.Alternates, listSpec(Doc), [] as any[]); + imgList && imgList.push(drop); + e.stopPropagation(); + } + } } })); // de.data.removeDocument() bcz: need to implement @@ -86,9 +92,9 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD } onPointerDown = (e: React.PointerEvent): void => { - if (e.shiftKey && e.ctrlKey) - - e.stopPropagation(); + if (e.shiftKey && e.ctrlKey) { + e.stopPropagation(); // allows default system drag drop of images with shift+ctrl only + } else e.preventDefault(); // if (Date.now() - this._lastTap < 300) { // if (e.buttons === 1) { // this._downX = e.clientX; @@ -188,8 +194,9 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD } @action onError = () => { let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount; - if (timeout < 10) + if (timeout < 10) { setTimeout(this.retryPath, Math.min(10000, timeout * 5)); + } } _curSuffix = "_m"; render() { @@ -205,24 +212,28 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"]; // this._curSuffix = ""; // if (w > 20) { - let field = this.Document[this.props.fieldKey]; + let alts = DocListCast(this.extensionDoc.Alternates); + let altpaths: string[] = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url)); + let field = this.dataDoc[this.props.fieldKey]; // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s"; // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m"; // else if (this._largeRetryCount < 10) this._curSuffix = "_l"; if (field instanceof ImageField) paths = [this.choosePath(field.url)]; - else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => this.choosePath((p as ImageField).url)); + paths.push(...altpaths); // } let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; - let rotation = NumCast(this.props.Document.rotation, 0); - let aspect = (rotation % 180) ? this.props.Document[HeightSym]() / this.props.Document[WidthSym]() : 1; + let rotation = NumCast(this.dataDoc.rotation, 0); + let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1; let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; + Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey); + let srcpath = paths[Math.min(paths.length, this._photoIndex)]; return ( - <div id={id} className={`imageBox-cont${interactive}`} + <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <img id={id} key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys - src={paths[Math.min(paths.length, this._photoIndex)]} + src={srcpath} style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} // style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} width={nativeWidth} diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index 20cae03d4..87a9565e8 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -91,12 +91,12 @@ $header-height: 30px; width: 4px; float: left; height: 30px; - width: 10px; + width: 5px; z-index: 20; right: 0; top: 0; - border-radius: 10px; - background: gray; + border-radius: 0; + background: black; pointer-events: all; } .keyValueBox-dividerDragger{ diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 917be734d..4beb70284 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -7,13 +7,23 @@ import { FieldView, FieldViewProps } from './FieldView'; import "./KeyValueBox.scss"; import { KeyValuePair } from "./KeyValuePair"; import React = require("react"); -import { NumCast, Cast, FieldValue } from "../../../new_fields/Types"; -import { Doc, Field } from "../../../new_fields/Doc"; -import { ComputedField } from "../../../fields/ScriptField"; +import { NumCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types"; +import { Doc, Field, FieldResult } from "../../../new_fields/Doc"; +import { ComputedField } from "../../../new_fields/ScriptField"; +import { SetupDrag } from "../../util/DragManager"; +import { Docs } from "../../documents/Documents"; +import { RawDataOperationParameters } from "../../northstar/model/idea/idea"; +import { Templates } from "../Templates"; +import { List } from "../../../new_fields/List"; +import { TextField } from "../../util/ProsemirrorCopy/prompt"; +import { RichTextField } from "../../../new_fields/RichTextField"; +import { ImageField } from "../../../new_fields/URLField"; @observer export class KeyValueBox extends React.Component<FieldViewProps> { private _mainCont = React.createRef<HTMLDivElement>(); + private _keyHeader = React.createRef<HTMLTableHeaderCellElement>(); + @observable private rows: KeyValuePair[] = []; public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); } @observable private _keyInput: string = ""; @@ -38,10 +48,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } public static SetField(doc: Doc, key: string, value: string) { let eq = value.startsWith("="); + let target = eq ? doc : Doc.GetProto(doc); value = eq ? value.substr(1) : value; let dubEq = value.startsWith(":="); value = dubEq ? value.substr(2) : value; - let options: ScriptOptions = { addReturn: true }; + let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } }; if (dubEq) options.typecheck = false; let script = CompileScript(value, options); if (!script.compiled) { @@ -49,12 +60,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } let field = new ComputedField(script); if (!dubEq) { - let res = script.run(); + let res = script.run({ this: target }); if (!res.success) return false; field = res.result; } if (Field.IsField(field, true)) { - let target = !eq ? doc : Doc.GetProto(doc); target[key] = field; return true; } @@ -90,7 +100,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let rows: JSX.Element[] = []; let i = 0; for (let key of Object.keys(ids).sort()) { - rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />); + rows.push(<KeyValuePair doc={realDoc} ref={(el) => { if (el) this.rows.push(el); }} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />); } return rows; } @@ -134,6 +144,58 @@ export class KeyValueBox extends React.Component<FieldViewProps> { document.addEventListener('pointerup', this.onDividerUp); } + getTemplate = async () => { + let parent = Docs.FreeformDocument([], { width: 800, height: 800, title: "Template" }); + for (let row of this.rows.filter(row => row.isChecked)) { + await this.createTemplateField(parent, row); + row.uncheck(); + } + return parent; + } + + createTemplateField = async (parent: Doc, row: KeyValuePair) => { + let collectionKeyProp = `fieldKey={"data"}`; + let metaKey = row.props.keyName; + let metaKeyProp = `fieldKey={"${metaKey}"}`; + + let sourceDoc = await Cast(this.props.Document.data, Doc); + if (!sourceDoc) { + return; + } + let target = this.inferType(sourceDoc[metaKey], metaKey); + + let template = Doc.MakeAlias(target); + template.proto = parent; + template.title = metaKey; + template.nativeWidth = 300; + template.nativeHeight = 300; + template.embed = true; + template.isTemplate = true; + template.templates = new List<string>([Templates.TitleBar(metaKey)]); + if (target.backgroundLayout) { + let metaAnoKeyProp = `fieldKey={"${metaKey}"} fieldExt={"annotations"}`; + let collectionAnoKeyProp = `fieldKey={"annotations"}`; + template.layout = StrCast(target.layout).replace(collectionAnoKeyProp, metaAnoKeyProp); + template.backgroundLayout = StrCast(target.backgroundLayout).replace(collectionKeyProp, metaKeyProp); + } else { + template.layout = StrCast(target.layout).replace(collectionKeyProp, metaKeyProp); + } + Doc.AddDocToList(parent, "data", template); + row.uncheck(); + } + + inferType = (field: FieldResult, metaKey: string) => { + let options = { width: 300, height: 300, title: metaKey }; + if (field instanceof RichTextField || typeof field === "string" || typeof field === "number") { + return Docs.TextDocument(options); + } else if (field instanceof List) { + return Docs.FreeformDocument([], options); + } else if (field instanceof ImageField) { + return Docs.ImageDocument("https://www.freepik.com/free-icon/picture-frame-with-mountain-image_748687.htm", options); + } + return new Doc; + } + render() { let dividerDragger = this.splitPercentage === 0 ? (null) : <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> @@ -144,7 +206,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> { <table className="keyValueBox-table"> <tbody className="keyValueBox-tbody"> <tr className="keyValueBox-header"> - <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }}>Key</th> + <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader} + onPointerDown={SetupDrag(this._keyHeader, this.getTemplate)} + >Key</th> <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th> </tr> {this.createTable()} diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index a1c5d5537..f78767234 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -3,6 +3,7 @@ .keyValuePair-td-key { display:inline-block; + .keyValuePair-td-key-container{ width:100%; height:100%; @@ -10,14 +11,23 @@ flex-direction: row; flex-wrap: nowrap; justify-content: space-between; + align-items: center; .keyValuePair-td-key-delete{ position: relative; background-color: transparent; color:red; } + .keyValuePair-td-key-check { + position: relative; + margin: 0; + } .keyValuePair-keyField { width:100%; - text-align: center; + margin-left: 20px; + margin-top: -1px; + font-family: monospace; + // text-align: center; + align-self: center; position: relative; overflow: auto; } @@ -26,12 +36,25 @@ .keyValuePair-td-value { display:inline-block; overflow: scroll; - img { - max-height: 36px; - width: auto; - } - .videoBox-cont{ - width: auto; - max-height: 36px; + font-family: monospace; + height: 30px; + .keyValuePair-td-value-container { + display: flex; + align-items: center; + align-content: center; + flex-direction: row; + justify-content: space-between; + flex-wrap: nowrap; + width: 100%; + height: 100%; + + img { + max-height: 36px; + width: auto; + } + .videoBox-cont{ + width: auto; + max-height: 36px; + } } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index e8bc17532..b5db52ac7 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -11,8 +11,8 @@ import "./KeyValuePair.scss"; import React = require("react"); import { Doc, Opt, Field } from '../../../new_fields/Doc'; import { FieldValue } from '../../../new_fields/Types'; -import { ComputedField } from '../../../fields/ScriptField'; import { KeyValueBox } from './KeyValueBox'; +import { DragManager, SetupDrag } from '../../util/DragManager'; // Represents one row in a key value plane @@ -24,15 +24,31 @@ export interface KeyValuePairProps { } @observer export class KeyValuePair extends React.Component<KeyValuePairProps> { + @observable private isPointerOver = false; + @observable public isChecked = false; + private checkbox = React.createRef<HTMLInputElement>(); + + @action + handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => { + this.isChecked = e.currentTarget.checked; + } + + @action + uncheck = () => { + this.checkbox.current!.checked = false; + this.isChecked = false; + } render() { let props: FieldViewProps = { Document: this.props.doc, + DataDoc: this.props.doc, ContainingCollectionView: undefined, fieldKey: this.props.keyName, + fieldExt: "", isSelected: returnFalse, select: emptyFunction, - isTopMost: false, + renderDepth: 1, selectOnLoad: false, active: returnFalse, whenActiveChanged: emptyFunction, @@ -40,15 +56,19 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { focus: emptyFunction, PanelWidth: returnZero, PanelHeight: returnZero, - addDocTab: emptyFunction + addDocTab: returnZero, }; let contents = <FieldView {...props} />; - let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; + // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; + let keyStyle = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? "black" : "blue"; + + let hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; + return ( - <tr className={this.props.rowStyle}> + <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}> <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> - <button className="keyValuePair-td-key-delete" onClick={() => { + <button style={hover} className="keyValuePair-td-key-delete" onClick={() => { if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { props.Document[props.fieldKey] = undefined; } @@ -56,21 +76,35 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { }}> X </button> - <div className="keyValuePair-keyField">{fieldKey}</div> + <input + className={"keyValuePair-td-key-check"} + type="checkbox" + style={hover} + onChange={this.handleCheck} + ref={this.checkbox} + /> + <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{props.fieldKey}</div> </div> </td> <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}> - <EditableView contents={contents} height={36} GetValue={() => { + <div className="keyValuePair-td-value-container"> + <EditableView + contents={contents} + height={36} + GetValue={() => { + const onDelegate = Object.keys(props.Document).includes(props.fieldKey); - let field = FieldValue(props.Document[props.fieldKey]); - if (Field.IsField(field)) { - return Field.toScriptString(field); - } - return ""; - }} - SetValue={(value: string) => - KeyValueBox.SetField(props.Document, props.fieldKey, value)}> - </EditableView></td> + let field = FieldValue(props.Document[props.fieldKey]); + if (Field.IsField(field)) { + return (onDelegate ? "=" : "") + Field.toScriptString(field); + } + return ""; + }} + SetValue={(value: string) => + KeyValueBox.SetField(props.Document, props.fieldKey, value)}> + </EditableView> + </div> + </td> </tr> ); } diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss deleted file mode 100644 index 639f83b38..000000000 --- a/src/client/views/nodes/LinkBox.scss +++ /dev/null @@ -1,66 +0,0 @@ -@import "../globalCssVariables"; -.link-container { - width: 100%; - height: 50px; - display: flex; - flex-direction: row; - border-top: 0.5px solid #bababa; -} - -.info-container { - width: 65%; - padding-top: 5px; - padding-left: 5px; - display: flex; - flex-direction: column -} - -.link-name { - font-size: 11px; -} - -.doc-name { - font-size: 8px; -} - -.button-container { - width: 35%; - padding-top: 8px; - display: flex; - flex-direction: row; -} - -.button { - height: 20px; - width: 20px; - margin: 8px 4px; - border-radius: 50%; - opacity: 0.9; - pointer-events: auto; - background-color: $dark-color; - color: $light-color; - text-transform: uppercase; - letter-spacing: 2px; - font-size: 60%; - transition: transform 0.2s; -} - -.button:hover { - background: $main-accent; - cursor: pointer; -} - -// .fa-icon-view { -// margin-left: 3px; -// margin-top: 5px; -// } - -.fa-icon-edit { - margin-left: 6px; - margin-top: 6px; -} - -.fa-icon-delete { - margin-left: 7px; - margin-top: 6px; -}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx deleted file mode 100644 index 68b692aad..000000000 --- a/src/client/views/nodes/LinkBox.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { observer } from "mobx-react"; -import { DocumentManager } from "../../util/DocumentManager"; -import { undoBatch } from "../../util/UndoManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import './LinkBox.scss'; -import React = require("react"); -import { Doc } from '../../../new_fields/Doc'; -import { Cast, NumCast } from '../../../new_fields/Types'; -import { listSpec } from '../../../new_fields/Schema'; -import { action } from 'mobx'; - - -library.add(faEye); -library.add(faEdit); -library.add(faTimes); - -interface Props { - linkDoc: Doc; - linkName: String; - pairedDoc: Doc; - type: String; - showEditor: () => void; -} - -@observer -export class LinkBox extends React.Component<Props> { - - @undoBatch - onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => { - e.stopPropagation(); - DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey); - } - - onEditButtonPressed = (e: React.PointerEvent): void => { - e.stopPropagation(); - - this.props.showEditor(); - } - - @action - onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => { - e.stopPropagation(); - const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]); - if (linkedFrom) { - const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc)); - if (linkedToDocs) { - linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1); - } - } - if (linkedTo) { - const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc)); - if (linkedFromDocs) { - linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1); - } - } - } - - render() { - - return ( - //<LinkEditor linkBox={this} linkDoc={this.props.linkDoc} /> - <div className="link-container"> - <div className="info-container" onPointerDown={this.onViewButtonPressed}> - <div className="link-name"> - <p>{this.props.linkName}</p> - </div> - <div className="doc-name"> - <p>{this.props.type}{this.props.pairedDoc.Title}</p> - </div> - </div> - - <div className="button-container"> - {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}> - <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */} - <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}> - <FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div> - <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}> - <FontAwesomeIcon className="fa-icon-delete" icon="times" size="sm" /></div> - </div> - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss index 9629585d7..3c49c2212 100644 --- a/src/client/views/nodes/LinkEditor.scss +++ b/src/client/views/nodes/LinkEditor.scss @@ -1,42 +1,133 @@ @import "../globalCssVariables"; -.edit-container { + +.linkEditor { width: 100%; height: auto; + font-size: 12px; // TODO +} + +.linkEditor-back { + margin-bottom: 6px; +} + +.linkEditor-info { + border-bottom: 0.5px solid $light-color-secondary; + padding-bottom: 6px; + margin-bottom: 6px; display: flex; - flex-direction: column; + justify-content: space-between; + + .linkEditor-linkedTo { + width: calc(100% - 26px); + } } -.name-input { - margin-bottom: 10px; - padding: 5px; - font-size: 12px; - border: 1px solid #bababa; +.linkEditor-button { + width: 20px; + height: 20px; + margin-left: 6px; + padding: 0; + // font-size: 12px; + border-radius: 10px; + + &:disabled { + background-color: gray; + } } -.description-input { - font-size: 11px; - padding: 5px; - margin-bottom: 10px; - border: 1px solid #bababa; +.linkEditor-groupsLabel { + display: flex; + justify-content: space-between; +} + +.linkEditor-group { + background-color: $light-color-secondary; + padding: 6px; + margin: 3px 0; + border-radius: 3px; + + .linkEditor-group-row { + // display: flex; + margin-bottom: 3px; + + .linkEditor-group-row-label { + margin-right: 6px; + } + } + + .linkEditor-metadata-row { + display: flex; + justify-content: space-between; + margin-bottom: 6px; + + .linkEditor-error { + border-color: red; + } + + input { + width: calc(50% - 16px); + height: 20px; + } + + button { + width: 20px; + height: 20px; + margin-left: 3px; + padding: 0; + font-size: 10px; + } + } } -.save-button { - width: 50px; - height: 22px; - pointer-events: auto; - background-color: $dark-color; - color: $light-color; - text-transform: uppercase; - letter-spacing: 2px; - padding: 2px; - font-size: 10px; - margin: 0 auto; - transition: transform 0.2s; - text-align: center; - line-height: 20px; + +.linkEditor-dropdown { + width: 100%; + position: relative; + z-index: 999; + + input { + width: 100%; + } + + .linkEditor-options-wrapper { + width: 100%; + position: absolute; + top: 19px; + left: 0; + display: flex; + flex-direction: column; + } + + .linkEditor-option { + background-color: $light-color-secondary; + border: 1px solid $intermediate-color; + border-top: 0; + padding: 3px; + cursor: pointer; + + &:hover { + background-color: lightgray; + } + } } -.save-button:hover { - background: $main-accent; - cursor: pointer; +.linkEditor-group-buttons { + height: 20px; + display: flex; + justify-content: flex-end; + + .linkEditor-button { + margin-left: 6px; + } + + // .linkEditor-groupOpts { + // width: calc(20% - 3px); + // height: 20px; + // padding: 0; + // font-size: 10px; + + // &:disabled { + // background-color: gray; + // } + // } }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx index 71a423338..22da732cf 100644 --- a/src/client/views/nodes/LinkEditor.tsx +++ b/src/client/views/nodes/LinkEditor.tsx @@ -1,57 +1,345 @@ -import { observable, computed, action } from "mobx"; +import { observable, computed, action, trace } from "mobx"; import React = require("react"); -import { SelectionManager } from "../../util/SelectionManager"; import { observer } from "mobx-react"; import './LinkEditor.scss'; -import { props } from "bluebird"; -import { DocumentView } from "./DocumentView"; -import { link } from "fs"; -import { StrCast } from "../../../new_fields/Types"; +import { StrCast, Cast, FieldValue } from "../../../new_fields/Types"; import { Doc } from "../../../new_fields/Doc"; +import { LinkManager } from "../../util/LinkManager"; +import { Docs } from "../../documents/Documents"; +import { Utils } from "../../../Utils"; +import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { SetupDrag } from "../../util/DragManager"; -interface Props { - linkDoc: Doc; - showLinks: () => void; +library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus); + + +interface GroupTypesDropdownProps { + groupType: string; + setGroupType: (group: string) => void; +} +// this dropdown could be generalized +@observer +class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> { + @observable private _searchTerm: string = ""; + @observable private _groupType: string = this.props.groupType; + + @action setSearchTerm = (value: string): void => { this._searchTerm = value; }; + @action setGroupType = (value: string): void => { this._groupType = value; }; + + @action + createGroup = (groupType: string): void => { + this.props.setGroupType(groupType); + LinkManager.Instance.addGroupType(groupType); + } + + onChange = (val: string): void => { + this.setSearchTerm(val); + this.setGroupType(val); + } + + renderOptions = (): JSX.Element[] | JSX.Element => { + if (this._searchTerm === "") return <></>; + + let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes()); + let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); + let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1; + + let options = groupOptions.map(groupType => { + return <div key={groupType} className="linkEditor-option" + onClick={() => { this.props.setGroupType(groupType); this.setGroupType(groupType); this.setSearchTerm(""); }}>{groupType}</div>; + }); + + // if search term does not already exist as a group type, give option to create new group type + if (!exactFound && this._searchTerm !== "") { + options.push(<div key={""} className="linkEditor-option" + onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm(""); }}>Define new "{this._searchTerm}" relationship</div>); + } + + return options; + } + + render() { + return ( + <div className="linkEditor-dropdown"> + <input type="text" value={this._groupType} placeholder="Search for or create a new group" + onChange={e => this.onChange(e.target.value)}></input> + <div className="linkEditor-options-wrapper"> + {this.renderOptions()} + </div> + </div > + ); + } } + +interface LinkMetadataEditorProps { + id: string; + groupType: string; + mdDoc: Doc; + mdKey: string; + mdValue: string; + changeMdIdKey: (id: string, newKey: string) => void; +} @observer -export class LinkEditor extends React.Component<Props> { +class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> { + @observable private _key: string = this.props.mdKey; + @observable private _value: string = this.props.mdValue; + @observable private _keyError: boolean = false; - @observable private _nameInput: string = StrCast(this.props.linkDoc.title); - @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription); + @action + setMetadataKey = (value: string): void => { + let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType); + // don't allow user to create existing key + let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase()); + if (newIndex > -1) { + this._keyError = true; + this._key = value; + return; + } else { + this._keyError = false; + } - onSaveButtonPressed = (e: React.PointerEvent): void => { - e.stopPropagation(); + // set new value for key + let currIndex = groupMdKeys.findIndex(key => { + return StrCast(key).toUpperCase() === this._key.toUpperCase(); + }); + if (currIndex === -1) console.error("LinkMetadataEditor: key was not found"); + groupMdKeys[currIndex] = value; - let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc; - linkDoc.title = this._nameInput; - linkDoc.linkDescription = this._descriptionInput; + this.props.changeMdIdKey(this.props.id, value); + this._key = value; + LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, [...groupMdKeys]); + } - this.props.showLinks(); + @action + setMetadataValue = (value: string): void => { + if (!this._keyError) { + this._value = value; + this.props.mdDoc[this._key] = value; + } } + @action + removeMetadata = (): void => { + let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType); + let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase()); + if (index === -1) console.error("LinkMetadataEditor: key was not found"); + groupMdKeys.splice(index, 1); - render() { + LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys); + this._key = ""; + } + render() { return ( - <div className="edit-container"> - <input onChange={this.onNameChanged} className="name-input" type="text" value={this._nameInput} placeholder="Name . . ."></input> - <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._descriptionInput} placeholder="Description . . ."></textarea> - <div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div> + <div className="linkEditor-metadata-row"> + <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key === "new key" ? "" : this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>: + <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input> + <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button> </div> + ); + } +} + +interface LinkGroupEditorProps { + sourceDoc: Doc; + linkDoc: Doc; + groupDoc: Doc; +} +@observer +export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> { + private _metadataIds: Map<string, string> = new Map(); + + constructor(props: LinkGroupEditorProps) { + super(props); + + let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type)); + groupMdKeys.forEach(key => { + this._metadataIds.set(key, Utils.GenerateGuid()); + }); + } + + @action + setGroupType = (groupType: string): void => { + this.props.groupDoc.type = groupType; + } + + removeGroupFromLink = (groupType: string): void => { + LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType); + } + + deleteGroup = (groupType: string): void => { + LinkManager.Instance.deleteGroupType(groupType); + } + + copyGroup = (groupType: string): void => { + let sourceGroupDoc = this.props.groupDoc; + let sourceMdDoc = Cast(sourceGroupDoc.metadata, Doc, new Doc); + + let destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); + // let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc); + let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); + + // create new metadata doc with copied kvp + let destMdDoc = new Doc(); + destMdDoc.anchor1 = StrCast(sourceMdDoc.anchor2); + destMdDoc.anchor2 = StrCast(sourceMdDoc.anchor1); + keys.forEach(key => { + let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]); + destMdDoc[key] = val; + }); + + // create new group doc with new metadata doc + let destGroupDoc = new Doc(); + destGroupDoc.type = groupType; + destGroupDoc.metadata = destMdDoc; + + LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true); + } + + @action + addMetadata = (groupType: string): void => { + this._metadataIds.set("new key", Utils.GenerateGuid()); + let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType); + // only add "new key" if there is no other key with value "new key"; prevents spamming + if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key"); + LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys); + } + + // for key rendering purposes + changeMdIdKey = (id: string, newKey: string) => { + this._metadataIds.set(newKey, id); + } + + renderMetadata = (): JSX.Element[] => { + let metadata: Array<JSX.Element> = []; + let groupDoc = this.props.groupDoc; + const mdDoc = FieldValue(Cast(groupDoc.metadata, Doc)); + if (!mdDoc) { + return []; + } + let groupType = StrCast(groupDoc.type); + let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType); + + groupMdKeys.forEach((key) => { + let val = StrCast(mdDoc[key]); + metadata.push( + <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} /> + ); + }); + return metadata; + } + + viewGroupAsTable = (groupType: string): JSX.Element => { + let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); + let index = keys.indexOf(""); + if (index > -1) keys.splice(index, 1); + let cols = ["anchor1", "anchor2", ...[...keys]]; + let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); + let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); + let ref = React.createRef<HTMLDivElement>(); + return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>; + } + + render() { + let groupType = StrCast(this.props.groupDoc.type); + // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") { + let buttons; + if (groupType === "") { + buttons = ( + <> + <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button> + <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button> + <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button> + <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button> + <button className="linkEditor-button" disabled title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button> + </> + ); + } else { + buttons = ( + <> + <button className="linkEditor-button" onClick={() => this.addMetadata(groupType)} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button> + <button className="linkEditor-button" onClick={() => this.copyGroup(groupType)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button> + <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button> + <button className="linkEditor-button" onClick={() => this.deleteGroup(groupType)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button> + {this.viewGroupAsTable(groupType)} + </> + ); + } + trace(); + return ( + <div className="linkEditor-group"> + <div className="linkEditor-group-row"> + <p className="linkEditor-group-row-label">type:</p> + <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} /> + </div> + {this.renderMetadata().length > 0 ? <p className="linkEditor-group-row-label">metadata:</p> : <></>} + {this.renderMetadata()} + <div className="linkEditor-group-buttons"> + {buttons} + </div> + </div> ); } +} + + +interface LinkEditorProps { + sourceDoc: Doc; + linkDoc: Doc; + showLinks: () => void; +} +@observer +export class LinkEditor extends React.Component<LinkEditorProps> { @action - onNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - this._nameInput = e.target.value; + deleteLink = (): void => { + LinkManager.Instance.deleteLink(this.props.linkDoc); + this.props.showLinks(); } @action - onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - this._descriptionInput = e.target.value; + addGroup = (): void => { + // create new metadata document for group + let mdDoc = new Doc(); + mdDoc.anchor1 = this.props.sourceDoc.title; + mdDoc.anchor2 = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title; + + // create new group document + let groupDoc = new Doc(); + groupDoc.type = ""; + groupDoc.metadata = mdDoc; + + LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, this.props.sourceDoc, groupDoc); + } + + render() { + let destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); + + let groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc); + let groups = groupList.map(groupDoc => { + return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />; + }); + + return ( + <div className="linkEditor"> + <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button> + <div className="linkEditor-info"> + <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p> + <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button> + </div> + <div className="linkEditor-groupsLabel"> + <b>Relationships:</b> + <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group"><FontAwesomeIcon icon="plus" size="sm" /></button> + </div> + {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>} + </div> + + ); } }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss index dedcce6ef..a4018bd2d 100644 --- a/src/client/views/nodes/LinkMenu.scss +++ b/src/client/views/nodes/LinkMenu.scss @@ -1,21 +1,137 @@ -#linkMenu-container { +@import "../globalCssVariables"; + +.linkMenu { width: 100%; height: auto; - display: flex; - flex-direction: column; } -#linkMenu-searchBar { - width: 100%; - padding: 5px; - margin-bottom: 10px; +.linkMenu-list { + max-height: 200px; + overflow-y: scroll; +} + +.linkMenu-group { + border-bottom: 0.5px solid lightgray; + padding: 5px 0; + + + &:last-child { + border-bottom: none; + } + + .linkMenu-group-name { + display: flex; + + &:hover { + p { + background-color: lightgray; + } + p.expand-one { + width: calc(100% - 26px); + } + .linkEditor-tableButton { + display: block; + } + } + + p { + width: 100%; + padding: 4px 6px; + line-height: 12px; + border-radius: 5px; + font-weight: bold; + } + + .linkEditor-tableButton { + display: none; + } + } +} + +.linkMenu-item { + // border-top: 0.5px solid $main-accent; + position: relative; + display: flex; font-size: 12px; - border: 1px solid #bababa; + + + .link-name { + position: relative; + + p { + padding: 4px 6px; + line-height: 12px; + border-radius: 5px; + overflow-wrap: break-word; + } + } + + .linkMenu-item-content { + width: 100%; + } + + .link-metadata { + padding: 0 10px 0 16px; + margin-bottom: 4px; + color: $main-accent; + font-style: italic; + font-size: 10.5px; + } + + &:hover { + .linkMenu-item-buttons { + display: flex; + } + .linkMenu-item-content { + &.expand-two p { + width: calc(100% - 52px); + background-color: lightgray; + } + &.expand-three p { + width: calc(100% - 84px); + background-color: lightgray; + } + } + } } -#linkMenu-list { - margin-top: 5px; - width: 100%; - height: 100px; - overflow-y: scroll; -}
\ No newline at end of file +.linkMenu-item-buttons { + display: none; + position: absolute; + top: 50%; + right: 0; + transform: translateY(-50%); + + .button { + width: 20px; + height: 20px; + margin: 0; + margin-right: 6px; + border-radius: 50%; + cursor: pointer; + pointer-events: auto; + background-color: $dark-color; + color: $light-color; + font-size: 65%; + transition: transform 0.2s; + text-align: center; + position: relative; + + .fa-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + &:last-child { + margin-right: 0; + } + &:hover { + background: $main-accent; + } + } +} + + + diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 3f09d6214..cccf3c329 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -1,13 +1,20 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { DocumentView } from "./DocumentView"; -import { LinkBox } from "./LinkBox"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Doc } from "../../../new_fields/Doc"; +import { LinkManager } from "../../util/LinkManager"; +import { LinkMenuGroup } from "./LinkMenuGroup"; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +library.add(faTrash); import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; +import { DocTypes } from "../../documents/Documents"; interface Props { docView: DocumentView; @@ -19,34 +26,46 @@ export class LinkMenu extends React.Component<Props> { @observable private _editingLink?: Doc; - renderLinkItems(links: Doc[], key: string, type: string) { - 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} />; - } + @action + componentWillReceiveProps() { + this._editingLink = undefined; + } + + clearAllLinks = () => { + LinkManager.Instance.deleteAllLinksOnAnchor(this.props.docView.props.Document); + } + + renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => { + let linkItems: Array<JSX.Element> = []; + groups.forEach((group, groupType) => { + linkItems.push( + <LinkMenuGroup key={groupType} sourceDoc={this.props.docView.props.Document} group={group} groupType={groupType} showEditor={action((linkDoc: Doc) => this._editingLink = linkDoc)} /> + ); }); + + // if source doc has no links push message + if (linkItems.length === 0) linkItems.push(<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>); + + return linkItems; } render() { - //get list of links from document - let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs); - let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs); + let sourceDoc = this.props.docView.props.Document; + let groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc); if (this._editingLink === undefined) { return ( - <div id="linkMenu-container"> + <div className="linkMenu"> + <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button> {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */} - <div id="linkMenu-list"> - {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")} - {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")} + <div className="linkMenu-list"> + {this.renderAllGroups(groups)} </div> </div> ); } else { return ( - <LinkEditor linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor> + <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor> ); } - } }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx new file mode 100644 index 000000000..e4cf56d20 --- /dev/null +++ b/src/client/views/nodes/LinkMenuGroup.tsx @@ -0,0 +1,92 @@ +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import { DocumentView } from "./DocumentView"; +import { LinkMenuItem } from "./LinkMenuItem"; +import { LinkEditor } from "./LinkEditor"; +import './LinkMenu.scss'; +import React = require("react"); +import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { LinkManager } from "../../util/LinkManager"; +import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager"; +import { emptyFunction } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +interface LinkMenuGroupProps { + sourceDoc: Doc; + group: Doc[]; + groupType: string; + showEditor: (linkDoc: Doc) => void; +} + +@observer +export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { + + private _drag = React.createRef<HTMLDivElement>(); + private _table = React.createRef<HTMLDivElement>(); + + onLinkButtonDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.addEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + document.addEventListener("pointerup", this.onLinkButtonUp); + } + + onLinkButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + e.stopPropagation(); + } + + onLinkButtonMoved = async (e: PointerEvent) => { + if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) { + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + + let draggedDocs = this.props.group.map(linkDoc => LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc)); + let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined)); + + DragManager.StartLinkedDocumentDrag([this._drag.current], this.props.sourceDoc, dragData, e.x, e.y, { + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); + } + e.stopPropagation(); + } + + viewGroupAsTable = (groupType: string): JSX.Element => { + let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); + let index = keys.indexOf(""); + if (index > -1) keys.splice(index, 1); + let cols = ["anchor1", "anchor2", ...[...keys]]; + let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); + let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); + let ref = React.createRef<HTMLDivElement>(); + return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>; + } + + render() { + let groupItems = this.props.group.map(linkDoc => { + let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc); + return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType} + linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />; + }); + + return ( + <div className="linkMenu-group"> + <div className="linkMenu-group-name"> + <p ref={this._drag} onPointerDown={this.onLinkButtonDown} + className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p> + {this.props.groupType === "*" || this.props.groupType === "" ? <></> : this.viewGroupAsTable(this.props.groupType)} + </div> + <div className="linkMenu-group-wrapper"> + {groupItems} + </div> + </div > + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx new file mode 100644 index 000000000..732e6de84 --- /dev/null +++ b/src/client/views/nodes/LinkMenuItem.tsx @@ -0,0 +1,111 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observer } from "mobx-react"; +import { DocumentManager } from "../../util/DocumentManager"; +import { undoBatch } from "../../util/UndoManager"; +import './LinkMenu.scss'; +import React = require("react"); +import { Doc } from '../../../new_fields/Doc'; +import { StrCast, Cast } from '../../../new_fields/Types'; +import { observable, action } from 'mobx'; +import { LinkManager } from '../../util/LinkManager'; +import { DragLinkAsDocument } from '../../util/DragManager'; +import { CollectionDockingView } from '../collections/CollectionDockingView'; +library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp); + + +interface LinkMenuItemProps { + groupType: string; + linkDoc: Doc; + sourceDoc: Doc; + destinationDoc: Doc; + showEditor: (linkDoc: Doc) => void; +} + +@observer +export class LinkMenuItem extends React.Component<LinkMenuItemProps> { + private _drag = React.createRef<HTMLDivElement>(); + @observable private _showMore: boolean = false; + @action toggleShowMore() { this._showMore = !this._showMore; } + + @undoBatch + onFollowLink = async (e: React.PointerEvent): Promise<void> => { + e.stopPropagation(); + if (DocumentManager.Instance.getDocumentView(this.props.destinationDoc)) { + DocumentManager.Instance.jumpToDocument(this.props.destinationDoc, e.altKey); + } else { + CollectionDockingView.Instance.AddRightSplit(this.props.destinationDoc, undefined); + } + } + + onEdit = (e: React.PointerEvent): void => { + e.stopPropagation(); + this.props.showEditor(this.props.linkDoc); + } + + renderMetadata = (): JSX.Element => { + let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc); + let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase()); + let groupDoc = index > -1 ? groups[index] : undefined; + + let mdRows: Array<JSX.Element> = []; + if (groupDoc) { + let mdDoc = Cast(groupDoc.metadata, Doc, new Doc); + let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); + mdRows = keys.map(key => { + return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>); + }); + } + + return (<div className="link-metadata">{mdRows}</div>); + } + + onLinkButtonDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.addEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + document.addEventListener("pointerup", this.onLinkButtonUp); + } + + onLinkButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + e.stopPropagation(); + } + + onLinkButtonMoved = async (e: PointerEvent) => { + if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) { + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + + DragLinkAsDocument(this._drag.current, e.x, e.y, this.props.linkDoc, this.props.sourceDoc); + } + e.stopPropagation(); + } + + render() { + + let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); + let canExpand = keys ? keys.length > 0 : false; + + return ( + <div className="linkMenu-item"> + <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}> + <div className="link-name"> + <p ref={this._drag} onPointerDown={this.onLinkButtonDown}>{StrCast(this.props.destinationDoc.title)}</p> + <div className="linkMenu-item-buttons"> + {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}> + <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>} + <div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div> + <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> + </div> + </div> + {this._showMore ? this.renderMetadata() : <></>} + </div> + + </div > + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 449408a61..9a38d6241 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -2,40 +2,136 @@ transform-origin: left top; position: absolute; top: 0; - left:0; + left: 0; } + .react-pdf__Page__textContent span { user-select: text; } + .react-pdf__Document { position: absolute; } + .pdfBox-buttonTray { - position:absolute; + position: absolute; top: 0; - left:0; + left: 0; z-index: 25; pointer-events: all; } + .pdfBox-thumbnail { position: absolute; width: 100%; } + .pdfButton { pointer-events: all; width: 100px; - height:100px; + height: 100px; +} + +.pdfBox-cont, .pdfBox-cont-interactive { + display: flex; + flex-direction: row; + height: 100%; + overflow-y: scroll; + overflow-x: hidden; } .pdfBox-cont { - pointer-events: none ; - span { - pointer-events: none !important; + pointer-events: none; + .textlayer { + pointer-events: none; + + span { + pointer-events: none !important; + } + } + + .page-cont { + pointer-events: none; } } + .pdfBox-cont-interactive { pointer-events: all; + display: flex; + flex-direction: row; + + .textlayer { + span { + pointer-events: all !important; + user-select: text; + } + } } + .pdfBox-contentContainer { position: absolute; transform-origin: left top; +} + +.pdfBox-settingsCont { + position: absolute; + right: 0; + top: 0; + + .pdfBox-settingsButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 70px; + background: none; + padding: 0; + + .pdfBox-settingsButton-arrow { + width: 0; + height: 0; + border-top: 25px solid transparent; + border-bottom: 25px solid transparent; + border-right: 25px solid #121721; + transition: all 0.5s; + } + + .pdfBox-settingsButton-iconCont { + background: #121721; + height: 50px; + width: 70px; + display: flex; + justify-content: center; + align-items: center; + margin-left: -2px; + border-radius: 3px; + } + } + + .pdfBox-settingsButton:hover { + background: none; + } + + .pdfBox-settingsFlyout { + width: 600px; + position: absolute; + background: #323232; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + left: -400px; + border-radius: 7px; + padding: 20px; + display: flex; + flex-direction: column; + font-size: 30px; + transition: all 0.5s; + + .pdfBox-settingsFlyout-title { + color: white; + } + + .pdfBox-settingsFlyout-kvpInput { + margin-top: 20px; + display: grid; + grid-template-columns: 47.5% 5% 47.5%; + } + } }
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 855c744e6..8fb18e8fc 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,52 +1,28 @@ -import * as htmlToImage from "html-to-image"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx'; +import { action, IReactionDisposer, observable, reaction, trace, untracked, computed } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; -import Measure from "react-measure"; -//@ts-ignore -import { Document, Page } from "react-pdf"; -import 'react-pdf/dist/Page/AnnotationLayer.css'; -import { Id } from "../../../new_fields/FieldSymbols"; +import { WidthSym, Doc } from "../../../new_fields/Doc"; import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; -import { ImageField, PdfField } from "../../../new_fields/URLField"; +import { Cast, NumCast, BoolCast } from "../../../new_fields/Types"; +import { PdfField } from "../../../new_fields/URLField"; +//@ts-ignore +// import { Document, Page } from "react-pdf"; +// import 'react-pdf/dist/Page/AnnotationLayer.css'; import { RouteStore } from "../../../server/RouteStore"; -import { Utils } from '../../../Utils'; -import { DocServer } from "../../DocServer"; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; -import { SearchBox } from "../SearchBox"; +import { FilterBox } from "../search/FilterBox"; import { Annotation } from './Annotation'; +import { PDFViewer } from "../pdf/PDFViewer"; import { positionSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; -var path = require('path'); import React = require("react"); -import { ContextMenu } from "../ContextMenu"; - -/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx - * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, - * area selection (I call it stickies), embedded ink node for directly annotating using a pen or - * mouse, and pagination. - * - * - * HOW TO USE: - * AREA selection: - * 1) Click on Area button. - * 2) click on any part of the PDF, and drag to get desired sized area shape - * 3) You can write on the area (hence the reason why it's called sticky) - * 4) to make another area, you need to click on area button AGAIN. - * - * HIGHLIGHT: (Buggy. No multiline/multidiv text highlighting for now...) - * 1) just click and drag on a text - * 2) click highlight - * 3) for annotation, just pull your cursor over to that text - * 4) another method: click on highlight first and then drag on your desired text - * 5) To make another highlight, you need to reclick on the button - * - * written by: Andrew Kim - */ +import { CompileScript } from '../../util/Scripting'; +import { Flyout, anchorPoints } from '../DocumentDecorations'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ScriptField } from '../../../new_fields/ScriptField'; type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const PdfDocument = makeInterface(positionSchema, pageSchema); @@ -55,349 +31,220 @@ const PdfDocument = makeInterface(positionSchema, pageSchema); export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) { public static LayoutString() { return FieldView.LayoutString(PDFBox); } - private _mainDiv = React.createRef<HTMLDivElement>(); - private renderHeight = 2400; - - @observable private _renderAsSvg = true; @observable private _alt = false; + @observable private _scrollY: number = 0; + @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + @observable private _flyout: boolean = false; + private _mainCont: React.RefObject<HTMLDivElement>; private _reactionDisposer?: IReactionDisposer; + private _keyValue: string = ""; + private _valueValue: string = ""; + private _scriptValue: string = ""; + private _keyRef: React.RefObject<HTMLInputElement>; + private _valueRef: React.RefObject<HTMLInputElement>; + private _scriptRef: React.RefObject<HTMLInputElement>; - @observable private _perPageInfo: Object[] = []; //stores pageInfo - @observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno + constructor(props: FieldViewProps) { + super(props); - @observable private _currAnno: any = []; - @observable private _interactive: boolean = false; - - @computed private get curPage() { return NumCast(this.Document.curPage, 1); } - @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); } - - componentDidMount() { - let wasSelected = this.props.active(); + this._mainCont = React.createRef(); this._reactionDisposer = reaction( - () => [this.props.active(), this.curPage], + () => this.props.Document.scrollY, () => { - setTimeout(action(() => { // this forces the active() check to happen after all changes in a transaction have occurred. - if (this.curPage > 0 && !this.props.isTopMost && this.curPage !== this.thumbnailPage && wasSelected && !this.props.active()) { - this.saveThumbnail(); - } - wasSelected = this._interactive = this.props.active(); - }), 0); - }, - { fireImmediately: true }); + if (this._mainCont.current) { + this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.scrollY), behavior: "auto" }); + } + } + ); + this._keyRef = React.createRef(); + this._valueRef = React.createRef(); + this._scriptRef = React.createRef(); + } + + componentDidMount() { + if (this.props.setPdfBox) this.props.setPdfBox(this); } componentWillUnmount() { - if (this._reactionDisposer) this._reactionDisposer(); + this._reactionDisposer && this._reactionDisposer(); } - /** - * highlighting helper function - */ - makeEditableAndHighlight = (colour: string) => { - var range, sel = window.getSelection(); - if (sel && sel.rangeCount && sel.getRangeAt) { - range = sel.getRangeAt(0); + public GetPage() { + return Math.floor(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1; + } + public BackPage() { + let cp = Math.ceil(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1; + cp = cp - 1; + if (cp > 0) { + this.props.Document.curPage = cp; + this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight); } - document.designMode = "on"; - if (!document.execCommand("HiliteColor", false, colour)) { - document.execCommand("HiliteColor", false, colour); + } + public GotoPage(p: number) { + if (p > 0 && p <= NumCast(this.props.Document.numPages)) { + this.props.Document.curPage = p; + this.props.Document.scrollY = (p - 1) * NumCast(this.dataDoc.pdfHeight); } + } - if (range && sel) { - sel.removeAllRanges(); - sel.addRange(range); - - let obj: Object = { parentDivs: [], spans: [] }; - //@ts-ignore - if (range.commonAncestorContainer.className === 'react-pdf__Page__textContent') { //multiline highlighting case - obj = this.highlightNodes(range.commonAncestorContainer.childNodes); - } else { //single line highlighting case - let parentDiv = range.commonAncestorContainer.parentElement; - if (parentDiv) { - if (parentDiv.className === 'react-pdf__Page__textContent') { //when highlight is overwritten - obj = this.highlightNodes(parentDiv.childNodes); - } else { - parentDiv.childNodes.forEach((child) => { - if (child.nodeName === 'SPAN') { - //@ts-ignore - obj.parentDivs.push(parentDiv); - //@ts-ignore - child.id = "highlighted"; - //@ts-ignore - obj.spans.push(child); - // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler - } - }); - } - } - } - this._pageInfo.divs.push(obj); - + public ForwardPage() { + let cp = this.GetPage() + 1; + if (cp <= NumCast(this.props.Document.numPages)) { + this.props.Document.curPage = cp; + this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight); } - document.designMode = "off"; } - highlightNodes = (nodes: NodeListOf<ChildNode>) => { - let temp = { parentDivs: [], spans: [] }; - nodes.forEach((div) => { - div.childNodes.forEach((child) => { - if (child.nodeName === 'SPAN') { - //@ts-ignore - temp.parentDivs.push(div); - //@ts-ignore - child.id = "highlighted"; - //@ts-ignore - temp.spans.push(child); - // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler - } - }); - - }); - return temp; + private newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._keyValue = e.currentTarget.value; } - /** - * when the cursor enters the highlight, it pops out annotation. ONLY WORKS FOR SINGLE DIV LINES - */ - @action - onEnter = (e: any) => { - let span: HTMLSpanElement = e.toElement; - let index: any; - this._pageInfo.divs.forEach((obj: any) => { - obj.spans.forEach((element: any) => { - if (element === span && !index) { - index = this._pageInfo.divs.indexOf(obj); - } - }); - }); - - if (this._pageInfo.anno.length >= index + 1) { - if (this._currAnno.length === 0) { - this._currAnno.push(this._pageInfo.anno[index]); - } - } else { - if (this._currAnno.length === 0) { //if there are no current annotation - let div = span.offsetParent; - //@ts-ignore - let divX = div.style.left; - //@ts-ignore - let divY = div.style.top; - //slicing "px" from the end - divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span) - divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span) - let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} />; - this._pageInfo.anno.push(annotation); - this._currAnno.push(annotation); - } - } - + private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._valueValue = e.currentTarget.value; } - /** - * highlight function for highlighting actual text. This works fine. - */ - highlight = (color: string) => { - if (window.getSelection()) { - try { - if (!document.execCommand("hiliteColor", false, color)) { - this.makeEditableAndHighlight(color); - } - } catch (ex) { - this.makeEditableAndHighlight(color); - } - } + @action + private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._scriptValue = e.currentTarget.value; } - /** - * controls the area highlighting (stickies) Kinda temporary - */ - onPointerDown = (e: React.PointerEvent) => { - if (this.props.isSelected() && !InkingControl.Instance.selectedTool && e.buttons === 1) { - if (e.altKey) { - this._alt = true; - } else { - if (e.metaKey) { - e.stopPropagation(); - } - } - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); + private applyFilter = () => { + let scriptText = ""; + if (this._scriptValue.length > 0) { + scriptText = this._scriptValue; + } else if (this._keyValue.length > 0 && this._valueValue.length > 0) { + scriptText = `return this.${this._keyValue} === ${this._valueValue}`; } - if (this.props.isSelected() && e.buttons === 2) { - runInAction(() => this._alt = true); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); + else { + scriptText = "return true"; } - } - - /** - * controls area highlighting and partially highlighting. Kinda temporary - */ - @action - onPointerUp = (e: PointerEvent) => { - this._alt = false; - document.removeEventListener("pointerup", this.onPointerUp); - if (this.props.isSelected()) { - this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color. + let script = CompileScript(scriptText, { params: { this: Doc.name } }); + if (script.compiled) { + this.props.Document.filterScript = new ScriptField(script); } - this._interactive = true; } - @action - saveThumbnail = () => { - this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1); - this._renderAsSvg = false; - setTimeout(() => { - runInAction(() => this._smallRetryCount = this._mediumRetryCount = this._largeRetryCount = 0); - let nwidth = FieldValue(this.Document.nativeWidth, 0); - let nheight = FieldValue(this.Document.nativeHeight, 0); - htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 0.8 }) - .then(action((dataUrl: string) => { - SearchBox.convertDataUri(dataUrl, "icon" + this.Document[Id] + "_" + this.curPage).then((returnedFilename) => { - if (returnedFilename) { - let url = DocServer.prepend(returnedFilename); - this.props.Document.thumbnail = new ImageField(new URL(url)); - } - runInAction(() => this._renderAsSvg = true); - }) - })) - .catch(function (error: any) { - console.error('oops, something went wrong!', error); - }); - }, 1250); + private toggleFlyout = () => { + this._flyout = !this._flyout; } @action - onLoaded = (page: any) => { - // bcz: the number of pages should really be set when the document is imported. - this.props.Document.numPages = page._transport.numPages; - if (this._perPageInfo.length === 0) { //Makes sure it only runs once - this._perPageInfo = [...Array(page._transport.numPages)]; + private resetFilters = () => { + this._keyValue = this._valueValue = ""; + this._scriptValue = "return true"; + if (this._keyRef.current) { + this._keyRef.current.value = ""; } - } - - @action - setScaling = (r: any) => { - // bcz: the nativeHeight should really be set when the document is imported. - // also, the native dimensions could be different for different pages of the canvas - // so this design is flawed. - var nativeWidth = FieldValue(this.Document.nativeWidth, 0); - if (!FieldValue(this.Document.nativeHeight, 0)) { - var nativeHeight = nativeWidth * r.offset.height / r.offset.width; - this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0); - this.props.Document.nativeHeight = nativeHeight; + if (this._valueRef.current) { + this._valueRef.current.value = ""; } - } - @computed - get pdfPage() { - return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />; - } - @computed - get pdfContent() { - let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); - if (!pdfUrl) { - return <p>No pdf url to render</p>; + if (this._scriptRef.current) { + this._scriptRef.current.value = ""; } - let pdfpage = this.pdfPage; - let body = this.Document.nativeHeight ? - pdfpage : - <Measure offset onResize={this.setScaling}> - {({ measureRef }) => - <div className="pdfBox-page" ref={measureRef}> - {pdfpage} - </div> - } - </Measure>; - let xf = (this.Document.nativeHeight || 0) / this.renderHeight; - return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> - <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg || this.props.isTopMost ? "svg" : "canvas"}> - {body} - </Document> - </div >; + this.applyFilter(); } - @computed - get pdfRenderer() { - let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); - let proxy = this.imageProxyRenderer; - if ((!this._interactive && proxy && (!this.props.ContainingCollectionView || !this.props.ContainingCollectionView.props.isTopMost)) || !pdfUrl) { - return proxy; + scrollTo(y: number) { + if (this._mainCont.current) { + this._mainCont.current.scrollTo({ top: y }); } - return [ - proxy, - this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element), - this._currAnno.map((element: any) => element), - this.pdfContent - ]; - } - - choosePath(url: URL) { - if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1) - return url.href; - let ext = path.extname(url.href); - return url.href.replace(ext, this._curSuffix + ext); } - @observable _smallRetryCount = 1; - @observable _mediumRetryCount = 1; - @observable _largeRetryCount = 1; - @action retryPath = () => { - if (this._curSuffix === "_s") this._smallRetryCount++; - if (this._curSuffix === "_m") this._mediumRetryCount++; - if (this._curSuffix === "_l") this._largeRetryCount++; - } - @action onError = () => { - let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount; - if (timeout < 10) - setTimeout(this.retryPath, Math.min(10000, timeout * 5)); - } - _curSuffix = "_m"; - - @computed - get imageProxyRenderer() { - let thumbField = this.props.Document.thumbnail; - if (thumbField && this._renderAsSvg && NumCast(this.props.Document.thumbnailPage, 0) === this.Document.curPage) { - - // let transform = this.props.ScreenToLocalTransform().inverse(); - let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; - // var [sptX, sptY] = transform.transformPoint(0, 0); - // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight()); - // let w = bptX - sptX; - let path = thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg"; - // this._curSuffix = ""; - // if (w > 20) { - let field = thumbField; - // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s"; - // else if (w < 400 && this._mediumRetryCount < 10) this._curSuffix = "_m"; - // else if (this._largeRetryCount < 10) this._curSuffix = "_l"; - if (field instanceof ImageField) path = this.choosePath(field.url); - // } - return <img className="pdfBox-thumbnail" key={path + (this._mediumRetryCount).toString()} src={path} onError={this.onError} />; + settingsPanel() { + return !this.props.active() ? (null) : + ( + <div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}> + <button className="pdfBox-settingsButton" onClick={this.toggleFlyout} title="Open Annotation Settings" + style={{ marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px` }}> + <div className="pdfBox-settingsButton-arrow" + style={{ + borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`, + borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`, + borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`, + transform: `scaleX(${this._flyout ? -1 : 1})` + }}></div> + <div className="pdfBox-settingsButton-iconCont"> + <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" /> + </div> + </button> + <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} > + <div className="pdfBox-settingsFlyout-title"> + Annotation View Settings + </div> + <div className="pdfBox-settingsFlyout-kvpInput"> + <input placeholder="Key" className="pdfBox-settingsFlyout-input" onChange={this.newKeyChange} + style={{ gridColumn: 1 }} ref={this._keyRef} /> + <input placeholder="Value" className="pdfBox-settingsFlyout-input" onChange={this.newValueChange} + style={{ gridColumn: 3 }} ref={this._valueRef} /> + </div> + <div className="pdfBox-settingsFlyout-kvpInput"> + <input placeholder="Custom Script" onChange={this.newScriptChange} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} /> + </div> + <div className="pdfBox-settingsFlyout-kvpInput"> + <button style={{ gridColumn: 1 }} onClick={this.resetFilters}> + <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" /> + Reset Filters + </button> + <button style={{ gridColumn: 3 }} onClick={this.applyFilter}> + <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" /> + Apply + </button> + </div> + </div> + </div> + ); + } + + loaded = (nw: number, nh: number, np: number) => { + if (this.props.Document) { + let doc = this.dataDoc; + doc.numPages = np; + if (doc.nativeWidth && doc.nativeHeight) return; + let oldaspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1); + doc.nativeWidth = nw; + if (doc.nativeHeight) doc.nativeHeight = nw * oldaspect; + else doc.nativeHeight = nh; + let ccv = this.props.ContainingCollectionView; + if (ccv) { + ccv.props.Document.pdfHeight = nh; + } + doc.height = nh * (doc[WidthSym]() / nw); } - return (null); } - @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true); - @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false); - onContextMenu = (e: React.MouseEvent): void => { - let field = Cast(this.Document[this.props.fieldKey], PdfField); - if (field) { - let url = field.url.href; - ContextMenu.Instance.addItem({ - description: "Copy path", event: () => { - Utils.CopyText(url); - }, icon: "expand-arrows-alt" - }); + + @action + onScroll = (e: React.UIEvent<HTMLDivElement>) => { + if (e.currentTarget) { + this._scrollY = e.currentTarget.scrollTop; + let ccv = this.props.ContainingCollectionView; + if (ccv) { + ccv.props.Document.scrollY = this._scrollY; + } } } + render() { - let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); + // uses mozilla pdf as default + const pdfUrl = Cast(this.props.Document.data, PdfField); + if (!(pdfUrl instanceof PdfField)) return <div>{`pdf, ${this.props.Document.data}, not found`}</div>; + let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); return ( - <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onContextMenu={this.onContextMenu} > - {this.pdfRenderer} - </div > + <div className={classname} + onScroll={this.onScroll} + style={{ + marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px` + }} + ref={this._mainCont} + onWheel={(e: React.WheelEvent) => { + e.stopPropagation(); + }}> + <PDFViewer url={pdfUrl.url.pathname} loaded={this.loaded} scrollY={this._scrollY} parent={this} /> + {/* <div style={{ width: "100px", height: "300px" }}></div> */} + {this.settingsPanel()} + </div> ); } diff --git a/src/client/views/pdf/PDFAnnotationLayer.tsx b/src/client/views/pdf/PDFAnnotationLayer.tsx new file mode 100644 index 000000000..1f49e0d2f --- /dev/null +++ b/src/client/views/pdf/PDFAnnotationLayer.tsx @@ -0,0 +1,24 @@ +import React = require("react"); +import { observer } from "mobx-react"; + +interface IAnnotationProps { + +} + +@observer +export class PDFAnnotationLayer extends React.Component { + onPointerDown = (e: React.PointerEvent) => { + if (e.ctrlKey) { + console.log("annotating"); + e.stopPropagation(); + } + } + + render() { + return ( + <div className="pdfAnnotationLayer-cont" style={{ width: "100%", height: "100%", position: "relative", top: "-200%" }} onPointerDown={this.onPointerDown}> + + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss new file mode 100644 index 000000000..a4624b1f6 --- /dev/null +++ b/src/client/views/pdf/PDFMenu.scss @@ -0,0 +1,32 @@ +.pdfMenu-cont { + position: absolute; + z-index: 10000; + height: 35px; + background: #323232; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + border-radius: 0px 6px 6px 6px; + overflow: hidden; + display: flex; + + .pdfMenu-button { + background-color: transparent; + width: 35px; + height: 35px; + } + + .pdfMenu-button:hover { + background-color: #121212; + } + + .pdfMenu-dragger { + height: 100%; + transition: width .2s; + } + + .pdfMenu-addTag { + display: grid; + width: 200px; + padding: 5px; + grid-template-columns: 90px 20px 90px; + } +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx new file mode 100644 index 000000000..aeed5213c --- /dev/null +++ b/src/client/views/pdf/PDFMenu.tsx @@ -0,0 +1,270 @@ +import React = require("react"); +import "./PDFMenu.scss"; +import { observable, action, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { emptyFunction, returnZero, returnTrue, returnFalse } from "../../../Utils"; +import { Doc } from "../../../new_fields/Doc"; +import { DragManager } from "../../util/DragManager"; +import { DocUtils } from "../../documents/Documents"; + +@observer +export default class PDFMenu extends React.Component { + static Instance: PDFMenu; + + @observable private _top: number = -300; + @observable private _left: number = -300; + @observable private _opacity: number = 1; + @observable private _transition: string = "opacity 0.5s"; + @observable private _transitionDelay: string = ""; + + + StartDrag: (e: PointerEvent) => void = emptyFunction; + Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction; + Delete: () => void = emptyFunction; + Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; + AddTag: (key: string, value: string) => boolean = returnFalse; + + @observable public Highlighting: boolean = false; + @observable public Status: "pdf" | "annotation" | "snippet" | "" = ""; + @observable public Pinned: boolean = false; + + public Marquee: { left: number; top: number; width: number; height: number; } | undefined; + + private _offsetY: number = 0; + private _offsetX: number = 0; + private _mainCont: React.RefObject<HTMLDivElement>; + private _dragging: boolean = false; + private _snippetButton: React.RefObject<HTMLButtonElement>; + @observable private _keyValue: string = ""; + @observable private _valueValue: string = ""; + @observable private _added: boolean = false; + + constructor(props: Readonly<{}>) { + super(props); + + PDFMenu.Instance = this; + + this._mainCont = React.createRef(); + this._snippetButton = React.createRef(); + } + + pointerDown = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.pointerMove); + document.addEventListener("pointermove", this.pointerMove); + document.removeEventListener("pointerup", this.pointerUp); + document.addEventListener("pointerup", this.pointerUp); + + e.stopPropagation(); + e.preventDefault(); + } + + pointerMove = (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (this._dragging) { + return; + } + + this.StartDrag(e); + this._dragging = true; + } + + pointerUp = (e: PointerEvent) => { + this._dragging = false; + document.removeEventListener("pointermove", this.pointerMove); + document.removeEventListener("pointerup", this.pointerUp); + e.stopPropagation(); + e.preventDefault(); + } + + @action + jumpTo = (x: number, y: number, forceJump: boolean = false) => { + if (!this.Pinned || forceJump) { + this._transition = this._transitionDelay = ""; + this._opacity = 1; + this._left = x; + this._top = y; + } + } + + @action + fadeOut = (forceOut: boolean) => { + if (!this.Pinned) { + if (this._opacity === 0.2) { + this._transition = "opacity 0.1s"; + this._transitionDelay = ""; + this._opacity = 0; + this._left = this._top = -300; + } + + if (forceOut) { + this._transition = ""; + this._transitionDelay = ""; + this._opacity = 0; + this._left = this._top = -300; + } + } + } + + @action + pointerLeave = (e: React.PointerEvent) => { + if (!this.Pinned) { + this._transition = "opacity 0.5s"; + this._transitionDelay = "1s"; + this._opacity = 0.2; + setTimeout(() => this.fadeOut(false), 3000); + } + } + + @action + pointerEntered = (e: React.PointerEvent) => { + this._transition = "opacity 0.1s"; + this._transitionDelay = ""; + this._opacity = 1; + } + + @action + togglePin = (e: React.MouseEvent) => { + this.Pinned = !this.Pinned; + if (!this.Pinned) { + this.Highlighting = false; + } + } + + @action + dragging = (e: PointerEvent) => { + this._left = e.pageX - this._offsetX; + this._top = e.pageY - this._offsetY; + + e.stopPropagation(); + e.preventDefault(); + } + + dragEnd = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + e.stopPropagation(); + e.preventDefault(); + } + + dragStart = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.addEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + document.addEventListener("pointerup", this.dragEnd); + + this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX; + this._offsetY = e.nativeEvent.offsetY; + + e.stopPropagation(); + e.preventDefault(); + } + + @action + highlightClicked = (e: React.MouseEvent) => { + if (!this.Pinned) { + this.Highlight(undefined, "#f4f442"); + } + else { + this.Highlighting = !this.Highlighting; + this.Highlight(undefined, "#f4f442"); + } + } + + deleteClicked = (e: React.PointerEvent) => { + this.Delete(); + } + + handleContextMenu = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + } + + snippetStart = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.snippetDrag); + document.addEventListener("pointermove", this.snippetDrag); + document.removeEventListener("pointerup", this.snippetEnd); + document.addEventListener("pointerup", this.snippetEnd); + + e.stopPropagation(); + e.preventDefault(); + } + + snippetDrag = (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + if (this._dragging) { + return; + } + this._dragging = true; + + if (this.Marquee) { + this.Snippet(this.Marquee); + } + } + + snippetEnd = (e: PointerEvent) => { + this._dragging = false; + document.removeEventListener("pointermove", this.snippetDrag); + document.removeEventListener("pointerup", this.snippetEnd); + e.stopPropagation(); + e.preventDefault(); + } + + @action + keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._keyValue = e.currentTarget.value; + } + + @action + valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._valueValue = e.currentTarget.value; + } + + @action + addTag = (e: React.PointerEvent) => { + if (this._keyValue.length > 0 && this._valueValue.length > 0) { + this._added = this.AddTag(this._keyValue, this._valueValue); + + setTimeout( + () => { + runInAction(() => { + this._added = false; + }); + }, 1000 + ); + } + } + + render() { + let buttons = this.Status === "pdf" || this.Status === "snippet" ? [ + <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} key="1" + style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> + <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> + </button>, + <button className="pdfMenu-button" title="Drag to Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" key="2" /></button>, + this.Status === "snippet" ? <button className="pdfMenu-button" title="Drag to Snippetize Selection" onPointerDown={this.snippetStart} ref={this._snippetButton}><FontAwesomeIcon icon="cut" size="lg" /></button> : undefined, + <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} key="3" + style={this.Pinned ? { backgroundColor: "#121212" } : {}}> + <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> + </button> + ] : [ + <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>, + <div className="pdfMenu-addTag" key="2"> + <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> + <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> + </div>, + <button className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="3" /></button>, + ]; + + return ( + <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} + style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> + {buttons} + <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> + </div > + ); + } +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss new file mode 100644 index 000000000..11d3d7e27 --- /dev/null +++ b/src/client/views/pdf/PDFViewer.scss @@ -0,0 +1,128 @@ + +.textLayer { + div { + user-select: text; + } +} +.viewer-button-cont { + position: absolute; + display: flex; + justify-content: space-evenly; + align-items: center; +} + +.viewer-previousPage, +.viewer-nextPage { + background: grey; + font-weight: bold; + opacity: 0.5; + padding: 0 10px; + border-radius: 5px; +} + +.textLayer { + user-select: auto; +} +.viewer { + // position: absolute; + // top: 0; +} + +.pdfViewer-text { + + .page { + .canvasWrapper { + display: none; + } + + .textLayer { + position: relative; + user-select: none; + } + } +} + +.page-cont { + .textLayer { + user-select: auto; + + div { + user-select: text; + } + } +} + +.pdfViewer-overlayCont { + position: absolute; + width: 100%; + height: 100px; + background: #121721; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + overflow: hidden; + transition: left .5s; +} + +.pdfViewer-overlaySearchBar { + width: 20%; + height: 100%; + font-size: 30px; + padding: 5px; +} + +.pdfViewer-overlayButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 70px; + background: none; + padding: 0; + position: absolute; + + .pdfViewer-overlayButton-arrow { + width: 0; + height: 0; + border-top: 25px solid transparent; + border-bottom: 25px solid transparent; + border-right: 25px solid #121721; + transition: all 0.5s; + } + + .pdfViewer-overlayButton-iconCont { + background: #121721; + height: 50px; + width: 70px; + display: flex; + justify-content: center; + align-items: center; + margin-left: -2px; + border-radius: 3px; + } +} + +.pdfViewer-overlayButton:hover { + background: none; +} + +.pdfViewer-annotationBox { + position: absolute; + background-color: red; + opacity: 0.1; +} + +.pdfViewer-annotationLayer { + position: absolute; + top: 0; +} + + + +.pdfViewer-pinAnnotation { + background-color: red; + position: absolute; + border-radius: 100%; +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx new file mode 100644 index 000000000..bafc3cbae --- /dev/null +++ b/src/client/views/pdf/PDFViewer.tsx @@ -0,0 +1,822 @@ +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as Pdfjs from "pdfjs-dist"; +import "pdfjs-dist/web/pdf_viewer.css"; +import * as rp from "request-promise"; +import { Dictionary } from "typescript-collections"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types"; +import { emptyFunction } from "../../../Utils"; +import { DocServer } from "../../DocServer"; +import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents"; +import { DocumentManager } from "../../util/DocumentManager"; +import { DragManager } from "../../util/DragManager"; +import { DocumentView } from "../nodes/DocumentView"; +import { PDFBox } from "../nodes/PDFBox"; +import Page from "./Page"; +import "./PDFViewer.scss"; +import React = require("react"); +import PDFMenu from "./PDFMenu"; +import { UndoManager } from "../../util/UndoManager"; +import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); + +export const scale = 2; +interface IPDFViewerProps { + url: string; + loaded: (nw: number, nh: number, np: number) => void; + scrollY: number; + parent: PDFBox; +} + +/** + * Wrapper that loads the PDF and cascades the pdf down + */ +@observer +export class PDFViewer extends React.Component<IPDFViewerProps> { + @observable _pdf: Opt<Pdfjs.PDFDocumentProxy>; + private _mainDiv = React.createRef<HTMLDivElement>(); + + @action + componentDidMount() { + Pdfjs.getDocument(this.props.url).promise.then(pdf => runInAction(() => this._pdf = pdf)); + } + + render() { + return ( + <div ref={this._mainDiv}> + {!this._pdf ? (null) : + <Viewer pdf={this._pdf} loaded={this.props.loaded} scrollY={this.props.scrollY} parent={this.props.parent} mainCont={this._mainDiv} url={this.props.url} />} + </div> + ); + } +} + +interface IViewerProps { + pdf: Pdfjs.PDFDocumentProxy; + loaded: (nw: number, nh: number, np: number) => void; + scrollY: number; + parent: PDFBox; + mainCont: React.RefObject<HTMLDivElement>; + url: string; +} + +/** + * Handles rendering and virtualization of the pdf + */ +@observer +class Viewer extends React.Component<IViewerProps> { + // _visibleElements is the array of JSX elements that gets rendered + @observable.shallow private _visibleElements: JSX.Element[] = []; + // _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder + @observable private _isPage: string[] = []; + @observable private _pageSizes: { width: number, height: number }[] = []; + @observable private _annotations: Doc[] = []; + @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @observable private _script: CompileResult | undefined; + @observable private _searching: boolean = false; + + @observable public Index: number = -1; + + private _pageBuffer: number = 1; + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + private _reactionDisposer?: IReactionDisposer; + private _annotationReactionDisposer?: IReactionDisposer; + private _dropDisposer?: DragManager.DragDropDisposer; + private _filterReactionDisposer?: IReactionDisposer; + private _activeReactionDisposer?: IReactionDisposer; + private _viewer: React.RefObject<HTMLDivElement>; + private _mainCont: React.RefObject<HTMLDivElement>; + // private _textContent: Pdfjs.TextContent[] = []; + private _pdfFindController: any; + private _searchString: string = ""; + private _rendered: boolean = false; + private _pageIndex: number = -1; + private _matchIndex: number = 0; + + constructor(props: IViewerProps) { + super(props); + + let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField); + this._script = scriptfield ? scriptfield.script : CompileScript("return true"); + this._viewer = React.createRef(); + this._mainCont = React.createRef(); + } + + componentDidUpdate = (prevProps: IViewerProps) => { + if (this.scrollY !== prevProps.scrollY) { + this.renderPages(); + } + } + + @action + componentDidMount = () => { + this._reactionDisposer = reaction( + + () => [this.props.parent.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0], + async () => { + await this.initialLoad(); + this.renderPages(); + }, { fireImmediately: true }); + + this._annotationReactionDisposer = reaction( + () => this.props.parent.Document && DocListCast(this.props.parent.Document.annotations), + (annotations: Doc[]) => + annotations && annotations.length && this.renderAnnotations(annotations, true), + { fireImmediately: true }); + + this._activeReactionDisposer = reaction( + () => this.props.parent.props.active(), + () => { + runInAction(() => { + if (!this.props.parent.props.active()) { + this._searching = false; + this._pdfFindController = null; + if (this._viewer.current) { + let cns = this._viewer.current.childNodes; + for (let i = cns.length - 1; i >= 0; i--) { + cns.item(i).remove(); + } + } + } + }); + } + ); + + if (this.props.parent.props.ContainingCollectionView) { + this._filterReactionDisposer = reaction( + () => this.props.parent.Document.filterScript, + () => { + runInAction(() => { + let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField); + this._script = scriptfield ? scriptfield.script : CompileScript("return true"); + if (this.props.parent.props.ContainingCollectionView) { + let ccvAnnos = DocListCast(this.props.parent.props.ContainingCollectionView.props.Document.annotations); + ccvAnnos.forEach(d => { + if (this._script && this._script.compiled) { + let run = this._script.run(d); + if (run.success) { + d.opacity = run.result ? 1 : 0; + } + } + }); + } + }); + } + ); + } + + if (this._mainCont.current) { + this._dropDisposer = this._mainCont.current && DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); + } + } + + componentWillUnmount = () => { + this._reactionDisposer && this._reactionDisposer(); + this._annotationReactionDisposer && this._annotationReactionDisposer(); + this._filterReactionDisposer && this._filterReactionDisposer(); + this._dropDisposer && this._dropDisposer(); + } + + scrollTo(y: number) { + this.props.parent.scrollTo(y); + } + + @action + initialLoad = async () => { + if (this._pageSizes.length === 0) { + let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); + this._isPage = Array<string>(this.props.pdf.numPages); + // this._textContent = Array<Pdfjs.TextContent>(this.props.pdf.numPages); + for (let i = 0; i < this.props.pdf.numPages; i++) { + await this.props.pdf.getPage(i + 1).then(page => runInAction(() => { + // pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale }; + let x = page.getViewport(scale); + // page.getTextContent().then((text: Pdfjs.TextContent) => { + // // let tc = new Pdfjs.TextContentItem() + // // let tc = {str: } + // this._textContent[i] = text; + // // text.items.forEach(t => { + // // tcStr += t.str; + // // }) + // }); + pageSizes[i] = { width: x.width, height: x.height }; + })); + } + runInAction(() => + Array.from(Array((this._pageSizes = pageSizes).length).keys()).map(this.getPlaceholderPage)); + this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); + // this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); + + let startY = NumCast(this.props.parent.Document.startY); + let ccv = this.props.parent.props.ContainingCollectionView; + if (ccv) { + ccv.props.Document.panY = startY; + } + this.props.parent.Document.scrollY = 0; + this.props.parent.Document.scrollY = startY + 1; + } + } + + makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => { + let annoDocs: Doc[] = []; + let mainAnnoDoc = Docs.CreateInstance(new Doc(), "", {}); + + mainAnnoDoc.page = Math.round(Math.random()); + this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => { + for (let anno of value) { + let annoDoc = new Doc(); + if (anno.style.left) annoDoc.x = parseInt(anno.style.left) / scale; + if (anno.style.top) annoDoc.y = parseInt(anno.style.top) / scale; + if (anno.style.height) annoDoc.height = parseInt(anno.style.height) / scale; + if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale; + annoDoc.page = key; + annoDoc.target = sourceDoc; + annoDoc.group = mainAnnoDoc; + annoDoc.color = color; + annoDoc.type = AnnotationTypes.Region; + annoDocs.push(annoDoc); + anno.remove(); + } + }); + + mainAnnoDoc.annotations = new List<Doc>(annoDocs); + if (sourceDoc) { + DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title)); + } + this._savedAnnotations.clear(); + return mainAnnoDoc; + } + + drop = async (e: Event, de: DragManager.DropEvent) => { + if (de.data instanceof DragManager.LinkDragData) { + let sourceDoc = de.data.linkSourceDocument; + let destDoc = this.makeAnnotationDocument(sourceDoc, 1, "red"); + let targetAnnotations = DocListCast(this.props.parent.Document.annotations); + if (targetAnnotations) { + targetAnnotations.push(destDoc); + this.props.parent.Document.annotations = new List<Doc>(targetAnnotations); + } + else { + this.props.parent.Document.annotations = new List<Doc>([destDoc]); + } + e.stopPropagation(); + } + } + /** + * Called by the Page class when it gets rendered, initializes the lists and + * puts a placeholder with all of the correct page sizes when all of the pages have been loaded. + */ + @action + pageLoaded = (index: number, page: Pdfjs.PDFPageViewport): void => { + this.props.loaded(page.width, page.height, this.props.pdf.numPages); + } + + @action + getPlaceholderPage = (page: number) => { + if (this._isPage[page] !== "none") { + this._isPage[page] = "none"; + this._visibleElements[page] = ( + <div key={`${this.props.url}-placeholder-${page + 1}`} className="pdfviewer-placeholder" + style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }} /> + ); + } + } + + @action + getRenderedPage = (page: number) => { + if (this._isPage[page] !== "page") { + this._isPage[page] = "page"; + this._visibleElements[page] = ( + <Page + size={this._pageSizes[page]} + pdf={this.props.pdf} + page={page} + numPages={this.props.pdf.numPages} + key={`${this.props.url}-rendered-${page + 1}`} + name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`} + pageLoaded={this.pageLoaded} + parent={this.props.parent} + makePin={emptyFunction} + renderAnnotations={this.renderAnnotations} + createAnnotation={this.createAnnotation} + sendAnnotations={this.receiveAnnotations} + makeAnnotationDocuments={this.makeAnnotationDocument} + getScrollFromPage={this.getScrollFromPage} + {...this.props} /> + ); + } + } + + // change the address to be the file address of the PNG version of each page + // file address of the pdf + @action + getPageImage = async (page: number) => { + let handleError = () => this.getRenderedPage(page); + if (this._isPage[page] !== "image") { + this._isPage[page] = "image"; + const address = this.props.url; + try { + let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`))); + runInAction(() => this._visibleElements[page] = + <img key={res.path} src={res.path} onError={handleError} + style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />); + } catch (e) { + + } + } + } + + @computed get scrollY(): number { return this.props.scrollY; } + + // startIndex: where to start rendering pages + @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.scrollY) - this._pageBuffer); } + + // endIndex: where to end rendering pages + @computed get endIndex(): number { + return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.scrollY + this._pageSizes[0].height) + this._pageBuffer); + } + + @action + renderPages = () => { + for (let i = 0; i < this.props.pdf.numPages; i++) { + if (i < this.startIndex || i > this.endIndex) { + this.getPlaceholderPage(i); // pages outside of the pdf use empty stand-in divs + } else { + if (this.props.parent.props.active()) { + this.getRenderedPage(i); + } else { + this.getPageImage(i); + } + } + } + } + + @action + private renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => { + if (removeOldAnnotations) { + this._annotations = annotations; + } + else { + this._annotations.push(...annotations); + this._annotations = new Array<Doc>(...this._annotations); + } + } + + @action + receiveAnnotations = (annotations: HTMLDivElement[], page: number) => { + if (page === -1) { + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations)); + } + else { + this._savedAnnotations.setValue(page, annotations); + } + } + + sendAnnotations = (page: number): HTMLDivElement[] | undefined => { + return this._savedAnnotations.getValue(page); + } + + // get the page index that the vertical offset passed in is on + getPageFromScroll = (vOffset: number) => { + let index = 0; + let currOffset = vOffset; + while (index < this._pageSizes.length && currOffset - this._pageSizes[index].height > 0) { + currOffset -= this._pageSizes[index++].height; + } + return index; + } + + getScrollFromPage = (index: number): number => { + let counter = 0; + for (let i = 0; i < Math.min(this.props.pdf.numPages, index); i++) { + counter += this._pageSizes[i].height; + } + return counter; + } + + createAnnotation = (div: HTMLDivElement, page: number) => { + if (this._annotationLayer.current) { + if (div.style.top) { + div.style.top = (parseInt(div.style.top) + this.getScrollFromPage(page)).toString(); + } + this._annotationLayer.current.append(div); + let savedPage = this._savedAnnotations.getValue(page); + if (savedPage) { + savedPage.push(div); + this._savedAnnotations.setValue(page, savedPage); + } + else { + this._savedAnnotations.setValue(page, [div]); + } + } + } + + renderAnnotation = (anno: Doc, index: number): JSX.Element[] => { + let annotationDocs = DocListCast(anno.annotations); + let res = annotationDocs.map(a => { + let type = NumCast(a.type); + switch (type) { + // case AnnotationTypes.Pin: + // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + case AnnotationTypes.Region: + return <RegionAnnotation parent={this} document={a} index={index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + default: + return <div></div>; + } + }); + return res; + } + + @action + pointerDown = () => { + // this._searching = false; + } + + @action + search = (searchString: string) => { + if (searchString.length === 0) { + return; + } + + if (this._rendered) { + this._pdfFindController.executeCommand('find', + { + caseSensitive: false, + findPrevious: undefined, + highlightAll: true, + phraseSearch: true, + query: searchString + }); + } + else { + let container = this._mainCont.current; + if (container) { + container.addEventListener("pagerendered", () => { + console.log("rendered"); + this._pdfFindController.executeCommand('find', + { + caseSensitive: false, + findPrevious: undefined, + highlightAll: true, + phraseSearch: true, + query: searchString + }); + this._rendered = true; + }); + } + } + + // let viewer = this._viewer.current; + + // if (!this._pdfFindController) { + // if (container && viewer) { + // let simpleLinkService = new SimpleLinkService(); + // let pdfViewer = new PDFJSViewer.PDFViewer({ + // container: container, + // viewer: viewer, + // linkService: simpleLinkService + // }); + // simpleLinkService.setPdf(this.props.pdf); + // container.addEventListener("pagesinit", () => { + // pdfViewer.currentScaleValue = 1; + // }); + // container.addEventListener("pagerendered", () => { + // console.log("rendered"); + // this._pdfFindController.executeCommand('find', + // { + // caseSensitive: false, + // findPrevious: undefined, + // highlightAll: true, + // phraseSearch: true, + // query: searchString + // }); + // }); + // pdfViewer.setDocument(this.props.pdf); + // this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer); + // // this._pdfFindController._linkService = pdfLinkService; + // pdfViewer.findController = this._pdfFindController; + // } + // } + // else { + // this._pdfFindController.executeCommand('find', + // { + // caseSensitive: false, + // findPrevious: undefined, + // highlightAll: true, + // phraseSearch: true, + // query: searchString + // }); + // } + } + + searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._searchString = e.currentTarget.value; + } + + @action + toggleSearch = (e: React.MouseEvent) => { + e.stopPropagation(); + this._searching = !this._searching; + + if (this._searching) { + let container = this._mainCont.current; + let viewer = this._viewer.current; + + if (!this._pdfFindController) { + if (container && viewer) { + let simpleLinkService = new SimpleLinkService(); + let pdfViewer = new PDFJSViewer.PDFViewer({ + container: container, + viewer: viewer, + linkService: simpleLinkService + }); + simpleLinkService.setPdf(this.props.pdf); + container.addEventListener("pagesinit", () => { + pdfViewer.currentScaleValue = 1; + }); + container.addEventListener("pagerendered", () => { + console.log("rendered"); + this._rendered = true; + }); + pdfViewer.setDocument(this.props.pdf); + this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer); + // this._pdfFindController._linkService = pdfLinkService; + pdfViewer.findController = this._pdfFindController; + } + } + } + else { + this._pdfFindController = null; + if (this._viewer.current) { + let cns = this._viewer.current.childNodes; + for (let i = cns.length - 1; i >= 0; i--) { + cns.item(i).remove(); + } + } + } + } + + @action + prevAnnotation = (e: React.MouseEvent) => { + e.stopPropagation(); + + if (this.Index > 0) { + this.Index--; + } + } + + @action + nextAnnotation = (e: React.MouseEvent) => { + e.stopPropagation(); + + let compiled = this._script; + if (this.Index < this._annotations.filter(anno => { + if (compiled && compiled.compiled) { + let run = compiled.run({ this: anno }); + if (run.success) { + return run.result; + } + } + return true; + }).length) { + this.Index++; + } + } + + nextResult = () => { + // if (this._viewer.current) { + // let results = this._pdfFindController.pageMatches; + // if (results && results.length) { + // if (this._pageIndex === this.props.pdf.numPages && this._matchIndex === results[this._pageIndex].length - 1) { + // return; + // } + // if (this._pageIndex === -1 || this._matchIndex === results[this._pageIndex].length - 1) { + // this._matchIndex = 0; + // this._pageIndex++; + // } + // else { + // this._matchIndex++; + // } + // this._pdfFindController._nextMatch() + // let nextMatch = this._viewer.current.children[this._pageIndex].children[1].children[results[this._pageIndex][this._matchIndex]]; + // rconsole.log(nextMatch); + // this.props.parent.scrollTo(nextMatch.getBoundingClientRect().top); + // nextMatch.setAttribute("style", nextMatch.getAttribute("style") ? nextMatch.getAttribute("style") + ", background-color: green" : "background-color: green"); + // } + // } + } + + render() { + let compiled = this._script; + return ( + <div ref={this._mainCont} style={{ pointerEvents: "all" }} onPointerDown={this.pointerDown}> + <div className="viewer" style={this._searching ? { position: "absolute", top: 0 } : {}}> + {this._visibleElements} + </div> + <div className="pdfViewer-text" ref={this._viewer} style={{ transform: "scale(1.5)", transformOrigin: "top left" }} /> + <div className="pdfViewer-annotationLayer" + style={{ + height: this.props.parent.Document.nativeHeight, width: `100%`, + pointerEvents: this.props.parent.props.active() ? "none" : "all" + }}> + <div className="pdfViewer-annotationLayer-subCont" ref={this._annotationLayer}> + {this._annotations.filter(anno => { + if (compiled && compiled.compiled) { + let run = compiled.run({ this: anno }); + if (run.success) { + return run.result; + } + } + return true; + }).map((anno: Doc, index: number) => this.renderAnnotation(anno, index))} + </div> + </div> + <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()} + style={{ + bottom: -this.props.scrollY, + left: `${this._searching ? 0 : 100}%` + }}> + <button className="pdfViewer-overlayButton" title="Open Search Bar"></button> + {/* <button title="Previous Result" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="arrow-up" size="3x" color="white" /></button> + <button title="Next Result" onClick={this.nextResult}><FontAwesomeIcon icon="arrow-down" size="3x" color="white" /></button> */} + <input placeholder="Search" className="pdfViewer-overlaySearchBar" onChange={this.searchStringChanged} /> + <button title="Search" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="search" size="3x" color="white" /></button> + </div> + <button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation" + style={{ bottom: -this.props.scrollY + 280, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}> + <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="3x" /> + </div> + </button> + <button className="pdfViewer-overlayButton" onClick={this.nextAnnotation} title="Next Annotation" + style={{ bottom: -this.props.scrollY + 200, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}> + <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="3x" /> + </div> + </button> + <button className="pdfViewer-overlayButton" onClick={this.toggleSearch} title="Open Search Bar" + style={{ bottom: -this.props.scrollY + 10, right: 0, display: this.props.parent.props.active() ? "flex" : "none" }}> + <div className="pdfViewer-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> + <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> + <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="3x" /> + </div> + </button> + </div > + ); + } +} + +export enum AnnotationTypes { + Region +} + +interface IAnnotationProps { + x: number; + y: number; + width: number; + height: number; + index: number; + parent: Viewer; + document: Doc; +} + +@observer +class RegionAnnotation extends React.Component<IAnnotationProps> { + @observable private _backgroundColor: string = "red"; + + private _reactionDisposer?: IReactionDisposer; + private _scrollDisposer?: IReactionDisposer; + private _mainCont: React.RefObject<HTMLDivElement>; + + constructor(props: IAnnotationProps) { + super(props); + + this._mainCont = React.createRef(); + } + + componentDidMount() { + this._reactionDisposer = reaction( + () => BoolCast(this.props.document.delete), + () => { + if (BoolCast(this.props.document.delete)) { + if (this._mainCont.current) { + this._mainCont.current.style.display = "none"; + } + } + }, + { fireImmediately: true } + ); + + this._scrollDisposer = reaction( + () => this.props.parent.Index, + () => { + if (this.props.parent.Index === this.props.index) { + this.props.parent.scrollTo(this.props.y - 50); + } + } + ); + } + + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); + this._scrollDisposer && this._scrollDisposer(); + } + + deleteAnnotation = () => { + let annotation = DocListCast(this.props.parent.props.parent.Document.annotations); + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group && annotation.indexOf(group) !== -1) { + let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); + this.props.parent.props.parent.Document.annotations = new List<Doc>(newAnnotations); + } + + if (group) { + let groupAnnotations = DocListCast(group.annotations); + groupAnnotations.forEach(anno => anno.delete = true); + } + + PDFMenu.Instance.fadeOut(true); + } + + @action + onPointerDown = (e: React.PointerEvent) => { + if (e.button === 0) { + let targetDoc = Cast(this.props.document.target, Doc, null); + if (targetDoc) { + DocumentManager.Instance.jumpToDocument(targetDoc, true); + } + } + if (e.button === 2) { + PDFMenu.Instance.Status = "annotation"; + PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); + PDFMenu.Instance.Pinned = false; + PDFMenu.Instance.AddTag = this.addTag.bind(this); + PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); + } + } + + addTag = (key: string, value: string): boolean => { + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group) { + let valNum = parseInt(value); + group[key] = isNaN(valNum) ? value : valNum; + return true; + } + return false; + } + + render() { + return ( + <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont} + style={{ + top: this.props.y * scale, + left: this.props.x * scale, + width: this.props.width * scale, + height: this.props.height * scale, + pointerEvents: "all", + backgroundColor: this.props.parent.Index === this.props.index ? "goldenrod" : StrCast(this.props.document.color) + }}></div> + ); + } +} + +class SimpleLinkService { + externalLinkTarget: any = null; + externalLinkRel: any = null; + pdf: any = null; + + navigateTo(dest: any) { } + + getDestinationHash(dest: any) { return "#"; } + + getAnchorUrl(hash: any) { return "#"; } + + setHash(hash: any) { } + + executeNamedAction(action: any) { } + + cachePageRef(pageNum: any, pageRef: any) { } + + get pagesCount() { + return this.pdf ? this.pdf.numPages : 0; + } + + get page() { + return 0; + } + + setPdf(pdf: any) { + this.pdf = pdf; + } + + get rotation() { + return 0; + } + set rotation(value: any) { } +}
\ No newline at end of file diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx new file mode 100644 index 000000000..01aa96705 --- /dev/null +++ b/src/client/views/pdf/Page.tsx @@ -0,0 +1,412 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { observable, action, runInAction, IReactionDisposer, reaction } from "mobx"; +import * as Pdfjs from "pdfjs-dist"; +import { Opt, Doc, FieldResult, Field, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import "./PDFViewer.scss"; +import "pdfjs-dist/web/pdf_viewer.css"; +import { PDFBox } from "../nodes/PDFBox"; +import { DragManager } from "../../util/DragManager"; +import { Docs, DocUtils } from "../../documents/Documents"; +import { List } from "../../../new_fields/List"; +import { emptyFunction } from "../../../Utils"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { listSpec } from "../../../new_fields/Schema"; +import { menuBar } from "prosemirror-menu"; +import { AnnotationTypes, PDFViewer, scale } from "./PDFViewer"; +import PDFMenu from "./PDFMenu"; +import { UndoManager } from "../../util/UndoManager"; +import { copy } from "typescript-collections/dist/lib/arrays"; + + +interface IPageProps { + size: { width: number, height: number }; + pdf: Opt<Pdfjs.PDFDocumentProxy>; + name: string; + numPages: number; + page: number; + pageLoaded: (index: number, page: Pdfjs.PDFPageViewport) => void; + parent: PDFBox; + renderAnnotations: (annotations: Doc[], removeOld: boolean) => void; + makePin: (x: number, y: number, page: number) => void; + sendAnnotations: (annotations: HTMLDivElement[], page: number) => void; + createAnnotation: (div: HTMLDivElement, page: number) => void; + makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string) => Doc; + getScrollFromPage: (page: number) => number; +} + +@observer +export default class Page extends React.Component<IPageProps> { + @observable private _state: string = "N/A"; + @observable private _width: number = this.props.size.width; + @observable private _height: number = this.props.size.height; + @observable private _page: Opt<Pdfjs.PDFPageProxy>; + @observable private _currPage: number = this.props.page + 1; + @observable private _marqueeX: number = 0; + @observable private _marqueeY: number = 0; + @observable private _marqueeWidth: number = 0; + @observable private _marqueeHeight: number = 0; + @observable private _rotate: string = ""; + + private _canvas: React.RefObject<HTMLCanvasElement>; + private _textLayer: React.RefObject<HTMLDivElement>; + private _annotationLayer: React.RefObject<HTMLDivElement>; + private _marquee: React.RefObject<HTMLDivElement>; + private _curly: React.RefObject<HTMLImageElement>; + private _marqueeing: boolean = false; + private _reactionDisposer?: IReactionDisposer; + + constructor(props: IPageProps) { + super(props); + this._canvas = React.createRef(); + this._textLayer = React.createRef(); + this._annotationLayer = React.createRef(); + this._marquee = React.createRef(); + this._curly = React.createRef(); + } + + componentDidMount = (): void => { + if (this.props.pdf) { + this.update(this.props.pdf); + } + } + + componentWillUnmount = (): void => { + if (this._reactionDisposer) { + this._reactionDisposer(); + } + } + + componentDidUpdate = (): void => { + if (this.props.pdf) { + this.update(this.props.pdf); + } + } + + private update = (pdf: Pdfjs.PDFDocumentProxy): void => { + if (pdf) { + this.loadPage(pdf); + } + else { + this._state = "loading"; + } + } + + private loadPage = (pdf: Pdfjs.PDFDocumentProxy): void => { + if (this._state === "rendering" || this._page) return; + + pdf.getPage(this._currPage).then( + (page: Pdfjs.PDFPageProxy): void => { + this._state = "rendering"; + this.renderPage(page); + } + ); + } + + @action + private renderPage = (page: Pdfjs.PDFPageProxy): void => { + // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes + let viewport = page.getViewport(scale); + let canvas = this._canvas.current; + let textLayer = this._textLayer.current; + if (canvas && textLayer) { + let ctx = canvas.getContext("2d"); + canvas.width = viewport.width; + this._width = viewport.width; + canvas.height = viewport.height; + this._height = viewport.height; + this.props.pageLoaded(this._currPage, viewport); + if (ctx) { + // renders the page onto the canvas context + page.render({ canvasContext: ctx, viewport: viewport }); + // renders text onto the text container + page.getTextContent().then((res: Pdfjs.TextContent): void => { + //@ts-ignore + Pdfjs.renderTextLayer({ + textContent: res, + container: textLayer, + viewport: viewport + }); + }); + + this._page = page; + } + } + } + + @action + highlight = (targetDoc?: Doc, color: string = "red") => { + // creates annotation documents for current highlights + let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color); + let targetAnnotations = Cast(this.props.parent.Document.annotations, listSpec(Doc)); + if (targetAnnotations === undefined) { + Doc.GetProto(this.props.parent.Document).annotations = new List([annotationDoc]); + } else { + targetAnnotations.push(annotationDoc); + } + return annotationDoc; + } + + /** + * This is temporary for creating annotations from highlights. It will + * start a drag event and create or put the necessary info into the drag event. + */ + @action + startDrag = (e: PointerEvent): void => { + e.preventDefault(); + e.stopPropagation(); + let thisDoc = this.props.parent.Document; + // document that this annotation is linked to + let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" }); + targetDoc.targetPage = this.props.page; + let annotationDoc = this.highlight(targetDoc, "red"); + // create dragData and star tdrag + let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDoc, targetDoc); + if (this._textLayer.current) { + DragManager.StartAnnotationDrag([this._textLayer.current], dragData, e.pageX, e.pageY, { + handlers: { + dragComplete: emptyFunction, + }, + hideSource: false + }); + } + } + + // cleans up events and boolean + endDrag = (e: PointerEvent): void => { + // document.removeEventListener("pointermove", this.startDrag); + // document.removeEventListener("pointerup", this.endDrag); + e.stopPropagation(); + } + + createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => { + let doc = this.props.parent.Document; + let view = Doc.MakeAlias(doc); + let data = Doc.MakeDelegate(doc.proto!); + view.proto = data; + view.nativeHeight = marquee.height; + view.startY = marquee.top + this.props.getScrollFromPage(this.props.page); + view.width = doc[WidthSym](); + let dragData = new DragManager.DocumentDragData([view], [undefined]); + DragManager.StartDocumentDrag([], dragData, 0, 0); + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + // if alt+left click, drag and annotate + if (e.altKey && e.button === 0) { + e.stopPropagation(); + + // document.removeEventListener("pointermove", this.startDrag); + // document.addEventListener("pointermove", this.startDrag); + // document.removeEventListener("pointerup", this.endDrag); + // document.addEventListener("pointerup", this.endDrag); + } + else if (e.button === 0) { + PDFMenu.Instance.StartDrag = this.startDrag; + PDFMenu.Instance.Highlight = this.highlight; + PDFMenu.Instance.Snippet = this.createSnippet; + PDFMenu.Instance.Status = "pdf"; + PDFMenu.Instance.fadeOut(true); + let target: any = e.target; + if (target && target.parentElement === this._textLayer.current) { + e.stopPropagation(); + } + else { + // set marquee x and y positions to the spatially transformed position + let current = this._textLayer.current; + if (current) { + let boundingRect = current.getBoundingClientRect(); + this._marqueeX = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width); + this._marqueeY = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height); + } + this._marqueeing = true; + if (this._marquee.current) this._marquee.current.style.opacity = "0.2"; + } + document.removeEventListener("pointermove", this.onSelectStart); + document.addEventListener("pointermove", this.onSelectStart); + document.removeEventListener("pointerup", this.onSelectEnd); + document.addEventListener("pointerup", this.onSelectEnd); + if (!e.ctrlKey) { + this.props.sendAnnotations([], -1); + } + } + } + + @action + onSelectStart = (e: PointerEvent): void => { + let target: any = e.target; + if (this._marqueeing) { + let current = this._textLayer.current; + if (current) { + // transform positions and find the width and height to set the marquee to + let boundingRect = current.getBoundingClientRect(); + this._marqueeWidth = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width) - this._marqueeX; + this._marqueeHeight = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height) - this._marqueeY; + let { background, opacity, transform: transform } = this.getCurlyTransform(); + if (this._marquee.current && this._curly.current) { + this._marquee.current.style.background = background; + this._curly.current.style.opacity = opacity; + this._rotate = transform; + } + } + e.stopPropagation(); + e.preventDefault(); + } + else if (target && target.parentElement === this._textLayer.current) { + e.stopPropagation(); + } + } + + getCurlyTransform = (): { background: string, opacity: string, transform: string } => { + let background = "", opacity = "", transform = ""; + if (this._marquee.current && this._curly.current) { + if (this._marqueeWidth > 100 && this._marqueeHeight > 100) { + background = "red"; + opacity = "0"; + } + else { + background = "transparent"; + opacity = "1"; + } + + // split up for simplicity, could be done in a nested ternary. please do not. -syip2 + let ratio = this._marqueeWidth / this._marqueeHeight; + if (ratio > 1.5) { + // vertical + transform = "rotate(90deg) scale(1, 5)"; + } + else if (ratio < 0.5) { + // horizontal + transform = "scale(2, 1)"; + } + else { + // diagonal + transform = "rotate(45deg) scale(1.5, 1.5)"; + } + } + return { background: background, opacity: opacity, transform: transform }; + } + + @action + onSelectEnd = (e: PointerEvent): void => { + if (this._marqueeing) { + this._marqueeing = false; + if (this._marquee.current) { + let copy = document.createElement("div"); + // make a copy of the marquee + let style = this._marquee.current.style; + copy.style.left = style.left; + copy.style.top = style.top; + copy.style.width = style.width; + copy.style.height = style.height; + + // apply the appropriate background, opacity, and transform + let { background, opacity, transform } = this.getCurlyTransform(); + copy.style.background = background; + // if curly bracing, add a curly brace + if (opacity === "1" && this._curly.current) { + copy.style.opacity = opacity; + let img = this._curly.current.cloneNode(); + (img as any).style.opacity = opacity; + (img as any).style.transform = transform; + copy.appendChild(img); + } + else { + copy.style.opacity = style.opacity; + } + copy.className = this._marquee.current.className; + this.props.createAnnotation(copy, this.props.page); + this._marquee.current.style.opacity = "0"; + } + + if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { + if (!e.ctrlKey) { + PDFMenu.Instance.Status = "snippet"; + PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight }; + } + PDFMenu.Instance.jumpTo(e.clientX, e.clientY); + } + + this._marqueeHeight = this._marqueeWidth = 0; + } + else { + let sel = window.getSelection(); + if (sel && sel.type === "Range") { + this.createTextAnnotation(sel); + PDFMenu.Instance.jumpTo(e.clientX, e.clientY); + } + } + + + if (PDFMenu.Instance.Highlighting) { + this.highlight(undefined, "#f4f442"); + } + else { + PDFMenu.Instance.StartDrag = this.startDrag; + PDFMenu.Instance.Highlight = this.highlight; + } + document.removeEventListener("pointermove", this.onSelectStart); + document.removeEventListener("pointerup", this.onSelectEnd); + } + + @action + createTextAnnotation = (sel: Selection) => { + let clientRects = sel.getRangeAt(0).getClientRects(); + if (this._textLayer.current) { + let boundingRect = this._textLayer.current.getBoundingClientRect(); + for (let i = 0; i < clientRects.length; i++) { + let rect = clientRects.item(i); + if (rect && rect.width !== this._textLayer.current.getBoundingClientRect().width && rect.height !== this._textLayer.current.getBoundingClientRect().height) { + let annoBox = document.createElement("div"); + annoBox.className = "pdfViewer-annotationBox"; + // transforms the positions from screen onto the pdf div + annoBox.style.top = ((rect.top - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)).toString(); + annoBox.style.left = ((rect.left - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)).toString(); + annoBox.style.width = (rect.width * this._textLayer.current.offsetWidth / boundingRect.width).toString(); + annoBox.style.height = (rect.height * this._textLayer.current.offsetHeight / boundingRect.height).toString(); + this.props.createAnnotation(annoBox, this.props.page); + } + } + } + // clear selection + if (sel.empty) { // Chrome + sel.empty(); + } else if (sel.removeAllRanges) { // Firefox + sel.removeAllRanges(); + } + } + + doubleClick = (e: React.MouseEvent) => { + let target: any = e.target; + // if double clicking text + if (target && target.parentElement === this._textLayer.current) { + // do something to select the paragraph ideally + } + + let current = this._textLayer.current; + if (current) { + let boundingRect = current.getBoundingClientRect(); + let x = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width); + let y = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height); + this.props.makePin(x, y, this.props.page); + } + } + + render() { + return ( + <div onPointerDown={this.onPointerDown} onDoubleClick={this.doubleClick} className={"page-cont"} style={{ "width": this._width, "height": this._height }}> + <div className="canvasContainer"> + <canvas ref={this._canvas} /> + </div> + <div className="pdfInkingLayer-cont" ref={this._annotationLayer} style={{ width: "100%", height: "100%", position: "relative", top: "-100%" }}> + <div className="pdfViewer-annotationBox" ref={this._marquee} + style={{ left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, background: "transparent" }}> + <img ref={this._curly} src="https://static.thenounproject.com/png/331760-200.png" style={{ width: "100%", height: "100%", transform: `${this._rotate}` }} /> + </div> + </div> + <div className="textlayer" ref={this._textLayer} style={{ "position": "relative", "top": `-${2 * this._height}px`, "height": `${this._height}px` }} /> + </div> + ); + } +} diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx new file mode 100644 index 000000000..4afc0210f --- /dev/null +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -0,0 +1,403 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { NumCast, BoolCast, StrCast, Cast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { observable, action, computed, runInAction } from "mobx"; +import "./PresentationView.scss"; +import { Utils } from "../../../Utils"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; + + +library.add(faArrowUp); +library.add(fileSolid); +library.add(faLocationArrow); +library.add(faSearch); +library.add(fileRegular); + +interface PresentationElementProps { + mainDocument: Doc; + document: Doc; + index: number; + deleteDocument(index: number): void; + gotoDocument(index: number, fromDoc: number): Promise<void>; + allListElements: Doc[]; + groupMappings: Map<String, Doc[]>; + presStatus: boolean; + presButtonBackUp: Doc; + presGroupBackUp: Doc; + + +} + +//enum for the all kinds of buttons a doc in presentation can have +export enum buttonIndex { + Show = 0, + Navigate = 1, + HideTillPressed = 2, + FadeAfter = 3, + HideAfter = 4, + Group = 5, + +} + +/** + * This class models the view a document added to presentation will have in the presentation. + * It involves some functionality for its buttons and options. + */ +@observer +export default class PresentationElement extends React.Component<PresentationElementProps> { + + @observable private selectedButtons: boolean[]; + + + constructor(props: PresentationElementProps) { + super(props); + this.selectedButtons = new Array(6); + } + + /** + * Getter to get the status of the buttons. + */ + @computed + get selected() { + return this.selectedButtons; + } + + //Lifecycle function that makes sure that button BackUp is received when mounted. + async componentDidMount() { + this.receiveButtonBackUp(); + + } + + //Lifecycle function that makes sure button BackUp is received when not re-mounted bu re-rendered. + async componentDidUpdate() { + this.receiveButtonBackUp(); + } + + receiveButtonBackUp = async () => { + + //get the list that stores docs that keep track of buttons + let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); + if (!castedList) { + this.props.presButtonBackUp.selectedButtonDocs = castedList = new List<Doc>(); + } + //if this is the first time this doc mounts, push a doc for it to store + if (castedList.length <= this.props.index) { + let newDoc = new Doc(); + let defaultBooleanArray: boolean[] = new Array(6); + newDoc.selectedButtons = new List(defaultBooleanArray); + castedList.push(newDoc); + //otherwise update the selected buttons depending on storage. + } else { + let curDoc: Doc = await castedList[this.props.index]; + let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null); + if (selectedButtonOfDoc !== undefined) { + runInAction(() => this.selectedButtons = selectedButtonOfDoc); + } + } + + } + + /** + * The function that is called to group docs together. It tries to group a doc + * that turned grouping option with the above document. If that doc is grouped with + * other documents. Those other documents will be grouped with doc's above document as well. + */ + @action + onGroupClick = (document: Doc, index: number, buttonStatus: boolean) => { + let p = this.props; + if (index >= 1) { + //checking if options was turned true + if (buttonStatus) { + //getting the id of the above-doc and the doc + let aboveGuid = StrCast(p.allListElements[index - 1].presentId, null); + let docGuid = StrCast(document.presentId, null); + //the case where above-doc is already in group + if (p.groupMappings.has(aboveGuid)) { + let aboveArray = p.groupMappings.get(aboveGuid)!; + //case where doc is already in group + if (p.groupMappings.has(docGuid)) { + let docsArray = p.groupMappings.get(docGuid)!; + docsArray.forEach((doc: Doc) => { + if (!aboveArray.includes(doc)) { + aboveArray.push(doc); + } + doc.presentId = aboveGuid; + }); + p.groupMappings.delete(docGuid); + //the case where doc was not in group + } else { + if (!aboveArray.includes(document)) { + aboveArray.push(document); + + } + + } + //the case where above-doc was not in group + } else { + let newAboveArray: Doc[] = []; + newAboveArray.push(p.allListElements[index - 1]); + + //the case where doc is in group + if (p.groupMappings.has(docGuid)) { + let docsArray = p.groupMappings.get(docGuid)!; + docsArray.forEach((doc: Doc) => { + newAboveArray.push(doc); + doc.presentId = aboveGuid; + }); + p.groupMappings.delete(docGuid); + + //the case where doc is not in a group + } else { + newAboveArray.push(document); + + } + p.groupMappings.set(aboveGuid, newAboveArray); + + } + document.presentId = aboveGuid; + + //when grouping is turned off + } else { + let curArray = p.groupMappings.get(StrCast(document.presentId, Utils.GenerateGuid()))!; + let targetIndex = curArray.indexOf(document); + let firstPart = curArray.slice(0, targetIndex); + let firstPartNewGuid = Utils.GenerateGuid(); + firstPart.forEach((doc: Doc) => doc.presentId = firstPartNewGuid); + let secondPart = curArray.slice(targetIndex); + p.groupMappings.set(StrCast(p.allListElements[index - 1].presentId, Utils.GenerateGuid()), firstPart); + p.groupMappings.set(StrCast(document.presentId, Utils.GenerateGuid()), secondPart); + + + } + + } + this.autoSaveGroupChanges(); + + } + + + /** + * This function is called at the end of each group update to update the group updates. + */ + @action + autoSaveGroupChanges = () => { + let castedList: List<Doc> = new List<Doc>(); + this.props.presGroupBackUp.groupDocs = castedList; + this.props.groupMappings.forEach((docArray: Doc[], id: String) => { + //create a new doc for each group + let newGroupDoc = new Doc(); + castedList.push(newGroupDoc); + //store the id of the group in the doc + newGroupDoc.presentIdStore = id.toString(); + //store the doc array which represents the group in the doc + newGroupDoc.grouping = new List(docArray); + }); + + } + + /** + * Function that is called on click to change the group status of a docus, by turning the option on/off. + */ + @action + changeGroupStatus = () => { + if (this.selectedButtons[buttonIndex.Group]) { + this.selectedButtons[buttonIndex.Group] = false; + } else { + this.selectedButtons[buttonIndex.Group] = true; + } + this.autoSaveButtonChange(buttonIndex.Group); + + } + + /** + * The function that is called on click to turn Hiding document till press option on/off. + * It also sets the beginning and end opacitys. + */ + @action + onHideDocumentUntilPressClick = (e: React.MouseEvent) => { + e.stopPropagation(); + const current = NumCast(this.props.mainDocument.selectedDoc); + if (this.selectedButtons[buttonIndex.HideTillPressed]) { + this.selectedButtons[buttonIndex.HideTillPressed] = false; + if (this.props.index >= current) { + this.props.document.opacity = 1; + } + } else { + this.selectedButtons[buttonIndex.HideTillPressed] = true; + if (this.props.presStatus) { + if (this.props.index > current) { + this.props.document.opacity = 0; + } + } + } + this.autoSaveButtonChange(buttonIndex.HideTillPressed); + } + + /** + * This function is called to get the updates for the changed buttons. + */ + @action + autoSaveButtonChange = async (index: buttonIndex) => { + let castedList = (await DocListCastAsync(this.props.presButtonBackUp.selectedButtonDocs))!; + castedList[this.props.index].selectedButtons = new List(this.selectedButtons); + + } + + /** + * The function that is called on click to turn Hiding document after presented option on/off. + * It also makes sure that the option swithches from fade-after to this one, since both + * can't coexist. + */ + @action + onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => { + e.stopPropagation(); + const current = NumCast(this.props.mainDocument.selectedDoc); + if (this.selectedButtons[buttonIndex.HideAfter]) { + this.selectedButtons[buttonIndex.HideAfter] = false; + if (this.props.index <= current) { + this.props.document.opacity = 1; + } + } else { + if (this.selectedButtons[buttonIndex.FadeAfter]) { + this.selectedButtons[buttonIndex.FadeAfter] = false; + } + this.selectedButtons[buttonIndex.HideAfter] = true; + if (this.props.presStatus) { + if (this.props.index < current) { + this.props.document.opacity = 0; + } + } + } + this.autoSaveButtonChange(buttonIndex.HideAfter); + + } + + /** + * The function that is called on click to turn fading document after presented option on/off. + * It also makes sure that the option swithches from hide-after to this one, since both + * can't coexist. + */ + @action + onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => { + e.stopPropagation(); + const current = NumCast(this.props.mainDocument.selectedDoc); + if (this.selectedButtons[buttonIndex.FadeAfter]) { + this.selectedButtons[buttonIndex.FadeAfter] = false; + if (this.props.index <= current) { + this.props.document.opacity = 1; + } + } else { + if (this.selectedButtons[buttonIndex.HideAfter]) { + this.selectedButtons[buttonIndex.HideAfter] = false; + } + this.selectedButtons[buttonIndex.FadeAfter] = true; + if (this.props.presStatus) { + if (this.props.index < current) { + this.props.document.opacity = 0.5; + } + } + } + this.autoSaveButtonChange(buttonIndex.FadeAfter); + + } + + /** + * The function that is called on click to turn navigation option of docs on/off. + */ + @action + onNavigateDocumentClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (this.selectedButtons[buttonIndex.Navigate]) { + this.selectedButtons[buttonIndex.Navigate] = false; + + } else { + if (this.selectedButtons[buttonIndex.Show]) { + this.selectedButtons[buttonIndex.Show] = false; + } + this.selectedButtons[buttonIndex.Navigate] = true; + const current = NumCast(this.props.mainDocument.selectedDoc); + if (current === this.props.index) { + this.props.gotoDocument(this.props.index, this.props.index); + } + } + + this.autoSaveButtonChange(buttonIndex.Navigate); + + } + + /** + * The function that is called on click to turn zoom option of docs on/off. + */ + @action + onZoomDocumentClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (this.selectedButtons[buttonIndex.Show]) { + this.selectedButtons[buttonIndex.Show] = false; + this.props.document.viewScale = 1; + + } else { + if (this.selectedButtons[buttonIndex.Navigate]) { + this.selectedButtons[buttonIndex.Navigate] = false; + } + this.selectedButtons[buttonIndex.Show] = true; + const current = NumCast(this.props.mainDocument.selectedDoc); + if (current === this.props.index) { + this.props.gotoDocument(this.props.index, this.props.index); + } + } + + this.autoSaveButtonChange(buttonIndex.Show); + + } + + + render() { + let p = this.props; + let title = p.document.title; + + //to get currently selected presentation doc + let selected = NumCast(p.mainDocument.selectedDoc, 0); + + let className = "presentationView-item"; + if (selected === p.index) { + //this doc is selected + className += " presentationView-selected"; + } + let onEnter = (e: React.PointerEvent) => { p.document.libraryBrush = true; }; + let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = false; }; + return ( + <div className={className} key={p.document[Id] + p.index} + onPointerEnter={onEnter} onPointerLeave={onLeave} + style={{ + outlineColor: "maroon", + outlineStyle: "dashed", + outlineWidth: BoolCast(p.document.libraryBrush, false) || BoolCast(p.document.protoBrush, false) ? `1px` : "0px", + }} + onClick={e => { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}> + <strong className="presentationView-name"> + {`${p.index + 1}. ${title}`} + </strong> + <button className="presentation-icon" onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button> + <br></br> + <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button> + <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button> + <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button> + <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} color={"gray"} /></button> + <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button> + <button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={(e) => { + e.stopPropagation(); + this.changeGroupStatus(); + this.onGroupClick(p.document, p.index, this.selectedButtons[buttonIndex.Group]); + }}> <FontAwesomeIcon icon={"arrow-up"} /> </button> + + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx new file mode 100644 index 000000000..7abd3e366 --- /dev/null +++ b/src/client/views/presentationview/PresentationList.tsx @@ -0,0 +1,104 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { action } from "mobx"; +import "./PresentationView.scss"; +import { Utils } from "../../../Utils"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { NumCast, StrCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import PresentationElement, { buttonIndex } from "./PresentationElement"; + + + + +interface PresListProps { + mainDocument: Doc; + deleteDocument(index: number): void; + gotoDocument(index: number, fromDoc: number): Promise<void>; + groupMappings: Map<String, Doc[]>; + presElementsMappings: Map<Doc, PresentationElement>; + setChildrenDocs: (docList: Doc[]) => void; + presStatus: boolean; + presButtonBackUp: Doc; + presGroupBackUp: Doc; +} + + +@observer +/** + * Component that takes in a document prop and a boolean whether it's collapsed or not. + */ +export default class PresentationViewList extends React.Component<PresListProps> { + + /** + * Method that initializes presentation ids for the + * docs that is in the presentation, when presentation list + * gets re-rendered. It makes sure to not assign ids to the + * docs that are in the group, so that mapping won't be disrupted. + */ + + @action + initializeGroupIds = async (docList: Doc[]) => { + docList.forEach(async (doc: Doc, index: number) => { + let docGuid = StrCast(doc.presentId, null); + //checking if part of group + let storedGuids: string[] = []; + let castedGroupDocs = await DocListCastAsync(this.props.presGroupBackUp.groupDocs); + //making sure the docs that were in groups, which were stored, to not get new guids. + if (castedGroupDocs !== undefined) { + castedGroupDocs.forEach((doc: Doc) => { + let storedGuid = StrCast(doc.presentIdStore, null); + if (storedGuid) { + storedGuids.push(storedGuid); + } + + }); + } + if (!this.props.groupMappings.has(docGuid) && !storedGuids.includes(docGuid)) { + doc.presentId = Utils.GenerateGuid(); + } + }); + } + + /** + * Initially every document starts with a viewScale 1, which means + * that they will be displayed in a canvas with scale 1. + */ + @action + initializeScaleViews = (docList: Doc[]) => { + docList.forEach((doc: Doc) => { + let curScale = NumCast(doc.viewScale, null); + if (curScale === undefined) { + doc.viewScale = 1; + } + }); + } + + render() { + const children = DocListCast(this.props.mainDocument.data); + this.initializeGroupIds(children); + this.initializeScaleViews(children); + this.props.setChildrenDocs(children); + return ( + + <div className="presentationView-listCont"> + {children.map((doc: Doc, index: number) => + <PresentationElement + ref={(e) => { if (e) { this.props.presElementsMappings.set(doc, e); } }} + key={doc[Id]} + mainDocument={this.props.mainDocument} + document={doc} + index={index} + deleteDocument={this.props.deleteDocument} + gotoDocument={this.props.gotoDocument} + groupMappings={this.props.groupMappings} + allListElements={children} + presStatus={this.props.presStatus} + presButtonBackUp={this.props.presButtonBackUp} + presGroupBackUp={this.props.presGroupBackUp} + /> + )} + </div> + ); + } +} diff --git a/src/client/views/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index fb4a851c4..a35a5849b 100644 --- a/src/client/views/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -47,20 +47,30 @@ padding-bottom: 3px; font-size: 25px; display: inline-block; + width: calc(100% - 160px); } .presentation-icon { float: right; } +.presentation-interaction { + float: left; +} + +.presentation-interaction-selected { + background: #505050; + float: left; +} + .presentationView-name { font-size: 15px; display: inline-block; } .presentation-button { - margin-right: 12.5%; - margin-left: 12.5%; + margin-right: 2.5%; + margin-left: 2.5%; width: 25%; } diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx new file mode 100644 index 000000000..20d0e113a --- /dev/null +++ b/src/client/views/presentationview/PresentationView.tsx @@ -0,0 +1,793 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { observable, action, runInAction, reaction } from "mobx"; +import "./PresentationView.scss"; +import { DocumentManager } from "../../util/DocumentManager"; +import { Utils } from "../../../Utils"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { listSpec } from "../../../new_fields/Schema"; +import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import PresentationElement, { buttonIndex } from "./PresentationElement"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit } from '@fortawesome/free-solid-svg-icons'; +import { Docs } from "../../documents/Documents"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; +import PresentationViewList from "./PresentationList"; + +library.add(faArrowLeft); +library.add(faArrowRight); +library.add(faPlay); +library.add(faStop); +library.add(faPlus); +library.add(faTimes); +library.add(faMinus); +library.add(faEdit); + + +export interface PresViewProps { + Documents: List<Doc>; +} + +@observer +export class PresentationView extends React.Component<PresViewProps> { + public static Instance: PresentationView; + + //Mapping from presentation ids to a list of doc that represent a group + @observable groupMappings: Map<String, Doc[]> = new Map(); + //mapping from docs to their rendered component + @observable presElementsMappings: Map<Doc, PresentationElement> = new Map(); + //variable that holds all the docs in the presentation + @observable childrenDocs: Doc[] = []; + //variable to hold if presentation is started + @observable presStatus: boolean = false; + //back-up so that presentation stays the way it's when refreshed + @observable presGroupBackUp: Doc = new Doc(); + @observable presButtonBackUp: Doc = new Doc(); + + //Keeping track of the doc for the current presentation + @observable curPresentation: Doc = new Doc(); + //Mapping of guids to presentations. + @observable presentationsMapping: Map<String, Doc> = new Map(); + //Mapping of presentations to guid, so that select option values can be given. + @observable presentationsKeyMapping: Map<Doc, String> = new Map(); + //Variable to keep track of guid of the current presentation + @observable currentSelectedPresValue: string | undefined; + //A flag to keep track if title input is open, which is used in rendering. + @observable PresTitleInputOpen: boolean = false; + //Variable that holds reference to title input, so that new presentations get titles assigned. + @observable titleInputElement: HTMLInputElement | undefined; + @observable PresTitleChangeOpen: boolean = false; + + //initilize class variables + constructor(props: PresViewProps) { + super(props); + PresentationView.Instance = this; + } + + //The first lifecycle function that gets called to set up the current presentation. + async componentWillMount() { + this.props.Documents.forEach(async (doc, index: number) => { + + //For each presentation received from mainContainer, a mapping is created. + let curDoc: Doc = await doc; + let newGuid = Utils.GenerateGuid(); + this.presentationsKeyMapping.set(curDoc, newGuid); + this.presentationsMapping.set(newGuid, curDoc); + + //The Presentation at first index gets set as default start presentation + if (index === 0) { + runInAction(() => this.currentSelectedPresValue = newGuid); + runInAction(() => this.curPresentation = curDoc); + } + }); + } + + //Second lifecycle function that gets called when component mounts. It makes sure to + //get the back-up information from previous session for the current presentation. + async componentDidMount() { + let docAtZero = await this.props.Documents[0]; + runInAction(() => this.curPresentation = docAtZero); + + this.setPresentationBackUps(); + + } + + + /** + * The function that retrieves the backUps for the current Presentation if present, + * otherwise initializes. + */ + setPresentationBackUps = async () => { + //getting both backUp documents + + let castedGroupBackUp = Cast(this.curPresentation.presGroupBackUp, Doc); + let castedButtonBackUp = Cast(this.curPresentation.presButtonBackUp, Doc); + //if instantiated before + if (castedGroupBackUp instanceof Promise) { + castedGroupBackUp.then(doc => { + let toAssign = doc ? doc : new Doc(); + this.curPresentation.presGroupBackUp = toAssign; + runInAction(() => this.presGroupBackUp = toAssign); + if (doc) { + if (toAssign[Id] === doc[Id]) { + this.retrieveGroupMappings(); + } + } + }); + + //if never instantiated a store doc yet + } else if (castedGroupBackUp instanceof Doc) { + let castedDoc: Doc = await castedGroupBackUp; + runInAction(() => this.presGroupBackUp = castedDoc); + this.retrieveGroupMappings(); + } else { + runInAction(() => { + let toAssign = new Doc(); + this.presGroupBackUp = toAssign; + this.curPresentation.presGroupBackUp = toAssign; + + }); + + } + //if instantiated before + if (castedButtonBackUp instanceof Promise) { + castedButtonBackUp.then(doc => { + let toAssign = doc ? doc : new Doc(); + this.curPresentation.presButtonBackUp = toAssign; + runInAction(() => this.presButtonBackUp = toAssign); + }); + + //if never instantiated a store doc yet + } else if (castedButtonBackUp instanceof Doc) { + let castedDoc: Doc = await castedButtonBackUp; + runInAction(() => this.presButtonBackUp = castedDoc); + + } else { + runInAction(() => { + let toAssign = new Doc(); + this.presButtonBackUp = toAssign; + this.curPresentation.presButtonBackUp = toAssign; + }); + + } + + + //storing the presentation status,ie. whether it was stopped or playing + let presStatusBackUp = BoolCast(this.curPresentation.presStatus, null); + runInAction(() => this.presStatus = presStatusBackUp); + } + + /** + * This is the function that is called to retrieve the groups that have been stored and + * push them to the groupMappings. + */ + retrieveGroupMappings = async () => { + let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs); + if (castedGroupDocs !== undefined) { + castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => { + let castedGrouping = await DocListCastAsync(groupDoc.grouping); + let castedKey = StrCast(groupDoc.presentIdStore, null); + if (castedGrouping) { + castedGrouping.forEach((doc: Doc) => { + doc.presentId = castedKey; + }); + } + if (castedGrouping !== undefined && castedKey !== undefined) { + this.groupMappings.set(castedKey, castedGrouping); + } + }); + } + } + + //observable means render is re-called every time variable is changed + @observable + collapsed: boolean = false; + closePresentation = action(() => this.curPresentation.width = 0); + next = async () => { + const current = NumCast(this.curPresentation.selectedDoc); + //asking to get document at current index + let docAtCurrentNext = await this.getDocAtIndex(current + 1); + if (docAtCurrentNext === undefined) { + return; + } + //asking for it's presentation id + let curNextPresId = StrCast(docAtCurrentNext.presentId); + let nextSelected = current + 1; + + //if curDoc is in a group, selection slides until last one, if not it's next one + if (this.groupMappings.has(curNextPresId)) { + let currentsArray = this.groupMappings.get(StrCast(docAtCurrentNext.presentId))!; + nextSelected = current + currentsArray.length - currentsArray.indexOf(docAtCurrentNext); + + //end of grup so go beyond + if (nextSelected === current) nextSelected = current + 1; + } + + this.gotoDocument(nextSelected, current); + + } + back = async () => { + const current = NumCast(this.curPresentation.selectedDoc); + //requesting for the doc at current index + let docAtCurrent = await this.getDocAtIndex(current); + if (docAtCurrent === undefined) { + return; + } + + //asking for its presentation id. + let curPresId = StrCast(docAtCurrent.presentId); + let prevSelected = current - 1; + let zoomOut: boolean = false; + + //checking if this presentation id is mapped to a group, if so chosing the first element in group + if (this.groupMappings.has(curPresId)) { + let currentsArray = this.groupMappings.get(StrCast(docAtCurrent.presentId))!; + prevSelected = current - currentsArray.length + (currentsArray.length - currentsArray.indexOf(docAtCurrent)) - 1; + //end of grup so go beyond + if (prevSelected === current) prevSelected = current - 1; + + //checking if any of the group members had used zooming in + currentsArray.forEach((doc: Doc) => { + if (this.presElementsMappings.get(doc)!.selected[buttonIndex.Show]) { + zoomOut = true; + return; + } + }); + + } + + // if a group set that flag to zero or a single element + //If so making sure to zoom out, which goes back to state before zooming action + if (current > 0) { + if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.selected[buttonIndex.Show]) { + let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null); + let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]); + if (prevScale !== undefined) { + if (prevScale !== curScale) { + DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale); + } + } + } + } + this.gotoDocument(prevSelected, current); + + } + + /** + * This is the method that checks for the actions that need to be performed + * after the document has been presented, which involves 3 button options: + * Hide Until Presented, Hide After Presented, Fade After Presented + */ + showAfterPresented = (index: number) => { + this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => { + let selectedButtons: boolean[] = presElem.selected; + //the order of cases is aligned based on priority + if (selectedButtons[buttonIndex.HideTillPressed]) { + if (this.childrenDocs.indexOf(key) <= index) { + key.opacity = 1; + } + } + if (selectedButtons[buttonIndex.HideAfter]) { + if (this.childrenDocs.indexOf(key) < index) { + key.opacity = 0; + } + } + if (selectedButtons[buttonIndex.FadeAfter]) { + if (this.childrenDocs.indexOf(key) < index) { + key.opacity = 0.5; + } + } + }); + } + + /** + * This is the method that checks for the actions that need to be performed + * before the document has been presented, which involves 3 button options: + * Hide Until Presented, Hide After Presented, Fade After Presented + */ + hideIfNotPresented = (index: number) => { + this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => { + let selectedButtons: boolean[] = presElem.selected; + + //the order of cases is aligned based on priority + + if (selectedButtons[buttonIndex.HideAfter]) { + if (this.childrenDocs.indexOf(key) >= index) { + key.opacity = 1; + } + } + if (selectedButtons[buttonIndex.FadeAfter]) { + if (this.childrenDocs.indexOf(key) >= index) { + key.opacity = 1; + } + } + if (selectedButtons[buttonIndex.HideTillPressed]) { + if (this.childrenDocs.indexOf(key) > index) { + key.opacity = 0; + } + } + }); + } + + /** + * This method makes sure that cursor navigates to the element that + * has the option open and last in the group. If not in the group, and it has + * te option open, navigates to that element. + */ + navigateToElement = async (curDoc: Doc, fromDoc: number) => { + let docToJump: Doc = curDoc; + let curDocPresId = StrCast(curDoc.presentId, null); + let willZoom: boolean = false; + + //checking if in group + if (curDocPresId !== undefined) { + if (this.groupMappings.has(curDocPresId)) { + let currentDocGroup = this.groupMappings.get(curDocPresId)!; + currentDocGroup.forEach((doc: Doc, index: number) => { + let selectedButtons: boolean[] = this.presElementsMappings.get(doc)!.selected; + if (selectedButtons[buttonIndex.Navigate]) { + docToJump = doc; + willZoom = false; + } + if (selectedButtons[buttonIndex.Show]) { + docToJump = doc; + willZoom = true; + } + }); + } + + } + //docToJump stayed same meaning, it was not in the group or was the last element in the group + if (docToJump === curDoc) { + //checking if curDoc has navigation open + let curDocButtons = this.presElementsMappings.get(curDoc)!.selected; + if (curDocButtons[buttonIndex.Navigate]) { + DocumentManager.Instance.jumpToDocument(curDoc, false); + } else if (curDocButtons[buttonIndex.Show]) { + let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(curDoc, true); + let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); + curDoc.viewScale = newScale; + + //saving the scale user was on before zooming in + if (curScale !== 1) { + this.childrenDocs[fromDoc].viewScale = curScale; + } + + } + return; + } + let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); + + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(docToJump, willZoom); + let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); + curDoc.viewScale = newScale; + //saving the scale that user was on + if (curScale !== 1) { + this.childrenDocs[fromDoc].viewScale = curScale; + } + + } + + /** + * Async function that supposedly return the doc that is located at given index. + */ + getDocAtIndex = async (index: number) => { + const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); + if (!list) { + return undefined; + } + if (index < 0 || index >= list.length) { + return undefined; + } + + this.curPresentation.selectedDoc = index; + //awaiting async call to finish to get Doc instance + const doc = await list[index]; + return doc; + } + + /** + * The function that removes a doc from a presentation. It also makes sure to + * do necessary updates to backUps and mappings stored locally. + */ + @action + public RemoveDoc = async (index: number) => { + const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); + if (value) { + let removedDoc = await value.splice(index, 1)[0]; + + //removing the Presentation Element stored for it + this.presElementsMappings.delete(removedDoc); + + let removedDocPresentId = StrCast(removedDoc.presentId); + + //Removing it from local mapping of the groups + if (this.groupMappings.has(removedDocPresentId)) { + let removedDocsGroup = this.groupMappings.get(removedDocPresentId); + if (removedDocsGroup) { + removedDocsGroup.splice(removedDocsGroup.indexOf(removedDoc), 1); + if (removedDocsGroup.length === 0) { + this.groupMappings.delete(removedDocPresentId); + } + } + } + + //removing it from the backUp of selected Buttons + let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); + if (castedList) { + castedList.splice(index, 1); + } + + //removing it from the backup of groups + let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs); + if (castedGroupDocs) { + castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => { + let castedKey = StrCast(groupDoc.presentIdStore, null); + if (castedKey === removedDocPresentId) { + let castedGrouping = await DocListCastAsync(groupDoc.grouping); + if (castedGrouping) { + castedGrouping.splice(castedGrouping.indexOf(removedDoc), 1); + if (castedGrouping.length === 0) { + castedGroupDocs!.splice(castedGroupDocs!.indexOf(groupDoc), 1); + } + } + } + + }); + + } + + + } + } + + //The function that is called when a document is clicked or reached through next or back. + //it'll also execute the necessary actions if presentation is playing. + @action + public gotoDocument = async (index: number, fromDoc: number) => { + const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); + if (!list) { + return; + } + if (index < 0 || index >= list.length) { + return; + } + this.curPresentation.selectedDoc = index; + + if (!this.presStatus) { + this.presStatus = true; + this.startPresentation(index); + } + + const doc = await list[index]; + if (this.presStatus) { + this.navigateToElement(doc, fromDoc); + this.hideIfNotPresented(index); + this.showAfterPresented(index); + } + + } + + //Function that is called to resetGroupIds, so that documents get new groupIds at + //first load, when presentation is changed. + resetGroupIds = async () => { + let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs); + if (castedGroupDocs !== undefined) { + castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => { + let castedGrouping = await DocListCastAsync(groupDoc.grouping); + if (castedGrouping) { + castedGrouping.forEach((doc: Doc) => { + doc.presentId = Utils.GenerateGuid(); + }); + } + }); + } + runInAction(() => this.groupMappings = new Map()); + } + + /** + * Adds a document to the presentation view + **/ + @action + public PinDoc(doc: Doc) { + //add this new doc to props.Document + const data = Cast(this.curPresentation.data, listSpec(Doc)); + if (data) { + data.push(doc); + } else { + this.curPresentation.data = new List([doc]); + } + + this.curPresentation.width = 400; + } + + //Function that sets the store of the children docs. + @action + setChildrenDocs = (docList: Doc[]) => { + this.childrenDocs = docList; + } + + //The function that is called to render the play or pause button depending on + //if presentation is running or not. + renderPlayPauseButton = () => { + if (this.presStatus) { + return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>; + } else { + return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>; + } + } + + //The function that starts or resets presentaton functionally, depending on status flag. + @action + startOrResetPres = () => { + if (this.presStatus) { + this.resetPresentation(); + } else { + this.presStatus = true; + this.startPresentation(0); + const current = NumCast(this.curPresentation.selectedDoc); + this.gotoDocument(0, current); + } + this.curPresentation.presStatus = this.presStatus; + } + + //The function that resets the presentation by removing every action done by it. It also + //stops the presentaton. + @action + resetPresentation = () => { + this.childrenDocs.forEach((doc: Doc) => { + doc.opacity = 1; + doc.viewScale = 1; + }); + this.curPresentation.selectedDoc = 0; + this.presStatus = false; + this.curPresentation.presStatus = this.presStatus; + if (this.childrenDocs.length === 0) { + return; + } + DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1); + } + + + //The function that starts the presentation, also checking if actions should be applied + //directly at start. + startPresentation = (startIndex: number) => { + let selectedButtons: boolean[]; + this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => { + selectedButtons = component.selected; + if (selectedButtons[buttonIndex.HideTillPressed]) { + if (this.childrenDocs.indexOf(doc) > startIndex) { + doc.opacity = 0; + } + + } + if (selectedButtons[buttonIndex.HideAfter]) { + if (this.childrenDocs.indexOf(doc) < startIndex) { + doc.opacity = 0; + } + } + if (selectedButtons[buttonIndex.FadeAfter]) { + if (this.childrenDocs.indexOf(doc) < startIndex) { + doc.opacity = 0.5; + } + } + + }); + + } + + /** + * The function that is called to add a new presentation to the presentationView. + * It sets up te mappings and local copies of it. Resets the groupings and presentation. + * Makes the new presentation current selected, and retrieve the back-Ups if present. + */ + @action + addNewPresentation = (presTitle: string) => { + //creating a new presentation doc + let newPresentationDoc = Docs.TreeDocument([], { title: presTitle }); + this.props.Documents.push(newPresentationDoc); + + //setting that new doc as current + this.curPresentation = newPresentationDoc; + + //storing the doc in local copies for easier access + let newGuid = Utils.GenerateGuid(); + this.presentationsMapping.set(newGuid, newPresentationDoc); + this.presentationsKeyMapping.set(newPresentationDoc, newGuid); + + //resetting the previous presentation's actions so that new presentation can be loaded. + this.resetGroupIds(); + this.resetPresentation(); + this.presElementsMappings = new Map(); + this.currentSelectedPresValue = newGuid; + this.setPresentationBackUps(); + + } + + /** + * The function that is called to change the current selected presentation. + * Changes the presentation, also resetting groupings and presentation in process. + * Plus retrieving the backUps for the newly selected presentation. + */ + @action + getSelectedPresentation = (e: React.ChangeEvent<HTMLSelectElement>) => { + //get the guid of the selected presentation + let selectedGuid = e.target.value; + //set that as current presentation + this.curPresentation = this.presentationsMapping.get(selectedGuid)!; + + //reset current Presentations local things so that new one can be loaded + this.resetGroupIds(); + this.resetPresentation(); + this.presElementsMappings = new Map(); + this.currentSelectedPresValue = selectedGuid; + this.setPresentationBackUps(); + + + } + + /** + * The function that is called to render either select for presentations, or title inputting. + */ + renderSelectOrPresSelection = () => { + let presentationList = DocListCast(this.props.Documents); + if (this.PresTitleInputOpen || this.PresTitleChangeOpen) { + return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />; + } else { + return <select value={this.currentSelectedPresValue} id="pres_selector" className="presentationView-title" onChange={this.getSelectedPresentation}> + {presentationList.map((doc: Doc, index: number) => { + let mappedGuid = this.presentationsKeyMapping.get(doc); + let docGuid: string = mappedGuid ? mappedGuid.toString() : ""; + return <option key={docGuid} value={docGuid}>{StrCast(doc.title)}</option>; + })} + </select>; + } + } + + /** + * The function that is called on enter press of title input. It gives the + * new presentation the title user entered. If nothing is entered, gives a default title. + */ + @action + submitPresentationTitle = (e: React.KeyboardEvent) => { + if (e.keyCode === 13) { + let presTitle = this.titleInputElement!.value; + this.titleInputElement!.value = ""; + if (this.PresTitleInputOpen) { + if (presTitle === "") { + presTitle = "Presentation"; + } + this.PresTitleInputOpen = false; + this.addNewPresentation(presTitle); + } else if (this.PresTitleChangeOpen) { + this.PresTitleChangeOpen = false; + this.changePresentationTitle(presTitle); + } + } + } + + /** + * The function that is called to remove a presentation from all its copies, and the main Container's + * list. Sets up the next presentation as current. + */ + @action + removePresentation = async () => { + if (this.presentationsMapping.size !== 1) { + let presentationList = Cast(this.props.Documents, listSpec(Doc)); + let batch = UndoManager.StartBatch("presRemoval"); + + //getting the presentation that will be removed + let removedDoc = this.presentationsMapping.get(this.currentSelectedPresValue!); + //that presentation is removed + presentationList!.splice(presentationList!.indexOf(removedDoc!), 1); + + //its mappings are removed from local copies + this.presentationsKeyMapping.delete(removedDoc!); + this.presentationsMapping.delete(this.currentSelectedPresValue!); + + //the next presentation is set as current + let remainingPresentations = this.presentationsMapping.values(); + let nextDoc = remainingPresentations.next().value; + this.curPresentation = nextDoc; + + + //Storing these for being able to undo changes + let curGuid = this.currentSelectedPresValue!; + let curPresStatus = this.presStatus; + + //resetting the groups and presentation actions so that next presentation gets loaded + this.resetGroupIds(); + this.resetPresentation(); + this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString(); + this.setPresentationBackUps(); + + //Storing for undo + let currentGroups = this.groupMappings; + let curPresElemMapping = this.presElementsMappings; + + //Event to undo actions that are not related to doc directly, aka. local things + UndoManager.AddEvent({ + undo: action(() => { + this.curPresentation = removedDoc!; + this.presentationsMapping.set(curGuid, removedDoc!); + this.presentationsKeyMapping.set(removedDoc!, curGuid); + this.currentSelectedPresValue = curGuid; + + this.presStatus = curPresStatus; + this.groupMappings = currentGroups; + this.presElementsMappings = curPresElemMapping; + this.setPresentationBackUps(); + + }), + redo: action(() => { + this.curPresentation = nextDoc; + this.presStatus = false; + this.presentationsKeyMapping.delete(removedDoc!); + this.presentationsMapping.delete(curGuid); + this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString(); + this.setPresentationBackUps(); + + }), + }); + + batch.end(); + } + } + + /** + * The function that is called to change title of presentation to what user entered. + */ + @undoBatch + changePresentationTitle = (newTitle: string) => { + if (newTitle === "") { + return; + } + this.curPresentation.title = newTitle; + } + + + render() { + + let width = NumCast(this.curPresentation.width); + + return ( + <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}> + <div className="presentationView-heading"> + {this.renderSelectOrPresSelection()} + <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button> + <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { + runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } }); + runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true); + }}><FontAwesomeIcon icon={"plus"} /></button> + <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button> + <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { + runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } }); + runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true); + }}><FontAwesomeIcon icon={"edit"} /></button> + </div> + <div className="presentation-buttons"> + <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> + {this.renderPlayPauseButton()} + <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + </div> + <PresentationViewList + mainDocument={this.curPresentation} + deleteDocument={this.RemoveDoc} + gotoDocument={this.gotoDocument} + groupMappings={this.groupMappings} + presElementsMappings={this.presElementsMappings} + setChildrenDocs={this.setChildrenDocs} + presStatus={this.presStatus} + presButtonBackUp={this.presButtonBackUp} + presGroupBackUp={this.presGroupBackUp} + /> + </div> + ); + } +} 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..1c62773b1 --- /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..f53a4e34f --- /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..5160d9469 --- /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, undefined); + }; + } + + 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 = () => { + DocumentManager.Instance.jumpToDocument(this.props.doc, false); + } + + @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..178578c5c --- /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/debug/Viewer.tsx b/src/debug/Viewer.tsx index b22300d0b..d880f1a28 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -10,6 +10,7 @@ import { List } from '../new_fields/List'; import { URLField } from '../new_fields/URLField'; import { EditableView } from '../client/views/EditableView'; import { CompileScript } from '../client/util/Scripting'; +import { RichTextField } from '../new_fields/RichTextField'; function applyToDoc(doc: { [index: string]: FieldResult }, key: string, scriptString: string): boolean; function applyToDoc(doc: { [index: number]: FieldResult }, key: number, scriptString: string): boolean; @@ -117,6 +118,8 @@ class DebugViewer extends React.Component<{ field: FieldResult, setValue(value: content = <p>"{field}"</p>; } else if (typeof field === "number" || typeof field === "boolean") { content = <p>{field}</p>; + } else if (field instanceof RichTextField) { + content = <p>RTF: {field.Data}</p>; } else if (field instanceof URLField) { content = <p>{field.url.href}</p>; } else if (field instanceof Promise) { diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index c2cfda079..b0184dd4e 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -2,7 +2,7 @@ import { observable, action } from "mobx"; import { serializable, primitive, map, alias, list } from "serializr"; import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; import { DocServer } from "../client/DocServer"; -import { setter, getter, getField, updateFunction, deleteProperty } from "./util"; +import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util"; import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types"; import { listSpec } from "./Schema"; import { ObjectField } from "./ObjectField"; @@ -132,6 +132,16 @@ export class Doc extends RefField { this[fKey] = value; } } + const unset = diff.$unset; + if (unset) { + for (const key in unset) { + if (!key.startsWith("fields.")) { + continue; + } + const fKey = key.substring(7); + delete this[fKey]; + } + } } } @@ -146,6 +156,15 @@ export namespace Doc { // return Cast(field, ctor); // }); // } + export function MakeReadOnly(): { end(): void } { + makeReadOnly(); + return { + end() { + makeEditable(); + } + }; + } + export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult { const self = doc[Self]; return getField(self, key, ignoreProto); @@ -156,6 +175,14 @@ export namespace Doc { export function IsPrototype(doc: Doc) { return GetT(doc, "isPrototype", "boolean", true); } + export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) { + let hasProto = doc.proto instanceof Doc; + let onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; + let onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1; + if (onDeleg || !hasProto || (!onProto && !defaultProto)) { + doc[key] = value; + } else doc.proto![key] = value; + } export async function SetOnPrototype(doc: Doc, key: string, value: Field) { const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc; @@ -197,7 +224,7 @@ export namespace Doc { // gets the document's prototype or returns the document if it is a prototype export function GetProto(doc: Doc) { - return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : doc.proto!; + return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc); } export function allKeys(doc: Doc): string[] { @@ -212,6 +239,40 @@ export namespace Doc { return Array.from(results); } + export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean) { + let list = Cast(target[key], listSpec(Doc)); + if (list) { + let ind = relativeTo ? list.indexOf(relativeTo) : -1; + if (ind === -1) list.push(doc); + else list.splice(before ? ind : ind + 1, 0, doc); + } + return true; + } + + // + // Resolves a reference to a field by returning 'doc' if o field extension is specified, + // otherwise, it returns the extension document stored in doc.<fieldKey>_ext. + // This mechanism allows any fields to be extended with an extension document that can + // be used to capture field-specific metadata. For example, an image field can be extended + // to store annotations, ink, and other data. + // + export function resolvedFieldDataDoc(doc: Doc, fieldKey: string, fieldExt: string) { + return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc; + } + + export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string) { + if (doc[fieldKey + "_ext"] === undefined) { + setTimeout(() => { + let docExtensionForField = new Doc(doc[Id] + fieldKey, true); + docExtensionForField.title = "Extension of " + doc.title + "'s field:" + fieldKey; + let proto: Doc | undefined = doc; + while (proto && !Doc.IsPrototype(proto)) { + proto = proto.proto; + } + (proto ? proto : doc)[fieldKey + "_ext"] = docExtensionForField; + }, 0); + } + } export function MakeAlias(doc: Doc) { if (!GetT(doc, "isPrototype", "boolean", true)) { return Doc.MakeCopy(doc); @@ -237,6 +298,7 @@ export namespace Doc { } } }); + return copy; } diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts index 130ec066e..38d874a68 100644 --- a/src/new_fields/Proxy.ts +++ b/src/new_fields/Proxy.ts @@ -48,9 +48,8 @@ export class ProxyField<T extends RefField> extends ObjectField { private failed = false; private promise?: Promise<any>; - value(callback?: ((field: T | undefined) => void)): T | undefined | FieldWaiting { + value(): T | undefined | FieldWaiting { if (this.cache) { - callback && callback(this.cache); return this.cache; } if (this.failed) { @@ -64,7 +63,6 @@ export class ProxyField<T extends RefField> extends ObjectField { return field; })); } - callback && this.promise.then(callback); return this.promise; } } diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 89d077a47..527c7b313 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -18,6 +18,6 @@ export class RichTextField extends ObjectField { } [ToScriptString]() { - return "invalid"; + return `new RichTextField("${this.Data}")`; } }
\ No newline at end of file diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts index 40ffaecd5..2355304d5 100644 --- a/src/new_fields/Schema.ts +++ b/src/new_fields/Schema.ts @@ -2,6 +2,7 @@ import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListS import { Doc, Field } from "./Doc"; import { ObjectField } from "./ObjectField"; import { RefField } from "./RefField"; +import { SelfProxy } from "./FieldSymbols"; type AllToInterface<T extends Interface[]> = { 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>, @@ -56,9 +57,10 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu } }); const fn = (doc: Doc) => { - if (!(doc instanceof Doc)) { - throw new Error("Currently wrapping a schema in another schema isn't supported"); - } + doc = doc[SelfProxy]; + // if (!(doc instanceof Doc)) { + // throw new Error("Currently wrapping a schema in another schema isn't supported"); + // } const obj = Object.create(proto, { doc: { value: doc, writable: false } }); return obj; }; diff --git a/src/fields/ScriptField.ts b/src/new_fields/ScriptField.ts index ac46ccf90..3d56e9374 100644 --- a/src/fields/ScriptField.ts +++ b/src/new_fields/ScriptField.ts @@ -1,9 +1,9 @@ -import { ObjectField } from "../new_fields/ObjectField"; +import { ObjectField } from "./ObjectField"; import { CompiledScript, CompileScript } from "../client/util/Scripting"; -import { Copy, ToScriptString, Parent, SelfProxy } from "../new_fields/FieldSymbols"; +import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols"; import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr"; import { Deserializable } from "../client/util/SerializationHelper"; -import { computed } from "mobx"; +import { Doc } from "../new_fields/Doc"; function optional(propSchema: PropSchema) { return custom(value => { @@ -23,35 +23,32 @@ const optionsSchema = createSimpleSchema({ requiredType: true, addReturn: true, typecheck: true, + readonly: true, params: optional(map(primitive())) }); +const scriptSchema = createSimpleSchema({ + options: object(optionsSchema), + originalScript: true +}); + function deserializeScript(script: ScriptField) { - const comp = CompileScript(script.scriptString, script.options); + const comp = CompileScript(script.script.originalScript, script.script.options); if (!comp.compiled) { throw new Error("Couldn't compile loaded script"); } - (script as any)._script = comp; + (script as any).script = comp; } @Deserializable("script", deserializeScript) export class ScriptField extends ObjectField { - protected readonly _script: CompiledScript; + @serializable(object(scriptSchema)) + readonly script: CompiledScript; constructor(script: CompiledScript) { super(); - this._script = script; - } - - @serializable(custom(object(optionsSchema).serializer, () => SKIP)) - get options() { - return this._script && this._script.options; - } - - @serializable(custom(primitive().serializer, () => SKIP)) - get scriptString(): string { - return this._script && this._script.originalScript; + this.script = script; } // init(callback: (res: Field) => any) { @@ -76,7 +73,7 @@ export class ScriptField extends ObjectField { // } [Copy](): ObjectField { - return new ScriptField(this._script); + return new ScriptField(this.script); } [ToScriptString]() { @@ -86,9 +83,9 @@ export class ScriptField extends ObjectField { @Deserializable("computed", deserializeScript) export class ComputedField extends ScriptField { - @computed - get value() { - const val = this._script.run({ this: (this[Parent] as any)[SelfProxy] }); + //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc + value(doc: Doc) { + const val = this.script.run({ this: doc }); if (val.success) { return val.result; } diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index 8cb1db953..abb777adf 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -6,10 +6,13 @@ import { FieldValue } from "./Types"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; import { action } from "mobx"; -import { Parent, OnUpdate, Update, Id, SelfProxy } from "./FieldSymbols"; -import { ComputedField } from "../fields/ScriptField"; +import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; +import { ComputedField } from "./ScriptField"; -export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { +function _readOnlySetter(): never { + throw new Error("Documents can't be modified in read-only mode"); +} +const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { if (SerializationHelper.IsSerializing()) { target[prop] = value; return true; @@ -18,6 +21,9 @@ export const setter = action(function (target: any, prop: string | symbol | numb target[prop] = value; return true; } + if (value !== undefined) { + value = value[SelfProxy] || value; + } const curValue = target.__fields[prop]; if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically @@ -28,11 +34,10 @@ export const setter = action(function (target: any, prop: string | symbol | numb value = new ProxyField(value); } if (value instanceof ObjectField) { - //TODO Instead of target, maybe use target[Self] - if (value[Parent] && value[Parent] !== target) { + if (value[Parent] && value[Parent] !== receiver) { throw new Error("Can't put the same object in multiple documents at the same time"); } - value[Parent] = target; + value[Parent] = receiver; value[OnUpdate] = updateFunction(target, prop, value, receiver); } if (curValue instanceof ObjectField) { @@ -53,6 +58,20 @@ export const setter = action(function (target: any, prop: string | symbol | numb return true; }); +let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl; + +export function makeReadOnly() { + _setter = _readOnlySetter; +} + +export function makeEditable() { + _setter = _setterImpl; +} + +export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { + return _setter(target, prop, value, receiver); +} + export function getter(target: any, prop: string | symbol | number, receiver: any): any { if (typeof prop === "symbol") { return target.__fields[prop] || target[prop]; @@ -60,25 +79,30 @@ export function getter(target: any, prop: string | symbol | number, receiver: an if (SerializationHelper.IsSerializing()) { return target[prop]; } - return getField(target, prop); + return getFieldImpl(target, prop, receiver); } -export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { +function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any { + receiver = receiver || target[SelfProxy]; const field = target.__fields[prop]; if (field instanceof ProxyField) { return field.value(); } if (field instanceof ComputedField) { - return field.value; + return field.value(receiver); } if (field === undefined && !ignoreProto && prop !== "proto") { - const proto = getField(target, "proto", true); + const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters if (proto instanceof Doc) { - return proto[prop]; + return getFieldImpl(proto[Self], prop, receiver, ignoreProto); } return undefined; } return field; + +} +export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { + return getFieldImpl(target, prop, undefined, ignoreProto); } export function deleteProperty(target: any, prop: string | number | symbol) { diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py new file mode 100644 index 000000000..fcffeac13 --- /dev/null +++ b/src/scraping/buxton/scraper.py @@ -0,0 +1,331 @@ +import os +import docx2txt +from docx import Document +from docx.opc.constants import RELATIONSHIP_TYPE as RT +import re +from pymongo import MongoClient +import shutil +import uuid +import datetime +from PIL import Image +import math +import sys + +source = "./source" +dist = "../../server/public/files" + +db = MongoClient("localhost", 27017)["Dash"] +schema_guids = [] + + +def extract_links(fileName): + links = [] + doc = Document(fileName) + rels = doc.part.rels + for rel in rels: + item = rels[rel] + if item.reltype == RT.HYPERLINK and ".aspx" not in item._target: + links.append(item._target) + return listify(links) + + +def extract_value(kv_string): + pieces = kv_string.split(":") + return (pieces[1] if len(pieces) > 1 else kv_string).strip() + + +def mkdir_if_absent(path): + try: + if not os.path.exists(path): + os.mkdir(path) + except OSError: + print("failed to create the appropriate directory structures for %s" % file_name) + + +def guid(): + return str(uuid.uuid4()) + + +def listify(list): + return { + "fields": list, + "__type": "list" + } + + +def protofy(fieldId): + return { + "fieldId": fieldId, + "__type": "proxy" + } + + +def write_schema(parse_results, display_fields, storage_key): + view_guids = parse_results["child_guids"] + + data_doc = parse_results["schema"] + fields = data_doc["fields"] + + view_doc_guid = guid() + + view_doc = { + "_id": view_doc_guid, + "fields": { + "proto": protofy(data_doc["_id"]), + "x": 10, + "y": 10, + "width": 900, + "height": 600, + "panX": 0, + "panY": 0, + "zoomBasis": 0.5, + "zIndex": 2, + "libraryBrush": False, + "viewType": 2 + }, + "__type": "Doc" + } + + fields["proto"] = protofy("collectionProto") + fields[storage_key] = listify(proxify_guids(view_guids)) + fields["schemaColumns"] = listify(display_fields) + fields["backgroundColor"] = "white" + fields["scale"] = 0.5 + fields["viewType"] = 2 + fields["author"] = "Bill Buxton" + fields["creationDate"] = { + "date": datetime.datetime.utcnow().microsecond, + "__type": "date" + } + fields["isPrototype"] = True + fields["page"] = -1 + + db.newDocuments.insert_one(data_doc) + db.newDocuments.insert_one(view_doc) + + data_doc_guid = data_doc["_id"] + print(f"inserted view document ({view_doc_guid})") + print(f"inserted data document ({data_doc_guid})\n") + + return view_doc_guid + + +def write_image(folder, name): + path = f"http://localhost:1050/files/{folder}/{name}" + + data_doc_guid = guid() + view_doc_guid = guid() + + view_doc = { + "_id": view_doc_guid, + "fields": { + "proto": protofy(data_doc_guid), + "x": 10, + "y": 10, + "width": 300, + "zIndex": 2, + "libraryBrush": False + }, + "__type": "Doc" + } + + image = Image.open(f"{dist}/{folder}/{name}") + native_width, native_height = image.size + + data_doc = { + "_id": data_doc_guid, + "fields": { + "proto": protofy("imageProto"), + "data": { + "url": path, + "__type": "image" + }, + "title": name, + "nativeWidth": native_width, + "author": "Bill Buxton", + "creationDate": { + "date": datetime.datetime.utcnow().microsecond, + "__type": "date" + }, + "isPrototype": True, + "page": -1, + "nativeHeight": native_height, + "height": native_height + }, + "__type": "Doc" + } + + db.newDocuments.insert_one(view_doc) + db.newDocuments.insert_one(data_doc) + + return view_doc_guid + + +def parse_document(file_name: str): + print(f"parsing {file_name}...") + pure_name = file_name.split(".")[0] + + result = {} + + dir_path = dist + "/" + pure_name + mkdir_if_absent(dir_path) + + raw = str(docx2txt.process(source + "/" + file_name, dir_path)) + + view_guids = [] + count = 0 + for image in os.listdir(dir_path): + count += 1 + view_guids.append(write_image(pure_name, image)) + os.rename(dir_path + "/" + image, dir_path + + "/" + image.replace(".", "_m.", 1)) + print(f"extracted {count} images...") + + def sanitize(line): return re.sub("[\n\t]+", "", line).replace(u"\u00A0", " ").replace( + u"\u2013", "-").replace(u"\u201c", '''"''').replace(u"\u201d", '''"''').strip() + + def sanitize_price(raw: str): + raw = raw.replace(",", "") + start = raw.find("$") + if start > -1: + i = start + 1 + while (i < len(raw) and re.match(r"[0-9\.]", raw[i])): + i += 1 + price = raw[start + 1: i + 1] + return float(price) + elif (raw.lower().find("nfs")): + return -1 + else: + return math.nan + + def remove_empty(line): return len(line) > 1 + + lines = list(map(sanitize, raw.split("\n"))) + lines = list(filter(remove_empty, lines)) + + result["file_name"] = file_name + result["title"] = lines[2].strip() + result["short_description"] = lines[3].strip().replace( + "Short Description: ", "") + + cur = 5 + notes = "" + while lines[cur] != "Device Details": + notes += lines[cur] + " " + cur += 1 + result["buxton_notes"] = notes.strip() + + cur += 1 + clean = list( + map(lambda data: data.strip().split(":"), lines[cur].split("|"))) + result["company"] = clean[0][len(clean[0]) - 1].strip() + result["year"] = clean[1][len(clean[1]) - 1].strip() + result["original_price"] = sanitize_price( + clean[2][len(clean[2]) - 1].strip()) + + cur += 1 + result["degrees_of_freedom"] = extract_value( + lines[cur]).replace("NA", "N/A") + cur += 1 + + dimensions = lines[cur].lower() + if dimensions.startswith("dimensions"): + dim_concat = dimensions[11:].strip() + cur += 1 + while lines[cur] != "Key Words": + dim_concat += (" " + lines[cur].strip()) + cur += 1 + result["dimensions"] = dim_concat + else: + result["dimensions"] = "N/A" + + cur += 1 + result["primary_key"] = extract_value(lines[cur]) + cur += 1 + result["secondary_key"] = extract_value(lines[cur]) + + while lines[cur] != "Links": + result["secondary_key"] += (" " + extract_value(lines[cur]).strip()) + cur += 1 + + cur += 1 + link_descriptions = [] + while lines[cur] != "Image": + link_descriptions.append(lines[cur].strip()) + cur += 1 + result["link_descriptions"] = listify(link_descriptions) + + result["hyperlinks"] = extract_links(source + "/" + file_name) + + images = [] + captions = [] + cur += 3 + while cur + 1 < len(lines) and lines[cur] != "NOTES:": + images.append(lines[cur]) + captions.append(lines[cur + 1]) + cur += 2 + result["images"] = listify(images) + result["captions"] = listify(captions) + + notes = [] + if (cur < len(lines) and lines[cur] == "NOTES:"): + cur += 1 + while cur < len(lines): + notes.append(lines[cur]) + cur += 1 + if len(notes) > 0: + result["notes"] = listify(notes) + + print("writing child schema...") + + return { + "schema": { + "_id": guid(), + "fields": result, + "__type": "Doc" + }, + "child_guids": view_guids + } + + +def proxify_guids(guids): + return list(map(lambda guid: {"fieldId": guid, "__type": "proxy"}, guids)) + + +if os.path.exists(dist): + shutil.rmtree(dist) +while os.path.exists(dist): + pass +os.mkdir(dist) +mkdir_if_absent(source) + +candidates = 0 +for file_name in os.listdir(source): + if file_name.endswith('.docx'): + candidates += 1 + schema_guids.append(write_schema( + parse_document(file_name), ["title", "data"], "image_data")) + +print("writing parent schema...") +parent_guid = write_schema({ + "schema": { + "_id": guid(), + "fields": {}, + "__type": "Doc" + }, + "child_guids": schema_guids +}, ["title", "short_description", "original_price"], "data") + +print("appending parent schema to main workspace...\n") +db.newDocuments.update_one( + {"fields.title": "WS collection 1"}, + {"$push": {"fields.data.fields": {"fieldId": parent_guid, "__type": "proxy"}}} +) + +print("rewriting .gitignore...\n") +lines = ['*', '!.gitignore'] +with open(dist + "/.gitignore", 'w') as f: + f.write('\n'.join(lines)) + +suffix = "" if candidates == 1 else "s" +print(f"conversion complete. {candidates} candidate{suffix} processed.") diff --git a/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx b/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx Binary files differnew file mode 100644 index 000000000..06094b4d3 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx b/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx Binary files differnew file mode 100644 index 000000000..356697092 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx diff --git a/src/scraping/buxton/source/Bill_Notes_CasioC801.docx b/src/scraping/buxton/source/Bill_Notes_CasioC801.docx Binary files differnew file mode 100644 index 000000000..cd89fb97b --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_CasioC801.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx b/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx Binary files differnew file mode 100644 index 000000000..a503cddfc --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx diff --git a/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx b/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx Binary files differnew file mode 100644 index 000000000..4d13a8cf5 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx b/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx Binary files differnew file mode 100644 index 000000000..578a1be08 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx diff --git a/src/scraping/buxton/source/Bill_Notes_FrogPad.docx b/src/scraping/buxton/source/Bill_Notes_FrogPad.docx Binary files differnew file mode 100644 index 000000000..d01e1bf5c --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_FrogPad.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx b/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx Binary files differnew file mode 100644 index 000000000..7bd28b376 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx b/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx Binary files differnew file mode 100644 index 000000000..0615c4953 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Matias.docx b/src/scraping/buxton/source/Bill_Notes_Matias.docx Binary files differnew file mode 100644 index 000000000..547603256 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_Matias.docx diff --git a/src/scraping/buxton/source/Bill_Notes_MousePen.docx b/src/scraping/buxton/source/Bill_Notes_MousePen.docx Binary files differnew file mode 100644 index 000000000..4e1056636 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_MousePen.docx diff --git a/src/scraping/buxton/source/Bill_Notes_NewO.docx b/src/scraping/buxton/source/Bill_Notes_NewO.docx Binary files differnew file mode 100644 index 000000000..a514926d2 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_NewO.docx diff --git a/src/scraping/buxton/source/Bill_Notes_OLPC.docx b/src/scraping/buxton/source/Bill_Notes_OLPC.docx Binary files differnew file mode 100644 index 000000000..bfca0a9bb --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_OLPC.docx diff --git a/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx b/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx Binary files differnew file mode 100644 index 000000000..c0cf6ba9a --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx b/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx Binary files differnew file mode 100644 index 000000000..ad06903f3 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx diff --git a/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx b/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx Binary files differnew file mode 100644 index 000000000..e4c659de9 --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx diff --git a/src/scraping/buxton/source/Bill_Notes_The_Tap.docx b/src/scraping/buxton/source/Bill_Notes_The_Tap.docx Binary files differnew file mode 100644 index 000000000..8ceebc71e --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_The_Tap.docx diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index c4af5cdaa..5c13495ff 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -16,6 +16,7 @@ export enum RouteStore { // USER AND WORKSPACES getCurrUser = "/getCurrentUser", + getUsers = "/getUsers", getUserDocumentId = "/getUserDocumentId", updateCursor = "/updateCursor", diff --git a/src/server/Search.ts b/src/server/Search.ts index 5ca5578a7..2db2b242c 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -14,16 +14,17 @@ export class Search { }); return res; } catch (e) { - console.warn("Search error: " + e + document); + // console.warn("Search error: " + e + document); } } - public async search(query: string) { + public async search(query: string, start: number = 0) { try { const searchResults = JSON.parse(await rp.get(this.url + "dash/select", { qs: { q: query, - fl: "id" + fl: "id", + start: start } })); const fields = searchResults.response.docs; diff --git a/src/server/database.ts b/src/server/database.ts index 70b3efced..d240bd909 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -120,9 +120,9 @@ export class Database { } } - public query(query: any): Promise<mongodb.Cursor> { + public query(query: any, collectionName = "newDocuments"): Promise<mongodb.Cursor> { if (this.db) { - return Promise.resolve<mongodb.Cursor>(this.db.collection('newDocuments').find(query)); + return Promise.resolve<mongodb.Cursor>(this.db.collection(collectionName).find(query)); } else { return new Promise<mongodb.Cursor>(res => { this.onConnect.push(() => res(this.query(query))); diff --git a/src/server/index.ts b/src/server/index.ts index eda1ab422..e645e29b4 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,3 +1,4 @@ +require('dotenv').config(); import * as bodyParser from 'body-parser'; import { exec } from 'child_process'; import * as cookieParser from 'cookie-parser'; @@ -7,6 +8,7 @@ import * as expressValidator from 'express-validator'; import * as formidable from 'formidable'; import * as fs from 'fs'; import * as sharp from 'sharp'; +import * as Pdfjs from 'pdfjs-dist'; const imageDataUri = require('image-data-uri'); import * as mobileDetect from 'mobile-detect'; import * as passport from 'passport'; @@ -27,6 +29,7 @@ import { MessageStore, Transferable, Types, Diff } from "./Message"; import { RouteStore } from './RouteStore'; const app = express(); const config = require('../../webpack.config'); +import { createCanvas, loadImage, Canvas } from "canvas"; const compiler = webpack(config); const port = 1050; // default port to listen const serverPort = 4321; @@ -36,11 +39,24 @@ import c = require("crypto"); import { Search } from './Search'; import { debug } from 'util'; import _ = require('lodash'); +import { Response } from 'express-serve-static-core'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); +const probe = require("probe-image-size"); const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); +const release = process.env.RELEASE === "true"; +if (process.env.RELEASE === "true") { + console.log("Running server in release mode"); +} else { + console.log("Running server in debug mode"); +} +console.log(process.env.PWD); +let clientUtils = fs.readFileSync("./src/client/util/ClientUtils.ts.temp", "utf8"); +clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(release))}`; +fs.writeFileSync("./src/client/util/ClientUtils.ts", clientUtils, "utf8"); + const mongoUrl = 'mongodb://localhost:27017/Dash'; mongoose.connect(mongoUrl); mongoose.connection.on('connected', () => console.log("connected")); @@ -132,6 +148,64 @@ app.get("/search", async (req, res) => { res.send(results); }); +app.get("/thumbnail/:filename", (req, res) => { + let filename = req.params.filename; + let noExt = filename.substring(0, filename.length - ".png".length); + let pagenumber = parseInt(noExt[noExt.length - 1]); + fs.exists(uploadDir + filename, (exists: boolean) => { + console.log(`${uploadDir + filename} ${exists ? "exists" : "does not exist"}`); + if (exists) { + let input = fs.createReadStream(uploadDir + filename); + probe(input, (err: any, result: any) => { + if (err) { + console.log(err); + return; + } + res.send({ path: "/files/" + filename, width: result.width, height: result.height }); + }); + } + else { + LoadPage(uploadDir + filename.substring(0, filename.length - "-n.png".length) + ".pdf", pagenumber, res); + } + }); +}); + +function LoadPage(file: string, pageNumber: number, res: Response) { + console.log(file); + Pdfjs.getDocument(file).promise + .then((pdf: Pdfjs.PDFDocumentProxy) => { + let factory = new NodeCanvasFactory(); + console.log(pageNumber); + pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => { + console.log("reading " + page); + let viewport = page.getViewport(1); + let canvasAndContext = factory.create(viewport.width, viewport.height); + let renderContext = { + canvasContext: canvasAndContext.context, + viewport: viewport, + canvasFactory: factory + }; + console.log("read " + pageNumber); + + page.render(renderContext).promise + .then(() => { + console.log("saving " + pageNumber); + let stream = canvasAndContext.canvas.createPNGStream(); + let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`; + let out = fs.createWriteStream(pngFile); + stream.pipe(out); + out.on("finish", () => { + console.log(`Success! Saved to ${pngFile}`); + let name = path.basename(pngFile); + res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height }); + }); + }, (reason: string) => { + console.error(reason + ` ${pageNumber}`); + }); + }); + }); +} + // anyone attempting to navigate to localhost at this port will // first have to login addSecureRoute( @@ -143,6 +217,17 @@ addSecureRoute( addSecureRoute( Method.GET, + async (_, res) => { + const cursor = await Database.Instance.query({}, "users"); + const results = await cursor.toArray(); + res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId }))); + }, + undefined, + RouteStore.getUsers +); + +addSecureRoute( + Method.GET, (user, res, req) => { let detector = new mobileDetect(req.headers['user-agent'] || ""); let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; @@ -167,7 +252,31 @@ addSecureRoute( RouteStore.getCurrUser ); +class NodeCanvasFactory { + create = (width: number, height: number) => { + var canvas = createCanvas(width, height); + var context = canvas.getContext('2d'); + return { + canvas: canvas, + context: context, + }; + } + + reset = (canvasAndContext: any, width: number, height: number) => { + canvasAndContext.canvas.width = width; + canvasAndContext.canvas.height = height; + } + + destroy = (canvasAndContext: any) => { + canvasAndContext.canvas.width = 0; + canvasAndContext.canvas.height = 0; + canvasAndContext.canvas = null; + canvasAndContext.context = null; + } +} + const pngTypes = [".png", ".PNG"]; +const pdfTypes = [".pdf", ".PDF"]; const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"]; const uploadDir = __dirname + "/public/files/"; // SETTERS @@ -202,6 +311,38 @@ app.post( }); isImage = true; } + else if (pdfTypes.includes(ext)) { + // Pdfjs.getDocument(uploadDir + file).promise + // .then((pdf: Pdfjs.PDFDocumentProxy) => { + // let numPages = pdf.numPages; + // let factory = new NodeCanvasFactory(); + // for (let pageNum = 0; pageNum < numPages; pageNum++) { + // console.log(pageNum); + // pdf.getPage(pageNum + 1).then((page: Pdfjs.PDFPageProxy) => { + // console.log("reading " + pageNum); + // let viewport = page.getViewport(1); + // let canvasAndContext = factory.create(viewport.width, viewport.height); + // let renderContext = { + // canvasContext: canvasAndContext.context, + // viewport: viewport, + // canvasFactory: factory + // } + // console.log("read " + pageNum); + + // page.render(renderContext).promise + // .then(() => { + // console.log("saving " + pageNum); + // let stream = canvasAndContext.canvas.createPNGStream(); + // let out = fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`); + // stream.pipe(out); + // out.on("finish", () => console.log(`Success! Saved to ${uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`}`)); + // }, (reason: string) => { + // console.error(reason + ` ${pageNum}`); + // }); + // }); + // } + // }); + } if (isImage) { resizers.forEach(resizer => { fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext)); @@ -277,11 +418,21 @@ app.post(RouteStore.reset, postReset); app.use(RouteStore.corsProxy, (req, res) => req.pipe(request(req.url.substring(1))).pipe(res)); -app.get(RouteStore.delete, (req, res) => - deleteFields().then(() => res.redirect(RouteStore.home))); +app.get(RouteStore.delete, (req, res) => { + if (release) { + res.send("no"); + return; + } + deleteFields().then(() => res.redirect(RouteStore.home)); +}); -app.get(RouteStore.deleteAll, (req, res) => - deleteAll().then(() => res.redirect(RouteStore.home))); +app.get(RouteStore.deleteAll, (req, res) => { + if (release) { + res.send("no"); + return; + } + deleteAll().then(() => res.redirect(RouteStore.home)); +}); app.use(wdm(compiler, { publicPath: config.output.publicPath })); @@ -306,7 +457,9 @@ server.on("connection", function (socket: Socket) { Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args)); Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); - Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); + if (!release) { + Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); + } Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff)); diff --git a/tance.jumpToDocument(linkedFwdDocs[altKey 1 0], ctrlKey, false, document = this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey 1 0], targetContext); b/tance.jumpToDocument(linkedFwdDocs[altKey 1 0], ctrlKey, false, document = this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey 1 0], targetContext); new file mode 100644 index 000000000..0aa3ad47b --- /dev/null +++ b/tance.jumpToDocument(linkedFwdDocs[altKey 1 0], ctrlKey, false, document = this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey 1 0], targetContext); @@ -0,0 +1,792 @@ +[33mcommit cc1f3b32d60786b56280a8b3c00059aa7823af89[m +Merge: a81677c deb8576 +Author: Fawn <fangrui_tong@brown.edu> +Date: Wed Jun 26 14:54:46 2019 -0400 + + merge + +[33mcommit a81677c7dffafa5134d4c5cbe893f7a886eaab63[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Wed Jun 26 14:48:16 2019 -0400 + + can clear links on a doc + +[33mcommit 69e37491908b5c189b94f780994c1f142c69be2e[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Wed Jun 26 14:15:40 2019 -0400 + + minor changes + +[33mcommit deb85766ac5648cc8e3ab4bf9d182ac5bbbbe144[m +Merge: 219cabb 5e47775 +Author: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> +Date: Wed Jun 26 12:51:18 2019 -0400 + + Merge pull request #170 from browngraphicslab/presentation-selection-mohammad + + Presentation selection mohammad + +[33mcommit 5e477755b392128ab8b39c082f16dd67708be0d2[m +Merge: 444f970 6d1f161 +Author: Sam Wilkins <samuel_wilkins@brown.edu> +Date: Wed Jun 26 12:48:45 2019 -0400 + + Merge branch 'presentation-selection-mohammad' of https://github.com/browngraphicslab/Dash-Web into presentation-selection-mohammad + +[33mcommit 444f970365a4280376e929e78c16090f6ae92739[m +Merge: 64ffa0a 219cabb +Author: Sam Wilkins <samuel_wilkins@brown.edu> +Date: Wed Jun 26 12:48:40 2019 -0400 + + merged with master + +[33mcommit 6d1f161de3c27ec07673b5e48a915961177b57b6[m +Author: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> +Date: Wed Jun 26 12:39:54 2019 -0400 + + long line wrap + +[33mcommit f0632e4f6b608d05ef6d9f77d93da259c58c1e8d[m +Author: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> +Date: Wed Jun 26 12:33:16 2019 -0400 + + long line wrap + +[33mcommit 0d5e2537520ca1e6a6b52f4d0f03aa2bcfc6c5c6[m +Author: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> +Date: Wed Jun 26 12:30:16 2019 -0400 + + cleanup + +[33mcommit 8954bac59b50aa3618625379a17dbefe9aceca72[m +Author: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> +Date: Wed Jun 26 12:29:07 2019 -0400 + + removed console.logs + +[33mcommit d0ff42632f8a155303e11945a1a974a15052f0db[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Wed Jun 26 11:40:36 2019 -0400 + + link menu styling + +[33mcommit a3c4aa24a9e9074da8f2421954f610c8178e10b1[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Tue Jun 25 21:28:15 2019 -0400 + + link metadata values appear on first load + +[33mcommit ca8a78de9957ad27d345ad51fdaee9dae3f096bd[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Tue Jun 25 20:44:34 2019 -0400 + + can't link to containing collection + +[33mcommit 2d300b0cd3d02c900865c61eacd539efed5289e6[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Tue Jun 25 20:18:14 2019 -0400 + + fixed link metadata rendering bug + +[33mcommit 2a698e88da5ef0a9fee1ff4ee69746f1242798c9[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Tue Jun 25 18:32:17 2019 -0400 + + fixed render links in treeview + +[33mcommit 7abe170ce5bd0c415e23456eb2bed26e8fdee7aa[m +Merge: 41cf1e8 219cabb +Author: Fawn <fangrui_tong@brown.edu> +Date: Tue Jun 25 18:23:26 2019 -0400 + + merge + +[33mcommit 41cf1e8536964764f18ab752140e484e36cbe464[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Tue Jun 25 17:09:36 2019 -0400 + + links can save + +[33mcommit 64ffa0accfc872c81035079527952aabaf56c6f6[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Tue Jun 25 13:16:45 2019 -0400 + + Small Css Fix On weight + +[33mcommit 219cabb3fe42ab199550efc3423b7aaed4e1ee93[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Mon Jun 24 22:45:19 2019 -0400 + + Switched shift drag of tabs to normal drag and added drag target for document drag + +[33mcommit d475b19e9ba7bc8870ec7bc1e10b5cc88decea0b[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Mon Jun 24 15:56:42 2019 -0400 + + fixed crash + +[33mcommit 522970375fe0227f9221a7e8be02875afd74ca63[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Mon Jun 24 14:01:29 2019 -0400 + + link menu styling + +[33mcommit addf0e443f64951a437701f0d5a087c1d5968faf[m +Merge: c9f77d5 d01039b +Author: tschicke-brown <tyler_schicke@brown.edu> +Date: Mon Jun 24 13:57:02 2019 -0400 + + Merge pull request #167 from browngraphicslab/schema_fixes + + Schema and scripting fixes + +[33mcommit d01039b10f0ebd328224c0b1a190b0f884a7c727[m +Merge: 6abf829 c9f77d5 +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Mon Jun 24 13:56:30 2019 -0400 + + Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web into schema_fixes + +[33mcommit c9f77d5aab98e6e7865cdcad957d5c937631775d[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Mon Jun 24 13:41:39 2019 -0400 + + Added ReadOnly mode for docs and changed computed values a bit + +[33mcommit e18662f2fa9e1d3dd1b0eb3b5531092258d05972[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Mon Jun 24 12:42:44 2019 -0400 + + Refactoring + +[33mcommit 52051829373bc4acfe9d705b64c30e3fddebf439[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Mon Jun 24 10:49:05 2019 -0400 + + Fixed image size stuff + +[33mcommit ac781d2fb714ca26fb364d00d5aeb7a20b008655[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Mon Jun 24 10:26:57 2019 -0400 + + Changed how zooming works + +[33mcommit 6e5cd0e991e2e6d7ae8de1d73ff273ba0737355c[m +Author: Tyler Schicke <tschicke@gmail.com> +Date: Sun Jun 23 17:23:33 2019 -0400 + + Fixed shift dragging with no open panes + +[33mcommit 32ef8d83d5829e2faadbebaf6f9b694df5d7ea02[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Fri Jun 21 17:41:20 2019 -0400 + + link menu styling + +[33mcommit 7962aff8431b692af5229cd8e6c390bbe1110336[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Fri Jun 21 16:29:31 2019 -0400 + + link menu styling + +[33mcommit a4b34adcb34184728be0b69b33a561f6d10f0a98[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Fri Jun 21 16:27:03 2019 -0400 + + can drag just a group of links on a doc + +[33mcommit e1f5f341854944c533efdb7d36306edd1e1dc747[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Fri Jun 21 14:53:08 2019 -0400 + + Some More documentation + +[33mcommit 542f25d4af36cf0948696d45afba2e9e19f5bc37[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Fri Jun 21 14:47:11 2019 -0400 + + Redo Grouping Fixed + +[33mcommit 60f9122ea31d660d60d5429890c4eb0ef6d8613b[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Fri Jun 21 13:41:25 2019 -0400 + + following link without viewdoc opens it to right + +[33mcommit d78c651322ad228152b862eaa378946fe65cc9f9[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Fri Jun 21 13:32:23 2019 -0400 + + dragged links from menu are aliases + +[33mcommit 179afa6e80631fcb8899408c3961bf1757e5b19b[m +Merge: ca5e29f a40e7bb +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Thu Jun 20 22:23:40 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit ca5e29fdc7c238274eaf90682a8fa2ddc90e4e17[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Thu Jun 20 22:22:57 2019 -0400 + + fix to open on right, fix to image drag fro web, and layout fixes for stacking view multi-column + +[33mcommit a40e7bb5e9d1256002083d7e3f3c4db60cd8e9df[m +Author: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> +Date: Thu Jun 20 19:41:39 2019 -0400 + + Fixed missed pointer up event + +[33mcommit f4b75a7c921181faeeee04fbd57cd24fbd57523e[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Thu Jun 20 19:16:42 2019 -0400 + + Undo/Redo First Version + +[33mcommit b1a2871fcca57ce934b8613b315a08eede188669[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Thu Jun 20 19:03:16 2019 -0400 + + link menu styling + +[33mcommit f2b54dc49205f8ea8944e26e43662a0c8dd08ed0[m +Merge: 0cab79a 7d0f6c1 +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 18:36:04 2019 -0400 + + Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web + +[33mcommit 0cab79a50719719e1dade40520a6967f7aa8f951[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 18:35:45 2019 -0400 + + Added debug and release modes to server and client + +[33mcommit fbfe9faca199b6dedd6844f1fa20cc02060a3c5a[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Thu Jun 20 18:25:49 2019 -0400 + + can see what docs are linked to in treeview: + +[33mcommit 7d0f6c18489f7155818611721985d9610b08d8e7[m +Merge: d2dfc0f 46a2a9e +Author: yipstanley <stanley_yip@brown.edu> +Date: Thu Jun 20 17:50:46 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit 1f172642d12c4669960b8526324e4bd034994be4[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 17:44:24 2019 -0400 + + Added arrange documents in grid command + +[33mcommit d2dfc0f9d35f0084a7c0dea73215f5d21055f2f3[m +Author: yipstanley <stanley_yip@brown.edu> +Date: Thu Jun 20 17:17:14 2019 -0400 + + pdf page sizes loading error + +[33mcommit e6ebed17e6ddb2ccee81d65fcb451a9b54302762[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Thu Jun 20 17:12:48 2019 -0400 + + links can be made from freeform view to treeview + +[33mcommit 46a2a9e1f10b63feeb21a1e186daeaef2ccbcda4[m +Merge: a39b285 a5dc0e0 +Author: bob <bcz@cs.brown.edu> +Date: Thu Jun 20 17:11:29 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit a39b2854b848006c19460685d7bf4005a9f650ae[m +Author: bob <bcz@cs.brown.edu> +Date: Thu Jun 20 17:09:50 2019 -0400 + + moved AddDocToList to Doc utils + +[33mcommit a5dc0e04add05f2f5bf1e17f1ac0a5e0aba1ea41[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 16:27:44 2019 -0400 + + Added hidden flag to documents + +[33mcommit e88538bb8af2ba648da2326d0f6edd3e0186766e[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Thu Jun 20 15:45:07 2019 -0400 + + Title changing to presentations added + +[33mcommit 9b3e80def0be6c09c31b5176817a54323d217d81[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 15:06:41 2019 -0400 + + Handled more events in editable view + +[33mcommit 1f24c5010a1cf6365265ea1f02327bb81a98134a[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 14:54:55 2019 -0400 + + Doc.GetProto change and swapped KVP syntax + +[33mcommit 4360287e6cafcb59af1ae62fc31ddc161bcf2e51[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Thu Jun 20 12:56:13 2019 -0400 + + styling of link proxy + +[33mcommit 711abbeba69e4d9afc634b8edf019b12b6dff915[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Thu Jun 20 12:54:41 2019 -0400 + + Documentation and reset Presentation at removal fixed + +[33mcommit a0246ef84396545f79fc4a8b21de1a56cbf06aca[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Thu Jun 20 11:34:28 2019 -0400 + + merge + +[33mcommit 8dbfb3029a99eaf37a5234e9d9e33cc64f779b03[m +Merge: af8e5cf e9d62f4 +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 11:33:01 2019 -0400 + + Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web + +[33mcommit af8e5cf1bfbfa2d57b4fd89c72306a71d8cabe1d[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 11:32:54 2019 -0400 + + Fixed context menu search + +[33mcommit cd2db5bf11fb89e3cd7016f7f798d65698c74c5e[m +Merge: 73f0378 e9d62f4 +Author: Fawn <fangrui_tong@brown.edu> +Date: Thu Jun 20 11:31:15 2019 -0400 + + merge + +[33mcommit 73f03785f938542a91b28b35043f2feda2bc1432[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Thu Jun 20 11:26:33 2019 -0400 + + merge + +[33mcommit e9d62f4ca0dbeb57e46239047041a8a04da7b504[m +Author: bob <bcz@cs.brown.edu> +Date: Thu Jun 20 11:26:16 2019 -0400 + + changed color picker. fixed delting selected docs. fixed scaling items in nested panels. + +[33mcommit a5478b2d4cc3b66c6b58471cbb05c623d0109724[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 10:04:51 2019 -0400 + + "Fixed" search + +[33mcommit 01aee875e626c695fe208addaaa6f58aad387dd6[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Thu Jun 20 10:02:08 2019 -0400 + + Mostly keep context menu on screen + +[33mcommit 38de022621175bda7410df4444fcd2bbee0919cb[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Wed Jun 19 23:43:47 2019 -0400 + + slight tweaks. + +[33mcommit 9e55bfaad39aa47ab0594c6af7f1aa68e2a8db7a[m +Merge: 118ecb1 827c589 +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Wed Jun 19 22:40:57 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit 118ecb14ce519bcbade12b3d52e11b22fcc371b3[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Wed Jun 19 22:40:54 2019 -0400 + + cleaned up and enhanced tree view + +[33mcommit c5e401cb0a7fec2279ceecbc8d1429dcdd2f04b9[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Wed Jun 19 22:27:21 2019 -0400 + + buttons on cut links functional except for when dragged from link menu + +[33mcommit 6fc6054dc7aea144fd967a8cb3fe7d8fe5ec6d6d[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Wed Jun 19 19:13:30 2019 -0400 + + Width of the presentations fixed, removal of presentations option added, backUP group and normal groups updated when a doc is removed from presentation by removing it from both + +[33mcommit 827c58950b649629c84211d41fdd4d041287801e[m +Merge: 05e50f2 96c26c5 +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Wed Jun 19 18:49:50 2019 -0400 + + Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web + +[33mcommit 96c26c57527d443784bde9752551bfa10b3ce4d2[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Wed Jun 19 18:34:45 2019 -0400 + + removed marquee summarizing icon + +[33mcommit 05e50f27a15e8a02ffb27606c51026d1b85bc677[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Wed Jun 19 17:36:52 2019 -0400 + + Added basic keyboard controls to context menu + +[33mcommit fa37e023b88127cb8a6b393a848200361a396fb4[m +Merge: 565b27c 5b2a498 +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Wed Jun 19 16:21:09 2019 -0400 + + Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web + +[33mcommit 565b27cca8953a60067de367cae4c0a99beb3cab[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Wed Jun 19 16:21:03 2019 -0400 + + started adding selection to context menu + +[33mcommit 5b2a498aca75bd53ffab61f998218bec546b8154[m +Merge: 358437e 39e8a7a +Author: bob <bcz@cs.brown.edu> +Date: Wed Jun 19 16:17:21 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit 358437eeafe42e029ffe27702bde15a3fad54a3b[m +Author: bob <bcz@cs.brown.edu> +Date: Wed Jun 19 16:17:18 2019 -0400 + + working version of embedded tree view docs. + +[33mcommit 4c1383e47f2203a00bc7f3d73c209f3149d6a772[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Wed Jun 19 15:53:05 2019 -0400 + + ... + +[33mcommit a288a2fd0a30a3a16dd01bc4e12dcf6bc117c766[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Wed Jun 19 15:25:24 2019 -0400 + + Navigation and Zoom Option For Manual Selection Added and New Presentation TItle Naming Added + + Now, You can manually click on navigate or zoom and navigate to that document if current was their index. A way to manually disregard groups, and just navigate to that doc. + +[33mcommit 39e8a7a365442cdc11024c4de8019184fd0057ac[m +Merge: 5b6f13d 9ab4739 +Author: Stanley Yip <33562077+yipstanley@users.noreply.github.com> +Date: Wed Jun 19 15:05:38 2019 -0400 + + Merge pull request #163 from browngraphicslab/pdf_fixes + + deleting annotations + +[33mcommit 5b6f13d64e9e38b94df0ae61ffedcb0b34290045[m +Merge: 35e73f3 4ebbdd8 +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Wed Jun 19 15:04:46 2019 -0400 + + Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web + +[33mcommit 35e73f369a2145d8a042e0011a43e71763d57998[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Wed Jun 19 15:02:48 2019 -0400 + + added better search to context menu + +[33mcommit 9ab47393a2ce3d174ad3238422c2c310764be9af[m +Author: yipstanley <stanley_yip@brown.edu> +Date: Wed Jun 19 14:40:28 2019 -0400 + + interaction improvements with delete button + +[33mcommit b9849810231e540a5898a56012abd32c197b23b5[m +Author: yipstanley <stanley_yip@brown.edu> +Date: Wed Jun 19 14:39:15 2019 -0400 + + anna + +[33mcommit b960a876d6a31b3eaebb0ac6eca6f191a0d4c900[m +Author: yipstanley <stanley_yip@brown.edu> +Date: Wed Jun 19 14:38:43 2019 -0400 + + oop + +[33mcommit 46d57bc21cda4703855b85a4603bd471975d845b[m +Author: yipstanley <stanley_yip@brown.edu> +Date: Wed Jun 19 14:25:47 2019 -0400 + + deleting annotations + +[33mcommit f362dbfc237536c6c4a8c6d088c3dc818080f7c2[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Wed Jun 19 12:50:58 2019 -0400 + + both tail ends of a cut link appear on hover/focus of an anchor + +[33mcommit fb62f3b2e39bbe2dd3da5eaffedbaa8e60f06dbb[m +Author: Mohammad Amoush <mohammad_amoush@brown.edu> +Date: Wed Jun 19 12:35:54 2019 -0400 + + Grouping for different presentations fixed + +[33mcommit 4ebbdd803cdf83806902509dfa0432ce3a139403[m +Merge: 0bb2052 c056ade +Author: Stanley Yip <33562077+yipstanley@users.noreply.github.com> +Date: Wed Jun 19 11:48:16 2019 -0400 + + Merge pull request #162 from browngraphicslab/pdf_fixes + + Pdf fixes + +[33mcommit c056adeca11f35972b5f75c6b1cc31292d5765d4[m +Author: yipstanley <stanley_yip@brown.edu> +Date: Wed Jun 19 11:47:20 2019 -0400 + + push + +[33mcommit 37f327ab659e6fa1221f9f4ed7649402c5dedc00[m +Author: yipstanley <stanley_yip@brown.edu> +Date: Wed Jun 19 11:24:32 2019 -0400 + + aspect ratio, dragging, and full screen scrolling fixed + +[33mcommit 0bb20528c8167b3ba1c4c88d97586d50ae183b4c[m +Author: bob <bcz@cs.brown.edu> +Date: Wed Jun 19 10:37:36 2019 -0400 + + added highlight for expanded tree view items + +[33mcommit f60398d5db9041e09c809c16a0b885936ac11a3d[m +Author: bob <bcz@cs.brown.edu> +Date: Wed Jun 19 10:21:37 2019 -0400 + + fixed multi-column stacking + +[33mcommit 0674331f3611d297028526c888c718a75b012e0a[m +Author: bob <bcz@cs.brown.edu> +Date: Wed Jun 19 09:36:21 2019 -0400 + + fixed resizing stacking views. changed defaults for new docs in treeView + +[33mcommit 1472d2b56aa64896f0a93f172322121d19cd1592[m +Author: bob <bcz@cs.brown.edu> +Date: Wed Jun 19 09:11:35 2019 -0400 + + fixed lint errors. + +[33mcommit 8c94bb92b23dea138fa752929b6134e7214dfb60[m +Merge: 3b880d7 13e301d +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Tue Jun 18 22:51:48 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit 3b880d7b15b7107049ae27601b9f759b17f7fde9[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Tue Jun 18 22:51:46 2019 -0400 + + added initial keyboard shortcuts for adding and moving docs in TreeView. fixed image drag bug. + +[33mcommit 13e301dea2f537b67b338cc6a98d3f3b5a8e1f36[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Tue Jun 18 20:58:32 2019 -0400 + + Fixed linter errors + +[33mcommit 464fa03d6ebb2a7aaef1d7622afa3e1e7ee816a3[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Tue Jun 18 20:11:31 2019 -0400 + + Context menu improvements and error fixes + +[33mcommit 4ffcff69a2fc767c6a03d46d7296b6a8c7ffd281[m +Author: madelinegr <mgriswold99@gmail.com> +Date: Tue Jun 18 19:13:45 2019 -0400 + + Presentations Listed, Option to Change Added, and + +[33mcommit ca126adda9e4def83fb5c2e07e382917ca0b4ee0[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Tue Jun 18 17:24:59 2019 -0400 + + Fixed docking view? + +[33mcommit b0ac30172019713e1c75083c1199485d902e0eed[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Tue Jun 18 16:37:28 2019 -0400 + + Fixed zoomBasis stuff and added deletion handling for reponse from server + +[33mcommit 8e5afb5bbb47324a381b5184254e77eba7bd8536[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Tue Jun 18 16:30:24 2019 -0400 + + can click on button link to node in different context than source + +[33mcommit 6fcd0d8d6fb1471b8af460f6d80bdf0d0e681566[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Tue Jun 18 15:17:27 2019 -0400 + + added button to delete a link + +[33mcommit d91e7eec9a62363b383b929166cdf600b124334c[m +Author: Fawn <fangrui_tong@brown.edu> +Date: Tue Jun 18 15:09:21 2019 -0400 + + links to nodes in different contexts render as a circle + +[33mcommit d3cad099d49690810166d0342f7c371bda0f007e[m +Merge: 04668e2 b1af251 +Author: bob <bcz@cs.brown.edu> +Date: Tue Jun 18 13:30:55 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit 04668e21313f6e62e5ab35ac737fc54191769a5a[m +Author: bob <bcz@cs.brown.edu> +Date: Tue Jun 18 13:30:41 2019 -0400 + + fixed cleanup of marquee keyhandler. + +[33mcommit b1af251b058743798aa3fa3895d22327c8560dfc[m +Author: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> +Date: Tue Jun 18 13:19:50 2019 -0400 + + Added pointer down flag for tab focus + +[33mcommit 9544576ec0167d64f564ae4c87d392eba07ff467[m +Author: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> +Date: Tue Jun 18 13:18:34 2019 -0400 + + Added tab focusing on hover + +[33mcommit 2633f61d311528e62d50d4ff56f5884b3b51ac61[m +Author: bob <bcz@cs.brown.edu> +Date: Tue Jun 18 13:12:15 2019 -0400 + + added undo/redo bindings for app. + +[33mcommit 3a25bad918c72f5d6de9a720de9e0d316c00f2fe[m +Author: bob <bcz@cs.brown.edu> +Date: Tue Jun 18 13:03:28 2019 -0400 + + fixed issues with expanding text boxes that have a dynamic title + +[33mcommit f4fcf306e2579b7479610899a01c06fb157d47de[m +Author: bob <bcz@cs.brown.edu> +Date: Tue Jun 18 12:03:14 2019 -0400 + + fixed goldenlayout nesting + +[33mcommit 4f0086f6ea948c1c5254db2acc93f6735987daa5[m +Merge: 749eef1 d7ebe7b +Author: bob <bcz@cs.brown.edu> +Date: Tue Jun 18 11:31:49 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit 749eef13af1338225b2bec4dbcd7a50a5650d285[m +Author: bob <bcz@cs.brown.edu> +Date: Tue Jun 18 11:31:46 2019 -0400 + + fixed image drag drop when not selected. + +[33mcommit d7ebe7b7d19cf7dc797443aa485293670c3ee4e2[m +Merge: 66d4cc9 08872de +Author: yipstanley <stanley_yip@brown.edu> +Date: Tue Jun 18 11:08:44 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit 66d4cc94bcc69f590d90dd35823f93b8e2fb90d8[m +Author: yipstanley <stanley_yip@brown.edu> +Date: Tue Jun 18 10:52:10 2019 -0400 + + selection fixes + +[33mcommit 08872def596af073c5f14336c8faf07f44561bbc[m +Merge: 8d00265 c50ba1c +Author: bob <bcz@cs.brown.edu> +Date: Tue Jun 18 10:28:31 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit 8d0026573ad9a196f864490bcf07c78f54082bad[m +Author: bob <bcz@cs.brown.edu> +Date: Tue Jun 18 10:28:29 2019 -0400 + + fixed selection within multicolumn stacking view. added drop of html image selections. + +[33mcommit c50ba1c4cc01d5cd085dee0dae6f633164efeb80[m +Merge: cc032e2 64e6a94 +Author: yipstanley <stanley_yip@brown.edu> +Date: Tue Jun 18 10:10:58 2019 -0400 + + Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web + +[33mcommit cc032e2f60015728f64f46ef009c9306e36746a0[m +Author: yipstanley <stanley_yip@brown.edu> +Date: Tue Jun 18 10:05:49 2019 -0400 + + fixes + +[33mcommit 64e6a941639aab8d7109178aa151a50909547309[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Tue Jun 18 09:05:41 2019 -0400 + + fixed index out of range + +[33mcommit 4b8324fcf44c5d3c3a4b3f6e98a4d1dfce84811b[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Tue Jun 18 08:53:01 2019 -0400 + + removed trace + +[33mcommit a3b8a57027d7c45ea19d259e1ec18fa6a8648c24[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Tue Jun 18 08:49:02 2019 -0400 + + looked like wrong code... + +[33mcommit 2f5c38c6a0a5220c2a31931c34d94e199854d703[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Tue Jun 18 08:36:37 2019 -0400 + + more streamlining + +[33mcommit 62c781c0c79ac395c5e117d208a90485ff1ba599[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Tue Jun 18 02:19:07 2019 -0400 + + faster loading of PDFs + +[33mcommit 4dc8c03562a0473becb895824740da487e16e771[m +Author: Bob Zeleznik <zzzman@gmail.com> +Date: Tue Jun 18 00:17:58 2019 -0400 + + added dropping of Dash urls from gmail + +[33mcommit 9c7ff72a8ad249c05b672a46e3fbbb69ffca3a2a[m +Merge: 8c64ffd 71b1cfb +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Mon Jun 17 23:04:22 2019 -0400 + + Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web + +[33mcommit 8c64ffd92e382050bc8727981cf9fb830e4f02a7[m +Author: Tyler Schicke <tyler_schicke@brown.edu> +Date: Mon Jun 17 23:04:07 2019 -0400 + + Added share with user functionality diff --git a/tsconfig.json b/tsconfig.json index 68f4b058a..9ea91ec49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,10 @@ { "compilerOptions": { "target": "es5", + // "module": "system", "removeComments": true, "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, "strict": true, "jsx": "react", "allowJs": true, |