diff options
Diffstat (limited to 'src')
24 files changed, 1497 insertions, 399 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index de6c5bc6a..df5c39562 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,11 @@ 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 { LinkButtonBox } from "../views/nodes/LinkButtonBox"; +import { LinkButtonField, LinkButtonData } from "../../new_fields/LinkButtonField"; +import { DocumentManager } from "../util/DocumentManager"; +import { Id } from "../../new_fields/FieldSymbols"; var requestImageSize = require('request-image-size'); var path = require('path'); @@ -66,29 +71,74 @@ export interface DocumentOptions { } const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; +// export interface LinkData { +// anchor1: Doc; +// anchor1Page: number; +// anchor1Tags: Array<{ tag: string, name: string, description: string }>; +// anchor2: Doc; +// anchor2Page: number; +// anchor2Tags: Array<{ tag: string, name: string, description: string }>; +// } + +// export interface TagData { +// tag: string; +// name: string; +// description: string; +// } + 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; + // 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; + export function MakeLink(source: Doc, target: Doc, targetContext?: Doc) { + if (LinkManager.Instance.doesLinkExist(source, target)) return; + UndoManager.RunInBatch(() => { + let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); let linkDocProto = Doc.GetProto(linkDoc); - linkDocProto.title = title === "" ? source.title + " to " + target.title : title; - linkDocProto.linkDescription = description; - linkDocProto.linkTags = tags; + // linkDocProto.title = title === "" ? source.title + " to " + target.title : title; + // linkDocProto.linkDescription = description; + // linkDocProto.linkTags = tags; + + linkDocProto.anchor1 = source; + linkDocProto.anchor1Page = source.curPage; + linkDocProto.anchor1Groups = new List<Doc>([]); + linkDocProto.anchor2 = target; + linkDocProto.anchor2Page = target.curPage; + linkDocProto.anchor2Groups = new List<Doc>([]); + + linkDocProto.context = targetContext; + + let sourceViews = DocumentManager.Instance.getDocumentViews(source); + let targetViews = DocumentManager.Instance.getDocumentViews(target); + sourceViews.forEach(sv => { + targetViews.forEach(tv => { - linkDocProto.linkedTo = target; - linkDocProto.linkedFrom = source; - linkDocProto.linkedToPage = target.curPage; - linkDocProto.linkedFromPage = source.curPage; - linkDocProto.linkedToContext = targetContext; + // TODO: do only for when diff contexts + let proxy1 = Docs.LinkButtonDocument( + { sourceViewId: StrCast(sv.props.Document[Id]), targetViewId: StrCast(tv.props.Document[Id]) }, + { width: 200, height: 100, borderRounding: 0 }); + let proxy1Proto = Doc.GetProto(proxy1); + proxy1Proto.sourceViewId = StrCast(sv.props.Document[Id]); + proxy1Proto.targetViewId = StrCast(tv.props.Document[Id]); + proxy1Proto.isLinkButton = true; + + let proxy2 = Docs.LinkButtonDocument( + { sourceViewId: StrCast(tv.props.Document[Id]), targetViewId: StrCast(sv.props.Document[Id]) }, + { width: 200, height: 100, borderRounding: 0 }); + let proxy2Proto = Doc.GetProto(proxy2); + proxy2Proto.sourceViewId = StrCast(tv.props.Document[Id]); + proxy2Proto.targetViewId = StrCast(sv.props.Document[Id]); + proxy2Proto.isLinkButton = true; + + LinkManager.Instance.linkProxies.push(proxy1); + LinkManager.Instance.linkProxies.push(proxy2); + }); + }); + + LinkManager.Instance.allLinks.push(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"); } @@ -107,6 +157,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"; @@ -117,6 +168,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 => { @@ -130,6 +182,7 @@ export namespace Docs { audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype(); pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype(); iconProto = fields[iconProtoId] as Doc || CreateIconPrototype(); + linkProto = fields[linkProtoId] as Doc || CreateLinkPrototype(); }); } @@ -162,6 +215,11 @@ export namespace Docs { { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) }); return iconProto; } + function CreateLinkPrototype(): Doc { + let linkProto = setupPrototypeOptions(linkProtoId, "LINK_PROTO", LinkButtonBox.LayoutString(), + { x: 0, y: 0, width: 300 }); + return linkProto; + } function CreateTextPrototype(): Doc { let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb" }); @@ -248,6 +306,9 @@ export namespace Docs { export function IconDocument(icon: string, options: DocumentOptions = {}) { return CreateInstance(iconProto, new IconField(icon), options); } + export function LinkButtonDocument(data: LinkButtonData, options: DocumentOptions = {}) { + return CreateInstance(linkProto, new LinkButtonField(data), options); + } export function PdfDocument(url: string, options: DocumentOptions = {}) { return CreateInstance(pdfProto, new PdfField(new URL(url)), options); } @@ -363,7 +424,7 @@ export namespace Docs { } /* - + this template requires an additional style setting on the collectionView-cont to make the layout relative .collectionView-cont { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 862395d74..bb87d09ec 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,7 +1,7 @@ import { computed, observable } 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,7 @@ 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'; export class DocumentManager { @@ -83,35 +84,28 @@ 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); + let linked = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => { + // console.log("FINDING LINKED DVs FOR", StrCast(dv.props.Document.title)); + let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document); 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 })); + let destination = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document); + if (destination) { + DocumentManager.Instance.getDocumentViews(destination).map(docView1 => { + // console.log("PUSHING LINK BETWEEN", StrCast(dv.props.Document.title), StrCast(docView1.props.Document.title)); + // TODO: if any docviews are not in the same context, draw a proxy + // let sameContent = dv.props.ContainingCollectionView === docView1.props.ContainingCollectionView; + pairs.push({ anchor1View: dv, anchor2View: docView1, linkDoc: 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)); + }, [] as { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[])); } return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); + }, [] as { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[]); + return linked; } @undoBatch diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index c3c92daa5..01193cab5 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -4,8 +4,11 @@ import { Cast } from "../../new_fields/Types"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import * as globalCssVariables from "../views/globalCssVariables.scss"; +import { LinkManager } from "./LinkManager"; import { URLField } from "../../new_fields/URLField"; import { SelectionManager } from "./SelectionManager"; +import { Docs } from "../documents/Documents"; +import { DocumentManager } from "./DocumentManager"; 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) { @@ -42,17 +45,36 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () return onItemDown; } +export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) { + let draggeddoc = LinkManager.Instance.findOppositeAnchor(linkDoc, sourceDoc); + + // TODO: if not in same context then don't drag + + let moddrag = await Cast(draggeddoc.annotationOn, Doc); + let dragData = new DragManager.DocumentDragData(moddrag ? [moddrag] : [draggeddoc]); + DragManager.StartDocumentDrag([dragEle], 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[] = []; + + // TODO: if not in same context then don't drag + 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.findAllRelatedLinks(srcTarg); + if (linkDocs) { + draggedDocs = linkDocs.map(link => { + return LinkManager.Instance.findOppositeAnchor(link, sourceDoc); + }); + } } - draggedDocs.push(...draggedFromDocs); + // draggedDocs.push(...draggedFromDocs); if (draggedDocs.length) { let moddrag: Doc[] = []; for (const draggedDoc of draggedDocs) { @@ -60,6 +82,9 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n if (doc) moddrag.push(doc); } let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); + // dragData.moveDocument = (document, targetCollection, addDocument) => { + // return false; + // }; DragManager.StartDocumentDrag([dragEle], dragData, x, y, { handlers: { dragComplete: action(emptyFunction), @@ -220,6 +245,11 @@ export namespace DragManager { StartDrag([ele], dragData, downX, downY, options); } + export function StartLinkProxyDrag(ele: HTMLElement, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { + runInAction(() => StartDragFunctions.map(func => func())); + StartDrag([ele], dragData, downX, downY, options); + } + export let AbortDrag: () => void = emptyFunction; function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) { diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts new file mode 100644 index 000000000..544f2edda --- /dev/null +++ b/src/client/util/LinkManager.ts @@ -0,0 +1,150 @@ +import { observable, action } from "mobx"; +import { StrCast, Cast } from "../../new_fields/Types"; +import { Doc, DocListCast } from "../../new_fields/Doc"; +import { listSpec } from "../../new_fields/Schema"; +import { List } from "../../new_fields/List"; + + +/* + * 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() { + } + + @observable public allLinks: Array<Doc> = []; // list of link docs + @observable public groupMetadataKeys: Map<string, Array<string>> = new Map(); + // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it hodls + @observable public linkProxies: Array<Doc> = []; // list of linkbutton docs - used to visualize link when an anchors are not in the same context + + // finds all links that contain the given anchor + public findAllRelatedLinks(anchor: Doc): Array<Doc> { + return LinkManager.Instance.allLinks.filter( + link => Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)) || Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc))); + } + + // returns map of group type to anchor's links in that group type + public findRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> { + let related = this.findAllRelatedLinks(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); + let group = anchorGroups.get(groupType); + if (group) group.push(link); + else group = [link]; + anchorGroups.set(groupType, group); + }); + } else { + // if link is in no groups then put it in default group + let group = anchorGroups.get("*"); + if (group) group.push(link); + else group = [link]; + anchorGroups.set("*", group); + } + + }); + return anchorGroups; + } + + // returns a list of all metadata docs associated with the given group type + public findAllMetadataDocsInGroup(groupType: string): Array<Doc> { + let md: Doc[] = []; + let allLinks = LinkManager.Instance.allLinks; + 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; + } + + // removes all group docs from all links with the given group type + public deleteGroup(groupType: string): void { + let deleted = LinkManager.Instance.groupMetadataKeys.delete(groupType); + if (deleted) { + LinkManager.Instance.allLinks.forEach(linkDoc => { + LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType); + LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType); + }); + } + } + + // 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); + } + + // checks if a link with the given anchors exists + public doesLinkExist(anchor1: Doc, anchor2: Doc) { + let allLinks = LinkManager.Instance.allLinks; + 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 findOppositeAnchor(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); + } + } + + // 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); + } + } + + @action + public addLinkProxy(proxy: Doc) { + LinkManager.Instance.linkProxies.push(proxy); + } + + public findLinkProxy(sourceViewId: string, targetViewId: string): Doc | undefined { + let index = LinkManager.Instance.linkProxies.findIndex(p => { + return StrCast(p.sourceViewId) === sourceViewId && StrCast(p.targetViewId) === targetViewId; + }); + return index > -1 ? LinkManager.Instance.linkProxies[index] : undefined; + } + +}
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 2c0e18bbb..926273633 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -25,6 +25,7 @@ import { TemplateMenu } from "./TemplateMenu"; import { Template, Templates } from "./Templates"; import React = require("react"); import { URLField } from '../../new_fields/URLField'; +import { LinkManager } from '../util/LinkManager'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -572,9 +573,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.findAllRelatedLinks(selFirst.props.Document).length; linkButton = (<Flyout anchorPoint={anchorPoints.RIGHT_TOP} content={<LinkMenu docView={selFirst} diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 69b9e77eb..c82027da5 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -24,6 +24,8 @@ import { SubCollectionViewProps } from "./CollectionSubView"; import { ParentDocSelector } from './ParentDocumentSelector'; import React = require("react"); import { MainView } from '../MainView'; +import { LinkManager } from '../../util/LinkManager'; + @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -352,9 +354,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.reactComponents = [upDiv]; tab.element.append(upDiv); counter.DashDocId = tab.contentItem.config.props.documentId; - tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title], + tab.reactionDisposer = reaction(() => [LinkManager.Instance.findAllRelatedLinks(doc), doc.title], () => { - counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length; + counter.innerHTML = LinkManager.Instance.findAllRelatedLinks(doc).length; tab.titleElement[0].textContent = doc.title; }, { fireImmediately: true }); tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 7a0fd2b31..d8d518147 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -1,18 +1,50 @@ -.collectionfreeformlinkview-linkLine { - stroke: black; +// .collectionfreeformlinkview-linkLine { +// stroke: black; +// transform: translate(10000px,10000px); +// opacity: 0.5; +// pointer-events: all; +// } +// .collectionfreeformlinkview-linkCircle { +// stroke: rgb(0,0,0); +// opacity: 0.5; +// transform: translate(10000px,10000px); +// pointer-events: all; +// cursor: pointer; +// } +// .collectionfreeformlinkview-linkText { +// stroke: rgb(0,0,0); +// opacity: 0.5; +// transform: translate(10000px,10000px); +// pointer-events: all; +// } + +.linkview-ele { transform: translate(10000px,10000px); - opacity: 0.5; pointer-events: all; + + &.linkview-line { + stroke: black; + stroke-width: 2px; + opacity: 0.5; + } } -.collectionfreeformlinkview-linkCircle { - stroke: rgb(0,0,0); - opacity: 0.5; - transform: translate(10000px,10000px); - pointer-events: all; -} -.collectionfreeformlinkview-linkText { - stroke: rgb(0,0,0); - opacity: 0.5; - transform: translate(10000px,10000px); - pointer-events: all; + +.linkview-button { + width: 200px; + height: 100px; + border-radius: 5px; + padding: 10px; + position: relative; + background-color: black; + cursor: pointer; + + p { + width: calc(100% - 20px); + color: white; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index ba7e6cf9e..13b5dc575 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -5,59 +5,68 @@ import { InkingControl } from "../../InkingControl"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); import v5 = require("uuid/v5"); +import { DocumentView } from "../../nodes/DocumentView"; +import { Docs } from "../../../documents/Documents"; export interface CollectionFreeFormLinkViewProps { - A: Doc; - B: Doc; - LinkDocs: Doc[]; - addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; - removeDocument: (document: Doc) => boolean; + // anchor1: Doc; + // anchor2: Doc; + // LinkDocs: Doc[]; + // addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + // removeDocument: (document: Doc) => boolean; + // sameContext: boolean; + + sourceView: DocumentView; + targetView: DocumentView; } @observer export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> { - onPointerDown = (e: React.PointerEvent) => { - if (e.button === 0 && !InkingControl.Instance.selectedTool) { - let a = this.props.A; - let b = this.props.B; - let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2); - 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); - }); - e.stopPropagation(); - e.preventDefault(); - } - } + // onPointerDown = (e: React.PointerEvent) => { + // if (e.button === 0 && !InkingControl.Instance.selectedTool) { + // let a = this.props.A; + // let b = this.props.B; + // let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2); + // 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); + // }); + // e.stopPropagation(); + // e.preventDefault(); + // } + // } + + render() { - 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); - let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2); - 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 l = this.props.LinkDocs; + // let a = this.props.A; + // let b = this.props.B; + let a1 = this.props.sourceView; + let a2 = this.props.targetView; + let x1 = NumCast(a1.Document.x) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.width) / NumCast(a1.Document.zoomBasis, 1) / 2); + let y1 = NumCast(a1.Document.y) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.height) / NumCast(a1.Document.zoomBasis, 1) / 2); + + let x2 = NumCast(a2.Document.x) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.width) / NumCast(a2.Document.zoomBasis, 1) / 2); + let y2 = NumCast(a2.Document.y) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.height) / NumCast(a2.Document.zoomBasis, 1) / 2); + return ( <> - <line key="linkLine" className="collectionfreeformlinkview-linkLine" - style={{ strokeWidth: `${2 * l.length / 2}` }} + <line className="linkview-line linkview-ele" + style={{ strokeWidth: `${2 * 1 / 2}` }} x1={`${x1}`} y1={`${y1}`} x2={`${x2}`} y2={`${y2}`} /> + {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle" cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */} - <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}> + {/* <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}> {text} - </text> + </text> */} </> ); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx new file mode 100644 index 000000000..a4d122af2 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx @@ -0,0 +1,131 @@ +import { observer } from "mobx-react"; +import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc"; +import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { InkingControl } from "../../InkingControl"; +import "./CollectionFreeFormLinkView.scss"; +import React = require("react"); +import v5 = require("uuid/v5"); +import { DocumentView } from "../../nodes/DocumentView"; +import { Docs } from "../../../documents/Documents"; +import { observable, action } from "mobx"; +import { CollectionDockingView } from "../CollectionDockingView"; +import { dropActionType, DragManager } from "../../../util/DragManager"; +import { emptyFunction } from "../../../../Utils"; +import { DocumentManager } from "../../../util/DocumentManager"; + +export interface CollectionFreeFormLinkViewProps { + sourceView: DocumentView; + targetView: DocumentView; + proxyDoc: Doc; + // addDocTab: (document: Doc, where: string) => void; +} + +@observer +export class CollectionFreeFormLinkWithProxyView extends React.Component<CollectionFreeFormLinkViewProps> { + + // @observable private _proxyX: number = NumCast(this.props.proxyDoc.x); + // @observable private _proxyY: number = NumCast(this.props.proxyDoc.y); + private _ref = React.createRef<HTMLDivElement>(); + private _downX: number = 0; + private _downY: number = 0; + @observable _x: number = 0; + @observable _y: number = 0; + // @observable private _proxyDoc: Doc = Docs.TextDocument(); // used for positioning + + @action + componentDidMount() { + let a2 = this.props.proxyDoc; + this._x = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2); + this._y = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2); + } + + + followButton = (e: React.PointerEvent): void => { + e.stopPropagation(); + let open = this.props.targetView.props.ContainingCollectionView ? this.props.targetView.props.ContainingCollectionView.props.Document : this.props.targetView.props.Document; + CollectionDockingView.Instance.AddRightSplit(open); + DocumentManager.Instance.jumpToDocument(this.props.targetView.props.Document, e.altKey); + } + + @action + setPosition(x: number, y: number) { + this._x = x; + this._y = y; + } + + startDragging(x: number, y: number) { + if (this._ref.current) { + let dragData = new DragManager.DocumentDragData([this.props.proxyDoc]); + + DragManager.StartLinkProxyDrag(this._ref.current, dragData, x, y, { + handlers: { + dragComplete: action(() => { + let a2 = this.props.proxyDoc; + let offset = NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2; + let x = NumCast(a2.x);// + NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2; + let y = NumCast(a2.y);// + NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2; + this.setPosition(x, y); + + // this is a hack :'( theres prob a better way to make the input doc not render + let views = DocumentManager.Instance.getDocumentViews(this.props.proxyDoc); + views.forEach(dv => { + dv.props.removeDocument && dv.props.removeDocument(dv.props.Document); + }); + }), + }, + hideSource: true //? + }); + } + } + + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + + e.stopPropagation(); + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + onPointerMove = (e: PointerEvent): void => { + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + this.startDragging(this._downX, this._downY); + } + e.stopPropagation(); + e.preventDefault(); + } + onPointerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + let a1 = this.props.sourceView; + let x1 = NumCast(a1.Document.x) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.width) / NumCast(a1.Document.zoomBasis, 1) / 2); + let y1 = NumCast(a1.Document.y) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.height) / NumCast(a1.Document.zoomBasis, 1) / 2); + + let context = this.props.targetView.props.ContainingCollectionView ? + (" in the context of " + StrCast(this.props.targetView.props.ContainingCollectionView.props.Document.title)) : ""; + let text = "link to " + StrCast(this.props.targetView.props.Document.title) + context; + + return ( + <> + <line className="linkview-line linkview-ele" + // style={{ strokeWidth: `${2 * 1 / 2}` }} + x1={`${x1}`} y1={`${y1}`} + x2={`${this._x}`} y2={`${this._y}`} /> + <foreignObject className="linkview-button-wrapper linkview-ele" width={200} height={100} x={this._x - 100} y={this._y - 50}> + <div className="linkview-button" onPointerDown={this.onPointerDown} onPointerUp={this.followButton} ref={this._ref}> + <p>{text}</p> + </div> + </foreignObject> + </> + ); + } +} + +//onPointerDown={this.followButton}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index be75c6c5c..211c90c29 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,4 +1,4 @@ -import { computed, IReactionDisposer, reaction } from "mobx"; +import { computed, IReactionDisposer, reaction, action } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; @@ -11,6 +11,10 @@ import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); +import { CollectionFreeFormLinkWithProxyView } from "./CollectionFreeFormLinkWithProxyView"; +import { Docs } from "../../../documents/Documents"; +import { LinkButtonField } from "../../../../new_fields/LinkButtonField"; +import { LinkManager } from "../../../util/LinkManager"; @observer export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> { @@ -92,40 +96,156 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document); } - @computed - get uniqueConnections() { - 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 => { - if (!drawnPairs.reduce((found, drawnPair) => { - let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b)); - let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a)); - let match = match1 || match2; - if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) { - drawnPair.l.push(connection.l); + // @computed + // get uniqueConnections() { + // // console.log("\n"); + // let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { + // // console.log("CONNECTION BETWEEN", StrCast(connection.anchor1View.props.Document.title), StrCast(connection.anchor2View.props.Document.title)); + // let srcViews = this.documentAnchors(connection.anchor1View); + // // srcViews.forEach(sv => { + // // console.log("DOCANCHORS SRC", StrCast(connection.anchor1View.Document.title), StrCast(sv.Document.title)); + // // }); + + // let targetViews = this.documentAnchors(connection.anchor2View); + // // targetViews.forEach(sv => { + // // console.log("DOCANCHORS TARG", StrCast(connection.anchor2View.Document.title), StrCast(sv.Document.title)); + // // }); + + // // console.log("lengths", srcViews.length, targetViews.length); + + // // srcViews.forEach(v => { + // // console.log("SOURCE VIEW", StrCast(v.props.Document.title)); + // // }); + // // targetViews.forEach(v => { + // // console.log("TARGET VIEW", StrCast(v.Document.title)); + // // }); + + // let possiblePairs: { anchor1: Doc, anchor2: Doc }[] = []; + // // srcViews.map(sv => { + // // console.log("SOURCE VIEW", StrCast(sv.props.Document.title)); + // // targetViews.map(tv => { + // // console.log("TARGET VIEW", StrCast(tv.props.Document.title)); + // // // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title)); + // // possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document }); + // // }); + // // console.log("END\n"); + // // }); + // srcViews.forEach(sv => { + // // console.log("SOURCE VIEW", StrCast(sv.props.Document.title)); + // targetViews.forEach(tv => { + // // console.log("TARGET VIEW", StrCast(tv.props.Document.title)); + // // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title)); + // possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document }); + // }); + // // console.log("END\n"); + // }); + // // console.log("POSSIBLE PAIRS LENGTH", possiblePairs.length); + // possiblePairs.map(possiblePair => { + // // console.log("POSSIBLEPAIR", StrCast(possiblePair.anchor1.title), StrCast(possiblePair.anchor2.title)); + // if (!drawnPairs.reduce((found, drawnPair) => { + // let match1 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor1) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor2)); + // let match2 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor2) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor1)); + // let match = match1 || match2; + // if (match && !drawnPair.linkDocs.reduce((found, link) => found || link[Id] === connection.linkDoc[Id], false)) { + // drawnPair.linkDocs.push(connection.linkDoc); + // } + // return match || found; + // }, false)) { + // drawnPairs.push({ anchor1: possiblePair.anchor1, anchor2: possiblePair.anchor2, linkDocs: [connection.linkDoc] }); + // } + // }); + // return drawnPairs; + // }, [] as { anchor1: Doc, anchor2: Doc, linkDocs: Doc[] }[]); + // return connections.map(c => { + // let x = c.linkDocs.reduce((p, l) => p + l[Id], ""); + // return <CollectionFreeFormLinkView key={x} anchor1={c.anchor1} anchor2={c.anchor2} />; + // }); + // } + + findUniquePairs = (): JSX.Element[] => { + let connections = DocumentManager.Instance.LinkedDocumentViews; + + let unique: Set<{ sourceView: DocumentView, targetView: DocumentView, linkDoc: Doc }> = new Set(); + connections.forEach(c => { + + // let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView)); + // let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView)); + let match1 = unique.has({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc }); + let match2 = unique.has({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc }); + let sameContext = c.anchor1View.props.ContainingCollectionView === c.anchor2View.props.ContainingCollectionView; + + // console.log("CONNECTION", StrCast(c.anchor1View.props.Document.title), StrCast(c.anchor2View.props.Document.title), match1, match2); + + + // if in same context, push if docview pair does not already exist + // else push both directions of pair + if (sameContext) { + if (!(match1 || match2)) unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc }); + } else { + unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc }); + unique.add({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc }); + } + }); + + let uniqueList: JSX.Element[] = []; + unique.forEach(u => { + // TODO: make better key + let key = StrCast(u.sourceView.Document[Id]) + "-link-" + StrCast(u.targetView.Document[Id]) + "-" + Date.now() + Math.random(); + let sourceIn = u.sourceView.props.ContainingCollectionView ? u.sourceView.props.ContainingCollectionView.props.Document === this.props.Document : false; + let targetIn = u.targetView.props.ContainingCollectionView ? u.targetView.props.ContainingCollectionView.props.Document === this.props.Document : false; + let sameContext = u.sourceView.props.ContainingCollectionView === u.targetView.props.ContainingCollectionView; + let inContainer = sameContext ? sourceIn || targetIn : sourceIn; + + if (inContainer) { + // let alias = Doc.MakeAlias(proxy); + if (sameContext) { + uniqueList.push(<CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} />); + } else { + let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id])); + if (!proxy) { + proxy = Docs.LinkButtonDocument( + { sourceViewId: StrCast(u.sourceView.props.Document[Id]), targetViewId: StrCast(u.targetView.props.Document[Id]) }, + { width: 200, height: 100, borderRounding: 0 }); + let proxy1Proto = Doc.GetProto(proxy); + proxy1Proto.sourceViewId = StrCast(u.sourceView.props.Document[Id]); + proxy1Proto.targetViewId = StrCast(u.targetView.props.Document[Id]); + proxy1Proto.isLinkButton = true; + + // LinkManager.Instance.linkProxies.push(proxy); + LinkManager.Instance.addLinkProxy(proxy); } +<<<<<<< HEAD + uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} proxyDoc={proxy} />); + + // let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id])); + // if (proxy) { + // this.props.addDocument(proxy, false); + // uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} />); + // } + // let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2"; + // let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc); + // this.props.addDocument(proxy, false); + + // uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} + // proxyDoc={proxy} addDocTab={this.props.addDocTab} />); +======= return match || found; }, false)) { drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }); +>>>>>>> e9d62f4ca0dbeb57e46239047041a8a04da7b504 } - }); - return drawnPairs; - }, [] as { a: Doc, b: Doc, l: Doc[] }[]); - return connections.map(c => { - let x = c.l.reduce((p, l) => p + l[Id], ""); - return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l} - removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />; + } }); + return uniqueList; } render() { + this.findUniquePairs(); return ( <div className="collectionfreeformlinksview-container"> <svg className="collectionfreeformlinksview-svgCanvas"> - {this.uniqueConnections} + {/* {this.uniqueConnections} */} + {this.findUniquePairs()} </svg> {this.props.children} </div> diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 02396c3af..940b00a90 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -12,6 +12,7 @@ import "./DocumentView.scss"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; import { IconBox } from "./IconBox"; +import { LinkButtonBox } from "./LinkButtonBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; @@ -103,7 +104,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { render() { 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 }} + components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, LinkButtonBox }} bindings={this.CreateBindings()} jsx={this.finalLayout} showWarnings={true} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index acd5e4cf2..1d87205ab 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -31,7 +31,12 @@ import "./DocumentView.scss"; import React = require("react"); import { Id, Copy } from '../../../new_fields/FieldSymbols'; import { ContextMenuProps } from '../ContextMenuItem'; +<<<<<<< HEAD +import { list, object, createSimpleSchema } from 'serializr'; +import { LinkManager } from '../../util/LinkManager'; +======= import { RouteStore } from '../../../server/RouteStore'; +>>>>>>> e9d62f4ca0dbeb57e46239047041a8a04da7b504 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); @@ -53,16 +58,17 @@ 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>; @@ -280,8 +286,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.findAllRelatedLinks(this.props.Document); let expandedDocs: Doc[] = []; expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs; expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; @@ -316,24 +321,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.collapseTargetsToPoint(scrpt, expandedProtoDocs); } } - else if (linkedToDocs.length || linkedFromDocs.length) { - let linkedFwdDocs = [ - linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], - linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; - - let linkedFwdContextDocs = [ - linkedToDocs.length ? await (linkedToDocs[0].linkedToContext) as Doc : linkedFromDocs.length ? await PromiseValue(linkedFromDocs[0].linkedFromContext) as Doc : undefined, - linkedFromDocs.length ? await (linkedFromDocs[0].linkedFromContext) as Doc : linkedToDocs.length ? await PromiseValue(linkedToDocs[0].linkedToContext) as Doc : undefined]; - - let linkedFwdPage = [ - linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined, - linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined]; - - if (!linkedFwdDocs.some(l => l instanceof Promise)) { - let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab"); - let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); - } + else if (linkedDocs.length) { + let linkedDoc = linkedDocs.length ? linkedDocs[0] : expandedDocs[0]; + let linkedPages = [linkedDocs.length ? NumCast(linkedDocs[0].anchor1Page, undefined) : NumCast(linkedDocs[0].anchor2Page, undefined), + linkedDocs.length ? NumCast(linkedDocs[0].anchor2Page, undefined) : NumCast(linkedDocs[0].anchor1Page, undefined)]; + let maxLocation = StrCast(linkedDoc.maximizeLocation, "inTab"); + DocumentManager.Instance.jumpToDocument(linkedDoc, ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedPages[altKey ? 1 : 0]); + // else if (linkedToDocs.length || linkedFromDocs.length) { + // let linkedFwdDocs = [ + // linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], + // linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; + + // let linkedFwdContextDocs = [ + // linkedToDocs.length ? await (linkedToDocs[0].linkedToContext) as Doc : linkedFromDocs.length ? await PromiseValue(linkedFromDocs[0].linkedFromContext) as Doc : undefined, + // linkedFromDocs.length ? await (linkedFromDocs[0].linkedFromContext) as Doc : linkedToDocs.length ? await PromiseValue(linkedToDocs[0].linkedToContext) as Doc : undefined]; + + // let linkedFwdPage = [ + // linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined, + // linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined]; + + // if (!linkedFwdDocs.some(l => l instanceof Promise)) { + // let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab"); + // let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; + // DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); + // } } } } @@ -526,6 +537,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }; onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }; + onDragOver = (e: React.DragEvent): void => { + this.props.Document.libraryBrush = true; + console.log("dragOver"); + }; + onDragLeave = (e: React.DragEvent): void => { this.props.Document.libraryBrush = false; }; isSelected = () => SelectionManager.IsSelected(this); @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; @@ -540,7 +556,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu render() { var scaling = this.props.ContentScaling(); var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%"; +<<<<<<< HEAD + + // // for linkbutton docs + // let isLinkButton = BoolCast(this.props.Document.isLinkButton); + // let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)); + // let display = isLinkButton ? activeDvs.reduce((found, dv) => { + // let matchSv = this.props.Document.sourceViewId === StrCast(dv.props.Document[Id]); + // let matchTv = this.props.Document.targetViewId === StrCast(dv.props.Document[Id]); + // let match = matchSv || matchTv; + // return match || found; + // }, false) : true; + +======= var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; +>>>>>>> e9d62f4ca0dbeb57e46239047041a8a04da7b504 return ( <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`} ref={this._mainCont} @@ -555,10 +585,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu background: this.Document.backgroundColor || "", width: nativeWidth, height: nativeHeight, - transform: `scale(${scaling}, ${scaling})` + transform: `scale(${scaling}, ${scaling})`, + // display: display ? "block" : "none" }} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - + onDragOver={this.onDragOver} onDragLeave={this.onDragLeave} 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..acfa9fc69 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -19,6 +19,8 @@ import { IconBox } from "./IconBox"; import { ImageBox } from "./ImageBox"; import { VideoBox } from "./VideoBox"; import { PDFBox } from "./PDFBox"; +import { LinkButtonField } from "../../../new_fields/LinkButtonField"; +import { LinkButtonBox } from "./LinkButtonBox"; // @@ -51,6 +53,7 @@ export interface FieldViewProps { @observer export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") { + console.log("LAYOUT STRING", fieldType.name, fieldStr); return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} />`; } @@ -76,6 +79,9 @@ export class FieldView extends React.Component<FieldViewProps> { else if (field instanceof IconField) { return <IconBox {...this.props} />; } + else if (field instanceof LinkButtonField) { + return <LinkButtonBox {...this.props} />; + } else if (field instanceof VideoField) { return <VideoBox {...this.props} />; } 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/LinkButtonBox.scss b/src/client/views/nodes/LinkButtonBox.scss new file mode 100644 index 000000000..24bfd2c9f --- /dev/null +++ b/src/client/views/nodes/LinkButtonBox.scss @@ -0,0 +1,18 @@ +.linkBox-cont { + width: 200px; + height: 100px; + background-color: black; + text-align: center; + color: white; + padding: 10px; + border-radius: 5px; + position: relative; + + .linkBox-cont-wrapper { + width: calc(100% - 20px); + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkButtonBox.tsx b/src/client/views/nodes/LinkButtonBox.tsx new file mode 100644 index 000000000..8a7c1ed8b --- /dev/null +++ b/src/client/views/nodes/LinkButtonBox.tsx @@ -0,0 +1,63 @@ +import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./LinkButtonBox.scss"; +import { DocumentView } from "./DocumentView"; +import { Doc } from "../../../new_fields/Doc"; +import { LinkButtonField } from "../../../new_fields/LinkButtonField"; +import { Cast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { DocumentManager } from "../../util/DocumentManager"; +import { Id } from "../../../new_fields/FieldSymbols"; + +library.add(faCaretUp); +library.add(faObjectGroup); +library.add(faStickyNote); +library.add(faFilePdf); +library.add(faFilm); + +@observer +export class LinkButtonBox extends React.Component<FieldViewProps> { + public static LayoutString() { return FieldView.LayoutString(LinkButtonBox); } + + followLink = (): void => { + console.log("follow link???"); + let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" })); + let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId); + if (targetView && targetView.props.ContainingCollectionView) { + CollectionDockingView.Instance.AddRightSplit(targetView.props.ContainingCollectionView.props.Document); + } + } + + render() { + + let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" })); + let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId); + + let text = "Could not find link"; + if (targetView) { + let context = targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(targetView.props.ContainingCollectionView.props.Document.title)) : ""; + text = "Link to " + StrCast(targetView.props.Document.title) + context; + } + + let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)); + let display = activeDvs.reduce((found, dv) => { + let matchSv = field.data.sourceViewId === StrCast(dv.props.Document[Id]); + let matchTv = field.data.targetViewId === StrCast(dv.props.Document[Id]); + let match = matchSv || matchTv; + return match || found; + }, false); + + return ( + <div className="linkBox-cont" style={{ display: display ? "block" : "none" }}> + <div className="linkBox-cont-wrapper"> + <p>{text}</p> + </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..154b4abe3 100644 --- a/src/client/views/nodes/LinkEditor.scss +++ b/src/client/views/nodes/LinkEditor.scss @@ -1,42 +1,131 @@ @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-delete { + width: 20px; + height: 20px; + margin-left: 6px; + padding: 0; + } } -.name-input { - margin-bottom: 10px; - padding: 5px; - font-size: 12px; - border: 1px solid #bababa; +.linkEditor-groupsLabel { + display: flex; + justify-content: space-between; + + button { + width: 20px; + height: 20px; + margin-left: 6px; + padding: 0; + font-size: 14px; + } } -.description-input { - font-size: 11px; - padding: 5px; - margin-bottom: 10px; - border: 1px solid #bababa; +.linkEditor-group { + background-color: $light-color-secondary; + padding: 6px; + margin: 3px 0; + border-radius: 3px; + + .linkEditor-group-row { + display: flex; + margin-bottom: 6px; + + .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% - 18px); + height: 20px; + } + + button { + width: 20px; + height: 20px; + margin-left: 6px; + padding: 0; + font-size: 14px; + } + } } -.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; + + .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: $intermediate-color; + font-weight: bold; + } + } } -.save-button:hover { - background: $main-accent; - cursor: pointer; +.linkEditor-group-buttons { + height: 20px; + display: flex; + justify-content: space-between; + + .linkEditor-groupOpts { + width: calc(20% - 3px); + height: 20px; + padding: 0; + font-size: 10px; + + &:disabled { + background-color: gray; + } + } + + .linkEditor-groupOpts button { + width: 100%; + height: 20px; + font-size: 10px; + padding: 0; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx index 71a423338..29ead7388 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 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 } 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 } 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); + + +interface GroupTypesDropdownProps { + groupId: string; + groupType: string; + setGroup: (groupId: string, 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.setGroup(this.props.groupId, groupType); + LinkManager.Instance.groupMetadataKeys.set(groupType, []); + } + + renderOptions = (): JSX.Element[] | JSX.Element => { + if (this._searchTerm === "") return <></>; + + let allGroupTypes = Array.from(LinkManager.Instance.groupMetadataKeys.keys()); + 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.setGroup(this.props.groupId, 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 a group or create a new group" + onChange={e => { this.setSearchTerm(e.target.value); this.setGroupType(e.target.value); }}></input> + <div className="linkEditor-options-wrapper"> + {this.renderOptions()} + </div> + </div> + ); + } } + +interface LinkMetadataEditorProps { + groupType: string; + mdDoc: Doc; + mdKey: string; + mdValue: string; +} @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 = new Array(...LinkManager.Instance.groupMetadataKeys.get(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 => 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._key = value; + LinkManager.Instance.groupMetadataKeys.set(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 = new Array(...LinkManager.Instance.groupMetadataKeys.get(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.groupMetadataKeys.set(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} 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()}>x</button> </div> - ); } +} + + +interface LinkEditorProps { + sourceDoc: Doc; + linkDoc: Doc; + showLinks: () => void; +} +@observer +export class LinkEditor extends React.Component<LinkEditorProps> { + + // map of temporary group id to the corresponding group doc + @observable private _groups: Map<string, Doc> = new Map(); + + constructor(props: LinkEditorProps) { + super(props); + + let groups = new Map<string, Doc>(); + let groupList = LinkManager.Instance.getAnchorGroups(props.linkDoc, props.sourceDoc); + groupList.forEach(groupDoc => { + let id = Utils.GenerateGuid(); + groups.set(id, groupDoc); + }); + this._groups = groups; + } @action - onNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - this._nameInput = e.target.value; + deleteLink = (): void => { + let index = LinkManager.Instance.allLinks.indexOf(this.props.linkDoc); + LinkManager.Instance.allLinks.splice(index, 1); + this.props.showLinks(); } @action - onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - this._descriptionInput = e.target.value; + addGroup = (): void => { + // new group only gets added if there is not already a group with type "new group" + let index = Array.from(this._groups.values()).findIndex(groupDoc => { + return groupDoc.type === "New Group"; + }); + if (index > -1) return; + + // create new metadata document for group + let mdDoc = Docs.TextDocument(); + mdDoc.proto!.anchor1 = this.props.sourceDoc.title; + mdDoc.proto!.anchor2 = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title; + + // create new group document + let groupDoc = Docs.TextDocument(); + groupDoc.proto!.type = "New Group"; + groupDoc.proto!.metadata = mdDoc; + + this._groups.set(Utils.GenerateGuid(), groupDoc); + + let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc; + LinkManager.Instance.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values())); + } + + @action + setGroupType = (groupId: string, groupType: string): void => { + let groupDoc = this._groups.get(groupId); + if (groupDoc) { + groupDoc.proto!.type = groupType; + this._groups.set(groupId, groupDoc); + LinkManager.Instance.setAnchorGroups(this.props.linkDoc, this.props.sourceDoc, Array.from(this._groups.values())); + } + } + + removeGroupFromLink = (groupId: string, groupType: string): void => { + let groupDoc = this._groups.get(groupId); + if (!groupDoc) console.error("LinkEditor: group not found"); + LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType); + this._groups.delete(groupId); + } + + deleteGroup = (groupId: string, groupType: string): void => { + let groupDoc = this._groups.get(groupId); + if (!groupDoc) console.error("LinkEditor: group not found"); + LinkManager.Instance.deleteGroup(groupType); + this._groups.delete(groupId); + } + + copyGroup = (groupId: string, groupType: string): void => { + let sourceGroupDoc = this._groups.get(groupId); + let sourceMdDoc = Cast(sourceGroupDoc!.metadata, Doc, new Doc); + let destDoc = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); + let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc); + let keys = LinkManager.Instance.groupMetadataKeys.get(groupType); + + // create new metadata doc with copied kvp + let destMdDoc = Docs.TextDocument(); + destMdDoc.proto!.anchor1 = StrCast(sourceMdDoc.anchor2); + destMdDoc.proto!.anchor2 = StrCast(sourceMdDoc.anchor1); + if (keys) { + keys.forEach(key => { + let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]); + destMdDoc[key] = val; + }); + } + + // create new group doc with new metadata doc + let destGroupDoc = Docs.TextDocument(); + destGroupDoc.proto!.type = groupType; + destGroupDoc.proto!.metadata = destMdDoc; + + // if group does not already exist on opposite anchor, create group doc + let index = destGroupList.findIndex(groupDoc => { StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase(); }); + if (index > -1) { + destGroupList[index] = destGroupDoc; + } else { + destGroupList.push(destGroupDoc); + } + + LinkManager.Instance.setAnchorGroups(this.props.linkDoc, destDoc, destGroupList); + } + + viewGroupAsTable = (groupId: string, groupType: string): JSX.Element => { + let keys = LinkManager.Instance.groupMetadataKeys.get(groupType); + let groupDoc = this._groups.get(groupId); + if (keys && groupDoc) { + let docs: Doc[] = LinkManager.Instance.findAllMetadataDocsInGroup(groupType); + let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" })); + let ref = React.createRef<HTMLDivElement>(); + return <div className="linkEditor-groupOpts" ref={ref}><button onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>; + } else { + return <button className="linkEditor-groupOpts" disabled><FontAwesomeIcon icon="table" size="sm" /></button>; + } + } + + renderGroup = (groupId: string, groupDoc: Doc): JSX.Element => { + let type = StrCast(groupDoc.type); + if ((type && LinkManager.Instance.groupMetadataKeys.get(type)) || type === "New Group") { + return ( + <div key={groupId} className="linkEditor-group"> + <div className="linkEditor-group-row"> + <p className="linkEditor-group-row-label">type:</p> + <GroupTypesDropdown groupId={groupId} groupType={StrCast(groupDoc.proto!.type)} setGroup={this.setGroupType} /> + </div> + {this.renderMetadata(groupId)} + <div className="linkEditor-group-buttons"> + {groupDoc.type === "New Group" ? <button className="linkEditor-groupOpts" disabled={true} title="Add KVP">+</button> : + <button className="linkEditor-groupOpts" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP">+</button>} + <button className="linkEditor-groupOpts" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor">↔</button> + <button className="linkEditor-groupOpts" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link">x</button> + <button className="linkEditor-groupOpts" onClick={() => this.deleteGroup(groupId, type)} title="Delete group">xx</button> + {this.viewGroupAsTable(groupId, type)} + </div> + </div> + ); + } else { + return <></>; + } + } + + + @action + addMetadata = (groupType: string): void => { + let mdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType); + if (mdKeys) { + // 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"); + } else { + mdKeys = ["new key"]; + } + LinkManager.Instance.groupMetadataKeys.set(groupType, mdKeys); + } + + renderMetadata = (groupId: string): JSX.Element[] => { + let metadata: Array<JSX.Element> = []; + let groupDoc = this._groups.get(groupId); + if (groupDoc) { + let mdDoc = Cast(groupDoc.proto!.metadata, Doc, new Doc); + let groupType = StrCast(groupDoc.proto!.type); + let groupMdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType); + if (groupMdKeys) { + groupMdKeys.forEach((key, index) => { + metadata.push( + <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} /> + ); + }); + } + } + return metadata; + } + + render() { + let destination = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); + + let groups: Array<JSX.Element> = []; + this._groups.forEach((groupDoc, groupId) => { + groups.push(this.renderGroup(groupId, 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-delete" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button> + </div> + <div className="linkEditor-groupsLabel"> + <b>Relationships:</b> + <button onClick={() => this.addGroup()} title="Add Group">+</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..09a830c9e 100644 --- a/src/client/views/nodes/LinkMenu.scss +++ b/src/client/views/nodes/LinkMenu.scss @@ -1,21 +1,14 @@ -#linkMenu-container { +@import "../globalCssVariables"; + +.linkMenu { width: 100%; height: auto; - display: flex; - flex-direction: column; } -#linkMenu-searchBar { - width: 100%; - padding: 5px; - margin-bottom: 10px; - font-size: 12px; - border: 1px solid #bababa; +.linkMenu-list { + max-height: 200px; + overflow-y: scroll; } -#linkMenu-list { - margin-top: 5px; - width: 100%; - height: 100px; - overflow-y: scroll; -}
\ No newline at end of file + + diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 3f09d6214..7e4c15312 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -1,13 +1,13 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { DocumentView } from "./DocumentView"; -import { LinkBox } from "./LinkBox"; +import { LinkMenuItem } from "./LinkMenuItem"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; +import { LinkManager } from "../../util/LinkManager"; interface Props { docView: DocumentView; @@ -19,34 +19,49 @@ export class LinkMenu extends React.Component<Props> { @observable private _editingLink?: Doc; - renderLinkItems(links: Doc[], key: string, type: string) { - return links.map(link => { - let doc = FieldValue(Cast(link[key], Doc)); - if (doc) { - return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />; - } + renderGroup = (group: Doc[], groupType: string): Array<JSX.Element> => { + let source = this.props.docView.Document; + return group.map(linkDoc => { + let destination = LinkManager.Instance.findOppositeAnchor(linkDoc, source); + return <LinkMenuItem key={destination[Id] + source[Id]} groupType={groupType} linkDoc={linkDoc} sourceDoc={source} destinationDoc={destination} showEditor={action(() => this._editingLink = linkDoc)} />; }); } + renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => { + let linkItems: Array<JSX.Element> = []; + groups.forEach((group, groupType) => { + linkItems.push( + <div key={groupType} className="link-menu-group"> + <p className="link-menu-group-name">{groupType}:</p> + <div className="link-menu-group-wrapper"> + {this.renderGroup(group, groupType)} + </div> + </div> + ); + }); + + // 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.findRelatedGroupedLinks(sourceDoc); if (this._editingLink === undefined) { return ( - <div id="linkMenu-container"> + <div className="linkMenu"> {/* <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/LinkMenuItem.scss b/src/client/views/nodes/LinkMenuItem.scss new file mode 100644 index 000000000..77462f611 --- /dev/null +++ b/src/client/views/nodes/LinkMenuItem.scss @@ -0,0 +1,76 @@ +@import "../globalCssVariables"; +.linkMenu-item { + border-top: 0.5px solid $main-accent; + padding: 6px; + position: relative; + display: flex; + font-size: 12px; + + .linkMenu-item-content { + width: 100%; + } + + .link-metadata { + margin-top: 6px; + padding: 3px; + border-top: 0.5px solid $light-color-secondary; + .link-metadata-row { + margin-left: 6px; + } + } + + &:last-child { + border-bottom: 0.5px solid $main-accent; + } + &:hover { + .linkMenu-item-buttons { + display: flex; + } + .linkMenu-item-content { + &.expand-two { + width: calc(100% - 46px); + } + &.expand-three { + width: calc(100% - 78px); + } + } + } +} + +.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; + } + } +}
\ 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..c68365584 --- /dev/null +++ b/src/client/views/nodes/LinkMenuItem.tsx @@ -0,0 +1,107 @@ +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 './LinkMenuItem.scss'; +import React = require("react"); +import { Doc } from '../../../new_fields/Doc'; +import { StrCast, Cast } from '../../../new_fields/Types'; +import { observable, action } from 'mobx'; +import { LinkManager } from '../../util/LinkManager'; +import { DragLinksAsDocuments, DragLinkAsDocument } from '../../util/DragManager'; +import { SelectionManager } from '../../util/SelectionManager'; +library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp); + + +interface LinkMenuItemProps { + groupType: string; + linkDoc: Doc; + sourceDoc: Doc; + destinationDoc: Doc; + showEditor: () => 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(); + DocumentManager.Instance.jumpToDocument(this.props.destinationDoc, e.altKey); + } + + onEdit = (e: React.PointerEvent): void => { + e.stopPropagation(); + this.props.showEditor(); + } + + 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.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.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>{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" ref={this._drag} onPointerDown={this.onLinkButtonDown} onPointerUp={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/new_fields/LinkButtonField.ts b/src/new_fields/LinkButtonField.ts new file mode 100644 index 000000000..92e1ed922 --- /dev/null +++ b/src/new_fields/LinkButtonField.ts @@ -0,0 +1,35 @@ +import { Deserializable } from "../client/util/SerializationHelper"; +import { serializable, primitive, createSimpleSchema, object } from "serializr"; +import { ObjectField } from "./ObjectField"; +import { Copy, ToScriptString } from "./FieldSymbols"; +import { Doc } from "./Doc"; +import { DocumentView } from "../client/views/nodes/DocumentView"; + +export type LinkButtonData = { + sourceViewId: string, + targetViewId: string +}; + +const LinkButtonSchema = createSimpleSchema({ + sourceViewId: true, + targetViewId: true +}); + +@Deserializable("linkButton") +export class LinkButtonField extends ObjectField { + @serializable(object(LinkButtonSchema)) + readonly data: LinkButtonData; + + constructor(data: LinkButtonData) { + super(); + this.data = data; + } + + [Copy]() { + return new LinkButtonField(this.data); + } + + [ToScriptString]() { + return "invalid"; + } +} |