aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionFreeForm
diff options
context:
space:
mode:
authorbob <bcz@cs.brown.edu>2019-04-03 11:45:15 -0400
committerbob <bcz@cs.brown.edu>2019-04-03 11:45:15 -0400
commitbb7d5a26ec68f283c5adb42d4d6554253de7176f (patch)
tree7efa817102ba54e916601611f608dd4bd761a437 /src/client/views/collections/collectionFreeForm
parent43a0768690caa89c606dd5d296d3cc8825c1702b (diff)
parentc406c8d123ce0aa9d63fb8a4dd90adfe83d2889d (diff)
merged with master
Diffstat (limited to 'src/client/views/collections/collectionFreeForm')
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx37
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss10
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx106
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx115
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss86
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx312
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss14
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx201
-rw-r--r--src/client/views/collections/collectionFreeForm/PreviewCursor.scss23
-rw-r--r--src/client/views/collections/collectionFreeForm/PreviewCursor.tsx119
11 files changed, 1029 insertions, 0 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
new file mode 100644
index 000000000..3b2f79be1
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -0,0 +1,6 @@
+.collectionfreeformlinkview-linkLine {
+ stroke: black;
+ stroke-width: 3;
+ transform: translate(10000px,10000px);
+ pointer-events: all;
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
new file mode 100644
index 000000000..e84f0c5ad
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -0,0 +1,37 @@
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { Utils } from "../../../../Utils";
+import "./CollectionFreeFormLinkView.scss";
+import React = require("react");
+import v5 = require("uuid/v5");
+
+export interface CollectionFreeFormLinkViewProps {
+ A: Document;
+ B: Document;
+ LinkDocs: Document[];
+}
+
+@observer
+export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
+
+ onPointerDown = (e: React.PointerEvent) => {
+ this.props.LinkDocs.map(l =>
+ console.log("Link:" + l.Title));
+ }
+ render() {
+ let l = this.props.LinkDocs;
+ let a = this.props.A;
+ let b = this.props.B;
+ let x1 = a.GetNumber(KeyStore.X, 0) + a.GetNumber(KeyStore.Width, 0) / 2;
+ let y1 = a.GetNumber(KeyStore.Y, 0) + a.GetNumber(KeyStore.Height, 0) / 2;
+ let x2 = b.GetNumber(KeyStore.X, 0) + b.GetNumber(KeyStore.Width, 0) / 2;
+ let y2 = b.GetNumber(KeyStore.Y, 0) + b.GetNumber(KeyStore.Height, 0) / 2;
+ return (
+ <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" onPointerDown={this.onPointerDown}
+ style={{ strokeWidth: `${l.length * 5}` }}
+ x1={`${x1}`} y1={`${y1}`}
+ x2={`${x2}`} y2={`${y2}`} />
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
new file mode 100644
index 000000000..4341c82f7
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -0,0 +1,10 @@
+.collectionfreeformlinksview-svgCanvas{
+ transform: translate(-10000px,-10000px);
+ position: absolute;
+ width: 20000px;
+ height: 20000px;
+ pointer-events: none;
+ }
+ .collectionfreeformlinksview-container {
+ pointer-events: none;
+ } \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
new file mode 100644
index 000000000..eb20b3100
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -0,0 +1,106 @@
+import { computed, reaction, runInAction, trace } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting } from "../../../../fields/Field";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { ListField } from "../../../../fields/ListField";
+import { Utils } from "../../../../Utils";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { DocumentView } from "../../nodes/DocumentView";
+import { CollectionViewProps } from "../CollectionViewBase";
+import "./CollectionFreeFormLinksView.scss";
+import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
+import React = require("react");
+import v5 = require("uuid/v5");
+
+@observer
+export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
+
+ componentDidMount() {
+ reaction(() => {
+ return DocumentManager.Instance.getAllDocumentViews(this.props.Document).map(dv => dv.props.Document.GetNumber(KeyStore.X, 0))
+ }, () => {
+ let views = DocumentManager.Instance.getAllDocumentViews(this.props.Document);
+ for (let i = 0; i < views.length; i++) {
+ for (let j = i + 1; j < views.length; j++) {
+ let srcDoc = views[j].props.Document;
+ let dstDoc = views[i].props.Document;
+ let x1 = srcDoc.GetNumber(KeyStore.X, 0);
+ let x1w = srcDoc.GetNumber(KeyStore.Width, -1);
+ let x2 = dstDoc.GetNumber(KeyStore.X, 0);
+ let x2w = dstDoc.GetNumber(KeyStore.Width, -1);
+ if (x1w < 0 || x2w < 0)
+ continue;
+ dstDoc.GetTAsync(KeyStore.Prototype, Document).then((protoDest) =>
+ srcDoc.GetTAsync(KeyStore.Prototype, Document).then((protoSrc) => runInAction(() => {
+ let dstTarg = (protoDest ? protoDest : dstDoc);
+ let srcTarg = (protoSrc ? protoSrc : srcDoc);
+ let findBrush = (field: ListField<Document>) => field.Data.findIndex(brush => {
+ let bdocs = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
+ return (bdocs.length == 0 || (bdocs[0] == dstTarg && bdocs[1] == srcTarg) || (bdocs[0] == srcTarg && bdocs[1] == dstTarg))
+ });
+ let brushAction = (field: ListField<Document>) => {
+ let found = findBrush(field);
+ if (found != -1)
+ field.Data.splice(found, 1);
+ };
+ if (Math.abs(x1 + x1w - x2) < 20 || Math.abs(x2 + x2w - x1) < 20) {
+ let linkDoc: Document = new Document();
+ linkDoc.SetText(KeyStore.Title, "Histogram Brush");
+ linkDoc.SetText(KeyStore.LinkDescription, "Brush between " + srcTarg.Title + " and " + dstTarg.Title);
+ linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField);
+
+ brushAction = brushAction = (field: ListField<Document>) => (findBrush(field) == -1) && field.Data.push(linkDoc);
+ }
+ dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
+ srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
+ }
+ )))
+ }
+ }
+ })
+ }
+ documentAnchors(view: DocumentView) {
+ let equalViews = [view];
+ let containerDoc = view.props.Document.GetT(KeyStore.AnnotationOn, Document);
+ if (containerDoc && containerDoc != FieldWaiting && containerDoc instanceof Document) {
+ equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.GetPrototype() as Document)
+ }
+ 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: Document, b: Document, }[] = [];
+ 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 match = (possiblePair.a == drawnPair.a && possiblePair.b == drawnPair.b);
+ if (match) {
+ if (!drawnPair.l.reduce((found, link) => found || link.Id == connection.l.Id, false))
+ drawnPair.l.push(connection.l);
+ }
+ return match || found;
+ }, false)) {
+ drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] as Document[] });
+ }
+ })
+ return drawnPairs
+ }, [] as { a: Document, b: Document, l: Document[] }[]);
+ return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />);
+ }
+
+ render() {
+ return (
+ <div className="collectionfreeformlinksview-container">
+ <svg className="collectionfreeformlinksview-svgCanvas">
+ {this.uniqueConnections}
+ </svg>
+ {this.props.children}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
new file mode 100644
index 000000000..19382e66f
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -0,0 +1,115 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting } from "../../../../fields/Field";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { TextField } from "../../../../fields/TextField";
+import { DragManager } from "../../../util/DragManager";
+import { Transform } from "../../../util/Transform";
+import { undoBatch } from "../../../util/UndoManager";
+import { InkingCanvas } from "../../InkingCanvas";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentContentsView } from "../../nodes/DocumentContentsView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
+import { COLLECTION_BORDER_WIDTH } from "../CollectionView";
+import { CollectionViewBase, CollectionViewProps, CursorEntry } from "../CollectionViewBase";
+import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
+import "./CollectionFreeFormView.scss";
+import { MarqueeView } from "./MarqueeView";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+
+@observer
+export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
+ protected getCursors(): CursorEntry[] {
+ let doc = this.props.Document;
+ let id = CurrentUserUtils.id;
+ let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []);
+ let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
+ return id ? notMe : [];
+ }
+
+ private crosshairs?: HTMLCanvasElement;
+ drawCrosshairs = (backgroundColor: string) => {
+ if (this.crosshairs) {
+ let c = this.crosshairs;
+ let ctx = c.getContext('2d');
+ if (ctx) {
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, 20, 20);
+
+ ctx.fillStyle = "black";
+ ctx.lineWidth = 0.5;
+
+ ctx.beginPath();
+
+ ctx.moveTo(10, 0);
+ ctx.lineTo(10, 8);
+
+ ctx.moveTo(10, 20);
+ ctx.lineTo(10, 12);
+
+ ctx.moveTo(0, 10);
+ ctx.lineTo(8, 10);
+
+ ctx.moveTo(20, 10);
+ ctx.lineTo(12, 10);
+
+ ctx.stroke();
+
+ // ctx.font = "10px Arial";
+ // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10);
+ }
+ }
+ }
+ @computed
+ get sharedCursors() {
+ return this.getCursors().map(entry => {
+ if (entry.Data.length > 0) {
+ let id = entry.Data[0][0];
+ let email = entry.Data[0][1];
+ let point = entry.Data[1];
+ this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22")
+ return (
+ <div
+ key={id}
+ style={{
+ position: "absolute",
+ transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`,
+ zIndex: 10000,
+ transformOrigin: 'center center',
+ }}
+ >
+ <canvas
+ ref={(el) => { if (el) this.crosshairs = el }}
+ width={20}
+ height={20}
+ style={{
+ position: 'absolute',
+ width: "20px",
+ height: "20px",
+ opacity: 0.5,
+ borderRadius: "50%",
+ border: "2px solid black"
+ }}
+ />
+ <p
+ style={{
+ fontSize: 14,
+ color: "black",
+ // fontStyle: "italic",
+ marginLeft: -12,
+ marginTop: 4
+ }}
+ >{email[0].toUpperCase()}</p>
+ </div>
+ );
+ }
+ })
+ }
+
+ render() {
+ return this.sharedCursors;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
new file mode 100644
index 000000000..81d21d89a
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -0,0 +1,86 @@
+@import "../../global_variables";
+
+.collectionfreeformview {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: left top;
+}
+.collectionfreeformview-container {
+ .collectionfreeformview > .jsx-parser {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ }
+
+ //nested freeform views
+ // .collectionfreeformview-container {
+ // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px),
+ // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
+ // background-size: 30px 30px;
+ // }
+
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ border: 0px solid $light-color-secondary;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position: relative;
+ overflow: hidden;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+.collectionfreeformview-overlay {
+ .collectionfreeformview > .jsx-parser {
+ position: absolute;
+ height: 100%;
+ }
+ .formattedTextBox-cont {
+ background: $light-color-secondary;
+ }
+
+ opacity: 0.99;
+ border: 0px solid transparent;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position:relative;
+ overflow: hidden;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ .collectionfreeformview {
+ .formattedTextBox-cont {
+ background:yellow;
+ }
+ }
+}
+
+// selection border...?
+.border {
+ border-style: solid;
+ box-sizing: border-box;
+ width: 98%;
+ height: 98%;
+ border-radius: $border-radius;
+}
+
+//this is an animation for the blinking cursor!
+@keyframes blink {
+ 0% {
+ opacity: 0;
+ }
+ 49% {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+#prevCursor {
+ animation: blink 1s infinite;
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
new file mode 100644
index 000000000..c5178f69d
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -0,0 +1,312 @@
+import { action, computed, observable, trace } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting } from "../../../../fields/Field";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { TextField } from "../../../../fields/TextField";
+import { DragManager } from "../../../util/DragManager";
+import { Transform } from "../../../util/Transform";
+import { undoBatch } from "../../../util/UndoManager";
+import { InkingCanvas } from "../../InkingCanvas";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentContentsView } from "../../nodes/DocumentContentsView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
+import { COLLECTION_BORDER_WIDTH } from "../CollectionView";
+import { CollectionViewBase } from "../CollectionViewBase";
+import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
+import "./CollectionFreeFormView.scss";
+import { MarqueeView } from "./MarqueeView";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
+import { PreviewCursor } from "./PreviewCursor";
+
+@observer
+export class CollectionFreeFormView extends CollectionViewBase {
+ public _canvasRef = React.createRef<HTMLDivElement>();
+ private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
+
+ public addLiveTextBox = (newBox: Document) => {
+ // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself and receive text input
+ this._selectOnLoaded = newBox.Id;
+ this.addDocument(newBox, false);
+ }
+
+ public addDocument = (newBox: Document, allowDuplicates: boolean) => {
+ let added = this.props.addDocument(newBox, false);
+ this.bringToFront(newBox);
+ return added;
+ }
+
+ public selectDocuments = (docs: Document[]) => {
+ this.props.CollectionView.SelectedDocs.length = 0;
+ docs.map(d => this.props.CollectionView.SelectedDocs.push(d.Id));
+ }
+
+ public getActiveDocuments = () => {
+ var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
+ return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).reduce((active, doc) => {
+ var page = doc.GetNumber(KeyStore.Page, -1);
+ if (page == curPage || page == -1) {
+ active.push(doc);
+ }
+ return active;
+ }, [] as Document[]);
+ }
+
+ @observable public DownX: number = 0;
+ @observable public DownY: number = 0;
+ @observable private _lastX: number = 0;
+ @observable private _lastY: number = 0;
+
+ @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) }
+ @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) }
+ @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
+ @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
+ @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
+ @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
+ @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
+ @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this.props.panelWidth() / 2 : 0; } // shift so pan position is at center of window for non-overlay collections
+ @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this.props.panelHeight() / 2 : 0; }// shift so pan position is at center of window for non-overlay collections
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (super.drop(e, de)) {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let screenX = de.x - (de.data.xOffset as number || 0);
+ let screenY = de.y - (de.data.yOffset as number || 0);
+ const [x, y] = this.getTransform().transformPoint(screenX, screenY);
+ let dragDoc = de.data.draggedDocuments[0];
+ let dragX = dragDoc.GetNumber(KeyStore.X, 0);
+ let dragY = dragDoc.GetNumber(KeyStore.Y, 0);
+ de.data.draggedDocuments.map(d => {
+ let docX = d.GetNumber(KeyStore.X, 0);
+ let docY = d.GetNumber(KeyStore.Y, 0);
+ d.SetNumber(KeyStore.X, x + (docX - dragX));
+ d.SetNumber(KeyStore.Y, y + (docY - dragY));
+ if (!d.GetNumber(KeyStore.Width, 0)) {
+ d.SetNumber(KeyStore.Width, 300);
+ d.SetNumber(KeyStore.Height, 300);
+ }
+ this.bringToFront(d);
+ })
+ }
+ return true;
+ }
+ return false;
+ }
+
+
+ @action
+ cleanupInteractions = () => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) && this.props.active()) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
+ this._lastX = this.DownX = e.pageX;
+ this._lastY = this.DownY = e.pageY;
+ if (this.props.isSelected())
+ e.stopPropagation();
+ }
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ e.stopPropagation();
+
+ this.cleanupInteractions();
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ if (!e.cancelBubble && this.props.active()) {
+ if ((!this.isAnnotationOverlay || this.zoomScaling != 1) && !e.shiftKey) {
+ let x = this.props.Document.GetNumber(KeyStore.PanX, 0);
+ let y = this.props.Document.GetNumber(KeyStore.PanY, 0);
+ let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
+ this.SetPan(x - dx, y - dy);
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ }
+
+ @action
+ onPointerWheel = (e: React.WheelEvent): void => {
+ this.props.select(false);
+ e.stopPropagation();
+ let coefficient = 1000;
+
+ if (e.ctrlKey) {
+ var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
+ var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
+ const coefficient = 1000;
+ let deltaScale = (1 - (e.deltaY / coefficient));
+ this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale);
+ this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale);
+ e.stopPropagation();
+ e.preventDefault();
+ } else {
+ // if (modes[e.deltaMode] == 'pixels') coefficient = 50;
+ // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height??
+ let transform = this.getTransform();
+
+ let deltaScale = (1 - (e.deltaY / coefficient));
+ if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay)
+ deltaScale = 1 / this.zoomScaling;
+ let [x, y] = transform.transformPoint(e.clientX, e.clientY);
+
+ let localTransform = this.getLocalTransform()
+ localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y)
+ // console.log(localTransform)
+
+ this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale);
+ this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale);
+ }
+ }
+
+ @action
+ private SetPan(panX: number, panY: number) {
+ var x1 = this.getLocalTransform().inverse().Scale;
+ const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX));
+ const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY));
+ this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX);
+ this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY);
+ }
+
+ @action
+ onDrop = (e: React.DragEvent): void => {
+ var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+ super.onDrop(e, { x: pt[0], y: pt[1] });
+ }
+
+ onDragOver = (): void => {
+ }
+
+ @action
+ bringToFront(doc: Document) {
+ const { fieldKey: fieldKey, Document: Document } = this.props;
+
+ const value: Document[] = Document.GetList<Document>(fieldKey, []).slice();
+ value.sort((doc1, doc2) => {
+ if (doc1 === doc) {
+ return 1;
+ }
+ if (doc2 === doc) {
+ return -1;
+ }
+ return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0);
+ }).map((doc, index) => {
+ doc.SetNumber(KeyStore.ZIndex, index + 1)
+ });
+ }
+
+ @computed get backgroundLayout(): string | undefined {
+ let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField);
+ if (field && field !== FieldWaiting) {
+ return field.Data;
+ }
+ }
+ @computed get overlayLayout(): string | undefined {
+ let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField);
+ if (field && field !== FieldWaiting) {
+ return field.Data;
+ }
+ }
+
+ focusDocument = (doc: Document) => {
+ let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2;
+ let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2;
+ this.SetPan(x, y);
+ this.props.focus(this.props.Document);
+ }
+
+ getDocumentViewProps(document: Document): DocumentViewProps {
+ return {
+ Document: document,
+ AddDocument: this.props.addDocument,
+ RemoveDocument: this.props.removeDocument,
+ ScreenToLocalTransform: this.getTransform,
+ isTopMost: false,
+ SelectOnLoad: document.Id == this._selectOnLoaded,
+ PanelWidth: document.Width,
+ PanelHeight: document.Height,
+ ContentScaling: this.noScaling,
+ ContainingCollectionView: this.props.CollectionView,
+ focus: this.focusDocument
+ }
+ }
+
+ @computed
+ get views() {
+ var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
+ return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => {
+ var page = doc.GetNumber(KeyStore.Page, -1);
+ if (page == curPage || page == -1)
+ prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
+ return prev;
+ }, [] as JSX.Element[])
+ }
+
+ @computed
+ get backgroundView() {
+ return !this.backgroundLayout ? (null) :
+ (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
+ layoutKey={KeyStore.BackgroundLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />);
+ }
+ @computed
+ get overlayView() {
+ return !this.overlayLayout ? (null) :
+ (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
+ layoutKey={KeyStore.OverlayLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />);
+ }
+
+ getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform())
+ getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH)
+ getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY);
+ noScaling = () => 1;
+ childViews = () => this.views;
+
+ render() {
+ let [dx, dy] = [this.centeringShiftX, this.centeringShiftY];
+
+ const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0);
+ const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0);
+
+ return (
+ <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`}
+ onPointerDown={this.onPointerDown} onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))}
+ onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onWheel={this.onPointerWheel}
+ style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} ref={this.createDropTarget}>
+ <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments}
+ addDocument={this.addDocument} removeDocument={this.props.removeDocument}
+ getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
+ <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox}
+ getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} >
+ <div className="collectionfreeformview" ref={this._canvasRef}
+ style={{ transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}>
+ {this.backgroundView}
+ <CollectionFreeFormLinksView {...this.props}>
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} >
+ {this.childViews}
+ </InkingCanvas>
+ </CollectionFreeFormLinksView>
+ <CollectionFreeFormRemoteCursors {...this.props} />
+ </div>
+ {this.overlayView}
+ </PreviewCursor>
+ </MarqueeView>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
new file mode 100644
index 000000000..1ee3b244b
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -0,0 +1,14 @@
+
+.marqueeView {
+ position: absolute;
+ width:100%;
+ height:100%;
+}
+.marquee {
+ border-style: dashed;
+ box-sizing: border-box;
+ position: absolute;
+ border-width: 1px;
+ border-color: black;
+ pointer-events: none;
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
new file mode 100644
index 000000000..e2239c8be
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -0,0 +1,201 @@
+import { action, computed, observable, trace } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting } from "../../../../fields/Field";
+import { InkField, StrokeData } from "../../../../fields/InkField";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { Documents } from "../../../documents/Documents";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Transform } from "../../../util/Transform";
+import { InkingCanvas } from "../../InkingCanvas";
+import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import "./MarqueeView.scss";
+import { PreviewCursor } from "./PreviewCursor";
+import React = require("react");
+
+interface MarqueeViewProps {
+ getContainerTransform: () => Transform;
+ getTransform: () => Transform;
+ container: CollectionFreeFormView;
+ addDocument: (doc: Document, allowDuplicates: false) => boolean;
+ activeDocuments: () => Document[];
+ selectDocuments: (docs: Document[]) => void;
+ removeDocument: (doc: Document) => boolean;
+}
+
+@observer
+export class MarqueeView extends React.Component<MarqueeViewProps>
+{
+ @observable _lastX: number = 0;
+ @observable _lastY: number = 0;
+ @observable _downX: number = 0;
+ @observable _downY: number = 0;
+ @observable _used: boolean = false;
+ @observable _visible: boolean = false;
+ static DRAG_THRESHOLD = 4;
+
+ @action
+ cleanupInteractions = (all: boolean = false) => {
+ if (all) {
+ document.removeEventListener("pointermove", this.onPointerMove, true)
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ } else {
+ this._used = true;
+ }
+ document.removeEventListener("keydown", this.marqueeCommand, true);
+ this._visible = false;
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.buttons == 1 && !e.altKey && !e.metaKey && this.props.container.props.active()) {
+ this._downX = this._lastX = e.pageX;
+ this._downY = this._lastY = e.pageY;
+ this._used = false;
+ document.addEventListener("pointermove", this.onPointerMove, true)
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ document.addEventListener("keydown", this.marqueeCommand, true);
+ }
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ if (!e.cancelBubble) {
+ if (!this._used && e.buttons == 1 && !e.altKey && !e.metaKey &&
+ (Math.abs(this._lastX - this._downX) > MarqueeView.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > MarqueeView.DRAG_THRESHOLD)) {
+ this._visible = true;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ this.cleanupInteractions(true);
+ this._visible = false;
+ let mselect = this.marqueeSelect();
+ if (!e.shiftKey) {
+ SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
+ }
+ this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
+ }
+
+ intersectRect(r1: { left: number, top: number, width: number, height: number },
+ r2: { left: number, top: number, width: number, height: number }) {
+ return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
+ }
+
+ @computed
+ get Bounds() {
+ let left = this._downX < this._lastX ? this._downX : this._lastX;
+ let top = this._downY < this._lastY ? this._downY : this._lastY;
+ let topLeft = this.props.getTransform().transformPoint(left, top);
+ let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }
+ }
+
+ @action
+ marqueeCommand = (e: KeyboardEvent) => {
+ if (e.key == "Backspace" || e.key == "Delete") {
+ this.marqueeSelect().map(d => this.props.removeDocument(d));
+ let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
+ if (ink && ink != FieldWaiting) {
+ this.marqueeInkDelete(ink.Data);
+ }
+ this.cleanupInteractions();
+ }
+ if (e.key == "c") {
+ let bounds = this.Bounds;
+ let selected = this.marqueeSelect().map(d => {
+ this.props.removeDocument(d);
+ d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2);
+ d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2);
+ d.SetNumber(KeyStore.Page, -1);
+ d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0));
+ return d;
+ });
+ let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
+ let inkData = ink && ink != FieldWaiting ? ink.Data : undefined;
+ //setTimeout(() => {
+ let newCollection = Documents.FreeformDocument(selected, {
+ x: bounds.left,
+ y: bounds.top,
+ panx: 0,
+ pany: 0,
+ width: bounds.width,
+ height: bounds.height,
+ backgroundColor: "Transparent",
+ ink: inkData ? this.marqueeInkSelect(inkData) : undefined,
+ title: "a nested collection"
+ });
+ this.props.addDocument(newCollection, false);
+ this.marqueeInkDelete(inkData);
+ // }, 100);
+ this.cleanupInteractions();
+ }
+ }
+ @action
+ marqueeInkSelect(ink: Map<any, any>) {
+ let idata = new Map();
+ let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin.
+ let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2);
+ ink.forEach((value: StrokeData, key: string, map: any) => {
+ if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) {
+ idata.set(key,
+ {
+ pathData: value.pathData.map(val => { return { x: val.x + centerShiftX, y: val.y + centerShiftY } }),
+ color: value.color,
+ width: value.width,
+ tool: value.tool,
+ page: -1
+ });
+ }
+ });
+ return idata;
+ }
+
+ @action
+ marqueeInkDelete(ink?: Map<any, any>) {
+ // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB.
+ // ink.forEach((value: StrokeData, key: string, map: any) =>
+ // InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key));
+
+ if (ink) {
+ let idata = new Map();
+ ink.forEach((value: StrokeData, key: string, map: any) =>
+ !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
+ this.props.container.props.Document.SetDataOnPrototype(KeyStore.Ink, idata, InkField);
+ }
+ }
+
+ marqueeSelect() {
+ let selRect = this.Bounds;
+ let selection: Document[] = [];
+ this.props.activeDocuments().map(doc => {
+ var x = doc.GetNumber(KeyStore.X, 0);
+ var y = doc.GetNumber(KeyStore.Y, 0);
+ var w = doc.GetNumber(KeyStore.Width, 0);
+ var h = doc.GetNumber(KeyStore.Height, 0);
+ if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect))
+ selection.push(doc)
+ })
+ return selection;
+ }
+
+ @computed
+ get marqueeDiv() {
+ let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
+ let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />
+ }
+
+ render() {
+ return <div className="marqueeView" onPointerDown={this.onPointerDown}>
+ {this.props.children}
+ {!this._visible ? (null) : this.marqueeDiv}
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
new file mode 100644
index 000000000..21210be2b
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
@@ -0,0 +1,23 @@
+
+.previewCursor {
+ color: black;
+ position: absolute;
+ transform-origin: left top;
+ pointer-events: none;
+}
+.previewCursorView {
+ position: absolute;
+ width:100%;
+ height:100%;
+}
+
+//this is an animation for the blinking cursor!
+// @keyframes blink {
+// 0% {opacity: 0}
+// 49%{opacity: 0}
+// 50% {opacity: 1}
+// }
+
+// #previewCursor {
+// animation: blink 1s infinite;
+// } \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
new file mode 100644
index 000000000..93c98f7b0
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
@@ -0,0 +1,119 @@
+import { action, observable, trace, computed, reaction } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { Documents } from "../../../documents/Documents";
+import { Transform } from "../../../util/Transform";
+import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import "./PreviewCursor.scss";
+import React = require("react");
+import { interfaceDeclaration } from "babel-types";
+
+
+export interface PreviewCursorProps {
+ getTransform: () => Transform;
+ getContainerTransform: () => Transform;
+ container: CollectionFreeFormView;
+ addLiveTextDocument: (doc: Document) => void;
+}
+
+@observer
+export class PreviewCursor extends React.Component<PreviewCursorProps> {
+ @observable _lastX: number = 0;
+ @observable _lastY: number = 0;
+ @observable public _visible: boolean = false;
+ @observable public DownX: number = 0;
+ @observable public DownY: number = 0;
+ _showOnUp: boolean = false;
+
+ @action
+ cleanupInteractions = () => {
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ document.removeEventListener("pointermove", this.onPointerMove, true);
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ if (e.button == 0 && this.props.container.props.active()) {
+ document.removeEventListener("keypress", this.onKeyPress, false);
+ this._showOnUp = true;
+ this.DownX = e.pageX;
+ this.DownY = e.pageY;
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ }
+ }
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ if (Math.abs(this.DownX - e.clientX) > 4 || Math.abs(this.DownY - e.clientY) > 4) {
+ this._showOnUp = false;
+ this._visible = false;
+ }
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ if (this._showOnUp) {
+ document.addEventListener("keypress", this.onKeyPress, false);
+ this._lastX = this.DownX;
+ this._lastY = this.DownY;
+ this._visible = true;
+ }
+ this.cleanupInteractions();
+ }
+
+ @action
+ onKeyPress = (e: KeyboardEvent) => {
+ // Mixing events between React and Native is finicky. In FormattedTextBox, we set the
+ // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore
+ // the keyPress here.
+ //if not these keys, make a textbox if preview cursor is active!
+ if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
+ //make textbox and add it to this collection
+ let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY);
+ let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" });
+ this.props.addLiveTextDocument(newBox);
+ document.removeEventListener("keypress", this.onKeyPress, false);
+ this._visible = false;
+ e.stopPropagation();
+ }
+ }
+
+ getPoint = () => this.props.getContainerTransform().transformPoint(this._lastX, this._lastY);
+ getVisible = () => this._visible;
+ setVisible = (v: boolean) => {
+ this._visible = v;
+ document.removeEventListener("keypress", this.onKeyPress, false);
+ }
+ render() {
+ return (
+ <div className="previewCursorView" onPointerDown={this.onPointerDown}>
+ {this.props.children}
+ <PreviewCursorPrompt setVisible={this.setVisible} getPoint={this.getPoint} getVisible={this.getVisible} />
+ </div>
+ )
+ }
+}
+
+export interface PromptProps {
+ getPoint: () => number[];
+ getVisible: () => boolean;
+ setVisible: (v: boolean) => void;
+}
+
+@observer
+export class PreviewCursorPrompt extends React.Component<PromptProps> {
+ private _promptRef = React.createRef<HTMLDivElement>();
+
+ //when focus is lost, this will remove the preview cursor
+ @action onBlur = (): void => this.props.setVisible(false);
+
+ render() {
+ let p = this.props.getPoint();
+ if (this.props.getVisible() && this._promptRef.current)
+ this._promptRef.current.focus();
+ return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._promptRef}
+ style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, opacity: this.props.getVisible() ? 1 : 0 }}>
+ I
+ </div >;
+ }
+} \ No newline at end of file