aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbob <bcz@cs.brown.edu>2019-10-17 16:02:37 -0400
committerbob <bcz@cs.brown.edu>2019-10-17 16:02:37 -0400
commitdcdefb2a5a80d3c4d5451d6c7fc7213565d5ea5f (patch)
treedc0d50ed9d9f5d3b5e34e12c3782dcf779d105d6 /src
parent17042f9598e20615668830a7c139a8a31dc6c109 (diff)
initial working version of draggable link anchors.
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts23
-rw-r--r--src/client/util/DragManager.ts4
-rw-r--r--src/client/views/DocumentButtonBar.tsx10
-rw-r--r--src/client/views/DocumentDecorations.tsx3
-rw-r--r--src/client/views/InkingControl.tsx12
-rw-r--r--src/client/views/nodes/DocuLinkView.tsx85
-rw-r--r--src/client/views/nodes/DocumentView.tsx114
7 files changed, 145 insertions, 106 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 6889936b8..66ed8be5d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -149,6 +149,29 @@ export namespace Utils {
}
+ export function clamp(n: number, lower: number, upper: number) {
+ return Math.max(lower, Math.min(upper, n));
+ }
+
+ export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
+ var r = l + w,
+ b = t + h;
+
+ var x = clamp(x, l, r),
+ y = clamp(y, t, b);
+
+ var dl = Math.abs(x - l),
+ dr = Math.abs(x - r),
+ dt = Math.abs(y - t),
+ db = Math.abs(y - b);
+
+ var m = Math.min(dl, dr, dt, db);
+
+ return (m === dt) ? [x, t] :
+ (m === db) ? [x, b] :
+ (m === dl) ? [l, y] : [r, y];
+ }
+
export function GetClipboardText(): string {
var textArea = document.createElement("textarea");
document.body.appendChild(textArea);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 06dab024e..576b16bc8 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -87,12 +87,12 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num
}
}
-export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
+export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc, singleLink?: Doc) {
let srcTarg = sourceDoc.proto;
let draggedDocs: Doc[] = [];
if (srcTarg) {
- let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
+ let linkDocs = singleLink ? [singleLink] : LinkManager.Instance.getAllRelatedLinks(srcTarg);
if (linkDocs) {
draggedDocs = linkDocs.map(link => {
let opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 959b120ed..ba87ecfb4 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -47,7 +47,6 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
private _aliasButton = React.createRef<HTMLDivElement>();
private _tooltipoff = React.createRef<HTMLDivElement>();
private _textDoc?: Doc;
- private _linkDrag?: UndoManager.Batch;
public static Instance: DocumentButtonBar;
constructor(props: { views: DocumentView[] }) {
@@ -139,15 +138,10 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
let selDoc = this.props.views[0];
let container = selDoc.props.ContainingCollectionDoc ? selDoc.props.ContainingCollectionDoc.proto : undefined;
let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
- this._linkDrag = UndoManager.StartBatch("Drag Link");
+ let _linkDrag = UndoManager.StartBatch("Drag Link");
DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
handlers: {
- dragComplete: () => {
- if (this._linkDrag) {
- this._linkDrag.end();
- this._linkDrag = undefined;
- }
- },
+ dragComplete: () => _linkDrag && _linkDrag.end()
},
hideSource: false
});
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6e8ba2d3d..927729487 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -167,7 +167,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
get Bounds(): { x: number, y: number, b: number, r: number } {
let x = this._forceUpdate;
this._lastBox = SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
- if (documentView.props.renderDepth === 0 ||
+ if (documentView._selectedLink !== -1 ||
+ documentView.props.renderDepth === 0 ||
Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
return bounds;
}
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index b8f3c2875..1aacdc8ec 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -81,18 +81,18 @@ export class InkingControl {
ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined;
ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb)));
}
- (!ruleProvider && targetDoc) && (targetDoc.backgroundColor = matchedColor);
+ (!ruleProvider && targetDoc) && (view.setBackround(matchedColor));
return {
target: targetDoc,
previous: oldColor
};
});
- let captured = this._selectedColor;
- UndoManager.AddEvent({
- undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
- redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
- });
+ //let captured = this._selectedColor;
+ // UndoManager.AddEvent({
+ // undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
+ // redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
+ // });
} else {
CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor);
}
diff --git a/src/client/views/nodes/DocuLinkView.tsx b/src/client/views/nodes/DocuLinkView.tsx
new file mode 100644
index 000000000..622d41047
--- /dev/null
+++ b/src/client/views/nodes/DocuLinkView.tsx
@@ -0,0 +1,85 @@
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, Opt } from "../../../new_fields/Doc";
+import { NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils } from '../../../Utils';
+import { DocumentManager } from "../../util/DocumentManager";
+import "./DocumentView.scss";
+import React = require("react");
+import { DragManager, DragLinksAsDocuments } from "../../util/DragManager";
+import { UndoManager } from "../../util/UndoManager";
+
+
+interface DocuLinkViewProps {
+ Document: Doc;
+ isSelected: () => boolean;
+ addDocTab: (doc: Doc, dataDoc: Opt<Doc>, where: string) => void;
+ anchor: string;
+ otherAnchor: string;
+ scale: () => number;
+ contentDiv: HTMLDivElement | null;
+ link: Doc;
+ index: number;
+ selectedLink: () => number;
+ selectLink: (id: number) => void;
+ blacklist: Opt<Doc>
+}
+
+@observer
+export class DocuLinkView extends React.Component<DocuLinkViewProps> {
+ _downx = 0;
+ _downy = 0;
+ @observable _x = 0;
+ @observable _y = 0;
+ @observable _selected = false;
+ _ref = React.createRef<HTMLDivElement>();
+
+ onPointerDown = (e: React.PointerEvent) => {
+ this._downx = e.clientX;
+ this._downy = e.clientY;
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ e.stopPropagation();
+ }
+ onPointerMove = action((e: PointerEvent) => {
+ if (this.props.contentDiv && (Math.abs(e.clientX - this._downx) > 5 || Math.abs(e.clientY - this._downy) > 5)) {
+ let bounds = this.props.contentDiv.getBoundingClientRect();
+ let pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
+ let separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
+ let dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy))
+ if (separation > 100) {
+ DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], this.props.Document, this.props.link);
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ } else if (dragdist > separation) {
+ this.props.link[this.props.anchor + "_x"] = (pt[0] - bounds.left) / bounds.width * 100;
+ this.props.link[this.props.anchor + "_y"] = (pt[1] - bounds.top) / bounds.height * 100;
+ }
+ }
+ })
+ onPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button === 2 || e.ctrlKey)) {
+ this.props.selectLink(this.props.selectedLink() === this.props.index ? -1 : this.props.index);
+ }
+ }
+ onClick = (e: React.MouseEvent) => {
+ if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey)) {
+ DocumentManager.Instance.FollowLink(this.props.link, this.props.link[this.props.anchor] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false);
+ }
+ e.stopPropagation();
+ }
+ render() {
+ let y = NumCast(this.props.link[this.props.anchor + "_y"], 100);
+ let x = NumCast(this.props.link[this.props.anchor + "_x"], 100);
+ let c = StrCast(this.props.link[this.props.anchor + "_background"], "lightblue");
+ return <div onPointerDown={this.onPointerDown} onClick={this.onClick} title={StrCast((this.props.link[this.props.otherAnchor]! as Doc).title)} ref={this._ref} style={{
+ cursor: "default", position: "absolute", background: c, width: "25px", height: "25px", borderRadius: "20px", textAlign: "center", left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
+ transform: `scale(${1 / this.props.scale()})`,
+ border: this.props.selectedLink() === this.props.index && this.props.isSelected() ? "solid 3px black" : undefined
+ }} />
+ }
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c217c9d86..d8a1841b1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -40,6 +40,7 @@ import SharingManager from '../../util/SharingManager';
import { Scripting } from '../../util/Scripting';
import { DictationOverlay } from '../DictationOverlay';
import { CollectionViewType } from '../collections/CollectionBaseView';
+import { DocuLinkView } from './DocuLinkView';
library.add(fa.faEdit);
library.add(fa.faTrash);
@@ -241,6 +242,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onPointerDown = (e: React.PointerEvent): void => {
if (e.nativeEvent.cancelBubble && e.button === 0) return;
+ runInAction(() => this._selectedLink = -1);
this._downX = e.clientX;
this._downY = e.clientY;
this._hitTemplateDrag = false;
@@ -626,6 +628,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
layoutKey="layout"
DataDoc={this.props.DataDoc} />);
}
+ linkEndpoint = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? "anchor1" : "anchor2";
+ linkOtherEndpoint = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? "anchor2" : "anchor1";
+ public setBackround(color: string) {
+ let selLink = this._selectedLink !== -1 && DocListCast(this.Document.links)[this._selectedLink];
+ if (selLink) {
+ let both = selLink["anchor1_background"] === selLink["anchor2_background"];
+ selLink[this.linkEndpoint(selLink) + "_background"] = color;
+ both && (selLink[this.linkOtherEndpoint(selLink) + "_background"] = color);
+ } else {
+ this.Document.backgroundColor = color;
+ }
+ }
+ @observable _selectedLink = -1;
+ selectLink = action((which: number) => {
+ SelectionManager.SelectDoc(this, false);
+ this._selectedLink = which;
+ })
+ selectedLink = () => this._selectedLink;
+
render() {
if (!this.props.Document) return (null);
let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined;
@@ -696,7 +717,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
>
- {this.props.Document.links && DocListCast(this.props.Document.links).map((d, i) => <DocuLink view={this} link={d} index={i} />)}
+ {this.props.Document.links && DocListCast(this.props.Document.links).map((d, i) =>
+ <DocuLinkView Document={this.props.Document} blacklist={this.props.ContainingCollectionDoc} anchor={this.linkEndpoint(d)} otherAnchor={this.linkOtherEndpoint(d)} addDocTab={this.props.addDocTab} contentDiv={this.ContentDiv}
+ scale={this.props.ContentScaling}
+ isSelected={this.isSelected} link={d} index={i} selectLink={this.selectLink} selectedLink={this.selectedLink} />)}
{!showTitle && !showCaption ?
this.Document.searchFields ?
(<div className="documentView-searchWrapper">
@@ -720,94 +744,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
-
-interface DocuLinkProps {
- view: DocumentView;
- link: Doc;
- index: number;
-}
-
-@observer
-export class DocuLink extends React.Component<DocuLinkProps> {
- _downx = 0;
- _downy = 0;
- @observable _x = 0;
- @observable _y = 0;
-
- constructor(props: DocuLinkProps) {
- super(props);
- }
-
- clamp(n: number, lower: number, upper: number) {
- return Math.max(lower, Math.min(upper, n));
- }
-
- getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
- var r = l + w,
- b = t + h;
-
- var x = this.clamp(x, l, r),
- y = this.clamp(y, t, b);
-
- var dl = Math.abs(x - l),
- dr = Math.abs(x - r),
- dt = Math.abs(y - t),
- db = Math.abs(y - b);
-
- var m = Math.min(dl, dr, dt, db);
-
- return (m === dt) ? [x, t] :
- (m === db) ? [x, b] :
- (m === dl) ? [l, y] : [r, y];
- }
-
- get linkEndpoint() {
- return Doc.AreProtosEqual(this.props.view.props.Document, Cast(this.props.link.anchor1, Doc) as Doc) ?
- "anchor1" : "anchor2";
- }
- get linkOtherEndpoint() {
- return !Doc.AreProtosEqual(this.props.view.props.Document, Cast(this.props.link.anchor1, Doc) as Doc) ?
- "anchor1" : "anchor2";
- }
-
- onPointerDown = (e: React.PointerEvent) => {
- this._downx = e.clientX;
- this._downy = e.clientY;
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointerup", this.onPointerUp);
- e.stopPropagation();
- }
- onPointerMove = action((e: PointerEvent) => {
- if (this.props.view.ContentDiv && (Math.abs(e.clientX - this._downx) > 3 || Math.abs(e.clientY - this._downy) > 3)) {
- let bounds = this.props.view.ContentDiv.getBoundingClientRect();
- let pt = this.getNearestPointInPerimeter(bounds.left - 25, bounds.top - 25, bounds.width, bounds.height, e.clientX, e.clientY);
- this.props.link[this.linkEndpoint + "_x"] = (pt[0] - bounds.left) / bounds.width * 100;
- this.props.link[this.linkEndpoint + "_y"] = (pt[1] - bounds.top) / bounds.height * 100;
- }
- })
- onPointerUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
- onClick = (e: React.MouseEvent) => {
- if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3) {
- DocumentManager.Instance.FollowLink(this.props.link, this.props.view.props.Document, document => this.props.view.props.addDocTab(document, undefined, "inTab"), false);
- }
- e.stopPropagation();
- }
- render() {
- return <div onPointerDown={this.onPointerDown} title={StrCast((this.props.link[this.linkOtherEndpoint]! as Doc).title)} style={{
- position: "absolute", background: "lightblue", width: "25px", height: "25px", borderRadius: "20px",
- left: `${NumCast(this.props.link[this.linkEndpoint + "_x"], 100)}%`, top: `${NumCast(this.props.link[this.linkEndpoint + "_y"], 100)}%`
- }} >
- {this.props.index}
- </div>
- }
-}
-
-
export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField: string, oldLayout?: Doc) {
let oldLayoutExt = oldLayout || await Cast(doc[oldLayoutField], Doc);
if (oldLayoutExt) {