aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts99
-rw-r--r--src/client/util/DocumentManager.ts38
-rw-r--r--src/client/util/DragManager.ts42
-rw-r--r--src/client/util/LinkManager.ts150
-rw-r--r--src/client/views/DocumentDecorations.tsx5
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss60
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx85
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx131
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx166
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx3
-rw-r--r--src/client/views/nodes/DocumentView.tsx93
-rw-r--r--src/client/views/nodes/FieldView.tsx6
-rw-r--r--src/client/views/nodes/LinkBox.scss66
-rw-r--r--src/client/views/nodes/LinkBox.tsx86
-rw-r--r--src/client/views/nodes/LinkButtonBox.scss18
-rw-r--r--src/client/views/nodes/LinkButtonBox.tsx63
-rw-r--r--src/client/views/nodes/LinkEditor.scss147
-rw-r--r--src/client/views/nodes/LinkEditor.tsx342
-rw-r--r--src/client/views/nodes/LinkMenu.scss23
-rw-r--r--src/client/views/nodes/LinkMenu.tsx49
-rw-r--r--src/client/views/nodes/LinkMenuItem.scss76
-rw-r--r--src/client/views/nodes/LinkMenuItem.tsx107
-rw-r--r--src/new_fields/LinkButtonField.ts35
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";
+ }
+}