diff options
author | Monika <monika_hedman@brown.edu> | 2019-07-01 15:39:37 -0400 |
---|---|---|
committer | Monika <monika_hedman@brown.edu> | 2019-07-01 15:39:37 -0400 |
commit | c5391bf0d352f69211ba28fc263d27204d7b8dc4 (patch) | |
tree | acbc632aff852e74015bd1c8eee4933a7565cb94 | |
parent | ee4155a168dadad182719eb55df3459d6a937a45 (diff) | |
parent | cd78d5b3371d73cc51dcb6f3dbfdca1a3bbcf6e1 (diff) |
edits made
102 files changed, 5273 insertions, 1432 deletions
diff --git a/package.json b/package.json index 86c722e62..3ec404561 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "", "main": "index.js", "scripts": { - "start": "cd solr-8.1.1/bin && solr.cmd start && cd ../../ && 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", diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex d70e95c0a..ff00f097e 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 4193e0b5f..7d7a1f02a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -25,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"; @@ -34,6 +34,8 @@ import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; import { UndoManager } from "../util/UndoManager"; import { RouteStore } from "../../server/RouteStore"; +import { LinkManager } from "../util/LinkManager"; +import { DocumentManager } from "../util/DocumentManager"; var requestImageSize = require('../util/request-image-size'); var path = require('path'); @@ -84,28 +86,31 @@ const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; export namespace DocUtils { export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") { - let protoSrc = source.proto ? source.proto : source; - let protoTarg = target.proto ? target.proto : target; + 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}); + let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); linkDoc.type = DocTypes.LINK; let linkDocProto = Doc.GetProto(linkDoc); + + linkDocProto.context = targetContext; linkDocProto.title = title === "" ? source.title + " to " + target.title : title; linkDocProto.linkDescription = description; linkDocProto.linkTags = tags; + linkDocProto.type = DocTypes.LINK; + + linkDocProto.anchor1 = source; + linkDocProto.anchor1Page = source.curPage; + linkDocProto.anchor1Groups = new List<Doc>([]); + linkDocProto.anchor2 = target; + linkDocProto.anchor2Page = target.curPage; + linkDocProto.anchor2Groups = new List<Doc>([]); - linkDocProto.linkedTo = target; - linkDocProto.linkedFrom = source; - linkDocProto.linkedToPage = target.curPage; - linkDocProto.linkedFromPage = source.curPage; - linkDocProto.linkedToContext = targetContext; + 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"); } @@ -122,6 +127,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"; @@ -132,6 +138,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 => { @@ -214,7 +221,7 @@ export namespace Docs { 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; @@ -263,6 +270,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); } @@ -359,7 +367,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> `); @@ -371,14 +379,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 { @@ -394,7 +402,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/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index bc9724b98..acd8dcef7 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,7 +1,7 @@ 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 { @@ -42,15 +44,14 @@ export class DocumentManager { DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document.proto; if (doc && doc[Id]) { - if(doc[Id] === id) - {toReturn.push(view);} + if (doc[Id] === id) { toReturn.push(view); } } }); } return toReturn; } - public getAllDocumentViews(doc: Doc){ + public getAllDocumentViews(doc: Doc) { return this.getDocumentViewsById(doc[Id]); } @@ -90,13 +91,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); } } @@ -107,37 +106,25 @@ 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, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => { let doc = Doc.GetProto(docDelegate); @@ -162,7 +149,7 @@ export class DocumentManager { docContext.panTransformType = "Ease"; targetContextView.props.focus(docDelegate, willZoom); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, undefined); setTimeout(() => { this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); }, 10); @@ -171,7 +158,7 @@ export class DocumentManager { const actualDoc = Doc.MakeAlias(docDelegate); actualDoc.libraryBrush = true; if (linkPage !== undefined) actualDoc.curPage = linkPage; - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, undefined); } } else { let contextView: DocumentView | null; @@ -180,7 +167,7 @@ export class DocumentManager { contextDoc.panTransformType = "Ease"; contextView.props.focus(docDelegate, willZoom); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, undefined); setTimeout(() => { this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); }, 10); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index b707dbe57..7dc48fb78 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,66 +1,94 @@ -import { action, runInAction, observable } from "mobx"; -import { Doc, DocListCastAsync } from "../../new_fields/Doc"; +import { action, runInAction } from "mobx"; +import { Doc } from "../../new_fields/Doc"; import { Cast } from "../../new_fields/Types"; +import { URLField } from "../../new_fields/URLField"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import * as globalCssVariables from "../views/globalCssVariables.scss"; -import { URLField } from "../../new_fields/URLField"; +import { DocumentManager } from "./DocumentManager"; +import { LinkManager } from "./LinkManager"; import { SelectionManager } from "./SelectionManager"; 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) { +export function SetupDrag( + _reference: React.RefObject<HTMLElement>, + docFunc: () => Doc | Promise<Doc>, + moveFunc?: DragManager.MoveFunction, + dropAction?: dropActionType, + options?: any, + dontHideOnDrop?: boolean, + dragStarted?: () => void +) { let onRowMove = async (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); 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], [undefined]); dragData.dropAction = dropAction; dragData.moveDocument = moveFunc; dragData.options = options; dragData.dontHideOnDrop = dontHideOnDrop; DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); + dragStarted && dragStarted(); }; let onRowUp = (): void => { document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); }; let onItemDown = async (e: React.PointerEvent) => { - // if (this.props.isSelected() || this.props.isTopMost) { if (e.button === 0) { e.stopPropagation(); + e.preventDefault(); 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), }, @@ -91,6 +119,8 @@ export namespace DragManager { hideSource: boolean | (() => boolean); + dragHasStarted?: () => void; + withoutShiftDrag?: boolean; } @@ -143,13 +173,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; @@ -180,12 +212,39 @@ export namespace DragManager { 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) { @@ -240,6 +299,8 @@ export namespace DragManager { const docs: Doc[] = 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; @@ -316,12 +377,12 @@ export namespace DragManager { } 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 943cdb4d1..63c879d67 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1,10 +1,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType, Slice } 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'; import { EditorState, Transaction, NodeSelection, TextSelection, Selection, } from "prosemirror-state"; import { EditorView, } from "prosemirror-view"; +import { View } from '@react-pdf/renderer'; const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; @@ -89,9 +90,9 @@ export const nodes: { [index: string]: NodeSpec } = { inline: true, attrs: { visibility: { default: false }, - oldtext: { default: undefined }, - oldtextslice: { default: undefined }, - oldtextlen: { default: 0 } + text: { default: undefined }, + textslice: { default: undefined }, + textlen: { default: 0 } }, group: "inline", @@ -280,8 +281,8 @@ export const marks: { [index: string]: MarkSpec } = { toDOM: () => ['sup'] }, - collapse: { - parseDOM: [{ style: 'color: blue' }], + highlight: { + parseDOM: [{ style: 'background: #d9dbdd' }], toDOM() { return ['span', { style: 'color: blue' @@ -349,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;' }], @@ -479,32 +490,37 @@ export class ImageResizeView { export class SummarizedView { // TODO: highlight text that is summarized. to find end of region, walk along mark _collapsed: HTMLElement; + _view: any; constructor(node: any, view: any, getPos: any) { this._collapsed = document.createElement("span"); this._collapsed.textContent = "㊉"; 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; 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"); let y = getPos(); - view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1 + node.attrs.oldtextlen))); - view.dispatch(view.state.tr.deleteSelection(view.state, () => { })); - //this._collapsed.textContent = "㊀"; + 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); + // update attrs of node + node.attrs.text = newSelection.content(); + node.attrs.textslice = newSelection.content().toJSON(); + 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"); let y = getPos(); - let mark = view.state.schema.mark(view.state.schema.marks.underline); - console.log("PASTING " + node.attrs.oldtext.toString()); + let mark = view.state.schema.mark(view.state.schema.marks.highlight); view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1))); - view.dispatch(view.state.tr.replaceSelection(node.attrs.oldtext).addMark(view.state.selection.from, view.state.selection.from + node.attrs.oldtextlen, mark)); - //this._collapsed.textContent = "㊉"; + const from = view.state.selection.from; + 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(); e.stopPropagation(); @@ -515,6 +531,27 @@ export class SummarizedView { selectNode() { } + updateSummarizedText(start?: any, mark?: any) { + let $start = this._view.state.doc.resolve(start); + 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: start, to: endPos }; + } + deselectNode() { } } @@ -533,7 +570,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/SelectionManager.ts b/src/client/util/SelectionManager.ts index 7dbb81e76..3bc71ad42 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,11 +1,13 @@ -import { observable, action, runInAction } from "mobx"; +import { observable, action, runInAction, IReactionDisposer, reaction, autorun } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; import { NumCast } from "../../new_fields/Types"; export namespace SelectionManager { + class Manager { + @observable IsDragging: boolean = false; @observable SelectedDocuments: Array<DocumentView> = []; @@ -18,6 +20,7 @@ export namespace SelectionManager { } manager.SelectedDocuments.push(docView); + // console.log(manager.SelectedDocuments); docView.props.whenActiveChanged(true); } } diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index 4d4eb386d..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; - background: whitesmoke; + 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: auto; - width:inherit; + height: 30px; + width:500px; .ProseMirror-example-setup-style hr { padding: 2px 10px; border: none; @@ -306,3 +309,10 @@ font-size: 12px; padding-right: 0px; } + .summarize{ + //margin-left: 15px; + color: white; + height: 20px; + // background-color: white; + text-align: center; + }
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index cbabadd64..9f8d0b2f6 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,53 +1,37 @@ -import { action, IReactionDisposer, reaction } from "mobx"; -import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css -import { baseKeymap, lift, deleteSelection } from "prosemirror-commands"; -import { history, redo, undo } from "prosemirror-history"; -import { keymap } from "prosemirror-keymap"; -import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; +import { action } from "mobx"; +import { Dropdown, MenuItem, icons, } from "prosemirror-menu"; //no import css +import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "./RichTextSchema"; -import { Schema, NodeType, MarkType, Mark } from "prosemirror-model"; -import React = require("react"); +import { Schema, NodeType, MarkType, Mark, ResolvedPos } from "prosemirror-model"; +import { Node as ProsNode } from "prosemirror-model"; import "./TooltipTextMenu.scss"; -const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); +const { toggleMark, setBlockType } = require("prosemirror-commands"); import { library } from '@fortawesome/fontawesome-svg-core'; -import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list'; -import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform'; -import { - faListUl, faGrinTongueSquint, -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { wrapInList, liftListItem, } from 'prosemirror-schema-list'; +import { faListUl } from '@fortawesome/free-solid-svg-icons'; import { FieldViewProps } from "../views/nodes/FieldView"; -import { throwStatement } from "babel-types"; const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js"); -import { View } from "@react-pdf/renderer"; import { DragManager } from "./DragManager"; import { Doc, Opt, Field } from "../../new_fields/Doc"; import { DocServer } from "../DocServer"; -import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { DocumentManager } from "./DocumentManager"; import { Id } from "../../new_fields/FieldSymbols"; -import { Utils } from "../../Utils"; -// import { wrap } from "module"; - -const SVG = "http://www.w3.org/2000/svg"; +import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { public tooltip: HTMLElement; - private num_icons = 0; private view: EditorView; private fontStyles: MarkType[]; private fontSizes: MarkType[]; private listTypes: NodeType[]; - private editorProps: FieldViewProps; - private state: EditorState; + private editorProps: FieldViewProps & FormattedTextBoxProps; private fontSizeToNum: Map<MarkType, number>; private fontStylesToName: Map<MarkType, string>; private listTypeToIcon: Map<NodeType, string>; - private fontSizeIndicator: HTMLSpanElement = document.createElement("span"); private link: HTMLAnchorElement; private linkEditor?: HTMLDivElement; @@ -58,15 +42,22 @@ 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 +69,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 +111,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 +123,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 +153,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 +175,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() { @@ -189,13 +199,13 @@ export class TooltipTextMenu { this.linkText.style.whiteSpace = "nowrap"; this.linkText.style.width = "150px"; this.linkText.style.overflow = "hidden"; - this.linkText.style.color = "black"; + this.linkText.style.color = "white"; this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); }; let linkBtn = document.createElement("div"); linkBtn.textContent = ">>"; linkBtn.style.width = "10px"; linkBtn.style.height = "10px"; - linkBtn.style.color = "black"; + linkBtn.style.color = "white"; linkBtn.style.cssFloat = "left"; linkBtn.onpointerdown = (e: PointerEvent) => { let node = this.view.state.selection.$from.nodeAfter; @@ -209,7 +219,7 @@ export class TooltipTextMenu { if (DocumentManager.Instance.getDocumentView(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, undefined); } })); } @@ -239,26 +249,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); - - let starButton = document.createElement("span"); - // starButton.style.width = '10px'; - // starButton.style.height = '10px'; - starButton.style.marginLeft = '10px'; - starButton.textContent = "Summarize"; - starButton.style.color = 'black'; - starButton.style.height = '20px'; - starButton.style.backgroundColor = 'white'; - starButton.style.textAlign = 'center'; - 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; @@ -284,18 +278,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, oldtext: 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.oldtext.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); } @@ -318,8 +311,8 @@ 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[]) { - let { empty, $cursor, ranges } = view.state.selection as TextSelection; + changeToMarkInGroup = (markType: MarkType, view: EditorView, fontMarks: MarkType[]) => { + let { $cursor, ranges } = view.state.selection as TextSelection; let state = view.state; let dispatch = view.dispatch; @@ -331,25 +324,34 @@ export class TooltipTextMenu { dispatch(state.tr.removeStoredMark(type)); } } else { - let has = false, tr = state.tr; + let has = false; for (let i = 0; !has && i < ranges.length; i++) { let { $from, $to } = ranges[i]; has = state.doc.rangeHasMark($from.pos, $to.pos, type); } for (let i of ranges) { - let { $from, $to } = i; if (has) { toggleMark(type)(view.state, view.dispatch, view); } } } } - }); //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); } //remove all node typeand apply the passed-in one to the selected text - changeToNodeType(nodeType: NodeType | undefined, view: EditorView, allNodes: NodeType[]) { + changeToNodeType(nodeType: NodeType | undefined, view: EditorView) { //remove old liftListItem(schema.nodes.list_item)(view.state, view.dispatch); if (nodeType) { //add new @@ -366,13 +368,69 @@ export class TooltipTextMenu { execEvent: "", class: "menuicon", css: css, - enable(state) { return true; }, + enable() { return true; }, run() { changeToMarkInGroup(markType, view, groupMarks); } }); } + createStar() { + return new MenuItem({ + title: "Summarize", + label: "Summarize", + icon: icons.join, + css: "color:white;", + class: "summarize", + execEvent: "", + run: (state, dispatch) => { + TooltipTextMenu.insertStar(state, dispatch); + } + + }); + } + + createCollapse() { + this._collapseBtn = new MenuItem({ + title: "Collapse", + //label: "Collapse", + icon: icons.join, + execEvent: "", + css: "color:white;", + class: "summarize", + run: () => { + 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({ @@ -419,7 +477,7 @@ export class TooltipTextMenu { execEvent: "", class: "menuicon", css: css, - enable(state) { return true; }, + enable() { return true; }, run() { changeToNodeInGroup(nodeType, view, groupNodes); } @@ -438,7 +496,7 @@ export class TooltipTextMenu { span.className = name + " menuicon"; span.title = title; span.textContent = text; - span.style.color = "black"; + span.style.color = "white"; return span; } @@ -502,90 +560,122 @@ 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"; - - // Otherwise, reposition it and update its content - 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(); } //finds all active marks on selection in given group activeMarksOnSelection(markGroup: MarkType[]) { //current selection - let { empty, $cursor, ranges } = this.view.state.selection as TextSelection; + let { empty, 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; + for (let i = 0; !has && i < ranges.length; i++) { + let { $from, $to } = ranges[i]; + 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) { + if (ref_node.isText) { } + 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) => { + if (node.isLeaf && !skip) { + ref_node = node; + skip = true; + } + + }); + } + } + return ref_node; + } + + destroy() { + this.tooltip.remove(); + } } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 2c0e18bbb..61e9d209a 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,9 @@ 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'; +import { ObjectField } from '../../new_fields/ObjectField'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -43,6 +45,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 +77,32 @@ 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 fieldTemplateView = SelectionManager.SelectedDocuments()[0]; + SelectionManager.DeselectAll(); + let fieldTemplate = fieldTemplateView.props.Document; + let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document; + let metaKey = text.slice(1, text.length); + + // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??) + let backgroundLayout = StrCast(fieldTemplate.backgroundLayout); + let layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`); + if (backgroundLayout) { + layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`); + backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`); + } + let nw = Cast(fieldTemplate.nativeWidth, "number"); + let nh = Cast(fieldTemplate.nativeHeight, "number"); + + fieldTemplate.title = metaKey; + fieldTemplate.layout = layout; + fieldTemplate.backgroundLayout = backgroundLayout; + fieldTemplate.nativeWidth = nw; + fieldTemplate.nativeHeight = nh; + fieldTemplate.embed = true; + fieldTemplate.isTemplate = true; + fieldTemplate.templates = new List<string>([Templates.TitleBar(metaKey)]); + fieldTemplate.proto = Doc.GetProto(docTemplate); } 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; @@ -468,10 +498,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> 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 = element.props.ScreenToLocalTransform().Scale; + let width = (doc.width || 0); + let height = (doc.height || (nheight / nwidth * width)); + let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling(); let actualdW = Math.max(width + (dW * scale), 20); let actualdH = Math.max(height + (dH * scale), 20); doc.x = (doc.x || 0) + dX * (actualdW - width); @@ -485,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 { - dW && (doc.width = zoomBasis * actualdW); - dH && (doc.height = zoomBasis * actualdH); - proto.autoHeight = undefined; + dW && (doc.width = actualdW); + dH && (doc.height = actualdH); + Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true); } } }); @@ -558,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; @@ -572,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} @@ -617,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={{ @@ -653,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/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts new file mode 100644 index 000000000..e467d7c61 --- /dev/null +++ b/src/client/views/GlobalKeyHandler.ts @@ -0,0 +1,145 @@ +import { UndoManager } from "../util/UndoManager"; +import { SelectionManager } from "../util/SelectionManager"; +import { CollectionDockingView } from "./collections/CollectionDockingView"; +import { MainView } from "./MainView"; +import { DragManager } from "../util/DragManager"; +import { action } from "mobx"; + +const modifiers = ["control", "meta", "shift", "alt"]; +type KeyHandler = (keycode: string) => KeyControlInfo; +type KeyControlInfo = { + preventDefault: boolean, + stopPropagation: boolean +}; + +export default class KeyManager { + public static Handler: KeyManager = new KeyManager(); + private router = new Map<string, KeyHandler>(); + + constructor() { + let isMac = navigator.platform.toLowerCase().indexOf("mac") >= 0; + + // SHIFT CONTROL ALT META + this.router.set("0000", this.unmodified); + this.router.set(isMac ? "0001" : "0100", this.ctrl); + this.router.set(isMac ? "0100" : "0010", this.alt); + this.router.set(isMac ? "1001" : "1100", this.ctrl_shift); + } + + public handle = (e: KeyboardEvent) => { + let keyname = e.key.toLowerCase(); + this.handleGreedy(keyname); + + if (modifiers.includes(keyname)) { + return; + } + + let bit = (value: boolean) => value ? "1" : "0"; + let modifierIndex = bit(e.shiftKey) + bit(e.ctrlKey) + bit(e.altKey) + bit(e.metaKey); + + let handleConstrained = this.router.get(modifierIndex); + if (!handleConstrained) { + return; + } + + let control = handleConstrained(keyname); + + control.stopPropagation && e.stopPropagation(); + control.preventDefault && e.preventDefault(); + } + + private handleGreedy = action((keyname: string) => { + switch (keyname) { + } + }); + + private unmodified = action((keyname: string) => { + switch (keyname) { + case "escape": + if (MainView.Instance.isPointerDown) { + DragManager.AbortDrag(); + } else { + if (CollectionDockingView.Instance.HasFullScreen()) { + CollectionDockingView.Instance.CloseFullScreen(); + } else { + SelectionManager.DeselectAll(); + } + } + break; + } + + return { + stopPropagation: false, + preventDefault: false + }; + }); + + private alt = action((keyname: string) => { + let stopPropagation = true; + let preventDefault = true; + + switch (keyname) { + case "n": + let toggle = MainView.Instance.addMenuToggle.current!; + toggle.checked = !toggle.checked; + break; + } + + return { + stopPropagation: stopPropagation, + preventDefault: preventDefault + }; + }); + + private ctrl = action((keyname: string) => { + let stopPropagation = true; + let preventDefault = true; + + switch (keyname) { + case "arrowright": + MainView.Instance.mainFreeform && CollectionDockingView.Instance.AddRightSplit(MainView.Instance.mainFreeform, undefined); + break; + case "arrowleft": + MainView.Instance.mainFreeform && CollectionDockingView.Instance.CloseRightSplit(MainView.Instance.mainFreeform); + break; + case "f": + MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible; + break; + case "o": + let target = SelectionManager.SelectedDocuments()[0]; + target && target.fullScreenClicked(); + break; + case "r": + preventDefault = false; + break; + case "y": + UndoManager.Redo(); + break; + case "z": + UndoManager.Undo(); + break; + } + + return { + stopPropagation: stopPropagation, + preventDefault: preventDefault + }; + }); + + private ctrl_shift = action((keyname: string) => { + let stopPropagation = true; + let preventDefault = true; + + switch (keyname) { + case "z": + UndoManager.Redo(); + break; + } + + return { + stopPropagation: stopPropagation, + preventDefault: preventDefault + }; + }); + +}
\ No newline at end of file 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 0837e07a9..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); @@ -39,7 +39,7 @@ export class InkingControl extends React.Component { @action switchColor = (color: ColorResult): void => { this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); - SelectionManager.SelectedDocuments().forEach(doc => Doc.GetProto(doc.props.Document).backgroundColor = this._selectedColor); + 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 b4ad5f4d7..d31319429 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -10,7 +10,6 @@ import { Transform } from '../util/Transform'; import { CollectionDockingView } from './collections/CollectionDockingView'; import "./MainOverlayTextBox.scss"; import { FormattedTextBox } from './nodes/FormattedTextBox'; -import { For } from 'babel-types'; interface MainOverlayTextBoxProps { } @@ -26,9 +25,16 @@ 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(); @@ -37,7 +43,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> (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"); @@ -82,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; @@ -102,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 | undefined, location: string) => { if (true) { // location === "onRight") { need to figure out stack to add "inTab" - CollectionDockingView.Instance.AddRightSplit(doc); + CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); } } render() { @@ -114,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 969b44329..fb88c284f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faThumbtack, faRedoAlt, faTable, faTree, faUndoAlt, faBell, faCommentAlt } 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'; @@ -36,24 +36,24 @@ 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"; +import _ from "lodash"; +import KeyManager from './GlobalKeyHandler'; @observer export class MainView extends React.Component { public static Instance: MainView; + @observable addMenuToggle = React.createRef<HTMLInputElement>(); @observable private _workspacesShown: boolean = false; @observable public pwidth: number = 0; @observable public pheight: number = 0; @computed private get mainContainer(): Opt<Doc> { return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc)); } - @computed private get mainFreeform(): Opt<Doc> { + @computed get mainFreeform(): Opt<Doc> { let docs = DocListCast(this.mainContainer!.data); return (docs && docs.length > 1) ? docs[1] : undefined; } - private globalDisplayFlags = observable({ - jumpToVisible: false - }); + public isPointerDown = false; private set mainContainer(doc: Opt<Doc>) { if (doc) { if (!("presentationView" in doc)) { @@ -64,12 +64,23 @@ export class MainView extends React.Component { } componentWillMount() { - document.removeEventListener("keydown", this.globalKeyHandler); - document.addEventListener("keydown", this.globalKeyHandler); + window.removeEventListener("keydown", KeyManager.Handler.handle); + window.addEventListener("keydown", KeyManager.Handler.handle); + + window.removeEventListener("pointerdown", this.pointerDown); + window.addEventListener("pointerdown", this.pointerDown); + + window.removeEventListener("pointerup", this.pointerUp); + window.addEventListener("pointerup", this.pointerUp); } + pointerDown = (e: PointerEvent) => this.isPointerDown = true; + pointerUp = (e: PointerEvent) => this.isPointerDown = false; + componentWillUnMount() { - document.removeEventListener("keydown", this.globalKeyHandler); + window.removeEventListener("keydown", KeyManager.Handler.handle); + window.removeEventListener("pointerdown", this.pointerDown); + window.removeEventListener("pointerup", this.pointerUp); } constructor(props: Readonly<{}>) { @@ -108,8 +119,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(); } @@ -118,18 +133,6 @@ export class MainView extends React.Component { // window.addEventListener("pointermove", (e) => this.reportLocation(e)) window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler - window.addEventListener("keydown", (e) => { - 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 document.addEventListener("pointerdown", action(function (e: PointerEvent) { @@ -161,8 +164,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(() => { @@ -194,7 +202,7 @@ export class MainView extends React.Component { openNotifsCol = () => { if (this._notifsCol && CollectionDockingView.Instance) { - CollectionDockingView.Instance.AddRightSplit(this._notifsCol); + CollectionDockingView.Instance.AddRightSplit(this._notifsCol, undefined); } } @@ -220,6 +228,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} @@ -227,7 +236,7 @@ export class MainView extends React.Component { ContentScaling={returnOne} PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} - isTopMost={true} + renderDepth={0} selectOnLoad={false} focus={emptyFunction} parentActive={returnTrue} @@ -282,7 +291,7 @@ export class MainView extends React.Component { ]; return < div id="add-nodes-menu" > - <input type="checkbox" id="add-menu-toggle" /> + <input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} /> <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label> <div id="add-options-content"> @@ -290,8 +299,7 @@ export class MainView extends React.Component { <li key="search"><button className="add-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button></li> <li key="undo"><button className="add-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li> <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 }} > - + <li key="color"><button className="add-button round-button" title="Select Color" onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} > <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> @@ -352,38 +360,6 @@ export class MainView extends React.Component { this.isSearchVisible = !this.isSearchVisible; } - @action - globalKeyHandler = (e: KeyboardEvent) => { - if (e.key === "Control" || !e.ctrlKey) return; - - if(e.key === "v") return; - if(e.key === "c") return; - - e.preventDefault(); - e.stopPropagation(); - - switch (e.key) { - case "ArrowRight": - if (this.mainFreeform) { - CollectionDockingView.Instance.AddRightSplit(this.mainFreeform!); - } - break; - case "ArrowLeft": - if (this.mainFreeform) { - CollectionDockingView.Instance.CloseRightSplit(this.mainFreeform!); - } - break; - case "o": - this.globalDisplayFlags.jumpToVisible = true; - break; - case "escape": - _.mapValues(this.globalDisplayFlags, () => false); - break; - case "f": - this.isSearchVisible = !this.isSearchVisible; - } - } - render() { return ( @@ -394,7 +370,6 @@ export class MainView extends React.Component { <ContextMenu /> {this.nodesMenu()} {this.miscButtons} - <InkingControl /> <PDFMenu /> <MainOverlayTextBox /> </div> diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index 3d5f7b6ea..5bb8d454a 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -49,8 +49,8 @@ export namespace Templates { export const Title = new Template("Title", TemplatePosition.InnerTop, `<div> - <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 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="height:calc(100% - 25px);"> <div style="width:100%;overflow:auto">{layout}</div> @@ -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 ae77ef42d..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,57 +74,21 @@ 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; } @@ -132,22 +97,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { removeDocument(doc: Doc): boolean { 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)) { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index dd99649b8..13b0579d2 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -32,7 +32,7 @@ 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', @@ -40,6 +40,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], + dataDocumentId: dataDoc ? dataDoc[Id] : "" //collectionDockingView: CollectionDockingView.Instance } }; @@ -50,6 +51,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp private _flush: boolean = false; private _ignoreStateChange = ""; private _isPointerDown = false; + private _maximizedSrc: Opt<DocumentView>; constructor(props: SubCollectionViewProps) { super(props); @@ -60,28 +62,45 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } 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] : undefined, true).contentItems[0].tab._dragListener. onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } @action - public OpenFullScreen(document: Doc) { + public OpenFullScreen(docView: DocumentView) { + let document = Doc.MakeAlias(docView.props.Document); + let dataDoc = docView.dataDoc; 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); docconfig.callDownwards('_$init'); this._goldenLayout._$maximiseItem(docconfig); + this._maximizedSrc = docView; this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); this.stateChanged(); } + public CloseFullScreen = () => { + let target = this._goldenLayout._maximisedItem; + if (target !== null && this._maximizedSrc) { + this._goldenLayout._maximisedItem.remove(); + SelectionManager.SelectDoc(this._maximizedSrc, false); + this._maximizedSrc = undefined; + this.stateChanged(); + } + } + + public HasFullScreen = () => { + return this._goldenLayout._maximisedItem !== null; + } + @undoBatch @action public CloseRightSplit = (document: Doc): boolean => { @@ -127,14 +146,14 @@ 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); @@ -165,12 +184,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 | undefined) => { 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(); @@ -255,7 +274,6 @@ 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); @@ -286,14 +304,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp 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, @@ -344,45 +364,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 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]), 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)} />, 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; - } - }); + 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) { + 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 @@ -445,6 +465,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp interface DockedFrameProps { documentId: FieldId; + dataDocumentId: FieldId; //collectionDockingView: CollectionDockingView } @observer @@ -453,6 +474,7 @@ 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) { @@ -462,7 +484,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } 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._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); @@ -499,23 +526,26 @@ 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 | undefined, 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) { return (null); } + let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc; 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={resolvedDataDoc} bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} @@ -523,7 +553,7 @@ 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} diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index b2d016934..31a73ab36 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -43,6 +43,10 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { ); } + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); + } + public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(CollectionPDFView, fieldKey); } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 97781f722..b0d46953c 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -11,10 +11,10 @@ import { Doc, DocListCast, DocListCastAsync, Field } from "../../../new_fields/D import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; import { Docs } from "../../documents/Documents"; import { Gateway } from "../../northstar/manager/Gateway"; -import { SetupDrag } from "../../util/DragManager"; +import { SetupDrag, DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; @@ -29,6 +29,8 @@ import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; +import { undoBatch } from "../../util/UndoManager"; +import { timesSeries } from "async"; library.add(faCog); @@ -98,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, @@ -114,9 +118,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { }; let fieldContentView = <FieldView {...props} />; let reference = React.createRef<HTMLDivElement>(); - 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 onItemDown = (e: React.PointerEvent) => { + (!this.props.CollectionView.props.isSelected() ? undefined : + SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); + }; let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); if (!res.success) return false; @@ -280,9 +285,9 @@ 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( @@ -344,24 +349,38 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; } + @computed get previewPanel() { - trace(); - return <CollectionSchemaPreview - Document={this.previewDocument} - 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} - />; + // let layoutDoc = this.previewDocument; + // let resolvedDataDoc = (layoutDoc !== this.props.DataDoc) ? this.props.DataDoc : undefined; + // if (layoutDoc && !(Cast(layoutDoc.layout, Doc) instanceof Doc) && + // resolvedDataDoc && resolvedDataDoc !== layoutDoc) { + // // ... so change the layout to be an expanded view of the template layout. This allows the view override the template's properties and be referenceable as its own document. + // layoutDoc = Doc.expandTemplateLayout(layoutDoc, resolvedDataDoc); + // } + + let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined; + return <div ref={this.createTarget}> + <CollectionSchemaPreview + Document={layoutDoc} + DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} + 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) => { @@ -383,6 +402,9 @@ 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; @@ -392,13 +414,15 @@ interface CollectionSchemaPreviewProps { removeDocument: (document: Doc) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; - addDocTab: (document: Doc, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, 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,26 +432,55 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre } return wscale; } - private PanelWidth = () => this.nativeWidth * this.contentScaling(); - private PanelHeight = () => this.nativeHeight * this.contentScaling(); + 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 + 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.props.width() - this.nativeWidth * this.contentScaling()) / 2; } + 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} moveDocument={this.props.moveDocument} + <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} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 485ecf1de..034a09eaa 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -39,6 +39,13 @@ color: $light-color; } + .collectionStackingView-columnDragger { + width: 15; + height: 15; + position: absolute; + margin-left: -5; + } + .collectionStackingView-columnDoc, .collectionStackingView-masonryDoc { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 597e4795c..b10907937 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,20 +1,21 @@ 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 { BoolCast, NumCast, Cast } from "../../../new_fields/Types"; +import { emptyFunction, Utils } from "../../../Utils"; import { ContextMenu } from "../ContextMenu"; -import { DocumentView } from "../nodes/DocumentView"; import { CollectionSchemaPreview } from "./CollectionSchemaView"; import "./CollectionStackingView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { Transform } from "../../util/Transform"; +import { resolve } from "bluebird"; @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); } @@ -66,16 +67,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { get singleColumnChildren() { let children = this.childDocs.filter(d => !d.isMinimized); return children.map((d, i) => { + let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc); let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]()); + let dxf = () => this.getDocTransform(layoutDoc, dref.current!).scale(this.columnWidth / d[WidthSym]()); let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; - let height = () => this.singleColDocHeight(d); + let height = () => this.singleColDocHeight(layoutDoc); return <div className="collectionStackingView-columnDoc" key={d[Id]} ref={dref} style={{ width: width(), height: height() }} > <CollectionSchemaPreview - Document={d} + Document={layoutDoc} + DataDocument={d !== this.props.DataDoc ? this.props.DataDoc : undefined} + renderDepth={this.props.renderDepth} width={width} height={height} getTransform={dxf} @@ -107,6 +111,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { 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} @@ -123,6 +129,35 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { </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 ContextMenu.Instance.addItem({ @@ -139,7 +174,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return ( <div className="collectionStackingView" ref={this.createRef} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > <div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`} - style={{ padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`, margin: "auto", @@ -152,6 +186,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }} > {this.singleColumn ? this.singleColumnChildren : this.children} + {this.singleColumn ? (null) : this.columnDragger} </div> </div> ); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 699bddc7c..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; @@ -45,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 diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index a85604e58..5205f4313 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -43,13 +43,12 @@ display: inline; } - - .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; @@ -59,6 +58,12 @@ background: lightgray; } +.collectionTreeView-subtitle { + font-style: italic; + font-size: 8pt; + color: $intermediate-color; +} + .docContainer { position: relative; text-overflow: ellipsis; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 18654905f..927aa363f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,7 +1,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight, faCaretDown, faCaretRight, faTrashAlt, faCaretSquareRight, faCaretSquareDown } from '@fortawesome/free-solid-svg-icons'; +import { faAngleRight, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; @@ -9,7 +9,7 @@ import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { emptyFunction, Utils } from '../../../Utils'; -import { Docs } from '../../documents/Documents'; +import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SelectionManager } from '../../util/SelectionManager'; @@ -25,14 +25,18 @@ import { CollectionSchemaPreview } from './CollectionSchemaView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); +import { LinkManager } from '../../util/LinkManager'; export interface TreeViewProps { document: Doc; + 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 | undefined, where: string) => void; panelWidth: () => number; panelHeight: () => number; addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; @@ -59,9 +63,32 @@ class TreeView extends React.Component<TreeViewProps> { private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); - @observable _chosenKey: string = "data"; + @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"; + } + + @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; } + protected createTreeDropTarget = (ele: HTMLDivElement) => { this._treedropDisposer && this._treedropDisposer(); if (ele) { @@ -69,8 +96,8 @@ class TreeView extends React.Component<TreeViewProps> { } } - @undoBatch delete = () => this.props.deleteDoc(this.props.document); - @undoBatch openRight = async () => this.props.addDocTab(this.props.document, "onRight"); + @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc); + @undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight"); onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); onPointerEnter = (e: React.PointerEvent): void => { @@ -101,7 +128,7 @@ class TreeView extends React.Component<TreeViewProps> { @action remove = (document: Document, key: string): boolean => { - let children = Cast(this.props.document[key], listSpec(Doc), []); + let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []); if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); return true; @@ -117,8 +144,8 @@ class TreeView extends React.Component<TreeViewProps> { indent = () => this.props.addDocument(this.props.document) && this.delete() renderBullet() { - let docList = Cast(this.props.document.data, listSpec(Doc)); - let doc = Cast(this.props.document.data, Doc); + 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")} />} @@ -136,15 +163,15 @@ class TreeView extends React.Component<TreeViewProps> { editableView = (key: string, style?: string) => (<EditableView oneLine={true} display={"inline"} - editing={this.props.document[Id] === TreeView.loadId} + 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.props.document)[key] = value) ? true : true} + SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true} OnFillDown={(value: string) => { - Doc.GetProto(this.props.document)[key] = value; + 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); @@ -153,43 +180,41 @@ class TreeView extends React.Component<TreeViewProps> { />) @computed get keyList() { - 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); + let keys = Array.from(Object.keys(this.resolvedDataDoc)); + if (this.resolvedDataDoc.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); } - let keyList: string[] = []; - keys.map(key => { - let docList = Cast(this.props.document[key], listSpec(Doc)); - let doc = Cast(this.props.document[key], Doc); - if (doc instanceof Doc || docList) { - keyList.push(key); - } - }); - if (keyList.indexOf("data") !== -1) { - keyList.splice(keyList.indexOf("data"), 1); + let keyList: string[] = keys.reduce((l, key) => { + let listspec = DocListCast(this.resolvedDataDoc[key]); + if (listspec && listspec.length) return [...l, key]; + return 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, "data"); - return keyList; + keyList.splice(0, 0, this.fieldKey); + return keyList.filter((item, index) => keyList.indexOf(item) >= index); } /** * 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 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 = this.keyList[ind]; })} > {this._chosenKey} </span>); - let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : []; - let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : ( + 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" /> </div>); @@ -210,13 +235,13 @@ class TreeView extends React.Component<TreeViewProps> { 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, this.props.dataDoc ? this.props.dataDoc : 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, false)) }); + 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 { @@ -232,10 +257,16 @@ class TreeView extends React.Component<TreeViewProps> { 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._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; }; } @@ -243,10 +274,10 @@ class TreeView extends React.Component<TreeViewProps> { e.stopPropagation(); 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.props.document, before) || added, false) + 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.props.document, addDoc) || added, false) - : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before), false); + 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; } @@ -259,25 +290,49 @@ class TreeView extends React.Component<TreeViewProps> { 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; + } + render() { let contentElement: (JSX.Element | null) = null; - let docList = Cast(this.props.document[this._chosenKey], listSpec(Doc)); + 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.props.document, this._chosenKey, doc, addBefore, before); - let doc = Cast(this.props.document[this._chosenKey], Doc); + 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"}> - {TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, 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._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 { - contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.props.panelHeight() }} key={this.props.document[Id]}> + let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); + contentElement = <div ref={this._dref} style={{ display: "inline-block", height: layoutDoc[HeightSym]() }} key={this.props.document[Id]}> <CollectionSchemaPreview - Document={this.props.document} + Document={layoutDoc} + DataDocument={this.resolvedDataDoc} + renderDepth={this.props.renderDepth} width={docWidth} - height={this.props.panelHeight} + height={layoutDoc[HeightSym]} getTransform={this.docTransform} CollectionView={undefined} addDocument={emptyFunction as any} @@ -306,18 +361,21 @@ class TreeView extends React.Component<TreeViewProps> { 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) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, - addDocTab: (doc: Doc, where: string) => void, + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void, screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, active: () => boolean, panelWidth: () => number, + renderDepth: number ) { - let docList = docs.filter(child => !child.excludeFromLibrary && (key !== "data" || !child.isMinimized)); + let docList = docs.filter(child => !child.excludeFromLibrary); let rowWidth = () => panelWidth() - 20; return docList.map((child, i) => { let indent = i === 0 ? undefined : () => { @@ -337,9 +395,12 @@ class TreeView extends React.Component<TreeViewProps> { }; return <TreeView document={child} + dataDoc={dataDoc} + containingCollection={containingCollection} treeViewId={treeViewId} key={child[Id]} indentDocument={indent} + renderDepth={renderDepth} deleteDoc={remove} addDocument={addDocument} panelWidth={rowWidth} @@ -388,6 +449,8 @@ export class CollectionTreeView extends CollectionSubView(Document) { } } + @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, {}); @@ -398,28 +461,27 @@ export class CollectionTreeView extends CollectionSubView(Document) { return !this.childDocs ? (null) : ( <div id="body" className="collectionTreeView-dropTarget" + 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) => (Doc.GetProto(this.props.Document).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); - }} /> - </div> - <ul className="no-indent"> + <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.fieldKey, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth) + 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/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 68eefab4c..e500e5c70 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, undefined, "onRight"); + }), icon: "project-diagram" + }); } } diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index f11af04a3..b29a30069 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 | undefined, 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, undefined, "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.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss index a80e09fa8..30e158603 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss @@ -1,4 +1,4 @@ -p.collectionfreeformlinksview-svgCanvas{ +.collectionfreeformlinksview-svgCanvas{ transform: translate(-10000px,-10000px); position: absolute; top: 0; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index be75c6c5c..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 => { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 5ac2e1f9c..ccf261c95 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,7 +1,7 @@ @import "../../globalCssVariables"; .collectionfreeformview-ease { - position: absolute; + position: inherit; top: 0; left: 0; width: 100%; @@ -25,8 +25,9 @@ height: 100%; width: 100%; } + >.jsx-parser { - z-index:0; + z-index: 0; } //nested freeform views @@ -40,25 +41,27 @@ border-radius: $border-radius; box-sizing: border-box; position: absolute; + .marqueeView { overflow: hidden; } + top: 0; left: 0; width: 100%; height: 100%; } - + .collectionfreeformview-overlay { .collectionfreeformview>.jsx-parser { position: inherit; height: 100%; } - + >.jsx-parser { - position:absolute; - z-index:0; + position: absolute; + z-index: 0; } .formattedTextBox-cont { @@ -72,9 +75,11 @@ box-sizing: border-box; position:absolute; z-index: -1; + .marqueeView { overflow: hidden; } + top: 0; left: 0; width: 100%; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 663fa5f9d..b1aba10bf 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, DocListCastAsync } 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"; @@ -25,8 +27,7 @@ import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); -import PDFMenu from "../../pdf/PDFMenu"; -import { ContextMenu } from "../../ContextMenu"; + export const panZoomSchema = createSchema({ panX: "number", @@ -47,11 +48,11 @@ 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; - private zoomScaling = () => this.Document.scale || 1; + private panX = () => this.props.fitToBox ? this.props.fitToBox[0] : this.Document.panX || 0; + private panY = () => this.props.fitToBox ? this.props.fitToBox[1] : this.Document.panY || 0; + private zoomScaling = () => this.props.fitToBox ? this.props.fitToBox[2] : this.Document.scale || 1; private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0; // shift so pan position is at center of window for non-overlay collections private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); @@ -163,7 +164,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); @@ -198,7 +199,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(); @@ -239,8 +240,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); - this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; - this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY; + // this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; + // this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY; + this.props.Document.panX = panX; + if (this.props.Document.scrollY) { + this.props.Document.scrollY = panY; + this.props.Document.panY = panY; + } + else { + + this.props.Document.panY = panY; + } } @action @@ -326,17 +336,43 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } - getDocumentViewProps(document: Doc): DocumentViewProps { + getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps { + let resolvedDataDoc = this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; + let layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc); return { - Document: document, + DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : undefined, + 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, + parentActive: this.props.active, + whenActiveChanged: this.props.whenActiveChanged, + bringToFront: this.bringToFront, + addDocTab: this.props.addDocTab, + zoomToScale: this.zoomToScale, + getScale: this.getScale + }; + } + getDocumentViewProps(layoutDoc: Doc): DocumentViewProps { + return { + DataDoc: this.props.DataDoc, + Document: this.props.Document, + addDocument: this.props.addDocument, + removeDocument: this.props.removeDocument, + moveDocument: this.props.moveDocument, + ScreenToLocalTransform: this.getTransform, + 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, @@ -358,7 +394,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (Math.round(page) === Math.round(curPage) || page === -1) { let minim = BoolCast(doc.isMinimized, false); if (minim === undefined || !minim) { - prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />); + prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getChildDocumentViewProps(doc)} />); } } return prev; @@ -408,6 +444,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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" }} @@ -419,7 +457,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> @@ -436,7 +474,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; @@ -446,8 +484,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 2b8abeffa..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 { FilterBox } from "../../search/FilterBox"; import { Templates } from "../../Templates"; import { CollectionViewType } from "../CollectionBaseView"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; @@ -50,7 +49,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> document.removeEventListener("pointerup", this.onPointerUp, true); document.removeEventListener("pointermove", this.onPointerMove, true); } - if (all) { + if (rem_keydown) { document.removeEventListener("keydown", this.marqueeCommand, true); } this._visible = false; @@ -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); } diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index cfbf4aab8..fec105516 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -19,6 +19,7 @@ $serif: "Crimson Text", serif; // misc values $border-radius: 0.3em; // +$search-thumnail-size: 175; // dragged items $contextMenu-zindex: 1000; // context menu shows up over everything @@ -33,4 +34,5 @@ $MAX_ROW_HEIGHT: 44px; COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH; MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE; MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; + SEARCH_THUMBNAIL_SIZE: $search-thumnail-size; }
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts index 9788d31f7..d95cec9d8 100644 --- a/src/client/views/globalCssVariables.scss.d.ts +++ b/src/client/views/globalCssVariables.scss.d.ts @@ -4,6 +4,7 @@ interface IGlobalScss { COLLECTION_BORDER_WIDTH: string; MINIMIZED_ICON_SIZE: string; MAX_ROW_HEIGHT: string; + SEARCH_THUMBNAIL_SIZE: string; } declare const globalCssVariables: IGlobalScss; 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.tsx b/src/client/views/nodes/DocumentView.tsx index 10d777865..9edde896f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -31,6 +31,8 @@ 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? @@ -53,25 +55,28 @@ 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); +// 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; + fitToBox?: number[]; 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; @@ -80,7 +85,7 @@ export interface DocumentViewProps { parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc) => void; - addDocTab: (doc: Doc, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void; zoomToScale: (scale: number) => void; getScale: () => number; @@ -123,7 +128,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) { @@ -212,11 +217,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; @@ -248,7 +255,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); } let isMinimized: boolean | undefined; - expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { + expandedDocs.map(maximizedDoc => { let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { if (isMinimized === undefined) { @@ -270,8 +277,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; } @@ -285,8 +294,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; @@ -314,31 +322,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), undefined, maxLocation))); } } else { let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); 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, false, 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, undefined, 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, undefined, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); + // } } } } @@ -348,7 +363,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. @@ -379,7 +394,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } 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"); }; + fieldsClicked = (): void => { let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.dataDoc, "onRight"); }; makeBtnClicked = (): void => { let doc = Doc.GetProto(this.props.Document); doc.isButton = !BoolCast(doc.isButton, false); @@ -392,8 +407,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu doc.nativeWidth = doc.nativeHeight = undefined; } } - fullScreenClicked = (): void => { - CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeCopy(this.props.Document, false)); + public fullScreenClicked = (): void => { + CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this); SelectionManager.DeselectAll(); } @@ -476,10 +491,10 @@ 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, 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" }); @@ -489,7 +504,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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, {}), undefined, "onRight"); // bcz: dataDoc? }, icon: "search" }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); @@ -546,28 +561,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (this.Document.hidden) { return null; } - var scaling = this.props.ContentScaling(); + 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})`, - opacity: NumCast(this.props.Document.opacity, 1) + 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 1f1582f22..3f5a2e744 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -7,18 +7,19 @@ 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 { VideoBox } from "./VideoBox"; import { PDFBox } from "./PDFBox"; +import { VideoBox } from "./VideoBox"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { BoolCast, Cast } from "../../../new_fields/Types"; +import { DarpaDatasetDoc } from "../../northstar/model/idea/idea"; // @@ -28,14 +29,18 @@ import { PDFBox } from "./PDFBox"; // export interface FieldViewProps { fieldKey: string; + fieldExt: string; + leaveNativeSize?: boolean; + fitToBox?: number[]; 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 | undefined, where: string) => void; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; @@ -71,7 +76,7 @@ export class FieldView extends React.Component<FieldViewProps> { return <FormattedTextBox {...this.props} />; } else if (field instanceof ImageField) { - return <ImageBox {...this.props} />; + return <ImageBox {...this.props} leaveNativeSize={true} />; } else if (field instanceof IconField) { return <IconBox {...this.props} />; @@ -85,7 +90,7 @@ export class FieldView extends React.Component<FieldViewProps> { return <p>{field.date.toLocaleString()}</p>; } else if (field instanceof Doc) { - return <p><b>{field.title}</b></p>; + return <p><b>{field.title + " : id= " + field[Id]}</b></p>; // let returnHundred = () => 100; // return ( // <DocumentContentsView Document={field} @@ -96,7 +101,7 @@ export class FieldView extends React.Component<FieldViewProps> { // ContentScaling={returnOne} // PanelWidth={returnHundred} // PanelHeight={returnHundred} - // isTopMost={true} //TODO Why is this top most? + // renderDepth={0} //TODO Why is renderDepth reset? // selectOnLoad={false} // focus={emptyFunction} // isSelected={this.props.isSelected} diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 4a29c1949..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: auto; + 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 376b5a574..07cd43ce3 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, runInAction } 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"; @@ -45,6 +45,7 @@ export interface FormattedTextBoxProps { hideOnLeave?: boolean; height?: string; color?: string; + outer_div?: (domminus: HTMLElement) => void; } const richTextSchema = createSchema({ @@ -60,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; @@ -97,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) { @@ -104,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 ? "..." : ""); } } @@ -144,14 +151,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe e.stopPropagation(); } else { if (de.data instanceof DragManager.DocumentDragData) { - let ldocs = Cast(this.props.Document.subBulletDocs, listSpec(Doc)); + let ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc)); if (!ldocs) { - this.props.Document.subBulletDocs = new List<Doc>([]); + this.dataDoc.subBulletDocs = new List<Doc>([]); } - ldocs = Cast(this.props.Document.subBulletDocs, listSpec(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.props.Document.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 })); + 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; @@ -201,21 +208,26 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe 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 && + field => this._editorView && !this._applyingChange && this.props.Document[this.props.fieldKey] instanceof RichTextField && 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, { @@ -235,6 +247,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this.props.selectOnLoad) { if (!this.props.isOverlay) this.props.select(false); else this._editorView!.focus(); + this.tryUpdateHeight(); } } @@ -254,7 +267,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; @@ -268,7 +281,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe 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, document => this.props.addDocTab(document, "inTab")); + (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, false, document => this.props.addDocTab(document, undefined, "inTab")); }); e.stopPropagation(); e.preventDefault(); @@ -289,7 +302,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(); @@ -334,6 +347,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); } }); + //this.props.Document.tooltip = self._toolTipTextMenu; } tooltipLinkingMenuPlugin() { @@ -360,23 +374,28 @@ 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) { this._undoTyping = UndoManager.StartBatch("undoTyping"); } + this.tryUpdateHeight(); + } + + @action + tryUpdateHeight() { 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; } } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index f56a2d926..06bf65f73 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'; @@ -20,6 +20,8 @@ import { positionSchema } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); +import { RouteStore } from '../../../server/RouteStore'; +var requestImageSize = require('../../util/request-image-size'); var path = require('path'); @@ -27,7 +29,7 @@ library.add(faImage); export const pageSchema = createSchema({ - curPage: "number" + curPage: "number", }); type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>; @@ -36,16 +38,17 @@ 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; private _lastTap: number = 0; - @observable private _photoIndex: number = 0; @observable private _isOpen: boolean = false; 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 +64,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 @@ -109,20 +116,21 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD e.stopPropagation(); } + @action lightbox = (images: string[]) => { if (this._isOpen) { return (<Lightbox - mainSrc={images[this._photoIndex]} - nextSrc={images[(this._photoIndex + 1) % images.length]} - prevSrc={images[(this._photoIndex + images.length - 1) % images.length]} + mainSrc={images[this.Document.curPage || 0]} + nextSrc={images[((this.Document.curPage || 0) + 1) % images.length]} + prevSrc={images[((this.Document.curPage || 0) + images.length - 1) % images.length]} onCloseRequest={action(() => this._isOpen = false )} onMovePrevRequest={action(() => - this._photoIndex = (this._photoIndex + images.length - 1) % images.length + this.Document.curPage = ((this.Document.curPage || 0) + images.length - 1) % images.length )} onMoveNextRequest={action(() => - this._photoIndex = (this._photoIndex + 1) % images.length + this.Document.curPage = ((this.Document.curPage || 0) + 1) % images.length )} />); } @@ -154,7 +162,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD @action onDotDown(index: number) { - this._photoIndex = index; this.Document.curPage = index; } @@ -164,7 +171,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let left = (nativeWidth - paths.length * dist) / 2; return paths.map((p, i) => <div className="imageBox-placer" key={i} > - <div className="imageBox-dot" style={{ background: (i === this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} /> + <div className="imageBox-dot" style={{ background: (i === this.Document.curPage ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} /> </div> ); } @@ -193,6 +200,22 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD } } _curSuffix = "_m"; + + resize(srcpath: string, layoutdoc: Doc) { + requestImageSize(window.origin + RouteStore.corsProxy + "/" + srcpath) + .then((size: any) => { + let aspect = size.height / size.width; + if (Math.abs(layoutdoc[HeightSym]() / layoutdoc[WidthSym]() - aspect) > 0.01) { + setTimeout(action(() => { + layoutdoc.height = layoutdoc[WidthSym]() * aspect; + layoutdoc.nativeHeight = size.height; + layoutdoc.nativeWidth = size.width; + }), 0); + } + }) + .catch((err: any) => console.log(err)); + } + render() { // 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; @@ -206,26 +229,33 @@ 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]; + Doc.UpdateDocumentExtensionForField(this.extensionDoc, 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; + let srcpath = paths[Math.min(paths.length, this.Document.curPage || 0)]; + + if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document); + return ( <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") }} + // style={{ objectFit: (this.Document.curPage === 0 ? undefined : "contain") }} width={nativeWidth} ref={this._imgRef} onError={this.onError} /> 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 cd65c42bc..0e798d291 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 { 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 = ""; @@ -89,8 +99,16 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let rows: JSX.Element[] = []; let i = 0; + const self = this; 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={(function () { + let oldEl: KeyValuePair | undefined; + return (el: KeyValuePair) => { + if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1); + oldEl = el; + if (el) self.rows.push(el); + }; + })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />); } return rows; } @@ -134,6 +152,60 @@ export class KeyValueBox extends React.Component<FieldViewProps> { document.addEventListener('pointerup', this.onDividerUp); } + getTemplate = async () => { + let parent = Docs.StackingDocument([], { width: 800, height: 800, title: "Template" }); + parent.singleColumn = false; + parent.columnWidth = 50; + 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 = 0; + template.nativeHeight = 0; + 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.StackingDocument([], 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 +216,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()} @@ -154,4 +228,4 @@ export class KeyValueBox extends React.Component<FieldViewProps> { {dividerDragger} </div>); } -}
\ No newline at end of file +} 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 da0aa6ac4..b5db52ac7 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -12,6 +12,7 @@ import React = require("react"); import { Doc, Opt, Field } from '../../../new_fields/Doc'; import { FieldValue } from '../../../new_fields/Types'; import { KeyValueBox } from './KeyValueBox'; +import { DragManager, SetupDrag } from '../../util/DragManager'; // Represents one row in a key value plane @@ -23,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, @@ -42,12 +59,16 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { 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; } @@ -55,22 +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={() => { - const onDelegate = Object.keys(props.Document).includes(props.fieldKey); + <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 (onDelegate ? "=" : "") + 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 c34ccdccb..cccf3c329 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -1,11 +1,17 @@ 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"; @@ -20,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={DocTypes.LINK} />; - } + @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..4dee6741f --- /dev/null +++ b/src/client/views/nodes/LinkMenuItem.tsx @@ -0,0 +1,116 @@ +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, BoolCast, FieldValue } 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(); + let jumpToDoc = this.props.destinationDoc; + let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc)); + if (pdfDoc) { + jumpToDoc = pdfDoc; + } + if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey); + } else { + CollectionDockingView.Instance.AddRightSplit(jumpToDoc, 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 8bcae4f1e..9a38d6241 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -32,16 +32,23 @@ height: 100px; } -.pdfBox-cont { - pointer-events: none; +.pdfBox-cont, .pdfBox-cont-interactive { display: flex; flex-direction: row; + height: 100%; + overflow-y: scroll; + overflow-x: hidden; +} +.pdfBox-cont { + pointer-events: none; .textlayer { pointer-events: none; + span { pointer-events: none !important; } } + .page-cont { pointer-events: none; } @@ -51,6 +58,7 @@ pointer-events: all; display: flex; flex-direction: row; + .textlayer { span { pointer-events: all !important; @@ -62,4 +70,68 @@ .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 02b883d57..83dedb71d 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,9 +1,9 @@ -import { action, IReactionDisposer, observable, reaction, trace, untracked } from 'mobx'; +import { action, IReactionDisposer, observable, reaction, trace, untracked, computed } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; -import { WidthSym } from "../../../new_fields/Doc"; +import { WidthSym, Doc } from "../../../new_fields/Doc"; import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast } from "../../../new_fields/Types"; +import { Cast, NumCast, BoolCast } from "../../../new_fields/Types"; import { PdfField } from "../../../new_fields/URLField"; //@ts-ignore // import { Document, Page } from "react-pdf"; @@ -19,6 +19,10 @@ import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); +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); @@ -29,27 +33,59 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @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>; + + constructor(props: FieldViewProps) { + super(props); + + this._mainCont = React.createRef(); + this._reactionDisposer = reaction( + () => this.props.Document.scrollY, + () => { + 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() { + this._reactionDisposer && this._reactionDisposer(); + } + public GetPage() { - return Math.floor(NumCast(this.props.Document.scrollY) / NumCast(this.Document.pdfHeight)) + 1; + 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.Document.pdfHeight)) + 1; + 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.Document.pdfHeight); + this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight); } } 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.Document.pdfHeight); + this.props.Document.scrollY = (p - 1) * NumCast(this.dataDoc.pdfHeight); } } @@ -57,20 +93,114 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen 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.Document.pdfHeight); + this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight); + } + } + + private newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._keyValue = e.currentTarget.value; + } + + private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._valueValue = e.currentTarget.value; + } + + @action + private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._scriptValue = e.currentTarget.value; + } + + 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}`; + } + else { + scriptText = "return true"; + } + let script = CompileScript(scriptText, { params: { this: Doc.name } }); + if (script.compiled) { + this.props.Document.filterScript = new ScriptField(script); } } - createRef = (ele: HTMLDivElement | null) => { - if (this._reactionDisposer) this._reactionDisposer(); - this._reactionDisposer = reaction(() => this.props.Document.scrollY, () => { - ele && ele.scrollTo({ top: NumCast(this.Document.scrollY), behavior: "auto" }); - }); + @action + private toggleFlyout = () => { + this._flyout = !this._flyout; + } + + @action + private resetFilters = () => { + this._keyValue = this._valueValue = ""; + this._scriptValue = "return true"; + if (this._keyRef.current) { + this._keyRef.current.value = ""; + } + if (this._valueRef.current) { + this._valueRef.current.value = ""; + } + if (this._scriptRef.current) { + this._scriptRef.current.value = ""; + } + this.applyFilter(); + } + + scrollTo(y: number) { + if (this._mainCont.current) { + this._mainCont.current.scrollTo({ top: y }); + } + } + + 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.props.Document.proto ? this.props.Document.proto : 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); @@ -87,10 +217,12 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @action onScroll = (e: React.UIEvent<HTMLDivElement>) => { + if (e.currentTarget) { this._scrollY = e.currentTarget.scrollTop; let ccv = this.props.ContainingCollectionView; if (ccv) { + ccv.props.Document.panTransformType = "None"; ccv.props.Document.scrollY = this._scrollY; } } @@ -98,21 +230,22 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen render() { // uses mozilla pdf as default - const pdfUrl = Cast(this.props.Document.data, PdfField, new PdfField(window.origin + RouteStore.corsProxy + "/https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf")); + 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 onScroll={this.onScroll} + <div className={classname} + onScroll={this.onScroll} style={{ - height: "100%", - overflowY: "scroll", overflowX: "hidden", marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px` }} - ref={this.createRef} + ref={this._mainCont} onWheel={(e: React.WheelEvent) => { e.stopPropagation(); - }} className={classname}> + }}> <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/Annotation.tsx b/src/client/views/pdf/Annotation.tsx new file mode 100644 index 000000000..73fca229b --- /dev/null +++ b/src/client/views/pdf/Annotation.tsx @@ -0,0 +1,144 @@ +import React = require("react"); +import { Doc, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { AnnotationTypes, Viewer, scale } from "./PDFViewer"; +import { observer } from "mobx-react"; +import { observable, IReactionDisposer, reaction, action } from "mobx"; +import { BoolCast, NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import PDFMenu from "./PDFMenu"; +import { DocumentManager } from "../../util/DocumentManager"; + +interface IAnnotationProps { + anno: Doc; + index: number; + parent: Viewer; +} + +export default class Annotation extends React.Component<IAnnotationProps> { + render() { + let annotationDocs = DocListCast(this.props.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.props.parent} document={a} index={this.props.index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + default: + return <div></div>; + } + }); + return res; + } +} + +interface IRegionAnnotationProps { + x: number; + y: number; + width: number; + height: number; + index: number; + parent: Viewer; + document: Doc; +} + +@observer +class RegionAnnotation extends React.Component<IRegionAnnotationProps> { + @observable private _backgroundColor: string = "red"; + + private _reactionDisposer?: IReactionDisposer; + private _scrollDisposer?: IReactionDisposer; + private _mainCont: React.RefObject<HTMLDivElement>; + + constructor(props: IRegionAnnotationProps) { + 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> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss index 22868082a..a4624b1f6 100644 --- a/src/client/views/pdf/PDFMenu.scss +++ b/src/client/views/pdf/PDFMenu.scss @@ -22,4 +22,11 @@ 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 index 39b15fb11..aeed5213c 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -1,10 +1,12 @@ import React = require("react"); import "./PDFMenu.scss"; -import { observable, action } from "mobx"; +import { observable, action, runInAction } from "mobx"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { emptyFunction } from "../../../Utils"; +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 { @@ -16,19 +18,27 @@ export default class PDFMenu extends React.Component { @observable private _transition: string = "opacity 0.5s"; @observable private _transitionDelay: string = ""; - @observable public Pinned: boolean = false; 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" | "" = ""; + @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); @@ -36,6 +46,7 @@ export default class PDFMenu extends React.Component { PDFMenu.Instance = this; this._mainCont = React.createRef(); + this._snippetButton = React.createRef(); } pointerDown = (e: React.PointerEvent) => { @@ -171,34 +182,87 @@ export default class PDFMenu extends React.Component { 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" ? [ - <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} + 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" /></button>, - <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} + <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" /></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} - {/* <button className="pdfMenu-button" title="Highlight" onClick={this.highlightClicked} - 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="Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" /></button> - <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} - style={this._pinned ? { backgroundColor: "#121212" } : {}}> - <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this._pinned ? "rotate(45deg)" : "" }} /> - </button> */} <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> </div > ); diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 53c33ce0b..0fde764d0 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -1,9 +1,9 @@ + .textLayer { div { user-select: text; } } - .viewer-button-cont { position: absolute; display: flex; @@ -23,6 +23,93 @@ .textLayer { user-select: auto; } +.viewer { + // position: absolute; + // top: 0; +} + +.pdfViewer-text { + + .page { + .canvasWrapper { + display: none; + } + + .textLayer { + position: relative; + user-select: none; + } + } +} +.pdfViewer-viewerCont { + width:100%; +} + +.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; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 6adead626..bb148e738 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -10,7 +10,7 @@ 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 } from "../../documents/Documents"; +import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; import { DocumentView } from "../nodes/DocumentView"; @@ -20,6 +20,11 @@ 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"; +import Annotation from "./Annotation"; +const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); export const scale = 2; interface IPDFViewerProps { @@ -44,7 +49,7 @@ export class PDFViewer extends React.Component<IPDFViewerProps> { render() { return ( - <div ref={this._mainDiv}> + <div className="pdfViewer-viewerCont" 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> @@ -61,13 +66,11 @@ interface IViewerProps { url: string; } -const PinRadius = 25; - /** * Handles rendering and virtualization of the pdf */ @observer -class Viewer extends React.Component<IViewerProps> { +export 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 @@ -75,12 +78,35 @@ class Viewer extends React.Component<IViewerProps> { @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) { @@ -103,11 +129,62 @@ class Viewer extends React.Component<IViewerProps> { (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 @@ -115,30 +192,48 @@ class Viewer extends React.Component<IViewerProps> { 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); + const proms: Pdfjs.PDFPromise<any>[] = []; 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); - pageSizes[i] = { width: x.width, height: x.height }; - })); + proms.push(this.props.pdf.getPage(i + 1).then(page => runInAction(() => { + pageSizes[i] = { + width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale, + height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * 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 }; + }))); } + await Promise.all(proms); 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); - } - } - private mainCont = (div: HTMLDivElement | null) => { - this._dropDisposer && this._dropDisposer(); - if (div) { - this._dropDisposer = div && DragManager.MakeDropTarget(div, { handlers: { drop: this.drop.bind(this) } }); + 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 = new Doc(); + let mainAnnoDoc = Docs.CreateInstance(new Doc(), "", {}); + + mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title); + mainAnnoDoc.pdfDoc = this.props.parent.Document; this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => { for (let anno of value) { let annoDoc = new Doc(); @@ -156,6 +251,7 @@ class Viewer extends React.Component<IViewerProps> { } }); + mainAnnoDoc.y = Math.max((NumCast(annoDocs[0].y) * scale) - 100, 0); 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)); @@ -187,6 +283,7 @@ class Viewer extends React.Component<IViewerProps> { 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") { @@ -197,6 +294,7 @@ class Viewer extends React.Component<IViewerProps> { ); } } + @action getRenderedPage = (page: number) => { if (this._isPage[page] !== "page") { @@ -216,7 +314,7 @@ class Viewer extends React.Component<IViewerProps> { createAnnotation={this.createAnnotation} sendAnnotations={this.receiveAnnotations} makeAnnotationDocuments={this.makeAnnotationDocument} - receiveAnnotations={this.sendAnnotations} + getScrollFromPage={this.getScrollFromPage} {...this.props} /> ); } @@ -230,10 +328,14 @@ class Viewer extends React.Component<IViewerProps> { if (this._isPage[page] !== "image") { this._isPage[page] = "image"; const address = this.props.url; - 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` }} />); + 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) { + + } } } @@ -288,28 +390,6 @@ class Viewer extends React.Component<IViewerProps> { return this._savedAnnotations.getValue(page); } - // createPinAnnotation = (x: number, y: number, page: number): void => { - // let targetDoc = Docs.TextDocument({ width: 100, height: 50, title: "New Pin Annotation" }); - // let pinAnno = new Doc(); - // pinAnno.x = x; - // pinAnno.y = y + this.getScrollFromPage(page); - // pinAnno.width = pinAnno.height = PinRadius; - // pinAnno.page = page; - // pinAnno.target = targetDoc; - // pinAnno.type = AnnotationTypes.Pin; - // // this._annotations.push(pinAnno); - // let annoDoc = new Doc(); - // annoDoc.annotations = new List<Doc>([pinAnno]); - // let annotations = DocListCast(this.props.parent.Document.annotations); - // if (annotations && annotations.length) { - // annotations.push(annoDoc); - // this.props.parent.Document.annotations = new List<Doc>(annotations); - // } - // else { - // this.props.parent.Document.annotations = new List<Doc>([annoDoc]); - // } - // } - // get the page index that the vertical offset passed in is on getPageFromScroll = (vOffset: number) => { let index = 0; @@ -345,37 +425,244 @@ class Viewer extends React.Component<IViewerProps> { } } - renderAnnotation = (anno: Doc): 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} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; - default: - return <div></div>; + renderAnnotation = (anno: Doc, index: number): JSX.Element => { + return <Annotation anno={anno} index={index} parent={this} />; + } + + @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; + }); } - }); - return res; + } + + // 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" }}> - <div className="viewer"> + <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.map(anno => this.renderAnnotation(anno))} + {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 > ); } @@ -385,203 +672,37 @@ export enum AnnotationTypes { Region } -interface IAnnotationProps { - x: number; - y: number; - width: number; - height: number; - parent: Viewer; - document: Doc; -} +class SimpleLinkService { + externalLinkTarget: any = null; + externalLinkRel: any = null; + pdf: any = null; -// @observer -// class PinAnnotation extends React.Component<IAnnotationProps> { -// @observable private _backgroundColor: string = "green"; -// @observable private _display: string = "initial"; - -// private _mainCont: React.RefObject<HTMLDivElement>; - -// constructor(props: IAnnotationProps) { -// super(props); -// this._mainCont = React.createRef(); -// } - -// componentDidMount = () => { -// let selected = this.props.document.selected; -// if (!BoolCast(selected)) { -// runInAction(() => { -// this._backgroundColor = "red"; -// this._display = "none"; -// }); -// } -// if (selected) { -// if (BoolCast(selected)) { -// runInAction(() => { -// this._backgroundColor = "green"; -// this._display = "initial"; -// }); -// } -// else { -// runInAction(() => { -// this._backgroundColor = "red"; -// this._display = "none"; -// }); -// } -// } -// else { -// runInAction(() => { -// this._backgroundColor = "red"; -// this._display = "none"; -// }); -// } -// } - -// @action -// pointerDown = (e: React.PointerEvent) => { -// let selected = this.props.document.selected; -// if (selected && BoolCast(selected)) { -// this._backgroundColor = "red"; -// this._display = "none"; -// this.props.document.selected = false; -// } -// else { -// this._backgroundColor = "green"; -// this._display = "initial"; -// this.props.document.selected = true; -// } -// e.preventDefault(); -// e.stopPropagation(); -// } - -// @action -// doubleClick = (e: React.MouseEvent) => { -// if (this._mainCont.current) { -// let annotations = DocListCast(this.props.parent.props.parent.Document.annotations); -// if (annotations && annotations.length) { -// let index = annotations.indexOf(this.props.document); -// annotations.splice(index, 1); -// this.props.parent.props.parent.Document.annotations = new List<Doc>(annotations); -// } -// // this._mainCont.current.childNodes.forEach(e => e.remove()); -// this._mainCont.current.style.display = "none"; -// // if (this._mainCont.current.parentElement) { -// // this._mainCont.current.remove(); -// // } -// } -// e.stopPropagation(); -// } - -// render() { -// let targetDoc = Cast(this.props.document.target, Doc); -// if (targetDoc instanceof Doc) { -// return ( -// <div className="pdfViewer-pinAnnotation" onPointerDown={this.pointerDown} -// onDoubleClick={this.doubleClick} ref={this._mainCont} -// style={{ -// top: this.props.y * scale - PinRadius / 2, left: this.props.x * scale - PinRadius / 2, width: PinRadius, -// height: PinRadius, pointerEvents: "all", backgroundColor: this._backgroundColor -// }}> -// <div style={{ -// position: "absolute", top: "25px", left: "25px", transform: "scale(3)", transformOrigin: "top left", -// display: this._display, width: targetDoc[WidthSym](), height: targetDoc[HeightSym]() -// }}> -// <DocumentView Document={targetDoc} -// ContainingCollectionView={undefined} -// ScreenToLocalTransform={this.props.parent.props.parent.props.ScreenToLocalTransform} -// isTopMost={false} -// ContentScaling={() => 1} -// PanelWidth={() => NumCast(this.props.parent.props.parent.Document.nativeWidth)} -// PanelHeight={() => NumCast(this.props.parent.props.parent.Document.nativeHeight)} -// focus={emptyFunction} -// selectOnLoad={false} -// parentActive={this.props.parent.props.parent.props.active} -// whenActiveChanged={this.props.parent.props.parent.props.whenActiveChanged} -// bringToFront={emptyFunction} -// addDocTab={this.props.parent.props.parent.props.addDocTab} -// /> -// </div> -// </div > -// ); -// } -// return null; -// } -// } - -class RegionAnnotation extends React.Component<IAnnotationProps> { - @observable private _backgroundColor: string = "red"; + navigateTo(dest: any) { } - private _reactionDisposer?: IReactionDisposer; - private _mainCont: React.RefObject<HTMLDivElement>; + getDestinationHash(dest: any) { return "#"; } - constructor(props: IAnnotationProps) { - super(props); + getAnchorUrl(hash: any) { return "#"; } - this._mainCont = React.createRef(); - } + setHash(hash: any) { } - 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 } - ); - } + executeNamedAction(action: any) { } - componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - } + cachePageRef(pageNum: any, pageRef: any) { } - 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); + get pagesCount() { + return this.pdf ? this.pdf.numPages : 0; } + get page() { + return 0; + } - // annotateThis = (e: PointerEvent) => { - // e.preventDefault(); - // e.stopPropagation(); - // // document that this annotation is linked to - // let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" }); - // let group = FieldValue(Cast(this.props.document.group, Doc)); - // } - - @action - onPointerDown = (e: React.PointerEvent) => { - if (e.button === 0) { - let targetDoc = Cast(this.props.document.target, Doc, null); - if (targetDoc) { - DocumentManager.Instance.jumpToDocument(targetDoc); - } - } - if (e.button === 2) { - PDFMenu.Instance.Status = "annotation"; - PDFMenu.Instance.Delete = this.deleteAnnotation; - PDFMenu.Instance.Pinned = false; - PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); - } + setPdf(pdf: any) { + this.pdf = pdf; } - 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: StrCast(this.props.document.color) }}></div> - ); + 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 index b6f362702..57e36be43 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -16,6 +16,7 @@ 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 { @@ -29,9 +30,9 @@ interface IPageProps { renderAnnotations: (annotations: Doc[], removeOld: boolean) => void; makePin: (x: number, y: number, page: number) => void; sendAnnotations: (annotations: HTMLDivElement[], page: number) => void; - receiveAnnotations: (page: number) => HTMLDivElement[] | undefined; createAnnotation: (div: HTMLDivElement, page: number) => void; - makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string) => Doc; + makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string, linkTo: boolean) => Doc; + getScrollFromPage: (page: number) => number; } @observer @@ -136,7 +137,7 @@ export default class Page extends React.Component<IPageProps> { @action highlight = (targetDoc?: Doc, color: string = "red") => { // creates annotation documents for current highlights - let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color); + let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false); let targetAnnotations = Cast(this.props.parent.Document.annotations, listSpec(Doc)); if (targetAnnotations === undefined) { Doc.GetProto(this.props.parent.Document).annotations = new List([annotationDoc]); @@ -178,6 +179,18 @@ export default class Page extends React.Component<IPageProps> { 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 @@ -192,6 +205,7 @@ export default class Page extends React.Component<IPageProps> { 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; @@ -281,10 +295,11 @@ export default class Page extends React.Component<IPageProps> { if (this._marquee.current) { let copy = document.createElement("div"); // make a copy of the marquee - copy.style.left = this._marquee.current.style.left; - copy.style.top = this._marquee.current.style.top; - copy.style.width = this._marquee.current.style.width; - copy.style.height = this._marquee.current.style.height; + 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(); @@ -298,7 +313,7 @@ export default class Page extends React.Component<IPageProps> { copy.appendChild(img); } else { - copy.style.opacity = this._marquee.current.style.opacity; + copy.style.opacity = style.opacity; } copy.className = this._marquee.current.className; this.props.createAnnotation(copy, this.props.page); @@ -306,6 +321,10 @@ export default class Page extends React.Component<IPageProps> { } 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); } diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index aa9fbd158..d63c0b066 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -8,18 +8,15 @@ import "./PresentationView.scss"; import { Utils } from "../../../Utils"; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faFile as fileSolid, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons'; -import { faFile as fileRegular } from '@fortawesome/free-solid-svg-icons'; +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(fileRegular); library.add(faLocationArrow); +library.add(fileRegular as any); library.add(faSearch); interface PresentationElementProps { @@ -391,8 +388,8 @@ export default class PresentationElement extends React.Component<PresentationEle <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={fileRegular} color={"gray"} /></button> - <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={fileRegular} /></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(); diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index 8faf0a991..5d36e650b 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -64,7 +64,7 @@ export class FilterBox extends React.Component { componentDidMount = () => { document.addEventListener("pointerdown", (e) => { - if (e.timeStamp !== this._pointerTime) { + if (!e.defaultPrevented && e.timeStamp !== this._pointerTime) { SearchBox.Instance.closeSearch(); } }); @@ -72,9 +72,9 @@ export class FilterBox extends React.Component { setupAccordion() { $('document').ready(function () { - var acc = document.getElementsByClassName('filter-header'); - - for (var i = 0; i < acc.length; i++) { + const acc = document.getElementsByClassName('filter-header'); + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < acc.length; i++) { acc[i].addEventListener("click", function (this: HTMLElement) { this.classList.toggle("active"); @@ -103,6 +103,7 @@ export class FilterBox extends React.Component { $('document').ready(function () { var acc = document.getElementsByClassName('filter-header'); + // tslint:disable-next-line: prefer-for-of for (var i = 0; i < acc.length; i++) { let classList = acc[i].classList; if (classList.contains("active")) { @@ -247,8 +248,8 @@ export class FilterBox extends React.Component { filterDocsByType(docs: Doc[]) { let finalDocs: Doc[] = []; docs.forEach(doc => { - let layoutresult = Cast(doc.type, "string", ""); - if (this._icons.includes(layoutresult)) { + let layoutresult = Cast(doc.type, "string"); + if (!layoutresult || this._icons.includes(layoutresult)) { finalDocs.push(doc); } }); diff --git a/src/client/views/search/Pager.tsx b/src/client/views/search/Pager.tsx index e7d3b810f..9bcfb1676 100644 --- a/src/client/views/search/Pager.tsx +++ b/src/client/views/search/Pager.tsx @@ -3,7 +3,7 @@ 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 "./Pager.scss"; import { SearchBox } from './SearchBox'; import { observable, action } from 'mobx'; import { FilterBox } from './FilterBox'; @@ -20,7 +20,7 @@ export class Pager extends React.Component { @action onLeftClick(e: React.PointerEvent) { FilterBox.Instance._pointerTime = e.timeStamp; - if(SearchBox.Instance._pageNum > 0){ + if (SearchBox.Instance._pageNum > 0) { SearchBox.Instance._pageNum -= 1; } } @@ -28,7 +28,7 @@ export class Pager extends React.Component { @action onRightClick(e: React.PointerEvent) { FilterBox.Instance._pointerTime = e.timeStamp; - if(SearchBox.Instance._pageNum+1 < SearchBox.Instance._maxNum){ + if (SearchBox.Instance._pageNum + 1 < SearchBox.Instance._maxNum) { SearchBox.Instance._pageNum += 1; } } @@ -72,7 +72,7 @@ export class Pager extends React.Component { </div> </div> </div> - ) + ); } }
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index dbaa6eba2..af0c541f1 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -15,7 +15,6 @@ 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 { @@ -169,7 +168,7 @@ export class SearchBox extends React.Component { @action.bound closeSearch = () => { - console.log("closing search") + console.log("closing search"); FilterBox.Instance.closeFilter(); this.closeResults(); } diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss index 946680f0e..fa4c9cb38 100644 --- a/src/client/views/search/SearchItem.scss +++ b/src/client/views/search/SearchItem.scss @@ -5,6 +5,7 @@ flex-direction: row-reverse; justify-content: flex-end; height: 70px; + z-index: 0; .search-item { width: 500px; @@ -13,6 +14,7 @@ border-bottom-style: solid; padding: 10px; height: 70px; + z-index: 0; display: inline-block; .main-search-info { @@ -23,16 +25,17 @@ .search-title { text-transform: uppercase; text-align: left; - width: 80%; + width: 100%; font-weight: bold; } .search-info { display: flex; justify-content: flex-end; - width: 40%; .link-container.item { + margin-left: auto; + margin-right: auto; height: 26px; width: 26px; border-radius: 13px; @@ -41,7 +44,6 @@ 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; @@ -84,26 +86,42 @@ .link-container.item:hover .link-extended { opacity: 1; } + + .icon-icons { + width:50px + } + .icon-live { + width:175px; + } - .icon { - + .icon-icons, .icon-live { + height:50px; + margin:auto; + overflow: hidden; .search-type { - width: 25PX; - height: 25PX; - display: flex; + display: inline-block; + width:100%; + position: absolute; justify-content: center; align-items: center; position: relative; margin-right: 5px; } + .pdfBox-cont { + overflow: hidden; + + img { + width:100% !important; + height:auto !important; + } + } .search-type:hover+.search-label { opacity: 1; } .search-label { font-size: 10; - padding: 5px; position: relative; right: 0px; text-transform: capitalize; @@ -114,6 +132,18 @@ transition: opacity 0.2s ease-in-out; } } + + .icon-live:hover { + height:175px; + .pdfBox-cont { + img { + width:100% !important; + } + } + } + } + .search-info:hover { + width:60%; } } } @@ -143,7 +173,15 @@ -webkit-transform: scale(0); -ms-transform: scale(0); transform: scale(0); - // height: 100% } +} +.search-overview:hover { + z-index: 1; +} +.collection { + display:flex; +} +.collection-item { + width:35px; }
\ No newline at end of file diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 50bf1f6b8..f8a0c7b16 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -1,24 +1,26 @@ 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 { faCaretUp, faChartBar, faFilePdf, faFilm, faGlobeAsia, faImage, faLink, faMusic, faObjectGroup, faStickyNote } 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 { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { emptyFunction, returnFalse, returnOne } from "../../../Utils"; +import { DocTypes } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { SetupDrag } from "../../util/DragManager"; +import { LinkManager } from "../../util/LinkManager"; 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 { Transform } from "../../util/Transform"; +import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss"; import { CollectionViewType } from "../collections/CollectionBaseView"; -import { DocTypes } from "../../documents/Documents"; -import { FilterBox } from "./FilterBox"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; import { DocumentView } from "../nodes/DocumentView"; -import "./SelectorContextMenu.scss"; import { SearchBox } from "./SearchBox"; +import "./SearchItem.scss"; +import "./SelectorContextMenu.scss"; export interface SearchItemProps { doc: Doc; @@ -66,16 +68,23 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { col.panX = newPanX; col.panY = newPanY; } - CollectionDockingView.Instance.AddRightSplit(col); + CollectionDockingView.Instance.AddRightSplit(col, undefined); }; } - render() { return ( - < div className="parents"> + <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>)} + {[...this._docs, ...this._otherDocs].map(doc => { + let item = React.createRef<HTMLDivElement>(); + return <div className="collection" key={doc.col[Id] + doc.target[Id]} ref={item}> + <div className="collection-item" onPointerDown={ + SetupDrag(item, () => doc.col, undefined, undefined, undefined, undefined, () => SearchBox.Instance.closeSearch())}> + <FontAwesomeIcon icon={faStickyNote} /> + </div> + <a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a> + </div>; + })} </div> ); } @@ -87,12 +96,58 @@ export class SearchItem extends React.Component<SearchItemProps> { @observable _selected: boolean = false; onClick = () => { - CollectionDockingView.Instance.AddRightSplit(this.props.doc); + DocumentManager.Instance.jumpToDocument(this.props.doc, false); } + @observable _useIcons = true; + @observable _displayDim = 50; @computed public get DocumentIcon() { - let layoutresult = Cast(this.props.doc.type, "string", ""); + let layoutresult = StrCast(this.props.doc.type); + if (!this._useIcons) { + let renderDoc = this.props.doc; + let box: number[] = []; + if (layoutresult.indexOf(DocTypes.COL) !== -1) { + renderDoc = Doc.MakeDelegate(renderDoc); + let bounds = DocListCast(renderDoc.data).reduce((bounds, doc) => { + var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; + let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()]; + return { + x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), + r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) + }; + }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); + box = [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / (bounds.r - bounds.x), this._displayDim]; + } + let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); + let returnYDimension = () => this._displayDim; + let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); + return <div + onPointerDown={action(() => { this._useIcons = !this._useIcons; this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); })} + onPointerEnter={action(() => this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE))} + onPointerLeave={action(() => this._displayDim = 50)} > + <DocumentView + fitToBox={box} + Document={renderDoc} + addDocument={returnFalse} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + addDocTab={returnFalse} + renderDepth={1} + PanelWidth={returnXDimension} + PanelHeight={returnYDimension} + focus={emptyFunction} + selectOnLoad={false} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContentScaling={scale} + /> + </div>; + } let button = layoutresult.indexOf(DocTypes.PDF) !== -1 ? faFilePdf : layoutresult.indexOf(DocTypes.IMG) !== -1 ? faImage : @@ -104,7 +159,9 @@ export class SearchItem extends React.Component<SearchItemProps> { layoutresult.indexOf(DocTypes.HIST) !== -1 ? faChartBar : layoutresult.indexOf(DocTypes.WEB) !== -1 ? faGlobeAsia : faCaretUp; - return <FontAwesomeIcon icon={button} size="2x" />; + return <div onPointerDown={action(() => { this._useIcons = false; this._displayDim = Number(SEARCH_THUMBNAIL_SIZE); })} > + <FontAwesomeIcon icon={button} size="2x" /> + </div>; } collectionRef = React.createRef<HTMLDivElement>(); @@ -119,7 +176,7 @@ export class SearchItem extends React.Component<SearchItemProps> { } @computed - get linkCount() { return Cast(this.props.doc.linkedToDocs, listSpec(Doc), []).length + Cast(this.props.doc.linkedFromDocs, listSpec(Doc), []).length; } + get linkCount() { return LinkManager.Instance.getAllRelatedLinks(this.props.doc).length; } @computed get linkString(): string { @@ -130,40 +187,59 @@ export class SearchItem extends React.Component<SearchItemProps> { return num.toString() + " links"; } - pointerDown = (e: React.PointerEvent) => { SearchBox.Instance.openSearch(e); }; + @action + 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; - }); + if (this.props.doc.type === DocTypes.LINK) { + if (this.props.doc.anchor1 && this.props.doc.anchor2) { + + let doc1 = Cast(this.props.doc.anchor1, Doc, new Doc()); + let doc2 = Cast(this.props.doc.anchor2, Doc, new Doc()); + doc1.libraryBrush = true; + doc2.libraryBrush = true; + } + } else { + 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; - }); + if (this.props.doc.type === DocTypes.LINK) { + if (this.props.doc.anchor1 && this.props.doc.anchor2) { + + let doc1 = Cast(this.props.doc.anchor1, Doc, new Doc()); + let doc2 = Cast(this.props.doc.anchor2, Doc, new Doc()); + doc1.libraryBrush = false; + doc2.libraryBrush = false; + } + } else { + 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="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} ref={this.collectionRef} id="result" + onClick={this.onClick} onPointerDown={this.pointerDown} > <div className="main-search-info"> <div className="search-title" id="result" >{this.props.doc.title}</div> - <div className="search-info"> + <div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}> + <div className={`icon-${this._useIcons ? "icons" : "live"}`}> + <div className="search-type" >{this.DocumentIcon}</div> + <div className="search-label">{this.props.doc.type}</div> + </div> <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> diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx index 8a3aacf96..178578c5c 100644 --- a/src/client/views/search/ToggleBar.tsx +++ b/src/client/views/search/ToggleBar.tsx @@ -78,7 +78,7 @@ export class ToggleBar extends React.Component<ToggleBarProps>{ <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 className="toggle-button" id="toggle-button" ref={this._toggleButton} /> </div> </div> ); diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index cce4fff5d..734a90731 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -175,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; @@ -241,11 +249,57 @@ export namespace 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); } - return new Doc; + return Doc.MakeDelegate(doc); // bcz? + } + + export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) { + let resolvedDataDoc = (templateLayoutDoc !== dataDoc) ? dataDoc : undefined; + if (!dataDoc || !(templateLayoutDoc && !(Cast(templateLayoutDoc.layout, Doc) instanceof Doc) && resolvedDataDoc && resolvedDataDoc !== templateLayoutDoc)) { + return templateLayoutDoc; + } + // if we have a data doc that doesn't match the layout, then we're rendering a template. + // ... which means we change the layout to be an expanded view of the template layout. + // This allows the view override the template's properties and be referenceable as its own document. + + let expandedTemplateLayout = templateLayoutDoc["_expanded_" + dataDoc[Id]]; + if (expandedTemplateLayout instanceof Doc) { + return expandedTemplateLayout; + } + if (expandedTemplateLayout === undefined) { + setTimeout(() => { + templateLayoutDoc["_expanded_" + dataDoc[Id]] = Doc.MakeDelegate(templateLayoutDoc); + (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).title = templateLayoutDoc.title + " applied to " + dataDoc.title; + }, 0); + } + return templateLayoutDoc; } export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc { @@ -266,6 +320,7 @@ export namespace Doc { } } }); + return copy; } diff --git a/src/scraping/acm/chromedriver b/src/scraping/acm/chromedriver Binary files differnew file mode 100755 index 000000000..9e9b16717 --- /dev/null +++ b/src/scraping/acm/chromedriver diff --git a/src/scraping/acm/chromedriver.exe b/src/scraping/acm/chromedriver.exe Binary files differnew file mode 100644 index 000000000..6a362fd43 --- /dev/null +++ b/src/scraping/acm/chromedriver.exe diff --git a/src/scraping/acm/citations.txt b/src/scraping/acm/citations.txt new file mode 100644 index 000000000..e5018ddef --- /dev/null +++ b/src/scraping/acm/citations.txt @@ -0,0 +1,2 @@ +321046 +2412979
\ No newline at end of file diff --git a/src/scraping/acm/index.js b/src/scraping/acm/index.js new file mode 100644 index 000000000..ff4b099e7 --- /dev/null +++ b/src/scraping/acm/index.js @@ -0,0 +1,187 @@ +const { + Builder, + By +} = require('selenium-webdriver'); +const { + readFile, + writeFile +} = require('fs'); + +const driver_pause = 500; // milliseconds +const sample_line_char_max = 100; // characters +const target_browser = 'chrome'; + +// GENERAL UTILITY FUNCTIONS + +function log_read(content) { + process.stdout.write("reading " + content + "..."); +} + +function log_snippet(result, quotes = true) { + let snippet = "failed to create snippet"; + switch (typeof result) { + case "string": + let ellipse = result.length > sample_line_char_max; + let i = sample_line_char_max; + if (ellipse) { + while (result[i] != " " && i < -1) { + i--; + } + } + snippet = `${result.substring(0, i + 1).trim()}${ellipse ? "..." : ""}`; + snippet = quotes ? `"${snippet}"` : snippet; + break; + case "object": + snippet = result.map(res => { + switch (typeof res) { + case "string": + return res.substring(0, sample_line_char_max / result.length); + case "object": + return res[Object.keys(res)[0]]; + } + }).join(', '); + } + console.log(snippet); + return result; +} + +// DRIVER UTILITY FUNCTIONS + +async function navigate_to(url) { + await driver.get(url); + await driver.sleep(driver_pause); +} + +async function click_on(ref) { + await (await locate(ref)).click(); + await driver.sleep(driver_pause); +} + +async function locate(ref, multiple = false) { + let locator = ref.startsWith("//") ? By.xpath(ref) : By.id(ref); + return await multiple ? driver.findElements(locator) : driver.findElement(locator); +} + +async function text_of(ref) { + let element = await locate(ref); + return await element.getText(); +} + +async function text_of_all(ref) { + let elements = await locate(ref, true); + let results = []; + for (let element of elements) { + results.push(await element.getText()); + } + return results; +} + +async function logged_assign(key, value) { + log_read(key); + result[key] = log_snippet(value); +} + +// TEXT SCRAPING + +async function read_authors() { + await click_on('//*[@id="tab-1014-btnInnerEl"]/span'); + + let authors = await text_of('//*[@id="tabpanel-1009-body"]'); + let sanitize = line => line.length > 0 && !(line.startsWith("No contact information") || line.startsWith("View colleagues of") || line.startsWith("Bibliometrics:")); + let author_lines = authors.split("\n").map(line => line.trim()).filter(sanitize); + + let all_authors = []; + let i = 0; + while (i < author_lines.length) { + let individual = []; + while (!author_lines[i].startsWith("Average citations")) { + individual.push(author_lines[i]); + i++; + } + individual.push(author_lines[i]); + all_authors.push(individual); + i++; + } + + return all_authors; +} + +// JSON / DASH CONVERSION AND EXPORT + +function parse_authors(metadata) { + let publicationYears = metadata[1].substring(18).split("-"); + author = { + name: metadata[0], + publication_start: parseInt(publicationYears[0]), + publication_end: parseInt(publicationYears[1]) + }; + for (let count = 2; count < metadata.length; count++) { + let attr = metadata[count]; + let char = attr.length - 1; + while (attr[char] != " ") { + char--; + } + let key = attr.substring(0, char).toLowerCase().replace(/ /g, "_").replace(/[\(\)]/g, ""); + let value = parseFloat(attr.substring(char + 1).replace(/,/g, "")); + author[key] = value; + } + return author; +} + +function write_results() { + console.log(); + let output = ""; + results.forEach(res => output += (JSON.stringify(res, null, 4) + "\n")); + + writeFile("./results.txt", output, function errorHandler(exception) { + console.log(exception || "scraped references successfully written as JSON to ./results.txt\n"); + }); +} + +async function scrape_targets(error, data) { + if (error) { + console.log("\nUnable to collect target citations from a citations.txt file stored in this directory.\nPlease make sure one is provided."); + return; + } + + let references = data.split("\n").map(entry => entry.replace("\r", "")).filter(line => line.match(/\d+/g)); + let quota = references.length; + log_snippet(`found ${quota} references to scrape`, false); + + driver = await new Builder().forBrowser(target_browser).build(); + + for (let i = 0; i < quota; i++) { + try { + result = {}; + + let id = references[i]; + let url = `https://dl.acm.org/citation.cfm?id=${id}`; + console.log(`\nscraping ${i + 1}/${quota} (${id})`); + await navigate_to(url); + + logged_assign("url", url); + logged_assign("title", await text_of('//*[@id="divmain"]/div/h1')); + logged_assign("abstract", (await text_of_all("abstract-body")).join(" ")); + logged_assign("authors", (await read_authors()).map(parse_authors)); + } catch (e) { + console.log(e); + await driver.quit(); + } + + results.push(result); + } + + write_results(); + + await driver.quit(); +} + +let driver; +let results = []; +let result = {}; + +log_read("target references"); + +readFile("./citations.txt", { + encoding: "utf8" +}, scrape_targets);
\ No newline at end of file diff --git a/src/scraping/acm/package.json b/src/scraping/acm/package.json new file mode 100644 index 000000000..10f4d2156 --- /dev/null +++ b/src/scraping/acm/package.json @@ -0,0 +1,17 @@ +{ + "name": "scraper", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^0.19.0", + "cheerio": "^1.0.0-rc.3", + "selenium-webdriver": "^4.0.0-alpha.4" + } +} diff --git a/src/scraping/acm/results.txt b/src/scraping/acm/results.txt new file mode 100644 index 000000000..fffa7ff51 --- /dev/null +++ b/src/scraping/acm/results.txt @@ -0,0 +1,66 @@ +{ + "url": "https://dl.acm.org/citation.cfm?id=321046", + "title": "Integer Programming Formulation of Traveling Salesman Problems", + "abstract": "It has been observed by many people that a striking number of quite diverse mathematical problems can be formulated as problems in integer programming, that is, linear programming problems in which some or all of the variables are required to assume integral values. This fact is rendered quite interesting by recent research on such problems, notably by R. E. Gomory [2, 3], which gives promise of yielding efficient computational techniques for their solution. The present paper provides yet another example of the versatility of integer programming as a mathematical modeling device by representing a generalization of the well-known “Travelling Salesman Problem” in integer programming terms. The authors have developed several such models, of which the one presented here is the most efficient in terms of generality, number of variables, and number of constraints. This model is due to the second author [4] and was presented briefly at the Symposium on Combinatorial Problems held at Princeton University, April 1960, sponsored by SIAM and IBM. The problem treated is: (1) A salesman is required to visit each of n cities, indexed by 1, … , n. He leaves from a “base city” indexed by 0, visits each of the n other cities exactly once, and returns to city 0. During his travels he must return to 0 exactly t times, including his final return (here t may be allowed to vary), and he must visit no more than p cities in one tour. (By a tour we mean a succession of visits to cities without stopping at city 0.) It is required to find such an itinerary which minimizes the total distance traveled by the salesman. Note that if t is fixed, then for the problem to have a solution we must have tp ≧ n. For t = 1, p ≧ n, we have the standard traveling salesman problem. Let dij (i ≠ j = 0, 1, … , n) be the distance covered in traveling from city i to city j. The following integer programming problem will be shown to be equivalent to (1): (2) Minimize the linear form ∑0≦i≠j≦n∑ dijxij over the set determined by the relations ∑ni=0i≠j xij = 1 (j = 1, … , n) ∑nj=0j≠i xij = 1 (i = 1, … , n) ui - uj + pxij ≦ p - 1 (1 ≦ i ≠ j ≦ n) where the xij are non-negative integers and the ui (i = 1, …, n) are arbitrary real numbers. (We shall see that it is permissible to restrict the ui to be non-negative integers as well.) If t is fixed it is necessary to add the additional relation: ∑nu=1 xi0 = t Note that the constraints require that xij = 0 or 1, so that a natural correspondence between these two problems exists if the xij are interpreted as follows: The salesman proceeds from city i to city j if and only if xij = 1. Under this correspondence the form to be minimized in (2) is the total distance to be traveled by the salesman in (1), so the burden of proof is to show that the two feasible sets correspond; i.e., a feasible solution to (2) has xij which do define a legitimate itinerary in (1), and, conversely a legitimate itinerary in (1) defines xij, which, together with appropriate ui, satisfy the constraints of (2). Consider a feasible solution to (2). The number of returns to city 0 is given by ∑ni=1 xi0. The constraints of the form ∑ xij = 1, all xij non-negative integers, represent the conditions that each city (other than zero) is visited exactly once. The ui play a role similar to node potentials in a network and the inequalities involving them serve to eliminate tours that do not begin and end at city 0 and tours that visit more than p cities. Consider any xr0r1 = 1 (r1 ≠ 0). There exists a unique r2 such that xr1r2 = 1. Unless r2 = 0, there is a unique r3 with xr2r3 = 1. We proceed in this fashion until some rj = 0. This must happen since the alternative is that at some point we reach an rk = rj, j + 1 < k. Since none of the r's are zero we have uri - uri + 1 + pxriri + 1 ≦ p - 1 or uri - uri + 1 ≦ - 1. Summing from i = j to k - 1, we have urj - urk = 0 ≦ j + 1 - k, which is a contradiction. Thus all tours include city 0. It remains to observe that no tours is of length greater than p. Suppose such a tour exists, x0r1 , xr1r2 , … , xrprp+1 = 1 with all ri ≠ 0. Then, as before, ur1 - urp+1 ≦ - p or urp+1 - ur1 ≧ p. But we have urp+1 - ur1 + pxrp+1r1 ≦ p - 1 or urp+1 - ur1 ≦ p (1 - xrp+1r1) - 1 ≦ p - 1, which is a contradiction. Conversely, if the xij correspond to a legitimate itinerary, it is clear that the ui can be adjusted so that ui = j if city i is the jth city visited in the tour which includes city i, for we then have ui - uj = - 1 if xij = 1, and always ui - uj ≦ p - 1. The above integer program involves n2 + n constraints (if t is not fixed) in n2 + 2n variables. Since the inequality form of constraint is fundamental for integer programming calculations, one may eliminate 2n variables, say the xi0 and x0j, by means of the equation constraints and produce an equivalent problem with n2 + n inequalities and n2 variables. The currently known integer programming procedures are sufficiently regular in their behavior to cast doubt on the heuristic value of machine experiments with our model. However, it seems appropriate to report the results of the five machine experiments we have conducted so far. The solution procedure used was the all-integer algorithm of R. E. Gomory [3] without the ranking procedure he describes. The first three experiments were simple model verification tests on a four-city standard traveling salesman problem with distance matrix [ 20 23 4 30 7 27 25 5 25 3 21 26 ] The first experiment was with a model, now obsolete, using roughly twice as many constraints and variables as the current model (for this problem, 28 constraints in 21 variables). The machine was halted after 4000 pivot steps had failed to produce a solution. The second experiment used the earlier model with the xi0 and x0j eliminated, resulting in a 28-constraint, 15-variable problem. Here the machine produced the optimal solution in 41 pivot steps. The third experiment used the current formulation with the xi0 and x0j eliminated, yielding 13 constraints and 9 variables. The optimal solution was reached in 7 pivot steps. The fourth and fifth experiments were used on a standard ten-city problem, due to Barachet, solved by Dantzig, Johnson and Fulkerson [1]. The current formulation was used, yielding 91 constraints in 81 variables. The fifth problem differed from the fourth only in that the ordering of the rows was altered to attempt to introduce more favorable pivot choices. In each case the machine was stopped after over 250 pivot steps had failed to produce the solution. In each case the last 100 pivot steps had failed to change the value of the objective function. It seems hopeful that more efficient integer programming procedures now under development will yield a satisfactory algorithmic solution to the traveling salesman problem, when applied to this model. In any case, the model serves to illustrate how problems of this sort may be succinctly formulated in integer programming terms.", + "authors": [ + { + "name": "C. E. Miller", + "publication_start": 1960, + "publication_end": 1960, + "publication_count": 1, + "citation_count": 179, + "available_for_download": 1, + "downloads_6_weeks": 130, + "downloads_12_months": 1004, + "downloads_cumulative": 9792, + "average_downloads_per_article": 9792, + "average_citations_per_article": 179 + }, + { + "name": "A. W. Tucker", + "publication_start": 1960, + "publication_end": 1993, + "publication_count": 5, + "citation_count": 196, + "available_for_download": 1, + "downloads_6_weeks": 130, + "downloads_12_months": 1004, + "downloads_cumulative": 9792, + "average_downloads_per_article": 9792, + "average_citations_per_article": 39.2 + }, + { + "name": "R. A. Zemlin", + "publication_start": 1960, + "publication_end": 1964, + "publication_count": 2, + "citation_count": 188, + "available_for_download": 2, + "downloads_6_weeks": 130, + "downloads_12_months": 1009, + "downloads_cumulative": 10023, + "average_downloads_per_article": 5011.5, + "average_citations_per_article": 94 + } + ] +} +{ + "url": "https://dl.acm.org/citation.cfm?id=2412979", + "title": "STRUCT programming analysis system", + "abstract": "The STRUCT system utilizes the flexibility of a powerful graphics display system to provide a set of tools for program analysis. These tools allow the analysis of the static prograin structure and the dynamic execution behavior. of programs within the entire operating system/user program environment of the Brown University Graphics System (BUGS). Information is collected and presented in a manner which fully exploits two aspects of this environment. First, the operating system has been developed in a well-structured hierarcal manner following principles laid down by other researchers (2), (3). Second the programs under analysis have been written in a structured programming language following coding conventions which make available, at the source code level, valuable program control information. A new set of pictorial constructs is introduced for presenting a. program structure (static or dynamic) for inspection. These constructs combine the best features of an indented structured source code listing and the box odented nature of traditional flow charts. The graphical tools available are USed to provide for swift changes in. the desired level of detail displayed within a program structure, for traveling linearly through a program structure, for traveling through a complex program structure (following subroutine or system calls), for concurrently viewing multiple related program structures, and for presenting dynamic program behavior data using three-dimensional projections, The volume of a three-dimensional box representing a program block is proportional to the block's resource utilization. The scope of this paper is limited to a description of the STRUCT system. This system is currently being used to predict and analyze the performance advantages available through the migration of function (program modules) between levels of software and between software and firmware within BUGS. The results of this research on migration will be included in a doctoral dissertation currently being written.", + "authors": [ + { + "name": "Andries Van Dam", + "publication_start": 1975, + "publication_end": 1975, + "publication_count": 1, + "citation_count": 0, + "available_for_download": 0, + "downloads_6_weeks": 8, + "downloads_12_months": 97, + "downloads_cumulative": 97, + "average_downloads_per_article": 0, + "average_citations_per_article": 0 + } + ] +} diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py new file mode 100644 index 000000000..02c6d8b74 --- /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() + + image = Image.open(f"{dist}/{folder}/{name}") + native_width, native_height = image.size + + view_doc = { + "_id": view_doc_guid, + "fields": { + "proto": protofy(data_doc_guid), + "x": 10, + "y": 10, + "width": min(800, native_width), + "zIndex": 2, + "libraryBrush": False + }, + "__type": "Doc" + } + + 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/index.ts b/src/server/index.ts index d9343bf8e..91aacdb98 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -519,8 +519,8 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", - // "boolean": "_b", - // "image": ["_t", "url"], + "boolean": "_b", + "image": ["_t", "url"], "video": ["_t", "url"], "pdf": ["_t", "url"], "audio": ["_t", "url"], diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts index de1fd25e1..f5de00978 100644 --- a/src/server/updateSearch.ts +++ b/src/server/updateSearch.ts @@ -6,8 +6,8 @@ import pLimit from 'p-limit'; const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", - // "boolean": "_b", - // "image": ["_t", "url"], + "boolean": "_b", + "image": ["_t", "url"], "video": ["_t", "url"], "pdf": ["_t", "url"], "audio": ["_t", "url"], @@ -92,9 +92,21 @@ async function update() { } } await cursor.forEach(updateDoc); - await Promise.all(updates.map(update => { - return limit(() => Search.Instance.updateDocument(update)); - })); + for (let i = 0; i < updates.length; i++) { + console.log(i); + const result = await Search.Instance.updateDocument(updates[i]); + try { + console.log(JSON.parse(result).responseHeader.status); + } catch { + console.log("Error:"); + console.log(updates[i]); + console.log(result); + console.log("\n"); + } + } + // await Promise.all(updates.map(update => { + // return limit(() => Search.Instance.updateDocument(update)); + // })); cursor.close(); } 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 |