diff options
Diffstat (limited to 'src')
100 files changed, 4477 insertions, 1429 deletions
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(); } |